Spaces:
Sleeping
Sleeping
File size: 2,597 Bytes
d1ecac0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | """YAML-to-Pydantic config loader.
Why this exists separately from ``schema.py``:
* Schema is *what* a valid config looks like; loader is *how* you build one.
Splitting them lets tests build an ``AppConfig`` programmatically without
touching disk, and lets the loader gain features (env-file resolution,
multi-file merging) without changing the schema.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
from pydantic_settings import EnvSettingsSource
from captioning.config.schema import AppConfig
def load_config(path: str | Path) -> AppConfig:
"""Load a YAML file into an ``AppConfig``, apply env overrides, and validate it.
Precedence, highest first: ``CAPTIONING__*`` environment variables, the
YAML file, the schema defaults. An override replaces only the field it
names (``CAPTIONING__SERVE__BEAM_WIDTH=4`` keeps the rest of ``serve``);
a list value, such as ``CAPTIONING__SERVE__CORS_ALLOWED_ORIGINS``,
replaces the YAML list whole.
Args:
path: Path to a YAML file with the structure::
data: {...}
model: {...}
train: {...}
serve: {...}
Returns:
A fully validated, immutable ``AppConfig`` instance.
Raises:
FileNotFoundError: If the YAML path does not exist.
pydantic.ValidationError: If any field fails validation.
pydantic_settings.sources.SettingsError: If an env value for a list or
section isn't valid JSON.
"""
path = Path(path)
if not path.is_file():
raise FileNotFoundError(f"Config file not found: {path}")
with path.open(encoding="utf-8") as f:
raw: dict[str, Any] = yaml.safe_load(f) or {}
# pydantic-settings ranks constructor arguments above the environment, so
# ``AppConfig(**raw)`` alone would let the YAML win. Merge the env values
# over it first, parsed by the same source ``AppConfig`` uses.
env = EnvSettingsSource(AppConfig)()
return AppConfig(**_merge(raw, env))
def _merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
"""Return ``base`` updated by ``override``, recursing into sections both set.
Any other value, lists included, is replaced whole, as pydantic-settings
merges its own sources.
"""
merged = dict(base)
for key, value in override.items():
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key] = _merge(merged[key], value)
else:
merged[key] = value
return merged
|