dexverse-cli 0.1.1__tar.gz → 0.1.3.post1__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.1 → dexverse_cli-0.1.3.post1}/PKG-INFO +10 -1
  2. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/pyproject.toml +1 -1
  3. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/README.md +9 -0
  4. dexverse_cli-0.1.3.post1/src/dexverse_cli/__init__.py +1 -0
  5. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/cli.py +15 -8
  6. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/download.py +27 -15
  7. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/manifest.py +34 -10
  8. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/uv.lock +1 -1
  9. dexverse_cli-0.1.1/src/dexverse_cli/__init__.py +0 -1
  10. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/.gitignore +0 -0
  11. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/.python-version +0 -0
  12. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/alignment.py +0 -0
  13. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/hdf5_schema.py +0 -0
  14. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/http.py +0 -0
  15. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/jetson_encoder.py +0 -0
  16. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/pack.py +0 -0
  17. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/raw_schema.py +0 -0
  18. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/state.py +0 -0
  19. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/transaction.py +0 -0
  20. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/src/dexverse_cli/unpack.py +0 -0
  21. {dexverse_cli-0.1.1 → dexverse_cli-0.1.3.post1}/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.1
3
+ Version: 0.1.3.post1
4
4
  Summary: Download, pack, and unpack DexVerse datasets
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: av<17,>=14
@@ -49,6 +49,15 @@ corresponding HDF5 file is unpacked; only top-level `data_protocol` is removed
49
49
  from that algorithm-facing projection.
50
50
 
51
51
  Downloads use bounded parallelism, retries, and compatible partial-file resume.
52
+ Within a manifest, the first occurrence of a target path keeps its name; later
53
+ occurrences receive `-1`, `-2`, etc. before the final extension (for example,
54
+ `episode1.hdf5`, `episode1-1.hdf5`, `episode1-2.hdf5`). Numbering skips names and
55
+ paths already claimed by other entries, including partial files and metadata.
56
+ Different relative directories remain independent. Names are assigned in manifest
57
+ order, with Windows case-insensitive matching, before any downloads start.
58
+ Rerunning the same manifest preserves the assigned names for skipping and resume;
59
+ existing files on disk do not trigger new suffixes. This is not cross-batch name
60
+ allocation: changing the manifest entries or their order can change the mapping.
52
61
  When `sha256` is present it is always checked before the final file is replaced.
53
62
  TLS certificate verification is disabled by default for internal endpoints;
54
63
  pass `--secure` to enable certificate verification.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "dexverse-cli"
7
- version = "0.1.1"
7
+ version = "0.1.3.post1"
8
8
  description = "Download, pack, and unpack DexVerse datasets"
9
9
  readme = "src/dexverse_cli/README.md"
10
10
  requires-python = ">=3.10"
@@ -33,6 +33,15 @@ corresponding HDF5 file is unpacked; only top-level `data_protocol` is removed
33
33
  from that algorithm-facing projection.
34
34
 
35
35
  Downloads use bounded parallelism, retries, and compatible partial-file resume.
36
+ Within a manifest, the first occurrence of a target path keeps its name; later
37
+ occurrences receive `-1`, `-2`, etc. before the final extension (for example,
38
+ `episode1.hdf5`, `episode1-1.hdf5`, `episode1-2.hdf5`). Numbering skips names and
39
+ paths already claimed by other entries, including partial files and metadata.
40
+ Different relative directories remain independent. Names are assigned in manifest
41
+ order, with Windows case-insensitive matching, before any downloads start.
42
+ Rerunning the same manifest preserves the assigned names for skipping and resume;
43
+ existing files on disk do not trigger new suffixes. This is not cross-batch name
44
+ allocation: changing the manifest entries or their order can change the mapping.
36
45
  When `sha256` is present it is always checked before the final file is replaced.
37
46
  TLS certificate verification is disabled by default for internal endpoints;
38
47
  pass `--secure` to enable certificate verification.
@@ -0,0 +1 @@
1
+ __version__ = "0.1.3.post1"
@@ -87,14 +87,21 @@ def download(
87
87
  "warning: TLS certificate verification is disabled; pass --secure to enable it",
88
88
  err=True,
89
89
  )
