f8b82b65a3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
64 lines
1.9 KiB
Python
64 lines
1.9 KiB
Python
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
|