From a91e119171c396ae4f4bb5f9a16ac83df51b4e94 Mon Sep 17 00:00:00 2001 From: ChemaVX Date: Fri, 3 Jul 2026 15:12:36 +0000 Subject: [PATCH] refactor(config): migrar a SettingsConfigDict y quitar Field(env=...) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/config.py | 69 ++++++++++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 36 deletions(-) diff --git a/src/config.py b/src/config.py index 4ff517a..dabee50 100644 --- a/src/config.py +++ b/src/config.py @@ -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()