"""Load and merge TOML configuration files into AppConfig.""" from __future__ import annotations import os import tomllib from pathlib import Path from typing import Any from app.core.app_config_models import AppConfig _CONFIG_DIR_ENV = "CONFIG_DIR" def default_config_dir() -> Path: override = (os.environ.get(_CONFIG_DIR_ENV) or "").strip() if override: return Path(override).expanduser().resolve() # api/app/core/app_config_loader.py -> api/config return Path(__file__).resolve().parents[2] / "config" def _deep_merge(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]: merged = dict(base) for key, value in overlay.items(): if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): merged[key] = _deep_merge(merged[key], value) else: merged[key] = value return merged def _read_toml(path: Path) -> dict[str, Any]: with path.open("rb") as handle: return tomllib.load(handle) def load_app_config(app_environment: str, *, config_dir: Path | None = None) -> AppConfig: root = config_dir or default_config_dir() default_path = root / "default.toml" if not default_path.is_file(): raise FileNotFoundError(f"Missing default config: {default_path}") merged = _read_toml(default_path) env = (app_environment or "development").strip().lower() overlay_path = root / f"{env}.toml" if overlay_path.is_file(): merged = _deep_merge(merged, _read_toml(overlay_path)) return AppConfig.model_validate(merged)