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 from benchmark/data/splits/ so downloaders can resolve a split to a scene list before fetching the manifest.
  • rpx_croissant.json — copied from paper-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 in rpx_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)
log = <Logger rpx_benchmark.dataset_hub.staging (WARNING)>
SPLIT_TIER_FILES = ('easy.txt', 'medium.txt', 'hard.txt')
SCENE_SPLITS_JSON = 'scene_splits.json'
@dataclass(frozen=True)
class StagedFile:
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.

StagedFile(repo_path: str, src: Optional[pathlib.Path], bytes_: int)
repo_path: str
src: Optional[pathlib.Path]
bytes_: int
def stage_splits( staging_root: pathlib.Path, splits_src: Optional[pathlib.Path] = None, overwrite: bool = False, require_all: bool = True) -> List[StagedFile]:
 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.

def stage_paths( paths: Sequence[pathlib.Path], staging_root: pathlib.Path, repo_subdir: str = '', overwrite: bool = False) -> List[StagedFile]:
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.

def load_scene_splits(staging_root: pathlib.Path) -> dict[str, str]:
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.