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 typing import Optional
class Settings(BaseSettings):
# Telegram
telegram_bot_token: str = Field(..., env="TELEGRAM_BOT_TOKEN")
telegram_allowed_users: str = Field("", env="TELEGRAM_ALLOWED_USERS") # comma-separated user IDs
telegram_bot_token: str = Field(...)
telegram_allowed_users: str = Field("") # comma-separated user IDs
# Ollama
ollama_url: str = Field("http://ollama.chemavx.xyz", env="OLLAMA_URL")
ollama_model: str = Field("qwen2.5:3b", env="OLLAMA_MODEL")
ollama_embed_model: str = Field("qwen2.5:3b", env="OLLAMA_EMBED_MODEL")
ollama_url: str = Field("http://ollama.chemavx.xyz")
ollama_model: str = Field("qwen2.5:3b")
ollama_embed_model: str = Field("qwen2.5:3b")
# Claude fallback (optional)
anthropic_api_key: Optional[str] = Field(None, env="ANTHROPIC_API_KEY")
claude_model: str = Field("claude-haiku-4-5", env="CLAUDE_MODEL")
anthropic_api_key: Optional[str] = Field(None)
claude_model: str = Field("claude-haiku-4-5")
# Database
db_path: str = Field("/data/researchowl.db", env="DB_PATH")
db_path: str = Field("/data/researchowl.db")
# Scraping
max_depth: int = Field(3, env="MAX_DEPTH") # recursion depth
max_sources: int = Field(150, env="MAX_SOURCES") # hard cap
max_pages_per_search: int = Field(5, env="MAX_PAGES_PER_SEARCH")
max_depth: int = Field(3) # recursion depth
max_sources: int = Field(150) # hard cap
max_pages_per_search: int = Field(5)
searxng_url: str = Field(
"http://searxng-svc.researchowl.svc.cluster.local:8080/search",
env="SEARXNG_URL"
)
request_timeout: int = Field(30, env="REQUEST_TIMEOUT")
request_delay: float = Field(1.0, env="REQUEST_DELAY") # seconds between requests
min_content_length: int = Field(200, env="MIN_CONTENT_LENGTH") # chars
request_timeout: int = Field(30)
request_delay: float = Field(1.0) # seconds between requests
min_content_length: int = Field(200) # chars
# Fuentes opcionales — desactivadas por defecto: la IP del homelab está
# bloqueada por Reddit (403) y YouTube (transcripts vacíos), eran peso muerto.
enable_youtube: bool = Field(False, env="ENABLE_YOUTUBE")
enable_reddit: bool = Field(False, env="ENABLE_REDDIT")
enable_youtube: bool = Field(False)
enable_reddit: bool = Field(False)
# Processing
chunk_size: int = Field(800, env="CHUNK_SIZE") # tokens per chunk
chunk_overlap: int = Field(100, env="CHUNK_OVERLAP")
quality_threshold: float = Field(0.3, env="QUALITY_THRESHOLD") # 0-1, chunks below discarded
chunk_size: int = Field(800) # tokens per chunk
chunk_overlap: int = Field(100)
quality_threshold: float = Field(0.3) # 0-1, chunks below discarded
# Ghost CMS
ghost_url: Optional[str] = Field(None, env="GHOST_URL")
ghost_api_key: Optional[str] = Field(None, env="GHOST_API_KEY")
ghost_url_en: str = Field("", env="GHOST_URL_EN")
ghost_api_key_en: str = Field("", env="GHOST_API_KEY_EN")
ghost_url: Optional[str] = Field(None)
ghost_api_key: Optional[str] = Field(None)
ghost_url_en: str = Field("")
ghost_api_key_en: str = Field("")
# SEO autofill — "off" | "on" | "dryrun" (default off).
# 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
# writes a BARE draft (no seo fields), for inspection before trusting
# 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_ENABLED: bool = Field(False, env="NEWS_ENABLED")
NEWS_POLL_INTERVAL_HOURS: int = Field(6, env="NEWS_POLL_INTERVAL_HOURS")
NEWS_ENABLED: bool = Field(False)
NEWS_POLL_INTERVAL_HOURS: int = Field(6)
# Chat al que el monitor empuja novedades; si None -> 1er TELEGRAM_ALLOWED_USERS
# (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(
"ovni,ovnis,uap,ufo,desclasificado,desclasificacion,declassified,"
"avistamiento,sighting,extraterrestre,non-human,nhi,grusch,"
"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
cost_alert_threshold: float = Field(0.15, env="COST_ALERT_THRESHOLD")
cost_alert_threshold: float = Field(0.15)
# App
log_level: str = Field("INFO", env="LOG_LEVEL")
timezone: str = Field("Europe/Madrid", env="TIMEZONE")
log_level: str = Field("INFO")
timezone: str = Field("Europe/Madrid")
@property
def allowed_user_ids(self) -> list[int]:
@@ -102,8 +100,7 @@ class Settings(BaseSettings):
def seo_autofill_dryrun(self) -> bool:
return (self.seo_autofill or "").strip().lower() == "dryrun"
class Config:
env_file = ".env"
model_config = SettingsConfigDict(env_file=".env")
settings = Settings()