| """Small configuration loader with JSON fallback and dotted overrides.""" |
|
|
| from __future__ import annotations |
|
|
| import json |
| from collections.abc import Iterable |
| from pathlib import Path |
| from typing import Any |
|
|
|
|
| def load_config(path: str | Path) -> dict[str, Any]: |
| path = Path(path) |
| text = path.read_text(encoding="utf-8") |
| if path.suffix.lower() == ".json": |
| loaded = json.loads(text) |
| else: |
| try: |
| import yaml |
| except ImportError as exc: |
| raise ImportError( |
| "YAML config requires PyYAML; alternatively pass a .json config" |
| ) from exc |
| loaded = yaml.safe_load(text) |
| if not isinstance(loaded, dict): |
| raise ValueError("top-level config must be a mapping") |
| return loaded |
|
|
|
|
| def apply_overrides(config: dict[str, Any], overrides: Iterable[str]) -> dict[str, Any]: |
| """Apply ``section.key=<JSON value>`` updates in place.""" |
|
|
| for override in overrides: |
| if "=" not in override: |
| raise ValueError(f"override must be KEY=VALUE, got {override!r}") |
| dotted_key, raw_value = override.split("=", 1) |
| try: |
| value = json.loads(raw_value) |
| except json.JSONDecodeError: |
| value = raw_value |
| keys = dotted_key.split(".") |
| cursor: dict[str, Any] = config |
| for key in keys[:-1]: |
| child = cursor.setdefault(key, {}) |
| if not isinstance(child, dict): |
| raise ValueError(f"cannot descend into non-mapping config key {key!r}") |
| cursor = child |
| cursor[keys[-1]] = value |
| return config |
|
|