refactor(config): migrar a SettingsConfigDict y quitar Field(env=...)

Elimina las dos deprecaciones de Pydantic V2 (se rompían en V3):
- class Config -> model_config = SettingsConfigDict(env_file='.env')
- Field(env='X') en los 33 campos -> resolución automática por nombre
  de campo (case-insensitive en pydantic-settings); verificado que los
  33 nombres coinciden con su variable, así que el comportamiento es
  idéntico. pytest pasa de 34 warnings a 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-07-03 15:12:36 +00:00
co-authored by Claude Fable 5
parent 19efc70539
commit a91e119171
+33 -36
View File
@@ -1,52 +1,51 @@
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field from pydantic import Field
from typing import Optional from typing import Optional
class Settings(BaseSettings): class Settings(BaseSettings):
# Telegram # Telegram
telegram_bot_token: str = Field(..., env="TELEGRAM_BOT_TOKEN") telegram_bot_token: str = Field(...)
telegram_allowed_users: str = Field("", env="TELEGRAM_ALLOWED_USERS") # comma-separated user IDs telegram_allowed_users: str = Field("") # comma-separated user IDs
# Ollama # Ollama
ollama_url: str = Field("http://ollama.chemavx.xyz", env="OLLAMA_URL") ollama_url: str = Field("http://ollama.chemavx.xyz")
ollama_model: str = Field("qwen2.5:3b", env="OLLAMA_MODEL") ollama_model: str = Field("qwen2.5:3b")
ollama_embed_model: str = Field("qwen2.5:3b", env="OLLAMA_EMBED_MODEL") ollama_embed_model: str = Field("qwen2.5:3b")
# Claude fallback (optional) # Claude fallback (optional)
anthropic_api_key: Optional[str] = Field(None, env="ANTHROPIC_API_KEY") anthropic_api_key: Optional[str] = Field(None)
claude_model: str = Field("claude-haiku-4-5", env="CLAUDE_MODEL") claude_model: str = Field("claude-haiku-4-5")
# Database # Database
db_path: str = Field("/data/researchowl.db", env="DB_PATH") db_path: str = Field("/data/researchowl.db")
# Scraping # Scraping
max_depth: int = Field(3, env="MAX_DEPTH") # recursion depth max_depth: int = Field(3) # recursion depth
max_sources: int = Field(150, env="MAX_SOURCES") # hard cap max_sources: int = Field(150) # hard cap
max_pages_per_search: int = Field(5, env="MAX_PAGES_PER_SEARCH") max_pages_per_search: int = Field(5)
searxng_url: str = Field( searxng_url: str = Field(
"http://searxng-svc.researchowl.svc.cluster.local:8080/search", "http://searxng-svc.researchowl.svc.cluster.local:8080/search",
env="SEARXNG_URL"
) )
request_timeout: int = Field(30, env="REQUEST_TIMEOUT") request_timeout: int = Field(30)
request_delay: float = Field(1.0, env="REQUEST_DELAY") # seconds between requests request_delay: float = Field(1.0) # seconds between requests
min_content_length: int = Field(200, env="MIN_CONTENT_LENGTH") # chars min_content_length: int = Field(200) # chars
# Fuentes opcionales — desactivadas por defecto: la IP del homelab está # Fuentes opcionales — desactivadas por defecto: la IP del homelab está
# bloqueada por Reddit (403) y YouTube (transcripts vacíos), eran peso muerto. # bloqueada por Reddit (403) y YouTube (transcripts vacíos), eran peso muerto.
enable_youtube: bool = Field(False, env="ENABLE_YOUTUBE") enable_youtube: bool = Field(False)
enable_reddit: bool = Field(False, env="ENABLE_REDDIT") enable_reddit: bool = Field(False)
# Processing # Processing
chunk_size: int = Field(800, env="CHUNK_SIZE") # tokens per chunk chunk_size: int = Field(800) # tokens per chunk
chunk_overlap: int = Field(100, env="CHUNK_OVERLAP") chunk_overlap: int = Field(100)
quality_threshold: float = Field(0.3, env="QUALITY_THRESHOLD") # 0-1, chunks below discarded quality_threshold: float = Field(0.3) # 0-1, chunks below discarded
# Ghost CMS # Ghost CMS
ghost_url: Optional[str] = Field(None, env="GHOST_URL") ghost_url: Optional[str] = Field(None)
ghost_api_key: Optional[str] = Field(None, env="GHOST_API_KEY") ghost_api_key: Optional[str] = Field(None)
ghost_url_en: str = Field("", env="GHOST_URL_EN") ghost_url_en: str = Field("")
ghost_api_key_en: str = Field("", env="GHOST_API_KEY_EN") ghost_api_key_en: str = Field("")
# SEO autofill — "off" | "on" | "dryrun" (default off). # SEO autofill — "off" | "on" | "dryrun" (default off).
# off = today's exact behavior (bare draft, no second LLM call). # off = today's exact behavior (bare draft, no second LLM call).
@@ -55,28 +54,27 @@ class Settings(BaseSettings):
# dryrun = runs the full pipeline + reports the proposed SEO to Telegram, but # dryrun = runs the full pipeline + reports the proposed SEO to Telegram, but
# writes a BARE draft (no seo fields), for inspection before trusting # writes a BARE draft (no seo fields), for inspection before trusting
# the write path. A `/generate blog en dry` arg forces dryrun per-call. # the write path. A `/generate blog en dry` arg forces dryrun per-call.
seo_autofill: str = Field("off", env="SEO_AUTOFILL") seo_autofill: str = Field("off")
# News monitor (RSS de noticias UAP/OVNI) — inerte hasta NEWS_ENABLED=true. # News monitor (RSS de noticias UAP/OVNI) — inerte hasta NEWS_ENABLED=true.
NEWS_ENABLED: bool = Field(False, env="NEWS_ENABLED") NEWS_ENABLED: bool = Field(False)
NEWS_POLL_INTERVAL_HOURS: int = Field(6, env="NEWS_POLL_INTERVAL_HOURS") NEWS_POLL_INTERVAL_HOURS: int = Field(6)
# Chat al que el monitor empuja novedades; si None -> 1er TELEGRAM_ALLOWED_USERS # Chat al que el monitor empuja novedades; si None -> 1er TELEGRAM_ALLOWED_USERS
# (ver propiedad news_chat_id). # (ver propiedad news_chat_id).
NEWS_CHAT_ID: Optional[int] = Field(None, env="NEWS_CHAT_ID") NEWS_CHAT_ID: Optional[int] = Field(None)
NEWS_KEYWORDS: str = Field( NEWS_KEYWORDS: str = Field(
"ovni,ovnis,uap,ufo,desclasificado,desclasificacion,declassified," "ovni,ovnis,uap,ufo,desclasificado,desclasificacion,declassified,"
"avistamiento,sighting,extraterrestre,non-human,nhi,grusch," "avistamiento,sighting,extraterrestre,non-human,nhi,grusch,"
"aaro,pursue,fenomeno aereo,whistleblower", "aaro,pursue,fenomeno aereo,whistleblower",
env="NEWS_KEYWORDS",
) )
NEWS_MAX_ITEMS: int = Field(15, env="NEWS_MAX_ITEMS") NEWS_MAX_ITEMS: int = Field(15)
# Alerts # Alerts
cost_alert_threshold: float = Field(0.15, env="COST_ALERT_THRESHOLD") cost_alert_threshold: float = Field(0.15)
# App # App
log_level: str = Field("INFO", env="LOG_LEVEL") log_level: str = Field("INFO")
timezone: str = Field("Europe/Madrid", env="TIMEZONE") timezone: str = Field("Europe/Madrid")
@property @property
def allowed_user_ids(self) -> list[int]: def allowed_user_ids(self) -> list[int]:
@@ -102,8 +100,7 @@ class Settings(BaseSettings):
def seo_autofill_dryrun(self) -> bool: def seo_autofill_dryrun(self) -> bool:
return (self.seo_autofill or "").strip().lower() == "dryrun" return (self.seo_autofill or "").strip().lower() == "dryrun"
class Config: model_config = SettingsConfigDict(env_file=".env")
env_file = ".env"
settings = Settings() settings = Settings()