90
- report = download_files(
91
- files,
92
- out,
93
- parallel=parallel,
94
- secure=secure,
95
- read_timeout=http_read_timeout,
96
- max_retries=http_retries,
97
- )
90
+ with batch_progress(show_detail=True) as progress:
91
+ task_id = progress.add_task(
92
+ f"Downloading 0/{len(files)}",
93
+ total=len(files),
94
+ detail=f"{len(files)} files → {out}",
95
+ )
96
+ report = download_files(
97
+ files,
98
+ out,
99
+ parallel=parallel,
100
+ secure=secure,
101
+ read_timeout=http_read_timeout,
102
+ max_retries=http_retries,
103
+ on_progress=progress_callback(progress, task_id, "Downloading"),
104
+ )
98
105
  except (ManifestError, OSError) as exc:
99
106
  typer.echo(f"error: {exc}", err=True)
100
107
  raise typer.Exit(code=1) from exc
@@ -9,6 +9,7 @@ from dataclasses import dataclass
9
9
  from functools import partial
10
10
  from hashlib import sha256
11
11
  from pathlib import Path
12
+ from threading import Lock
12
13
 
13
14
  from dexverse_cli.http import (
14
15
  HttpClientProvider,
@@ -26,6 +27,8 @@ from dexverse_cli.state import (
26
27
  write_file_metadata,
27
28
  )
28
29
 
30
+ ProgressCallback = Callable[[int, int], None]
31
+
29
32
 
30
33
  @dataclass(frozen=True, slots=True)
31
34
  class DownloadReport:
@@ -74,7 +77,6 @@ class OutputRootPlan:
74
77
  ):
75
78
  raise UnsafeOutputPathError("unsafe output path")
76
79
  check_claim(path, self.root)
77
- reject_existing_aliases(self.claims)
78
80
 
79
81
 
80
82
  def safe_lstat(path: Path) -> os.stat_result | None:
@@ -140,24 +142,19 @@ def check_claim(path: Path, root: Path) -> None:
140
142
 
141
143
  def reject_existing_aliases(claims: tuple[Path, ...]) -> None:
142
144
  resolved: set[str] = set()
143
- existing: list[Path] = []
145
+ identities: set[tuple[int, int]] = set()
144
146
  for claim in claims:
145
147
  key = os.path.normcase(os.path.normpath(str(claim.resolve(strict=False))))
146
148
  if key in resolved:
147
149
  raise UnsafeOutputPathError("unsafe output path")
148
150
  resolved.add(key)
149
- if safe_lstat(claim) is not None:
150
- for other in existing:
151
- try:
152
- if os.path.samefile(claim, other):
153
- raise UnsafeOutputPathError("unsafe output path")
154
- except FileNotFoundError:
155
- continue
156
- except UnsafeOutputPathError:
157
- raise
158
- except OSError as exc:
159
- raise UnsafeOutputPathError("unsafe output path") from exc
160
- existing.append(claim)
151
+ value = safe_lstat(claim)
152
+ if value is None:
153
+ continue
154
+ identity = value.st_dev, value.st_ino
155
+ if identity in identities:
156
+ raise UnsafeOutputPathError("unsafe output path")
157
+ identities.add(identity)
161
158
 
162
159
 
163
160
  def build_output_plan(files: tuple[ManifestFile, ...], out: Path) -> OutputRootPlan:
@@ -295,17 +292,32 @@ def download_files(
295
292
  secure: bool,
296
293
  read_timeout: float,
297
294
  max_retries: int,
295
+ on_progress: ProgressCallback | None = None,
298
296
  ) -> DownloadReport:
299
297
  output = build_output_plan(files, out)
300
298
  if not output.files:
301
299
  return DownloadReport(downloaded=0, skipped=0, failures=())
300
+ if on_progress is not None:
301
+ on_progress(0, len(output.files))
302
302
  client_provider = HttpClientProvider(parallel=parallel, secure=secure, read_timeout=read_timeout)
