51 lines
1.5 KiB
Python
51 lines
1.5 KiB
Python
|
|
"""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)
|