"""Environment-based application configuration. All runtime configuration flows through this module. Nothing else in the codebase should read `os.environ` directly, so that configuration stays auditable and testable in one place. """ from __future__ import annotations from functools import lru_cache from pydantic import Field, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) # --- Runtime --- AURORA_ENV: str = Field(default="development") AURORA_LOG_LEVEL: str = Field(default="INFO") AURORA_API_DOCS_ENABLED: bool = Field(default=True) # --- Database --- DATABASE_URL: str = Field( default="postgresql+asyncpg://aurora:aurora@localhost:5432/aurora_core" ) DATABASE_URL_SYNC: str = Field( default="postgresql+psycopg2://aurora:aurora@localhost:5432/aurora_core" ) DATABASE_POOL_SIZE: int = Field(default=10) DATABASE_MAX_OVERFLOW: int = Field(default=20) # Set true when DATABASE_URL points at PgBouncer in transaction pooling # mode (docker-compose.prod.yml): asyncpg's server-side prepared # statements don't survive connections being handed to different clients # between transactions, so they must be disabled. DATABASE_BEHIND_TRANSACTION_POOLER: bool = Field(default=False) # --- Redis --- REDIS_URL: str = Field(default="redis://localhost:6379/0") REDIS_REQUIRED: bool = Field(default=False) # --- Security --- AURORA_SECRET_KEY: str = Field(default="changeme-generate-a-real-secret") AURORA_ADMIN_API_KEYS: str = Field(default="") # --- CORS --- CORS_ORIGINS: str = Field(default="http://localhost:3000") # --- Providers --- TMDB_API_KEY: str = Field(default="") # --- Workers --- WORKER_CONCURRENCY: int = Field(default=4) # --- Caching --- CACHE_DEFAULT_TTL: int = Field(default=300) # --- Rate limiting --- RATE_LIMIT_ENABLED: bool = Field(default=True) RATE_LIMIT_PER_MINUTE: int = Field(default=120) @field_validator("AURORA_ENV") @classmethod def _validate_env(cls, v: str) -> str: allowed = {"development", "test", "staging", "production"} if v not in allowed: raise ValueError(f"AURORA_ENV must be one of {allowed}, got {v!r}") return v @property def cors_origins_list(self) -> list[str]: return [origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()] @property def admin_api_keys_set(self) -> set[str]: return {key.strip() for key in self.AURORA_ADMIN_API_KEYS.split(",") if key.strip()} @property def is_production(self) -> bool: return self.AURORA_ENV == "production" @property def docs_url(self) -> str | None: if not self.AURORA_API_DOCS_ENABLED: return None return "/docs" @property def openapi_url(self) -> str | None: if not self.AURORA_API_DOCS_ENABLED: return None return "/openapi.json" @lru_cache def get_settings() -> Settings: return Settings()