"""Config loader + light schema validation (metadata-independent). Loads the project YAML configs and checks that required top-level keys are present, so a typo fails loudly rather than silently. Does NOT encode any ECMWF GRIB assumptions. """ import os _ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) CONFIG_DIR = os.path.join(_ROOT, "config") # minimal required top-level keys per config file (extend as configs grow) REQUIRED = { "targets": ["period", "horizon", "p90", "ehd_threshold", "weeks"], "domains": ["target_grid", "region_mask", "predictor_domains"], "data": ["observations", "ecmwf"], "verification": ["headline_weeks", "metrics", "leakage_rule", "regions"], "variables": ["surface", "pressure"], "model": ["architecture", "outputs"], "training": ["cross_validation", "compute_environment"], } def load_config(name): """Load config/.yaml as a dict and validate required keys.""" try: import yaml except Exception as e: # pragma: no cover raise RuntimeError("pyyaml not available; install it or parse manually") from e path = os.path.join(CONFIG_DIR, f"{name}.yaml") if not os.path.exists(path): raise FileNotFoundError(path) with open(path) as f: cfg = yaml.safe_load(f) missing = [k for k in REQUIRED.get(name, []) if k not in (cfg or {})] if missing: raise KeyError(f"{name}.yaml missing required keys: {missing}") return cfg def load_all(): return {name: load_config(name) for name in REQUIRED}