"""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" vision_api_key: SecretStr | None = None immich_api_key: SecretStr | None = None @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}" @classmethod def from_env(cls, environ: Mapping[str, str] | None = None) -> "Config": env = os.environ if environ is None else environ data = { name: env[ENV_PREFIX + name.upper()] for name in cls.model_fields if env.get(ENV_PREFIX + name.upper()) } return cls(**data)