from __future__ import annotations import os from pathlib import Path from typing import Any import yaml from dotenv import load_dotenv REQUIRED_KEYS = ["secret_key", "database.url"] _PREFIX = "FABLEDNETMON_" def load_config(config_path: Path | str | None = None) -> dict[str, Any]: """Load config from YAML file with env var overrides. Env var naming: FABLEDNETMON_ + uppercased dotted path, dots -> __ Example: database.url -> FABLEDNETMON_DATABASE__URL List values are never overridden by env vars. """ load_dotenv() if config_path is None: config_path = Path("config.yaml") config_path = Path(config_path) if config_path.exists(): with config_path.open() as f: cfg: dict[str, Any] = yaml.safe_load(f) or {} else: cfg = {} # Apply env var overrides (scalars only) for key, value in os.environ.items(): if not key.startswith(_PREFIX): continue path = key[len(_PREFIX):].lower().split("__") _set_nested(cfg, path, value) # Validate required keys for dotted in REQUIRED_KEYS: parts = dotted.split(".") node = cfg for part in parts: if not isinstance(node, dict) or part not in node: raise ValueError( f"Required config key '{dotted}' is missing. " f"Set it in config.yaml or via env var " f"FABLEDNETMON_{dotted.upper().replace('.', '__')}" ) node = node[part] return cfg def _set_nested(cfg: dict, path: list[str], value: str) -> None: """Set a scalar value at a nested dict path. Skips if target is a list.""" node = cfg for part in path[:-1]: node = node.setdefault(part, {}) key = path[-1] existing = node.get(key) if isinstance(existing, list): return # never override list values node[key] = value