"""Typed application configuration. Values come from ``PHOTO_PIPELINE_*`` environment variables. Secrets use ``SecretStr`` so they are masked in logs, reprs, and model dumps, and are never returned by the API. Only ``.get_secret_value()`` exposes them, and only where a real external call needs them. pydantic-settings would do this too, but a prefix-scan over the declared fields is a few lines and one fewer dependency. """ from __future__ import annotations import os from pathlib import Path from typing import Mapping from pydantic import BaseModel, ConfigDict, SecretStr ENV_PREFIX = "PHOTO_PIPELINE_" class Config(BaseModel): model_config = ConfigDict(frozen=True) data_dir: Path = Path("data") db_path: Path | None = None # defaults to data_dir/photo_pipeline.db host: str = "127.0.0.1" port: int = 8000 log_level: str = "INFO" log_format: str = "json" # "json" or "text" # Library boundary for path validation (os.pathsep-separated in the env var). library_roots: tuple[Path, ...] = () thumbnail_cache_quota_bytes: int = 500_000_000 thumbnail_max_pixels: int = 100_000_000 vision_api_key: SecretStr | None = None immich_api_key: SecretStr | None = None immich_server_url: str = "" immich_go_binary: str = "immich-go" @property def database_path(self) -> Path: return self.db_path or (self.data_dir / "photo_pipeline.db") @property def database_url(self) -> str: return f"sqlite:///{self.database_path}" @property def thumbnail_cache_dir(self) -> Path: return self.data_dir / "cache" / "thumbs" @classmethod def from_env(cls, environ: Mapping[str, str] | None = None) -> "Config": env = os.environ if environ is None else environ data: dict = {} for name in cls.model_fields: raw = env.get(ENV_PREFIX + name.upper()) if not raw: continue data[name] = raw.split(os.pathsep) if name == "library_roots" else raw return cls(**data)