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.
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/PKG-INFO +4 -2
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/pyproject.toml +2 -1
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/README.md +2 -1
- dexverse_cli-0.1.0.post2/src/dexverse_cli/__init__.py +1 -0
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/cli.py +70 -8
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/manifest.py +13 -5
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/pack.py +13 -3
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/unpack.py +17 -2
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/uv.lock +6 -4
- dexverse_cli-0.1.0/src/dexverse_cli/__init__.py +0 -1
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/.gitignore +0 -0
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/.python-version +0 -0
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/alignment.py +0 -0
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/download.py +0 -0
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/hdf5_schema.py +0 -0
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/http.py +0 -0
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/jetson_encoder.py +0 -0
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/raw_schema.py +0 -0
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/state.py +0 -0
- {dexverse_cli-0.1.0 → dexverse_cli-0.1.0.post2}/src/dexverse_cli/transaction.py +0 -0
- {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 `
|
|
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 `
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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"
|
|
120
|
-
|
|
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
|
-
|
|
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:
|
|
82
|
+
results: list[tuple[str, str | None]] = []
|
|
78
83
|
if len(sources) == 1:
|
|
79
|
-
results
|
|
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
|
-
|
|
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(
|
|
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 = "
|
|
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/
|
|
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/
|
|
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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|