"""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) # --- Artwork storage (section 10) --- # Local-disk self-hosting, deliberately simple for 0.1: fine for a # single-host deployment where the api and worker containers share one # volume (see docker-compose.yml). A genuinely multi-host deployment # would need real object storage (S3/R2/...) behind the same # ArtworkStorage interface instead — see app/services/artwork/storage.py. MEDIA_ROOT: str = Field(default="./media") # Root-relative URL path artwork is served under (app/main.py mounts # MEDIA_ROOT there). Kept relative rather than a full domain so it keeps # working whether Aurora is reached via localhost, a Traefik host, or # (once configured) amdbs.net. MEDIA_URL_PREFIX: str = Field(default="/media") ARTWORK_DOWNLOAD_MAX_BYTES: int = Field(default=15 * 1024 * 1024) @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()