""" Dataset / bundle layout helpers. The spec (Appendix C) defines a canonical on-disk layout. This module provides non-opinionated helpers for creating and validating directories without baking policy into unrelated code. """ from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Iterable, List, Optional @dataclass(frozen=True) class CaptureBundleLayout: root: Path @property def manifest_path(self) -> Path: return self.root / "manifest.json" @property def devices_dir(self) -> Path: return self.root / "devices" @property def calibration_dir(self) -> Path: return self.root / "calibration" @property def annotations_dir(self) -> Path: return self.root / "annotations" @property def teacher_outputs_dir(self) -> Path: return self.root / "teacher_outputs" def device_dir(self, device_subdir: str) -> Path: return self.devices_dir / device_subdir def discover_capture_bundles(root: Path) -> List[Path]: """ Discover capture bundles under a directory. We define a capture bundle as any directory containing a `manifest.json`. """ root = Path(root) if not root.exists(): return [] bundles: List[Path] = [] for child in root.iterdir(): if child.is_dir() and (child / "manifest.json").exists(): bundles.append(child) return sorted(bundles) def ensure_dir(path: Path) -> Path: path = Path(path) path.mkdir(parents=True, exist_ok=True) return path def resolve_relative(root: Path, rel: Optional[str]) -> Optional[Path]: if rel is None: return None return (Path(root) / rel).resolve() def validate_paths_exist(root: Path, rel_paths: Iterable[Optional[str]]) -> List[str]: """ Validate that each non-null relative path exists (relative to root). Returns a list of human-readable errors; empty means OK. """ errors: List[str] = [] for rel in rel_paths: if not rel: continue p = Path(root) / rel if not p.exists(): errors.append(f"Missing path: {rel} (resolved to {p})") return errors