from dataclasses import dataclass import os from dotenv import load_dotenv load_dotenv() DEFAULT_ALLOWED_MIME_TYPES = ( "image/jpeg", "image/png", "image/webp", "image/gif", ) VALID_LOG_LEVELS = {"critical", "error", "warning", "info", "debug"} def _env_bool(name: str, default: bool = False) -> bool: value = os.getenv(name) if value is None: return default return value.strip().lower() in {"1", "true", "yes", "on"} def _env_int(name: str, default: int, *, minimum: int | None = None, maximum: int | None = None) -> int: raw_value = os.getenv(name) value = default if raw_value is None else int(raw_value.strip()) if minimum is not None and value < minimum: raise ValueError(f"{name} must be >= {minimum}") if maximum is not None and value > maximum: raise ValueError(f"{name} must be <= {maximum}") return value def _env_csv(name: str, default: tuple[str, ...]) -> tuple[str, ...]: raw_value = os.getenv(name, "").strip() if not raw_value: return default values = tuple(part.strip().lower() for part in raw_value.split(",") if part.strip()) if not values: raise ValueError(f"{name} must contain at least one value when set") return values def _normalized_env() -> str: value = os.getenv("FACE_LOCK_ENV") or os.getenv("ENV") or "production" return value.strip().lower() def _validated_log_level() -> str: value = os.getenv("LOG_LEVEL", "info").strip().lower() if value not in VALID_LOG_LEVELS: raise ValueError(f"LOG_LEVEL must be one of: {', '.join(sorted(VALID_LOG_LEVELS))}") return value def _validated_header_name() -> str: value = os.getenv("FACE_LOCK_AUTH_HEADER", "X-API-Key").strip() if not value: raise ValueError("FACE_LOCK_AUTH_HEADER cannot be empty") if any(char in value for char in ("\r", "\n", ":")): raise ValueError("FACE_LOCK_AUTH_HEADER contains invalid characters") return value @dataclass(frozen=True) class Settings: env: str = _normalized_env() host: str = os.getenv("FACE_LOCK_HOST", "0.0.0.0").strip() or "0.0.0.0" port: int = _env_int("PORT", 8000, minimum=1, maximum=65535) log_level: str = _validated_log_level() test_ui_enabled: bool = _env_bool("FACE_LOCK_TEST_UI", False) docs_enabled: bool = _env_bool("FACE_LOCK_DOCS", True) auth_token: str = os.getenv("FACE_LOCK_AUTH_TOKEN", "").strip() auth_header_name: str = _validated_header_name() max_upload_bytes: int = _env_int("FACE_LOCK_MAX_UPLOAD_BYTES", 8 * 1024 * 1024, minimum=1024, maximum=25 * 1024 * 1024) allowed_mime_types: tuple[str, ...] = _env_csv("FACE_LOCK_ALLOWED_MIME_TYPES", DEFAULT_ALLOWED_MIME_TYPES) @property def auth_enabled(self) -> bool: return bool(self.auth_token) @property def is_production(self) -> bool: return self.env in {"prod", "production"} settings = Settings()