Files
life-echo/api/app/core/app_config_loader.py

51 lines
1.5 KiB
Python
Raw Permalink Normal View History

"""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)