Files
FabledSteward/fablednetmon/config.py
T
2026-03-17 18:43:57 -04:00

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