rpx_benchmark.dataset_hub.staging
File-staging helpers — non-tar artefacts that ship to the HF repo root.
The packer/manifest/uploader chain handles per-(scene, phase, modality) tar shards and the per-frame Parquet manifest. This module covers the small companion files HF Datasets needs alongside them:
splits/{scene_splits.json, easy.txt, medium.txt, hard.txt}— copied frombenchmark/data/splits/so downloaders can resolve a split to a scene list before fetching the manifest.rpx_croissant.json— copied frompaper-submission/croissant/with path reconciliation against the new tar layout (TODO 3 — handled in a sibling helper).README.md(HF dataset card) — generated from the scan totals (TODO 2 — handled inrpx_benchmark.dataset_hub.dataset_card).preview/(HF Dataset Viewer thumbnails) — handled in.preview(TODO 4).
Each helper returns a list of StagedFile records so the upload
step can verify what landed.
1"""File-staging helpers — non-tar artefacts that ship to the HF repo root. 2 3The packer/manifest/uploader chain handles per-(scene, phase, modality) 4tar shards and the per-frame Parquet manifest. This module covers the 5small companion files HF Datasets needs alongside them: 6 7* ``splits/{scene_splits.json, easy.txt, medium.txt, hard.txt}`` — 8 copied from ``benchmark/data/splits/`` so downloaders can resolve a 9 split to a scene list before fetching the manifest. 10* ``rpx_croissant.json`` — copied from 11 ``paper-submission/croissant/`` with path reconciliation 12 against the new tar layout (TODO 3 — handled in a sibling helper). 13* ``README.md`` (HF dataset card) — generated from the scan totals 14 (TODO 2 — handled in :mod:`.dataset_card`). 15* ``preview/`` (HF Dataset Viewer thumbnails) — handled in 16 :mod:`.preview` (TODO 4). 17 18Each helper returns a list of :class:`StagedFile` records so the upload 19step can verify what landed. 20""" 21 22from __future__ import annotations 23 24import json 25import shutil 26from dataclasses import dataclass 27from pathlib import Path 28from typing import List, Optional, Sequence 29 30from ..exceptions import ConfigError, DatasetError 31from ..logging_utils import get_logger 32 33log = get_logger(__name__) 34 35 36# Default location of the splits files relative to the repo root. Resolved 37# lazily so the helper still works when called from a non-repo cwd. 38def _default_splits_src() -> Path: 39 """``<repo_root>/benchmark/data/splits/``.""" 40 here = Path(__file__).resolve() 41 # __file__ -> .../benchmark/rpx_benchmark/dataset_hub/staging.py 42 benchmark_root = here.parents[2] 43 return benchmark_root / "data" / "splits" 44 45 46SPLIT_TIER_FILES = ("easy.txt", "medium.txt", "hard.txt") 47SCENE_SPLITS_JSON = "scene_splits.json" 48 49 50@dataclass(frozen=True) 51class StagedFile: 52 """One file the staging step copied / wrote.""" 53 54 repo_path: str # path relative to staging root, forward-slashes 55 src: Optional[Path] 56 bytes_: int 57 58 59def stage_splits( 60 staging_root: Path, 61 splits_src: Optional[Path] = None, 62 overwrite: bool = False, 63 require_all: bool = True, 64) -> List[StagedFile]: 65 """Copy ``splits/{scene_splits.json, easy.txt, medium.txt, hard.txt}`` 66 into ``<staging_root>/splits/``. 67 68 Parameters 69 ---------- 70 staging_root : Path 71 Where the packed/upload-ready tree lives. 72 splits_src : Path, optional 73 Source dir holding the splits files. Defaults to 74 ``benchmark/data/splits/`` next to this package. 75 overwrite : bool 76 If False (default), raises when a destination file already exists. 77 require_all : bool 78 If True (default), raises when any of the four expected files is 79 missing from ``splits_src``. Set False for partial-stage scenarios 80 (e.g., shipping with only the JSON manifest). 81 82 Returns 83 ------- 84 List[StagedFile] 85 One entry per copied file. 86 """ 87 src_root = Path(splits_src) if splits_src else _default_splits_src() 88 if not src_root.is_dir(): 89 raise ConfigError( 90 f"splits source dir does not exist: {src_root}", 91 hint="Splits files are bundled on the HuggingFace dataset " 92 "(splits/{scene_splits.json,easy.txt,medium.txt,hard.txt}); " 93 "make sure you've downloaded them, or pass splits_src= " 94 "pointing at a local copy.", 95 ) 96 97 staging_root = Path(staging_root) 98 out_dir = staging_root / "splits" 99 out_dir.mkdir(parents=True, exist_ok=True) 100 101 expected = (SCENE_SPLITS_JSON, *SPLIT_TIER_FILES) 102 missing = [name for name in expected if not (src_root / name).is_file()] 103 if missing and require_all: 104 raise DatasetError( 105 f"missing splits files in {src_root}: {missing}", 106 hint=( 107 "Re-download the HuggingFace dataset to recover the " 108 "splits/ tree, or pass require_all=False to ship a " 109 "partial splits dir." 110 ), 111 ) 112 113 staged: List[StagedFile] = [] 114 for name in expected: 115 src = src_root / name 116 if not src.is_file(): 117 log.warning("splits source missing: %s (skipping)", src) 118 continue 119 dst = out_dir / name 120 if dst.exists() and not overwrite: 121 raise DatasetError( 122 f"refusing to overwrite existing staged file: {dst}", 123 hint="Pass overwrite=True or remove the file first.", 124 ) 125 shutil.copy2(src, dst) 126 staged.append( 127 StagedFile( 128 repo_path=f"splits/{name}", 129 src=src, 130 bytes_=dst.stat().st_size, 131 ) 132 ) 133 log.info("staged splits/%s (%d bytes)", name, dst.stat().st_size) 134 return staged 135 136 137def stage_paths( 138 paths: Sequence[Path], 139 staging_root: Path, 140 repo_subdir: str = "", 141 overwrite: bool = False, 142) -> List[StagedFile]: 143 """Generic helper: copy a list of files into ``<staging>/<repo_subdir>/``. 144 145 Useful for ad-hoc staging of small files (Croissant JSON, custom 146 docs) without writing a dedicated module. ``repo_subdir`` may be 147 empty to land files at the staging root. 148 """ 149 staging_root = Path(staging_root) 150 target = staging_root / repo_subdir if repo_subdir else staging_root 151 target.mkdir(parents=True, exist_ok=True) 152 153 staged: List[StagedFile] = [] 154 for src in paths: 155 src = Path(src) 156 if not src.is_file(): 157 raise ConfigError( 158 f"source file does not exist: {src}", 159 hint="Check the path; staging does not create files.", 160 ) 161 dst = target / src.name 162 if dst.exists() and not overwrite: 163 raise DatasetError( 164 f"refusing to overwrite existing staged file: {dst}", 165 hint="Pass overwrite=True or remove the file first.", 166 ) 167 shutil.copy2(src, dst) 168 rel = (Path(repo_subdir) / src.name).as_posix() if repo_subdir else src.name 169 staged.append( 170 StagedFile( 171 repo_path=rel, 172 src=src, 173 bytes_=dst.stat().st_size, 174 ) 175 ) 176 log.info("staged %s (%d bytes)", rel, dst.stat().st_size) 177 return staged 178 179 180def load_scene_splits(staging_root: Path) -> dict[str, str]: 181 """Read ``<staging_root>/splits/scene_splits.json`` and return a 182 flat ``{scene_id: tier}`` mapping. Convenience for downstream steps 183 (manifest builder, dataset card) that need to look up a scene's tier. 184 185 Accepts both the canonical schema with a top-level ``"splits"`` key 186 holding ``{tier: [scene_id, ...]}`` and a flat ``{scene_id: tier}`` 187 shape. 188 """ 189 path = Path(staging_root) / "splits" / SCENE_SPLITS_JSON 190 if not path.is_file(): 191 raise ConfigError( 192 f"scene_splits.json not found at {path}", 193 hint="Run stage_splits() before load_scene_splits().", 194 ) 195 payload = json.loads(path.read_text(encoding="utf-8")) 196 if "splits" in payload and isinstance(payload["splits"], dict): 197 return {sid: tier for tier, ids in payload["splits"].items() for sid in ids} 198 return dict(payload)
51@dataclass(frozen=True) 52class StagedFile: 53 """One file the staging step copied / wrote.""" 54 55 repo_path: str # path relative to staging root, forward-slashes 56 src: Optional[Path] 57 bytes_: int
One file the staging step copied / wrote.
60def stage_splits( 61 staging_root: Path, 62 splits_src: Optional[Path] = None, 63 overwrite: bool = False, 64 require_all: bool = True, 65) -> List[StagedFile]: 66 """Copy ``splits/{scene_splits.json, easy.txt, medium.txt, hard.txt}`` 67 into ``<staging_root>/splits/``. 68 69 Parameters 70 ---------- 71 staging_root : Path 72 Where the packed/upload-ready tree lives. 73 splits_src : Path, optional 74 Source dir holding the splits files. Defaults to 75 ``benchmark/data/splits/`` next to this package. 76 overwrite : bool 77 If False (default), raises when a destination file already exists. 78 require_all : bool 79 If True (default), raises when any of the four expected files is 80 missing from ``splits_src``. Set False for partial-stage scenarios 81 (e.g., shipping with only the JSON manifest). 82 83 Returns 84 ------- 85 List[StagedFile] 86 One entry per copied file. 87 """ 88 src_root = Path(splits_src) if splits_src else _default_splits_src() 89 if not src_root.is_dir(): 90 raise ConfigError( 91 f"splits source dir does not exist: {src_root}", 92 hint="Splits files are bundled on the HuggingFace dataset " 93 "(splits/{scene_splits.json,easy.txt,medium.txt,hard.txt}); " 94 "make sure you've downloaded them, or pass splits_src= " 95 "pointing at a local copy.", 96 ) 97 98 staging_root = Path(staging_root) 99 out_dir = staging_root / "splits" 100 out_dir.mkdir(parents=True, exist_ok=True) 101 102 expected = (SCENE_SPLITS_JSON, *SPLIT_TIER_FILES) 103 missing = [name for name in expected if not (src_root / name).is_file()] 104 if missing and require_all: 105 raise DatasetError( 106 f"missing splits files in {src_root}: {missing}", 107 hint=( 108 "Re-download the HuggingFace dataset to recover the " 109 "splits/ tree, or pass require_all=False to ship a " 110 "partial splits dir." 111 ), 112 ) 113 114 staged: List[StagedFile] = [] 115 for name in expected: 116 src = src_root / name 117 if not src.is_file(): 118 log.warning("splits source missing: %s (skipping)", src) 119 continue 120 dst = out_dir / name 121 if dst.exists() and not overwrite: 122 raise DatasetError( 123 f"refusing to overwrite existing staged file: {dst}", 124 hint="Pass overwrite=True or remove the file first.", 125 ) 126 shutil.copy2(src, dst) 127 staged.append( 128 StagedFile( 129 repo_path=f"splits/{name}", 130 src=src, 131 bytes_=dst.stat().st_size, 132 ) 133 ) 134 log.info("staged splits/%s (%d bytes)", name, dst.stat().st_size) 135 return staged
Copy splits/{scene_splits.json, easy.txt, medium.txt, hard.txt}
into <staging_root>/splits/.
Parameters
staging_root : Path
Where the packed/upload-ready tree lives.
splits_src : Path, optional
Source dir holding the splits files. Defaults to
benchmark/data/splits/ next to this package.
overwrite : bool
If False (default), raises when a destination file already exists.
require_all : bool
If True (default), raises when any of the four expected files is
missing from splits_src. Set False for partial-stage scenarios
(e.g., shipping with only the JSON manifest).
Returns
List[StagedFile] One entry per copied file.
138def stage_paths( 139 paths: Sequence[Path], 140 staging_root: Path, 141 repo_subdir: str = "", 142 overwrite: bool = False, 143) -> List[StagedFile]: 144 """Generic helper: copy a list of files into ``<staging>/<repo_subdir>/``. 145 146 Useful for ad-hoc staging of small files (Croissant JSON, custom 147 docs) without writing a dedicated module. ``repo_subdir`` may be 148 empty to land files at the staging root. 149 """ 150 staging_root = Path(staging_root) 151 target = staging_root / repo_subdir if repo_subdir else staging_root 152 target.mkdir(parents=True, exist_ok=True) 153 154 staged: List[StagedFile] = [] 155 for src in paths: 156 src = Path(src) 157 if not src.is_file(): 158 raise ConfigError( 159 f"source file does not exist: {src}", 160 hint="Check the path; staging does not create files.", 161 ) 162 dst = target / src.name 163 if dst.exists() and not overwrite: 164 raise DatasetError( 165 f"refusing to overwrite existing staged file: {dst}", 166 hint="Pass overwrite=True or remove the file first.", 167 ) 168 shutil.copy2(src, dst) 169 rel = (Path(repo_subdir) / src.name).as_posix() if repo_subdir else src.name 170 staged.append( 171 StagedFile( 172 repo_path=rel, 173 src=src, 174 bytes_=dst.stat().st_size, 175 ) 176 ) 177 log.info("staged %s (%d bytes)", rel, dst.stat().st_size) 178 return staged
Generic helper: copy a list of files into <staging>/<repo_subdir>/.
Useful for ad-hoc staging of small files (Croissant JSON, custom
docs) without writing a dedicated module. repo_subdir may be
empty to land files at the staging root.
181def load_scene_splits(staging_root: Path) -> dict[str, str]: 182 """Read ``<staging_root>/splits/scene_splits.json`` and return a 183 flat ``{scene_id: tier}`` mapping. Convenience for downstream steps 184 (manifest builder, dataset card) that need to look up a scene's tier. 185 186 Accepts both the canonical schema with a top-level ``"splits"`` key 187 holding ``{tier: [scene_id, ...]}`` and a flat ``{scene_id: tier}`` 188 shape. 189 """ 190 path = Path(staging_root) / "splits" / SCENE_SPLITS_JSON 191 if not path.is_file(): 192 raise ConfigError( 193 f"scene_splits.json not found at {path}", 194 hint="Run stage_splits() before load_scene_splits().", 195 ) 196 payload = json.loads(path.read_text(encoding="utf-8")) 197 if "splits" in payload and isinstance(payload["splits"], dict): 198 return {sid: tier for tier, ids in payload["splits"].items() for sid in ids} 199 return dict(payload)
Read <staging_root>/splits/scene_splits.json and return a
flat {scene_id: tier} mapping. Convenience for downstream steps
(manifest builder, dataset card) that need to look up a scene's tier.
Accepts both the canonical schema with a top-level "splits" key
holding {tier: [scene_id, ...]} and a flat {scene_id: tier}
shape.