"""Runtime configuration loaded from environment + secret files. Secrets are read from files (``*_FILE`` env vars pointing at ``/run/secrets/``) so they never appear in ``docker inspect`` or process environment dumps. Plain ``*`` vars are the fallback for local development where mounting secret files is overkill. """ from __future__ import annotations from functools import lru_cache from pathlib import Path from typing import Optional from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", extra="ignore") database_url: str = Field( default="postgresql+psycopg://datatools_api@localhost:5432/datatools_licenses", validation_alias="DATABASE_URL", ) admin_token: Optional[str] = Field(default=None, validation_alias="DATATOOLS_ADMIN_TOKEN") admin_token_file: Optional[Path] = Field(default=None, validation_alias="DATATOOLS_ADMIN_TOKEN_FILE") license_privkey_hex: Optional[str] = Field(default=None, validation_alias="DATATOOLS_LICENSE_PRIVKEY") license_privkey_file: Optional[Path] = Field(default=None, validation_alias="DATATOOLS_LICENSE_PRIVKEY_FILE") license_pubkey_hex: Optional[str] = Field(default=None, validation_alias="DATATOOLS_LICENSE_PUBKEY") postmark_token: Optional[str] = Field(default=None, validation_alias="POSTMARK_TOKEN") postmark_token_file: Optional[Path] = Field(default=None, validation_alias="POSTMARK_TOKEN_FILE") gumroad_secret: Optional[str] = Field(default=None, validation_alias="GUMROAD_WEBHOOK_SECRET") gumroad_secret_file: Optional[Path] = Field(default=None, validation_alias="GUMROAD_WEBHOOK_SECRET_FILE") def resolve_admin_token(self) -> Optional[str]: return _resolve(self.admin_token, self.admin_token_file) def resolve_license_privkey(self) -> Optional[str]: return _resolve(self.license_privkey_hex, self.license_privkey_file) def resolve_postmark_token(self) -> Optional[str]: return _resolve(self.postmark_token, self.postmark_token_file) def resolve_gumroad_secret(self) -> Optional[str]: return _resolve(self.gumroad_secret, self.gumroad_secret_file) def _resolve(inline: Optional[str], path: Optional[Path]) -> Optional[str]: if inline: return inline.strip() if path and path.exists(): return path.read_text(encoding="utf-8").strip() return None @lru_cache(maxsize=1) def get_settings() -> Settings: return Settings()