303
- worker = partial(
303
+ download_worker = partial(
304
304
  download_one,
305
305
  output=output,
306
306
  client_provider=client_provider,
307
307
  max_retries=max_retries,
308
308
  )
309
+ completed = 0
310
+ progress_lock = Lock()
311
+
312
+ def worker(file: FileOutputPlan) -> tuple[str, str | None]:
313
+ nonlocal completed
314
+ result = download_worker(file)
315
+ if on_progress is not None:
316
+ with progress_lock:
317
+ completed += 1
318
+ on_progress(completed, len(output.files))
319
+ return result
320
+
309
321
  try:
310
322
  with ThreadPoolExecutor(max_workers=parallel) as executor:
311
323
  results = tuple(executor.map(worker, output.files))
@@ -4,7 +4,7 @@ import json
4
4
  import os
5
5
  import re
6
6
  import sys
7
- from dataclasses import dataclass
7
+ from dataclasses import dataclass, replace
8
8
  from pathlib import Path
9
9
  from typing import Any
10
10
  from urllib.parse import urlsplit, urlunsplit
@@ -199,18 +199,17 @@ def parse_manifest(text: str) -> tuple[ManifestFile, ...]:
199
199
  raise ManifestError(f"files[{index}].file_name must be a non-empty string")
200
200
  safe_name = safe_file_name(file_name, index=index)
201
201
  duplicate_key = os.path.normcase(safe_name)
202
- if duplicate_key in destinations:
203
- raise ManifestError(f"files[{index}].file_name resolves to a duplicate target")
204
- destinations.add(duplicate_key)
205
202
  final_claim = claim_parts(safe_name)
206
203
  part_claim = (*final_claim[:-1], f"{final_claim[-1]}.part")
207
204
  instruction_claim = (*final_claim[:-1], f"{final_claim[-1]}.json")
208
- if any(
209
- paths_conflict(claim, existing) for claim in (final_claim, part_claim) for existing in output_claims
210
- ) or any(paths_conflict(instruction_claim, existing) for existing in metadata_claims):
211
- raise ManifestError(f"files[{index}].file_name creates an ambiguous target collision")
212
- output_claims.extend((final_claim, part_claim))
213
- metadata_claims.append(instruction_claim)
205
+ if duplicate_key not in destinations:
206
+ if any(
207
+ paths_conflict(claim, existing) for claim in (final_claim, part_claim) for existing in output_claims
208
+ ) or any(paths_conflict(instruction_claim, existing) for existing in metadata_claims):
209
+ raise ManifestError(f"files[{index}].file_name creates an ambiguous target collision")
210
+ destinations.add(duplicate_key)
211
+ output_claims.extend((final_claim, part_claim))
212
+ metadata_claims.append(instruction_claim)
214
213
  files.append(
215
214
  ManifestFile(
216
215
  download_url=download_url,
@@ -225,6 +224,31 @@ def parse_manifest(text: str) -> tuple[ManifestFile, ...]:
225
224
  meta_data=metadata(item, index=index),
226
225
  )
227
226
  )
227
+ # Reserve every original name before allocating suffixes, independent of disk
228
+ # contents and worker order so the same manifest can resume at the same paths.
229
+ next_suffix: dict[str, int] = {}
230
+ for index, file in enumerate(files):
231
+ key = os.path.normcase(file.file_name)
232
+ if key not in next_suffix:
233
+ next_suffix[key] = 1
234
+ continue
235
+ stem, extension = os.path.splitext(file.file_name)
236
+ suffix = next_suffix[key]
237
+ while True:
238
+ candidate = f"{stem}-{suffix}{extension}"
239
+ suffix += 1
240
+ final_claim = claim_parts(candidate)
241
+ part_claim = (*final_claim[:-1], f"{final_claim[-1]}.part")
242
+ instruction_claim = (*final_claim[:-1], f"{final_claim[-1]}.json")
243
+ if any(
244
+ paths_conflict(claim, existing) for claim in (final_claim, part_claim) for existing in output_claims
245
+ ) or any(paths_conflict(instruction_claim, existing) for existing in metadata_claims):
246
+ continue
247
+ output_claims.extend((final_claim, part_claim))
248
+ metadata_claims.append(instruction_claim)
249
+ files[index] = replace(file, file_name=candidate)
250
+ next_suffix[key] = suffix
251
+ break
228
252
  return tuple(files)
229
253
 
230
254
 
@@ -108,7 +108,7 @@ wheels = [
108
108
 
109
109
  [[package]]
110
110
  name = "dexverse-cli"
111
- version = "0.1.1"
111
+ version = "0.1.3.post1"
112
112
  source = { editable = "." }
113
113
  dependencies = [
114
114
  { name = "av" },
@@ -1 +0,0 @@
1
- __version__ = "0.1.1"