dexverse-cli 0.0.4.post1__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. dexverse_cli-0.1.0/.gitignore +12 -0
  2. dexverse_cli-0.1.0/.python-version +1 -0
  3. dexverse_cli-0.1.0/PKG-INFO +112 -0
  4. dexverse_cli-0.1.0/pyproject.toml +67 -0
  5. dexverse_cli-0.1.0/src/dexverse_cli/README.md +97 -0
  6. dexverse_cli-0.1.0/src/dexverse_cli/__init__.py +1 -0
  7. dexverse_cli-0.1.0/src/dexverse_cli/alignment.py +45 -0
  8. dexverse_cli-0.1.0/src/dexverse_cli/cli.py +125 -0
  9. dexverse_cli-0.1.0/src/dexverse_cli/download.py +464 -0
  10. dexverse_cli-0.1.0/src/dexverse_cli/hdf5_schema.py +519 -0
  11. dexverse_cli-0.1.0/src/dexverse_cli/http.py +416 -0
  12. dexverse_cli-0.1.0/src/dexverse_cli/jetson_encoder.py +168 -0
  13. dexverse_cli-0.1.0/src/dexverse_cli/manifest.py +239 -0
  14. dexverse_cli-0.1.0/src/dexverse_cli/pack.py +157 -0
  15. dexverse_cli-0.1.0/src/dexverse_cli/raw_schema.py +418 -0
  16. dexverse_cli-0.1.0/src/dexverse_cli/state.py +141 -0
  17. dexverse_cli-0.1.0/src/dexverse_cli/transaction.py +126 -0
  18. dexverse_cli-0.1.0/src/dexverse_cli/unpack.py +170 -0
  19. dexverse_cli-0.1.0/src/dexverse_cli/unversioned_hdf5.py +321 -0
  20. dexverse_cli-0.1.0/uv.lock +1010 -0
  21. dexverse_cli-0.0.4.post1/.dockerignore +0 -14
  22. dexverse_cli-0.0.4.post1/.gitignore +0 -41
  23. dexverse_cli-0.0.4.post1/Dockerfile +0 -37
  24. dexverse_cli-0.0.4.post1/PKG-INFO +0 -201
  25. dexverse_cli-0.0.4.post1/README.md +0 -171
  26. dexverse_cli-0.0.4.post1/decision-traces/2026-08-12_1613_pypi_0_0_4_release_trace.md +0 -84
  27. dexverse_cli-0.0.4.post1/dexverse/__init__.py +0 -54
  28. dexverse_cli-0.0.4.post1/dexverse/cli.py +0 -422
  29. dexverse_cli-0.0.4.post1/dexverse/client.py +0 -128
  30. dexverse_cli-0.0.4.post1/dexverse/config.py +0 -85
  31. dexverse_cli-0.0.4.post1/dexverse/convert/__init__.py +0 -38
  32. dexverse_cli-0.0.4.post1/dexverse/convert/hdf5_to_raw.py +0 -500
  33. dexverse_cli-0.0.4.post1/dexverse/convert/image_video_encode.py +0 -449
  34. dexverse_cli-0.0.4.post1/dexverse/downloader/__init__.py +0 -26
  35. dexverse_cli-0.0.4.post1/dexverse/downloader/base.py +0 -101
  36. dexverse_cli-0.0.4.post1/dexverse/downloader/http_backend.py +0 -508
  37. dexverse_cli-0.0.4.post1/dexverse/downloader/manager.py +0 -365
  38. dexverse_cli-0.0.4.post1/dexverse/downloader/python_s3_backend.py +0 -137
  39. dexverse_cli-0.0.4.post1/dexverse/downloader/s5cmd_backend.py +0 -170
  40. dexverse_cli-0.0.4.post1/dexverse/downloader/state.py +0 -269
  41. dexverse_cli-0.0.4.post1/dexverse/downloader/verify.py +0 -114
  42. dexverse_cli-0.0.4.post1/dexverse/manifest.py +0 -313
  43. dexverse_cli-0.0.4.post1/dexverse/mock/__init__.py +0 -2
  44. dexverse_cli-0.0.4.post1/dexverse/mock/mock_tasks.json +0 -22
  45. dexverse_cli-0.0.4.post1/dexverse/mock/server.py +0 -211
  46. dexverse_cli-0.0.4.post1/docker_run.sh +0 -105
  47. dexverse_cli-0.0.4.post1/docs/DexVerse_Downloader_S3_Final_/345/256/236/346/226/275/346/226/271/346/241/210_/345/216/237/350/267/257/345/276/204/350/220/275/347/233/230/347/211/210.md +0 -1090
  48. dexverse_cli-0.0.4.post1/docs/PROMPT_copy_and_download_command.md +0 -130
  49. dexverse_cli-0.0.4.post1/docs/Python_vs_Node_CLI/345/257/271/346/257/224/345/210/206/346/236/220.md +0 -167
  50. dexverse_cli-0.0.4.post1/docs/Python_vs_Node_/346/216/245/345/205/245C++/345/257/271/346/257/224/345/210/206/346/236/220.md +0 -197
  51. dexverse_cli-0.0.4.post1/docs//345/210/206/345/217/221/347/273/231/347/256/227/346/263/225/345/233/242/351/230/237_/346/225/231/347/250/213.md +0 -315
  52. dexverse_cli-0.0.4.post1/docs//346/223/215/344/275/234/346/214/207/345/215/227--/345/234/250Dexechain/344/270/255/350/277/233/350/241/214/346/250/241/344/273/277/345/255/246/344/271/240.md +0 -120
  53. dexverse_cli-0.0.4.post1/docs//346/234/215/345/212/241/345/231/250/344/275/277/347/224/250/346/211/213/345/206/214.md +0 -64
  54. dexverse_cli-0.0.4.post1/examples/download (7).json +0 -55
  55. dexverse_cli-0.0.4.post1/examples/local_demo.py +0 -80
  56. dexverse_cli-0.0.4.post1/examples/manifest.json +0 -45
  57. dexverse_cli-0.0.4.post1/examples/manifest_presigned.json +0 -34
  58. dexverse_cli-0.0.4.post1/pyproject.toml +0 -59
  59. dexverse_cli-0.0.4.post1/set-creds.ps1 +0 -3
  60. dexverse_cli-0.0.4.post1/tests/conftest.py +0 -19
  61. dexverse_cli-0.0.4.post1/tests/test_cli.py +0 -134
  62. dexverse_cli-0.0.4.post1/tests/test_convert_additional_info.py +0 -101
  63. dexverse_cli-0.0.4.post1/tests/test_http_backend.py +0 -426
  64. dexverse_cli-0.0.4.post1/tests/test_http_resilience.py +0 -203
  65. dexverse_cli-0.0.4.post1/tests/test_manager_integration.py +0 -202
  66. dexverse_cli-0.0.4.post1/tests/test_manifest.py +0 -138
  67. dexverse_cli-0.0.4.post1/tests/test_manifest_aliases.py +0 -180
  68. dexverse_cli-0.0.4.post1/tests/test_mock_server.py +0 -72
  69. dexverse_cli-0.0.4.post1/tests/test_presigned_fixes.py +0 -189
  70. dexverse_cli-0.0.4.post1/tests/test_review_fixes.py +0 -265
  71. dexverse_cli-0.0.4.post1/tests/test_s5cmd_generation.py +0 -72
  72. dexverse_cli-0.0.4.post1/tests/test_state.py +0 -73
  73. dexverse_cli-0.0.4.post1/tests/test_verify.py +0 -67
  74. dexverse_cli-0.0.4.post1/tests/test_version.py +0 -13
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ .venv-smoke/
3
+ .venv-smoke-*/
4
+ .smoke-work/
5
+ .artifact-inspect/
6
+ .manifest/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ __pycache__/
11
+ *.py[cod]
12
+ dist/
@@ -0,0 +1 @@
1
+ 3.10
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.5
2
+ Name: dexverse-cli
3
+ Version: 0.1.0
4
+ Summary: Download, pack, and unpack DexVerse datasets
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: av<17,>=14
7
+ Requires-Dist: h5py<4,>=3.11
8
+ Requires-Dist: httpx<1,>=0.28
9
+ Requires-Dist: numpy<3,>=1.26
10
+ Requires-Dist: pillow<13,>=10
11
+ Requires-Dist: typer<1,>=0.16
12
+ Provides-Extra: jetson
13
+ Requires-Dist: pygobject<4,>=3.42; (platform_system == 'Linux') and extra == 'jetson'
14
+ Description-Content-Type: text/markdown
15
+
16
+ # dexverse-cli
17
+
18
+ `dexverse-cli` downloads DexVerse dataset manifests over HTTP(S), packs strict
19
+ raw sessions into versioned HDF5 files, and unpacks HDF5 sessions. It requires
20
+ Python 3.10 or newer.
21
+
22
+ ## Install
23
+
24
+ ```console
25
+ pip install dexverse-cli
26
+ ```
27
+
28
+ ## Download
29
+
30
+ Read a manifest from a file:
31
+
32
+ ```console
33
+ dexverse-dl download --manifest manifest.json --out ./dataset
34
+ ```
35
+
36
+ Or pipe a short-lived manifest directly to stdin:
37
+
38
+ ```console
39
+ type manifest.json | dexverse-dl download --manifest - --out ./dataset
40
+ ```
41
+
42
+ A manifest is either a JSON file list or an object containing `files`. Each
43
+ file requires `download_url` and `file_name`. Optional fields are
44
+ `storage_path`, `file_size`, `sha256`, and `meta_data`. Downloaded metadata is
45
+ stored in a versioned sidecar and becomes `task_instruction.json` when the
46
+ corresponding HDF5 file is unpacked; only top-level `data_protocol` is removed
47
+ from that algorithm-facing projection.
48
+
49
+ Downloads use bounded parallelism, retries, and compatible partial-file resume.
50
+ When `sha256` is present it is always checked before the final file is replaced.
51
+ TLS certificate verification is disabled by default for internal endpoints;
52
+ pass `--secure` to enable certificate verification.
53
+
54
+ ## Pack
55
+
56
+ Pack one raw session, or the immediate session directories under a parent:
57
+
58
+ ```console
59
+ dexverse-dl pack --input ./raw/session --output ./packed --format jpeg
60
+ ```
61
+
62
+ JPEG is the platform-independent default. Packing validates the complete raw
63
+ inventory, every image, all camera stream relationships, and pose data before
64
+ transactionally replacing an output. Camera and pose counts may differ; pose is
65
+ linearly aligned to the deterministic reference camera timeline.
66
+
67
+ Jetson MP4 packing is optional:
68
+
69
+ ```console
70
+ pip install "dexverse-cli[jetson]"
71
+ dexverse-dl pack --input ./raw/session --output ./packed --format video
72
+ ```
73
+
74
+ The Python extra does not install native Jetson GStreamer components. Video
75
+ packing reports an explicit error unless the required hardware pipeline is
76
+ available. JPEG packing, download, help, and unpack do not import GStreamer.
77
+
78
+ ## Unpack
79
+
80
+ Unpack one HDF5 file:
81
+
82
+ ```console
83
+ dexverse-dl unpack --input ./packed/session.hdf5 --output ./restored
84
+ ```
85
+
86
+ Or recursively unpack `*.hdf5` files while preserving their relative paths:
87
+
88
+ ```console
89
+ dexverse-dl unpack --input-dir ./packed --output ./restored
90
+ ```
91
+
92
+ Unpacking validates the complete versioned schema before transactionally
93
+ publishing a restored session. It also retains the supported unversioned HDF5
94
+ read path.
95
+
96
+ ## Convert
97
+
98
+ Convert one supported HDF5 file:
99
+
100
+ ```console
101
+ dexverse-dl convert --input ./dataset/session.hdf5 --output ./restored
102
+ ```
103
+
104
+ Or recursively convert `*.hdf5` files while preserving their relative paths:
105
+
106
+ ```console
107
+ dexverse-dl convert --input-dir ./dataset --output ./restored
108
+ ```
109
+
110
+ `convert` is the compatibility alias for `unpack`; it retains the same options,
111
+ target mapping, legacy HDF5 support, exit codes, and `converted=...` summary.
112
+ Batches continue after an item failure but exit with status 1 if any item fails.
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dexverse-cli"
7
+ version = "0.1.0"
8
+ description = "Download, pack, and unpack DexVerse datasets"
9
+ readme = "src/dexverse_cli/README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "av>=14,<17",
13
+ "h5py>=3.11,<4",
14
+ "httpx>=0.28,<1",
15
+ "numpy>=1.26,<3",
16
+ "pillow>=10,<13",
17
+ "typer>=0.16,<1",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ jetson = [
22
+ "PyGObject>=3.42,<4; platform_system == 'Linux'",
23
+ ]
24
+
25
+ [project.scripts]
26
+ dexverse-dl = "dexverse_cli.cli:app"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "mypy>=1.15,<2",
31
+ "pytest>=8.3,<10",
32
+ "ruff>=0.11,<1",
33
+ ]
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/dexverse_cli"]
37
+
38
+ [tool.hatch.build.targets.sdist]
39
+ include = [
40
+ "/pyproject.toml",
41
+ "/uv.lock",
42
+ "/.python-version",
43
+ "/src/dexverse_cli",
44
+ ]
45
+
46
+ [tool.pytest.ini_options]
47
+ addopts = "-ra"
48
+ testpaths = ["tests"]
49
+
50
+ [tool.ruff]
51
+ target-version = "py310"
52
+ line-length = 120
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF", "TID252"]
56
+
57
+ [tool.ruff.lint.flake8-tidy-imports]
58
+ ban-relative-imports = "all"
59
+
60
+ [tool.ruff.lint.per-file-ignores]
61
+ "tests/**/*.py" = ["TID252"]
62
+
63
+ [tool.mypy]
64
+ python_version = "3.10"
65
+ strict = true
66
+ warn_unreachable = true
67
+ ignore_missing_imports = true
@@ -0,0 +1,97 @@
1
+ # dexverse-cli
2
+
3
+ `dexverse-cli` downloads DexVerse dataset manifests over HTTP(S), packs strict
4
+ raw sessions into versioned HDF5 files, and unpacks HDF5 sessions. It requires
5
+ Python 3.10 or newer.
6
+
7
+ ## Install
8
+
9
+ ```console
10
+ pip install dexverse-cli
11
+ ```
12
+
13
+ ## Download
14
+
15
+ Read a manifest from a file:
16
+
17
+ ```console
18
+ dexverse-dl download --manifest manifest.json --out ./dataset
19
+ ```
20
+
21
+ Or pipe a short-lived manifest directly to stdin:
22
+
23
+ ```console
24
+ type manifest.json | dexverse-dl download --manifest - --out ./dataset
25
+ ```
26
+
27
+ A manifest is either a JSON file list or an object containing `files`. Each
28
+ file requires `download_url` and `file_name`. Optional fields are
29
+ `storage_path`, `file_size`, `sha256`, and `meta_data`. Downloaded metadata is
30
+ stored in a versioned sidecar and becomes `task_instruction.json` when the
31
+ corresponding HDF5 file is unpacked; only top-level `data_protocol` is removed
32
+ from that algorithm-facing projection.
33
+
34
+ Downloads use bounded parallelism, retries, and compatible partial-file resume.
35
+ When `sha256` is present it is always checked before the final file is replaced.
36
+ TLS certificate verification is disabled by default for internal endpoints;
37
+ pass `--secure` to enable certificate verification.
38
+
39
+ ## Pack
40
+
41
+ Pack one raw session, or the immediate session directories under a parent:
42
+
43
+ ```console
44
+ dexverse-dl pack --input ./raw/session --output ./packed --format jpeg
45
+ ```
46
+
47
+ JPEG is the platform-independent default. Packing validates the complete raw
48
+ inventory, every image, all camera stream relationships, and pose data before
49
+ transactionally replacing an output. Camera and pose counts may differ; pose is
50
+ linearly aligned to the deterministic reference camera timeline.
51
+
52
+ Jetson MP4 packing is optional:
53
+
54
+ ```console
55
+ pip install "dexverse-cli[jetson]"
56
+ dexverse-dl pack --input ./raw/session --output ./packed --format video
57
+ ```
58
+
59
+ The Python extra does not install native Jetson GStreamer components. Video
60
+ packing reports an explicit error unless the required hardware pipeline is
61
+ available. JPEG packing, download, help, and unpack do not import GStreamer.
62
+
63
+ ## Unpack
64
+
65
+ Unpack one HDF5 file:
66
+
67
+ ```console
68
+ dexverse-dl unpack --input ./packed/session.hdf5 --output ./restored
69
+ ```
70
+
71
+ Or recursively unpack `*.hdf5` files while preserving their relative paths:
72
+
73
+ ```console
74
+ dexverse-dl unpack --input-dir ./packed --output ./restored
75
+ ```
76
+
77
+ Unpacking validates the complete versioned schema before transactionally
78
+ publishing a restored session. It also retains the supported unversioned HDF5
79
+ read path.
80
+
81
+ ## Convert
82
+
83
+ Convert one supported HDF5 file:
84
+
85
+ ```console
86
+ dexverse-dl convert --input ./dataset/session.hdf5 --output ./restored
87
+ ```
88
+
89
+ Or recursively convert `*.hdf5` files while preserving their relative paths:
90
+
91
+ ```console
92
+ dexverse-dl convert --input-dir ./dataset --output ./restored
93
+ ```
94
+
95
+ `convert` is the compatibility alias for `unpack`; it retains the same options,
96
+ target mapping, legacy HDF5 support, exit codes, and `converted=...` summary.
97
+ Batches continue after an item failure but exit with status 1 if any item fails.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import numpy as np
6
+ import numpy.typing as npt
7
+
8
+ from dexverse_cli.raw_schema import RawSessionV1
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class AlignedPoseV1:
13
+ data: npt.NDArray[np.float32]
14
+ timestamps: npt.NDArray[np.uint64]
15
+ columns: tuple[str, ...]
16
+
17
+
18
+ class PackAlignmentPolicyV1:
19
+ """Linearly align pose values to the deterministic reference camera timeline."""
20
+
21
+ name = "linear_pose_to_reference_camera_v1"
22
+
23
+ def align(self, session: RawSessionV1) -> AlignedPoseV1:
24
+ target = np.asarray(
25
+ [record.timestamp for record in session.records_by_camera[session.reference_camera]],
26
+ dtype=np.float64,
27
+ )
28
+ timestamps = np.asarray(
29
+ [record.timestamp_ns for record in session.records_by_camera[session.reference_camera]],
30
+ dtype=np.uint64,
31
+ )
32
+ columns = session.pose_columns
33
+ result = np.empty((target.size, len(columns)), dtype=np.float32)
34
+ source_times = np.asarray([frame.timestamp for frame in session.pose_frames], dtype=np.float64)
35
+ for column_index, column in enumerate(columns):
36
+ values = np.asarray(
37
+ [frame.data[column] for frame in session.pose_frames],
38
+ dtype=np.float64,
39
+ )
40
+ if values.size == 1:
41
+ aligned = np.full(target.shape, values[0], dtype=np.float64)
42
+ else:
43
+ aligned = np.interp(target, source_times, values)
44
+ result[:, column_index] = aligned.astype(np.float32)
45
+ return AlignedPoseV1(result, timestamps, columns)
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Annotated, Literal
5
+
6
+ import typer
7
+
8
+ from dexverse_cli.download import download_files
9
+ from dexverse_cli.manifest import ManifestError, read_manifest
10
+ from dexverse_cli.pack import pack_batch
11
+ from dexverse_cli.unpack import unpack_batch
12
+
13
+ app = typer.Typer(no_args_is_help=True, add_completion=False)
14
+
15
+
16
+ @app.command()
17
+ def download(
18
+ manifest: Annotated[str, typer.Option("--manifest", "-m", help="Manifest JSON path or '-'")],
19
+ out: Annotated[Path, typer.Option("--out", "-o", help="Local download root")],
20
+ parallel: Annotated[int, typer.Option("--parallel", "-p", min=1, max=32)] = 4,
21
+ http_read_timeout: Annotated[float, typer.Option("--http-read-timeout", min=0.1)] = 120.0,
22
+ http_retries: Annotated[int, typer.Option("--http-retries", min=0)] = 5,
23
+ secure: Annotated[bool, typer.Option("--secure", help="Verify server TLS certificates")] = False,
24
+ ) -> None:
25
+ """Download all files in an HTTP(S) manifest."""
26
+ try:
27
+ files = read_manifest(manifest)
28
+ if not secure:
29
+ typer.echo(
30
+ "warning: TLS certificate verification is disabled; pass --secure to enable it",
31
+ err=True,
32
+ )
33
+ report = download_files(
34
+ files,
35
+ out,
36
+ parallel=parallel,
37
+ secure=secure,
38
+ read_timeout=http_read_timeout,
39
+ max_retries=http_retries,
40
+ )
41
+ except (ManifestError, OSError) as exc:
42
+ typer.echo(f"error: {exc}", err=True)
43
+ raise typer.Exit(code=1) from exc
44
+
45
+ typer.echo(f"downloaded={report.downloaded} skipped={report.skipped} failed={len(report.failures)}")
46
+ for failure in report.failures:
47
+ typer.echo(f"error: {failure}", err=True)
48
+ if not report.ok:
49
+ raise typer.Exit(code=1)
50
+
51
+
52
+ @app.command()
53
+ def pack(
54
+ input_path: Annotated[Path, typer.Option("--input", "-i")],
55
+ output: Annotated[Path, typer.Option("--output", "-o")],
56
+ output_format: Annotated[Literal["jpeg", "video"], typer.Option("--format")] = "jpeg",
57
+ skip_existing: Annotated[bool, typer.Option("--skip-existing")] = False,
58
+ workers: Annotated[int | None, typer.Option("--workers", min=1, max=32)] = None,
59
+ ) -> None:
60
+ """Pack one or more raw dataset sessions into Hdf5SchemaV1 files."""
61
+ try:
62
+ report = pack_batch(
63
+ input_path,
64
+ output,
65
+ output_format=output_format,
66
+ skip_existing=skip_existing,
67
+ workers=workers,
68
+ )
69
+ except (OSError, ValueError) as exc:
70
+ typer.echo(f"error: {exc}", err=True)
71
+ raise typer.Exit(code=1) from exc
72
+ typer.echo(f"packed={report.packed} skipped={report.skipped} failed={len(report.failures)}")
73
+ for failure in report.failures:
74
+ typer.echo(f"error: {failure}", err=True)
75
+ if not report.ok:
76
+ raise typer.Exit(code=1)
77
+
78
+
79
+ @app.command()
80
+ def unpack(
81
+ output: Annotated[Path, typer.Option("--output", "-o")],
82
+ input_file: Annotated[Path | None, typer.Option("--input", "-i")] = None,
83
+ input_dir: Annotated[Path | None, typer.Option("--input-dir")] = None,
84
+ strategy: Annotated[str, typer.Option("--strategy")] = "default",
85
+ ) -> None:
86
+ """Unpack HDF5 sessions to the restored raw dataset layout."""
87
+ run_unpack(output, input_file, input_dir, strategy, verb="unpacked")
88
+
89
+
90
+ @app.command()
91
+ def convert(
92
+ output: Annotated[Path, typer.Option("--output", "-o")],
93
+ input_file: Annotated[Path | None, typer.Option("--input", "-i")] = None,
94
+ input_dir: Annotated[Path | None, typer.Option("--input-dir")] = None,
95
+ strategy: Annotated[str, typer.Option("--strategy")] = "default",
96
+ ) -> None:
97
+ """Convert HDF5 sessions to the restored dataset layout."""
98
+ run_unpack(output, input_file, input_dir, strategy, verb="converted")
99
+
100
+
101
+ def run_unpack(
102
+ output: Path,
103
+ input_file: Path | None,
104
+ input_dir: Path | None,
105
+ strategy: str,
106
+ *,
107
+ verb: Literal["unpacked", "converted"],
108
+ ) -> None:
109
+ if strategy != "default":
110
+ typer.echo("error: --strategy must be 'default'", err=True)
111
+ raise typer.Exit(code=2)
112
+ if (input_file is None) == (input_dir is None):
113
+ typer.echo("error: pass exactly one of --input or --input-dir", err=True)
114
+ raise typer.Exit(code=2)
115
+ source = input_file if input_file is not None else input_dir
116
+ assert source is not None
117
+ if not source.exists():
118
+ typer.echo(f"error: input path does not exist: {source}", err=True)
119
+ raise typer.Exit(code=1)
120
+ report = unpack_batch(source, output)
121
+ typer.echo(f"{verb}={report.unpacked} frames={report.total_frames} failed={len(report.failures)}")
122
+ for failure in report.failures:
123
+ typer.echo(f"error: {failure}", err=True)
124
+ if not report.ok:
125
+ raise typer.Exit(code=1)