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:
co-authored by
Claude Fable 5
parent
19efc70539
commit
a91e119171
+33
-36
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user