|
9 | 9 | import yaml |
10 | 10 |
|
11 | 11 | from .models import ( |
| 12 | + StandardNameCatalogManifest, |
12 | 13 | StandardNameEntry, |
13 | 14 | StandardNameScalarEntry, |
14 | 15 | create_standard_name_entry, |
@@ -213,9 +214,45 @@ def __init__(self, root: str | Path, permissive: bool = False): |
213 | 214 | def yaml_files(self): |
214 | 215 | return sorted(list(self.root.rglob("*.yml")) + list(self.root.rglob("*.yaml"))) |
215 | 216 |
|
| 217 | + def load_manifest(self) -> StandardNameCatalogManifest | None: |
| 218 | + """Load the manifest sidecar (``catalog.yml``) beside the entries. |
| 219 | +
|
| 220 | + Checked first at ``root.parent/catalog.yml`` (the standard layout — |
| 221 | + manifest at repo root, entries under ``standard_names/``), then at |
| 222 | + ``root/catalog.yml`` (entries under the repo root directly). Returns |
| 223 | + ``None`` only when no manifest file is present at either location; a |
| 224 | + present-but-unparseable manifest is refused rather than treated as |
| 225 | + absent, since silently ignoring it would leave entries without an |
| 226 | + inline ``kind`` failing discrimination later with an opaque union |
| 227 | + error that hides the real cause. |
| 228 | + """ |
| 229 | + candidates = [self.root.parent / "catalog.yml", self.root / "catalog.yml"] |
| 230 | + for candidate in candidates: |
| 231 | + if not candidate.exists(): |
| 232 | + continue |
| 233 | + try: |
| 234 | + data = yaml.safe_load(candidate.read_text(encoding="utf-8")) |
| 235 | + except yaml.YAMLError as e: |
| 236 | + raise ValueError( |
| 237 | + f"Catalog manifest sidecar at {candidate} is not valid YAML: {e}" |
| 238 | + ) from e |
| 239 | + if not isinstance(data, dict): |
| 240 | + raise ValueError( |
| 241 | + f"Catalog manifest sidecar at {candidate} must be a mapping, " |
| 242 | + f"got {type(data).__name__}" |
| 243 | + ) |
| 244 | + try: |
| 245 | + return StandardNameCatalogManifest(**data) |
| 246 | + except Exception as e: |
| 247 | + raise ValueError( |
| 248 | + f"Catalog manifest sidecar at {candidate} failed validation: {e}" |
| 249 | + ) from e |
| 250 | + return None |
| 251 | + |
216 | 252 | # Load -------------------------------------------------------------------- |
217 | 253 | def load(self) -> list[StandardNameEntry]: |
218 | 254 | models: list[StandardNameEntry] = [] |
| 255 | + manifest = self.load_manifest() |
219 | 256 | for f in self.yaml_files(): |
220 | 257 | # Detect nested paths (legacy per-file layout) |
221 | 258 | relative = f.relative_to(self.root) |
@@ -269,7 +306,7 @@ def load(self) -> list[StandardNameEntry]: |
269 | 306 |
|
270 | 307 | # Handle Pydantic validation errors in permissive mode |
271 | 308 | try: |
272 | | - m = create_standard_name_entry(entry_data) |
| 309 | + m = create_standard_name_entry(entry_data, manifest=manifest) |
273 | 310 | models.append(m) |
274 | 311 | except Exception as e: |
275 | 312 | if self.permissive: |
|
0 commit comments