"""Application configuration, sourced from environment variables (see .env.example).""" from functools import lru_cache from typing import Literal from urllib.parse import quote_plus from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore") app_name: str = "PESCO NCR" app_base_url: str = "http://localhost:8080" log_level: str = "INFO" # ── Auth ──────────────────────────────────────────────────────────────── # "entra" validates Entra ID JWTs; "dev" trusts an X-Dev-User header and # must never be used outside local development. auth_mode: Literal["entra", "dev"] = "entra" entra_tenant_id: str = "" entra_client_id: str = "" entra_client_secret: str = "" entra_allowed_group_id: str = "" entra_api_audience: str = "" initial_admin_emails: str = "" # ── Database ──────────────────────────────────────────────────────────── # Full SQLAlchemy URL override (used by tests); otherwise assembled from # the MYSQL_* parts below. database_url: str = "" mysql_host: str = "mysql" mysql_port: int = 3306 mysql_database: str = "pesco_ncr" mysql_user: str = "ncr_app" mysql_password: str = "" # ── Attachments ───────────────────────────────────────────────────────── attachments_dir: str = "/data/attachments" max_upload_mb: int = 25 # ── Notifications ─────────────────────────────────────────────────────── notifications_enabled_default: bool = True # ── Job lookup (future VISUAL integration) ────────────────────────────── job_lookup_provider: Literal["null", "visual"] = "null" visual_db_host: str = "" visual_db_port: int = 1433 visual_db_name: str = "" visual_db_user: str = "" visual_db_password: str = "" visual_site_id: str = "" seed_demo_data: bool = False @property def effective_database_url(self) -> str: if self.database_url: return self.database_url return ( f"mysql+aiomysql://{quote_plus(self.mysql_user)}:{quote_plus(self.mysql_password)}" f"@{self.mysql_host}:{self.mysql_port}/{self.mysql_database}?charset=utf8mb4" ) @property def sync_database_url(self) -> str: """Synchronous-driver URL for Alembic.""" return self.effective_database_url.replace("+aiomysql", "+pymysql").replace( "+aiosqlite", "" ) @property def api_audiences(self) -> list[str]: if self.entra_api_audience: return [self.entra_api_audience] return [f"api://{self.entra_client_id}", self.entra_client_id] @property def initial_admin_email_set(self) -> set[str]: return {e.strip().lower() for e in self.initial_admin_emails.split(",") if e.strip()} @property def max_upload_bytes(self) -> int: return self.max_upload_mb * 1024 * 1024 @lru_cache def get_settings() -> Settings: return Settings()