from typing import List, Tuple, Type from functools import lru_cache from pydantic import Field from pydantic_settings import ( BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, YamlConfigSettingsSource, ) class Settings(BaseSettings): """Centralized settings. Priority (highest → lowest): 1. Environment variables 2. .env file ← secrets only (MISTRAL_API_KEY) 3. development.yml ← non-secret config (model, server, CORS) """ model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=True, extra="ignore", ) @classmethod def settings_customise_sources( cls, settings_cls: Type[PydanticBaseSettingsSource], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, ) -> Tuple[PydanticBaseSettingsSource, ...]: return ( init_settings, env_settings, dotenv_settings, YamlConfigSettingsSource(settings_cls, yaml_file="development.yml"), file_secret_settings, ) # ── Mistral (secret in .env, rest in development.yml) ───────────────────── MISTRAL_API_KEY: str MISTRAL_OCR_MODEL: str = "mistral-ocr-latest" MISTRAL_TABLE_FORMAT: str = "markdown" # ── Server (development.yml) ─────────────────────────────────────────────── APP_NAME: str = "Markdown & Layout Extractor" HOST: str = "127.0.0.1" PORT: int = 7860 # HF Spaces requires 7860; development.yml / env var can override LOG_LEVEL: str = "INFO" # ── CORS (development.yml) ───────────────────────────────────────────────── CORS_ALLOW_ORIGINS: List[str] = Field(default_factory=lambda: ["*"]) CORS_ALLOW_METHODS: List[str] = Field(default_factory=lambda: ["*"]) CORS_ALLOW_HEADERS: List[str] = Field(default_factory=lambda: ["*"]) @lru_cache def get_settings() -> Settings: """Cached settings instance — call this everywhere instead of instantiating.""" return Settings() settings = get_settings()