from __future__ import annotations from copy import deepcopy from pathlib import Path from typing import Any import yaml def load_config(path: str | Path) -> dict[str, Any]: with Path(path).open("r", encoding="utf-8") as handle: config = yaml.safe_load(handle) if not isinstance(config, dict): raise ValueError(f"Configuration must be a mapping: {path}") return config def apply_overrides(config: dict[str, Any], overrides: list[str]) -> dict[str, Any]: result = deepcopy(config) for item in overrides: if "=" not in item: raise ValueError(f"Override must have key=value form: {item}") dotted_key, raw_value = item.split("=", 1) keys = dotted_key.split(".") node = result for key in keys[:-1]: if key not in node or not isinstance(node[key], dict): raise KeyError(f"Unknown configuration path: {dotted_key}") node = node[key] if keys[-1] not in node: raise KeyError(f"Unknown configuration key: {dotted_key}") node[keys[-1]] = yaml.safe_load(raw_value) return result def save_config(config: dict[str, Any], path: str | Path) -> None: with Path(path).open("w", encoding="utf-8") as handle: yaml.safe_dump(config, handle, sort_keys=False)