"""Application configuration via pydantic-settings + YAML merge.""" from __future__ import annotations import functools from pathlib import Path from typing import Any import yaml from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict def _load_yaml(path: str | Path) -> dict[str, Any]: p = Path(path) if p.exists(): with open(p) as f: return yaml.safe_load(f) or {} return {} class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) app_env: str = "dev" # Stock Oracle stock_oracle_url: str = "http://localhost:18001" stock_oracle_timeout: int = 30 # Database postgres_dsn: str = "postgresql+asyncpg://acef:acef@localhost:5432/acef" # File storage data_root: str = "./data" # Symbol universe symbols_file: str = "configs/symbols.yaml" # Logging log_level: str = "INFO" # LLM (OpenAI — legacy placeholder) openai_api_key: str = "sk-placeholder" llm_enabled: bool = False # Ollama ollama_url: str = "http://localhost:11434" ollama_model: str = "llama3.2" ollama_timeout: int = 600 # App YAML overrides (loaded separately) _app_config: dict[str, Any] = {} @field_validator("log_level") @classmethod def normalize_log_level(cls, v: str) -> str: return v.upper() @property def exhibit_cache_dir(self) -> Path: return Path(self.data_root) / "cache" / "exhibits" @property def parquet_dir(self) -> Path: return Path(self.data_root) / "parquet" def get_app_config(self) -> dict[str, Any]: if not self._app_config: object.__setattr__(self, "_app_config", _load_yaml("configs/app.yaml")) return self._app_config def get_symbols(self) -> list[str]: cfg = _load_yaml(self.symbols_file) return cfg.get("symbols", []) def get_fred_series(self) -> list[dict[str, Any]]: cfg = _load_yaml("configs/fred_series.yaml") return cfg.get("series", []) @functools.lru_cache(maxsize=1) def get_settings() -> Settings: return Settings()