dexverse-cli 0.1.0__tar.gz → 0.1.0.post2__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 (21) hide show
  1. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/PKG-INFO +4 -2
  2. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/pyproject.toml +2 -1
  3. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/README.md +2 -1
  4. dexverse_cli-0.1.0.post2/src/dexverse_cli/__init__.py +1 -0
  5. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/cli.py +70 -8
  6. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/manifest.py +13 -5
  7. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/pack.py +13 -3
  8. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/unpack.py +17 -2
  9. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/uv.lock +6 -4
  10. dexverse_cli-0.1.0/src/dexverse_cli/__init__.py +0 -1
  11. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/.gitignore +0 -0
  12. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/.python-version +0 -0
  13. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/alignment.py +0 -0
  14. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/download.py +0 -0
  15. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/hdf5_schema.py +0 -0
  16. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/http.py +0 -0
  17. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/jetson_encoder.py +0 -0
  18. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/raw_schema.py +0 -0
  19. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/state.py +0 -0
  20. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/transaction.py +0 -0
  21. {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/unversioned_hdf5.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dexverse-cli
3
- Version: 0.1.0
3
+ Version: 0.1.0.post2
4
4
  Summary: Download, pack, and unpack DexVerse datasets
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: av<17,>=14
@@ -8,6 +8,7 @@ Requires-Dist: h5py<4,>=3.11
8
8
  Requires-Dist: httpx<1,>=0.28
9
9
  Requires-Dist: numpy<3,>=1.26
10
10
  Requires-Dist: pillow<13,>=10
11
+ Requires-Dist: rich<15,>=13.9
11
12
  Requires-Dist: typer<1,>=0.16
12
13
  Provides-Extra: jetson
13
14
  Requires-Dist: pygobject<4,>=3.42; (platform_system == 'Linux') and extra == 'jetson'
@@ -41,7 +42,8 @@ type manifest.json | dexverse-dl download --manifest - --out ./dataset
41
42
 
42
43
  A manifest is either a JSON file list or an object containing `files`. Each
43
44
  file requires `download_url` and `file_name`. Optional fields are
44
- `storage_path`, `file_size`, `sha256`, and `meta_data`. Downloaded metadata is
45
+ `storage_path`, `file_size`, `sha256`, `meta_data`, and `data_protocol`.
46
+ `data_protocol` is retained in the downloaded metadata sidecar. Downloaded metadata is
45
47
  stored in a versioned sidecar and becomes `task_instruction.json` when the
46
48
  corresponding HDF5 file is unpacked; only top-level `data_protocol` is removed
47
49
  from that algorithm-facing projection.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dexverse-cli"
7
- version = "0.1.0"
7
+ version = "0.1.0.post2"
8
8
  description = "Download, pack, and unpack DexVerse datasets"
9
9
  readme = "src/dexverse_cli/README.md"
10
10
  requires-python = ">=3.10"
@@ -14,6 +14,7 @@ dependencies = [
14
14
  "httpx>=0.28,<1",
15
15
  "numpy>=1.26,<3",
16
16
  "pillow>=10,<13",
17
+ "rich>=13.9,<15",
17
18
  "typer>=0.16,<1",
18
19
  ]
19
20
 
@@ -26,7 +26,8 @@ type manifest.json | dexverse-dl download --manifest - --out ./dataset
26
26
 
27
27
  A manifest is either a JSON file list or an object containing `files`. Each
28
28
  file requires `download_url` and `file_name`. Optional fields are
29
- `storage_path`, `file_size`, `sha256`, and `meta_data`. Downloaded metadata is
29
+ `storage_path`, `file_size`, `sha256`, `meta_data`, and `data_protocol`.
30
+ `data_protocol` is retained in the downloaded metadata sidecar. Downloaded metadata is
30
31
  stored in a versioned sidecar and becomes `task_instruction.json` when the
31
32
  corresponding HDF5 file is unpacked; only top-level `data_protocol` is removed
32
33
  from that algorithm-facing projection.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0.post2"
@@ -1,9 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Callable
3
4
  from pathlib import Path
4
5
  from typing import Annotated, Literal
5
6
 
6
7
  import typer
8
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskID, TextColumn, TimeElapsedColumn
9
+ from rich.table import Column
7
10
 
8
11
  from dexverse_cli.download import download_files
9
12
  from dexverse_cli.manifest import ManifestError, read_manifest
@@ -13,6 +16,60 @@ from dexverse_cli.unpack import unpack_batch
13
16
  app = typer.Typer(no_args_is_help=True, add_completion=False)
14
17
 
15
18
 
19
+ def progress_callback(progress: Progress, task_id: TaskID, verb: str) -> Callable[[int, int], None]:
20
+ def update(completed: int, total: int) -> None:
21
+ progress.update(task_id, completed=completed, total=total, description=f"{verb} {completed}/{total}")
22
+
23
+ return update
24
+
25
+
26
+ def unpack_progress_callback(
27
+ progress: Progress,
28
+ task_id: TaskID,
29
+ verb: str,
30
+ ) -> Callable[[int, int, int, Path | None, Path | None], None]:
31
+ def update(
32
+ completed: int,
33
+ total: int,
34
+ current: int,
35
+ source: Path | None,
36
+ target: Path | None,
37
+ ) -> None:
38
+ description = f"{verb} {current}/{total}" if current else f"{verb} 0/{total}"
39
+ detail = "Preparing files"
40
+ if source is not None and target is not None:
41
+ detail = f"{source.name} → {target}"
42
+ progress.update(
43
+ task_id,
44
+ completed=completed,
45
+ total=total,
46
+ description=description,
47
+ detail=detail,
48
+ )
49
+
50
+ return update
51
+
52
+
53
+ def batch_progress(*, show_detail: bool = False) -> Progress:
54
+ columns = [
55
+ SpinnerColumn(style="cyan"),
56
+ TextColumn("[progress.description]{task.description}"),
57
+ BarColumn(bar_width=None, style="bright_black", complete_style="cyan"),
58
+ ]
59
+ if show_detail:
60
+ columns.append(
61
+ TextColumn(
62
+ "[dim]{task.fields[detail]}",
63
+ table_column=Column(ratio=2, no_wrap=True, overflow="ellipsis"),
64
+ )
65
+ )
66
+ columns.append(TimeElapsedColumn())
67
+ return Progress(
68
+ *columns,
69
+ transient=True,
70
+ )
71
+
72
+
16
73
  @app.command()
17
74
  def download(
18
75
  manifest: Annotated[str, typer.Option("--manifest", "-m", help="Manifest JSON path or '-'")],
@@ -59,13 +116,16 @@ def pack(
59
116
  ) -> None:
60
117
  """Pack one or more raw dataset sessions into Hdf5SchemaV1 files."""
61
118
  try:
62
- report = pack_batch(
63
- input_path,
64
- output,
65
- output_format=output_format,
66
- skip_existing=skip_existing,
67
- workers=workers,
68
- )
119
+ with batch_progress() as progress:
120
+ task_id = progress.add_task("Finding sessions", total=None)
121
+ report = pack_batch(
122
+ input_path,
123
+ output,
124
+ output_format=output_format,
125
+ skip_existing=skip_existing,
126
+ workers=workers,
127
+ on_progress=progress_callback(progress, task_id, "Packing"),
128
+ )
69
129
  except (OSError, ValueError) as exc:
70
130
  typer.echo(f"error: {exc}", err=True)
71
131
  raise typer.Exit(code=1) from exc
@@ -117,7 +177,9 @@ def run_unpack(
117
177
  if not source.exists():
118
178
  typer.echo(f"error: input path does not exist: {source}", err=True)
119
179
  raise typer.Exit(code=1)
120
- report = unpack_batch(source, output)
180
+ with batch_progress(show_detail=True) as progress:
181
+ task_id = progress.add_task("Finding sessions", total=None, detail="Scanning input")
182
+ report = unpack_batch(source, output, on_progress=unpack_progress_callback(progress, task_id, verb.title()))
121
183
  typer.echo(f"{verb}={report.unpacked} frames={report.total_frames} failed={len(report.failures)}")
122
184
  for failure in report.failures:
123
185
  typer.echo(f"error: {failure}", err=True)
@@ -17,6 +17,7 @@ _ALLOWED_FIELDS = frozenset(
17
17
  "file_size",
18
18
  "sha256",
19
19
  "meta_data",
20
+ "data_protocol",
20
21
  }
21
22
  )
22
23
  _DRIVE_PREFIX = re.compile(r"^[A-Za-z]:")
@@ -116,12 +117,19 @@ def optional_sha256(value: object, *, index: int) -> str | None:
116
117
 
117
118
 
118
119
  def metadata(item: dict[str, Any], *, index: int) -> dict[str, Any] | None:
119
- if "meta_data" not in item:
120
- return None
121
- value = item["meta_data"]
122
- if not isinstance(value, dict):
120
+ value = item.get("meta_data") if "meta_data" in item else None
121
+ if "meta_data" in item and not isinstance(value, dict):
123
122
  raise ManifestError(f"files[{index}].meta_data must be a JSON object")
124
- return value
123
+ merged = dict(value) if value is not None else {}
124
+ if "data_protocol" not in item:
125
+ return merged or None
126
+ data_protocol = item["data_protocol"]
127
+ if not isinstance(data_protocol, str) or not data_protocol or data_protocol != data_protocol.strip():
128
+ raise ManifestError(f"files[{index}].data_protocol must be a non-empty string")
129
+ if "data_protocol" in merged and merged["data_protocol"] != data_protocol:
130
+ raise ManifestError(f"files[{index}] has conflicting data_protocol values")
131
+ merged["data_protocol"] = data_protocol
132
+ return merged
125
133
 
126
134
 
127
135
  def claim_parts(file_name: str) -> tuple[str, ...]:
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import io
4
4
  import os
5
+ from collections.abc import Callable
5
6
  from concurrent.futures import ThreadPoolExecutor
6
7
  from dataclasses import dataclass
7
8
  from pathlib import Path, PurePosixPath
@@ -16,6 +17,7 @@ from dexverse_cli.raw_schema import PackValidationPolicyV1, RawSessionV1, Valida
16
17
  from dexverse_cli.transaction import publish_file
17
18
 
18
19
  PackFormat = Literal["jpeg", "video"]
20
+ ProgressCallback = Callable[[int, int], None]
19
21
 
20
22
 
21
23
  @dataclass(frozen=True, slots=True)
@@ -52,10 +54,13 @@ def pack_batch(
52
54
  output_format: PackFormat = "jpeg",
53
55
  skip_existing: bool = False,
54
56
  workers: int | None = None,
57
+ on_progress: ProgressCallback | None = None,
55
58
  ) -> PackReport:
56
59
  sources = discover_raw_sessions(input_path)
57
60
  if not sources:
58
61
  return PackReport(0, 0, (f"no raw sessions found: {input_path}",))
62
+ if on_progress is not None:
63
+ on_progress(0, len(sources))
59
64
  if workers is None:
60
65
  workers = min(len(sources), os.cpu_count() or 1, 32)
61
66
  if workers < 1 or workers > 32:
@@ -74,12 +79,17 @@ def pack_batch(
74
79
  except Exception as exc:
75
80
  return "failed", f"{source}: {exc}"
76
81
 
77
- results: tuple[tuple[str, str | None], ...]
82
+ results: list[tuple[str, str | None]] = []
78
83
  if len(sources) == 1:
79
- results = (process(sources[0]),)
84
+ results.append(process(sources[0]))
85
+ if on_progress is not None:
86
+ on_progress(1, 1)
80
87
  else:
81
88
  with ThreadPoolExecutor(max_workers=workers) as executor:
82
- results = tuple(executor.map(process, sources))
89
+ for completed, result in enumerate(executor.map(process, sources), start=1):
90
+ results.append(result)
91
+ if on_progress is not None:
92
+ on_progress(completed, len(sources))
83
93
  return PackReport(
84
94
  packed=sum(status == "packed" for status, _message in results),
85
95
  skipped=sum(status == "skipped" for status, _message in results),
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import io
4
4
  import json
5
+ from collections.abc import Callable
5
6
  from dataclasses import dataclass
6
7
  from pathlib import Path, PurePosixPath
7
8
  from typing import Any
@@ -15,6 +16,8 @@ from dexverse_cli.state import file_metadata_path, load_file_metadata
15
16
  from dexverse_cli.transaction import publish_directory
16
17
  from dexverse_cli.unversioned_hdf5 import restore_unversioned_hdf5
17
18
 
19
+ ProgressCallback = Callable[[int, int, int, Path | None, Path | None], None]
20
+
18
21
 
19
22
  @dataclass(frozen=True, slots=True)
20
23
  class UnpackReport:
@@ -35,23 +38,35 @@ def discover_hdf5(input_path: Path) -> tuple[Path, ...]:
35
38
  return ()
36
39
 
37
40
 
38
- def unpack_batch(input_path: Path, output: Path) -> UnpackReport:
41
+ def unpack_batch(
42
+ input_path: Path,
43
+ output: Path,
44
+ *,
45
+ on_progress: ProgressCallback | None = None,
46
+ ) -> UnpackReport:
39
47
  sources = discover_hdf5(input_path)
40
48
  if not sources:
41
49
  return UnpackReport(0, 0, (f"no supported HDF5 files found: {input_path}",))
50
+ if on_progress is not None:
51
+ on_progress(0, len(sources), 0, None, None)
42
52
  unpacked = 0
43
53
  total_frames = 0
44
54
  failures: list[str] = []
45
- for source in sources:
55
+ for completed, source in enumerate(sources, start=1):
46
56
  target = (
47
57
  output / source.relative_to(input_path).with_suffix("") if input_path.is_dir() else output / source.stem
48
58
  )
59
+ if on_progress is not None:
60
+ on_progress(completed - 1, len(sources), completed, source, target)
49
61
  try:
50
62
  frames = unpack_transactionally(source, target)
51
63
  unpacked += 1
52
64
  total_frames += frames
53
65
  except Exception as exc:
54
66
  failures.append(f"{source}: {exc}")
67
+ finally:
68
+ if on_progress is not None:
69
+ on_progress(completed, len(sources), completed, source, target)
55
70
  return UnpackReport(unpacked, total_frames, tuple(failures))
56
71
 
57
72
 
@@ -108,7 +108,7 @@ wheels = [
108
108
 
109
109
  [[package]]
110
110
  name = "dexverse-cli"
111
- version = "0.1.0"
111
+ version = "0.1.0.post2"
112
112
  source = { editable = "." }
113
113
  dependencies = [
114
114
  { name = "av" },
@@ -118,6 +118,7 @@ dependencies = [
118
118
  { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
119
119
  { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
120
120
  { name = "pillow" },
121
+ { name = "rich" },
121
122
  { name = "typer" },
122
123
  ]
123
124
 
@@ -141,6 +142,7 @@ requires-dist = [
141
142
  { name = "numpy", specifier = ">=1.26,<3" },
142
143
  { name = "pillow", specifier = ">=10,<13" },
143
144
  { name = "pygobject", marker = "sys_platform == 'linux' and extra == 'jetson'", specifier = ">=3.42,<4" },
145
+ { name = "rich", specifier = ">=13.9,<15" },
144
146
  { name = "typer", specifier = ">=0.16,<1" },
145
147
  ]
146
148
  provides-extras = ["jetson"]
@@ -886,15 +888,15 @@ wheels = [
886
888
 
887
889
  [[package]]
888
890
  name = "rich"
889
- version = "15.0.0"
891
+ version = "14.3.4"
890
892
  source = { registry = "https://pypi.org/simple" }
891
893
  dependencies = [
892
894
  { name = "markdown-it-py" },
893
895
  { name = "pygments" },
894
896
  ]
895
- sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
897
+ sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" }
896
898
  wheels = [
897
- { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
899
+ { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" },
898
900
  ]
899
901
 
900
902
  [[package]]
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"