unitlab 3.0.4__tar.gz → 3.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 (46) hide show
  1. {unitlab-3.0.4 → unitlab-3.1.0}/PKG-INFO +149 -3
  2. {unitlab-3.0.4 → unitlab-3.1.0}/README.md +147 -1
  3. {unitlab-3.0.4 → unitlab-3.1.0}/pyproject.toml +2 -2
  4. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/__init__.py +4 -0
  5. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_http.py +14 -2
  6. unitlab-3.1.0/src/unitlab/_nuscenes.py +531 -0
  7. unitlab-3.1.0/src/unitlab/_scene_uploader.py +604 -0
  8. unitlab-3.1.0/src/unitlab/_scenes.py +833 -0
  9. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_uploader.py +152 -17
  10. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/cli.py +198 -1
  11. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/__init__.py +2 -0
  12. unitlab-3.1.0/src/unitlab/resources/annotation_imports.py +182 -0
  13. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/assets.py +69 -17
  14. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/projects.py +70 -13
  15. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/types.py +9 -0
  16. unitlab-3.1.0/tests/test_annotation_imports.py +326 -0
  17. unitlab-3.1.0/tests/test_http.py +243 -0
  18. unitlab-3.1.0/tests/test_nuscenes.py +484 -0
  19. unitlab-3.1.0/tests/test_scene_cli.py +250 -0
  20. unitlab-3.1.0/tests/test_scene_uploads.py +565 -0
  21. unitlab-3.1.0/tests/test_scenes.py +650 -0
  22. unitlab-3.0.4/tests/test_http.py +0 -99
  23. {unitlab-3.0.4 → unitlab-3.1.0}/.gitignore +0 -0
  24. {unitlab-3.0.4 → unitlab-3.1.0}/LICENSE.md +0 -0
  25. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/__main__.py +0 -0
  26. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_config.py +0 -0
  27. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_downloader.py +0 -0
  28. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_grouping.py +0 -0
  29. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/_waiter.py +0 -0
  30. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/client.py +0 -0
  31. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/exceptions.py +0 -0
  32. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/ontology.py +0 -0
  33. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/py.typed +0 -0
  34. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/_base.py +0 -0
  35. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/cloud.py +0 -0
  36. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/datasets.py +0 -0
  37. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/embeddings.py +0 -0
  38. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/ontologies.py +0 -0
  39. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/releases.py +0 -0
  40. {unitlab-3.0.4 → unitlab-3.1.0}/src/unitlab/resources/workflow.py +0 -0
  41. {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_cli.py +0 -0
  42. {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_client.py +0 -0
  43. {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_downloader.py +0 -0
  44. {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_embeddings.py +0 -0
  45. {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_tiled_uploads.py +0 -0
  46. {unitlab-3.0.4 → unitlab-3.1.0}/tests/test_uploader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unitlab
3
- Version: 3.0.4
3
+ Version: 3.1.0
4
4
  Summary: Python SDK for the Unitlab.ai data annotation platform
5
5
  Project-URL: Homepage, https://unitlab.ai
6
6
  Project-URL: Documentation, https://docs.unitlab.ai
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Programming Language :: Python :: 3.14
20
20
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
21
  Requires-Python: >=3.10
22
- Requires-Dist: httpx>=0.27
22
+ Requires-Dist: httpx[socks]>=0.27
23
23
  Requires-Dist: tqdm>=4.60
24
24
  Requires-Dist: typer>=0.9
25
25
  Provides-Extra: dev
@@ -113,7 +113,8 @@ print(batch.uploaded, batch.batch_queue_id)
113
113
 
114
114
  The directory may mix images, video, audio, text, PDFs, DICOM, NIfTI, and NRRD.
115
115
  One call creates one Batch Queue. Call `batch.wait()` when the next operation
116
- depends on server-side processing.
116
+ depends on server-side processing. Lidar scenes are uploaded with
117
+ `generic_type="pointcloud"`; see [Lidar scenes](#lidar-scenes).
117
118
 
118
119
  ### 2. Curate in Assets, publish a dataset, and attach it
119
120
 
@@ -234,6 +235,76 @@ upload directly to storage in bounded parts. Tiled processing can take hours,
234
235
  so use a longer timeout such as `batch.wait(timeout=25200)` when the next
235
236
  operation depends on the generated COG/DZI derivative.
236
237
 
238
+ Video, medical (NIfTI, NRRD, DICOM), audio and PDF files larger than 64 MiB
239
+ also upload directly to storage, up to 500 MiB each; smaller ones keep the
240
+ ordinary upload. A large medical file becomes one volume on its own and is not
241
+ grouped with other files of the batch into a series.
242
+
243
+ DICOM files named without a usable extension (`IM00001`, or a UID) are found by
244
+ their content when a folder is uploaded and are sent as medical; nothing needs
245
+ to be renamed.
246
+
247
+ ### Lidar scenes
248
+
249
+ A lidar scene is one item whatever it is made of. It is uploaded only when the
250
+ type is named; without it `.pcd`, `.ply`, `.bin` and scene `.zip` files are
251
+ skipped. The workspace needs lidar scenes enabled.
252
+
253
+ ```python
254
+ from unitlab import detect_scenes
255
+
256
+ # Look first: what would be uploaded, nothing is sent.
257
+ for scene in detect_scenes("./recordings").scenes:
258
+ print(scene.name, scene.frame_count, scene.point_sensors, scene.cameras)
259
+
260
+ batch = project.upload("./recordings", generic_type="pointcloud")
261
+ batch.wait()
262
+
263
+ for item_id in batch.data_item_ids:
264
+ status = project.scene_status(item_id)
265
+ print(status.status, status.frame_count, status.error)
266
+
267
+ project.retry_scene(item_id) # a failed scene, about a minute after it failed
268
+ ```
269
+
270
+ What `source` may be:
271
+
272
+ - One file: a `.pcd`, `.ply` or `.bin` cloud, or a `.zip` that contains such
273
+ frames or a `scene.json`. Any other `.zip` is refused.
274
+ - A folder with a `scene.json`, or with the point clouds of one sensor
275
+ (`drive_01/lidar/000.pcd`, ...). Image folders whose file names match the
276
+ frames become its cameras (`drive_01/cam_front/000.jpg`).
277
+ - A folder with one subfolder per sensor that share file names. This is read as
278
+ one scene with several sensors; pass `split_sensor_folders=True` when each
279
+ folder is a recording of its own.
280
+ - A folder that holds several such scenes: each one is uploaded as its own
281
+ item. Scene files left over in the folder are one scene each.
282
+ - A nuScenes dataset (the folder with `samples/` and `v1.0-*/`): one scene per
283
+ recording, key frames only, with the calibration taken from its tables.
284
+
285
+ Other files in the folder are not uploaded by this call; upload the folder once
286
+ more without the type for ordinary media. A scene is limited to 20 GiB and
287
+ 1,000 frames, and one data unit is used per frame.
288
+
289
+ `batch.wait()` covers scenes like any other upload. A failed scene is not in
290
+ `project.data_units()`; find it in the Batch Queue (`queue.data()`) and ask
291
+ `project.scene_status(item_id)` for the reason.
292
+
293
+ Scenes go into Assets the same way, and the existing Asset methods apply:
294
+
295
+ ```python
296
+ result = client.assets.upload(
297
+ "./recordings", generic_type="pointcloud", folder="Lidar"
298
+ )
299
+ asset = result.assets[0]
300
+ status = asset.wait() # status.frame_count, status.error
301
+ asset.retry() # a failed scene
302
+ asset = client.assets.asset("ASSET_ID") # a handle by ID, for status and retry
303
+ ```
304
+
305
+ `release.download_files()` writes a scene that was uploaded as a folder under
306
+ `<item file name>/` with the paths it was uploaded with.
307
+
237
308
  ### Data Units and project lifecycle
238
309
 
239
310
  ```python
@@ -345,6 +416,72 @@ batch = project.import_cloud(
345
416
  status = batch.wait()
346
417
  ```
347
418
 
419
+ ### Import annotation JSON
420
+
421
+ Media can come from S3 while the annotation JSON stays on your computer:
422
+
423
+ ```python
424
+ project = client.projects.get("PROJECT_ID")
425
+ media = project.import_cloud("CLOUD_STORAGE_ID", ["images/"])
426
+ status = media.wait()
427
+ if status.failed:
428
+ raise RuntimeError("Some media failed processing; inspect the Batch Queue.")
429
+
430
+ batch = project.annotation_imports.create(
431
+ "./predictions.json",
432
+ source_name="my-model", # Reuse this namespace for the same prediction source.
433
+ )
434
+ print(batch.preview["counts"], batch.preview["issues"])
435
+ # Inspect the preview before saving labels. Approval is a separate review step.
436
+ report = batch.commit()
437
+ print(report)
438
+ ```
439
+
440
+ Supported inputs are COCO JSON, Unitlab exports (UUEF item JSON, Audio JSON and
441
+ GeoJSON, as `.json` or `.geojson`), `unitlab-image/1` and `unitlab-video/1`. They
442
+ cover image, video, medical, document, audio, time series, text, HTML, geospatial
443
+ and pathology items. A UUEF file holds one item: import each file as its own batch.
444
+ Each JSON file is limited to 600 MiB. The backend matches original media paths/IDs,
445
+ with a unique-filename fallback; ambiguous names require an explicit mapping.
446
+ YOLO text/ZIP files and other tools' project files are not accepted by this importer.
447
+
448
+ For COCO video, pass `frame_mapping_file="./frames.json"` in
449
+ `unitlab-frame-map/1` format. Use `track_profile="attributes.track_id"` only for
450
+ COCO exports with that explicit track-ID convention. Import video media at its
451
+ original frame rate; annotations for frames omitted by sampling are rejected.
452
+
453
+ `project.annotation_imports.sources()` lists saved prediction sources, and
454
+ `project.annotation_imports.media(search="road.jpg")` finds media IDs for mappings.
455
+ Pass `source_id=` instead of `source_name=` to reuse a source by ID. Set
456
+ `options={"stable_ids": True}` only when your exporter keeps annotation IDs stable
457
+ between prediction runs. Otherwise the default protects existing objects from
458
+ unconfirmed ID reuse.
459
+
460
+ After processing completes or you supply mappings, call `batch.refresh()` to
461
+ rebuild the preview. `batch.refresh(options={...})` replaces all import options;
462
+ omitting `options` preserves them. Supported options are `stable_ids`,
463
+ `media_mapping`, `class_mapping`, `object_mapping`, and `review_mapping`.
464
+
465
+ `batch.commit()` preserves conflicts and human deletions by default. To resolve
466
+ a specific preview annotation, pass `decisions={annotation_key: "keep"}` (or
467
+ `"incoming"` / `"skip"`). Invalid records prevent the whole commit unless you
468
+ explicitly choose `allow_partial=True`. The report includes created, updated,
469
+ kept, unchanged and skipped counts when applicable. A stale preview raises
470
+ `ConflictError`; inspect a refreshed preview before committing again. If a
471
+ commit response is lost, repeat the same batch's `commit()` with the same
472
+ decisions; the server returns the saved report without duplicating labels.
473
+
474
+ For annotations already in connected storage, omit the local file and pass
475
+ `cloud_storage=storage`, `annotation_key="labels/predictions.json"`, and optionally
476
+ `frame_mapping_key="labels/frames.json"`. Keys are relative to that connection's
477
+ prefix. Media and annotations must resolve within the API key's workspace and
478
+ the selected project.
479
+
480
+ This requires a backend with the `/api/sdk/projects/{id}/annotation-imports/`
481
+ routes and the annotation-import database migration installed. Commit requires
482
+ a user-created API key so saved labels have an audit author. Legacy keys without
483
+ a creator can preview but receive `PermissionDeniedError` when committing.
484
+
348
485
  ### Data Groups
349
486
 
350
487
  Let Unitlab suggest a filename pattern:
@@ -423,6 +560,12 @@ unitlab project sources PROJECT_ID
423
560
  unitlab project detach-source PROJECT_ID SOURCE_LINK_ID --preview
424
561
  unitlab project upload PROJECT_ID --source ./data
425
562
  unitlab project upload PROJECT_ID --source ./maps
563
+ unitlab project upload PROJECT_ID --source ./recordings --type pointcloud --dry-run
564
+ unitlab project upload PROJECT_ID --source ./recordings --type pointcloud
565
+ unitlab project upload PROJECT_ID --source ./rig --type pointcloud \
566
+ --split-sensor-folders
567
+ unitlab project scene-status PROJECT_ID ITEM_ID
568
+ unitlab project scene-retry PROJECT_ID ITEM_ID
426
569
  unitlab batch-queue list PROJECT_ID
427
570
  unitlab batch-queue detail PROJECT_ID BATCH_QUEUE_ID
428
571
  unitlab batch-queue status PROJECT_ID BATCH_QUEUE_ID
@@ -438,6 +581,9 @@ unitlab project attach PROJECT_ID --dataset-version DATASET_ID:2
438
581
  # Assets
439
582
  unitlab assets upload ./data --folder "Raw data" --tag incoming
440
583
  unitlab assets upload ./slides --folder "Slides"
584
+ unitlab assets upload ./recordings --folder "Lidar" --type pointcloud
585
+ unitlab assets status ASSET_ID
586
+ unitlab assets retry ASSET_ID
441
587
 
442
588
  # Folders
443
589
  unitlab folders create "Raw data"
@@ -80,7 +80,8 @@ print(batch.uploaded, batch.batch_queue_id)
80
80
 
81
81
  The directory may mix images, video, audio, text, PDFs, DICOM, NIfTI, and NRRD.
82
82
  One call creates one Batch Queue. Call `batch.wait()` when the next operation
83
- depends on server-side processing.
83
+ depends on server-side processing. Lidar scenes are uploaded with
84
+ `generic_type="pointcloud"`; see [Lidar scenes](#lidar-scenes).
84
85
 
85
86
  ### 2. Curate in Assets, publish a dataset, and attach it
86
87
 
@@ -201,6 +202,76 @@ upload directly to storage in bounded parts. Tiled processing can take hours,
201
202
  so use a longer timeout such as `batch.wait(timeout=25200)` when the next
202
203
  operation depends on the generated COG/DZI derivative.
203
204
 
205
+ Video, medical (NIfTI, NRRD, DICOM), audio and PDF files larger than 64 MiB
206
+ also upload directly to storage, up to 500 MiB each; smaller ones keep the
207
+ ordinary upload. A large medical file becomes one volume on its own and is not
208
+ grouped with other files of the batch into a series.
209
+
210
+ DICOM files named without a usable extension (`IM00001`, or a UID) are found by
211
+ their content when a folder is uploaded and are sent as medical; nothing needs
212
+ to be renamed.
213
+
214
+ ### Lidar scenes
215
+
216
+ A lidar scene is one item whatever it is made of. It is uploaded only when the
217
+ type is named; without it `.pcd`, `.ply`, `.bin` and scene `.zip` files are
218
+ skipped. The workspace needs lidar scenes enabled.
219
+
220
+ ```python
221
+ from unitlab import detect_scenes
222
+
223
+ # Look first: what would be uploaded, nothing is sent.
224
+ for scene in detect_scenes("./recordings").scenes:
225
+ print(scene.name, scene.frame_count, scene.point_sensors, scene.cameras)
226
+
227
+ batch = project.upload("./recordings", generic_type="pointcloud")
228
+ batch.wait()
229
+
230
+ for item_id in batch.data_item_ids:
231
+ status = project.scene_status(item_id)
232
+ print(status.status, status.frame_count, status.error)
233
+
234
+ project.retry_scene(item_id) # a failed scene, about a minute after it failed
235
+ ```
236
+
237
+ What `source` may be:
238
+
239
+ - One file: a `.pcd`, `.ply` or `.bin` cloud, or a `.zip` that contains such
240
+ frames or a `scene.json`. Any other `.zip` is refused.
241
+ - A folder with a `scene.json`, or with the point clouds of one sensor
242
+ (`drive_01/lidar/000.pcd`, ...). Image folders whose file names match the
243
+ frames become its cameras (`drive_01/cam_front/000.jpg`).
244
+ - A folder with one subfolder per sensor that share file names. This is read as
245
+ one scene with several sensors; pass `split_sensor_folders=True` when each
246
+ folder is a recording of its own.
247
+ - A folder that holds several such scenes: each one is uploaded as its own
248
+ item. Scene files left over in the folder are one scene each.
249
+ - A nuScenes dataset (the folder with `samples/` and `v1.0-*/`): one scene per
250
+ recording, key frames only, with the calibration taken from its tables.
251
+
252
+ Other files in the folder are not uploaded by this call; upload the folder once
253
+ more without the type for ordinary media. A scene is limited to 20 GiB and
254
+ 1,000 frames, and one data unit is used per frame.
255
+
256
+ `batch.wait()` covers scenes like any other upload. A failed scene is not in
257
+ `project.data_units()`; find it in the Batch Queue (`queue.data()`) and ask
258
+ `project.scene_status(item_id)` for the reason.
259
+
260
+ Scenes go into Assets the same way, and the existing Asset methods apply:
261
+
262
+ ```python
263
+ result = client.assets.upload(
264
+ "./recordings", generic_type="pointcloud", folder="Lidar"
265
+ )
266
+ asset = result.assets[0]
267
+ status = asset.wait() # status.frame_count, status.error
268
+ asset.retry() # a failed scene
269
+ asset = client.assets.asset("ASSET_ID") # a handle by ID, for status and retry
270
+ ```
271
+
272
+ `release.download_files()` writes a scene that was uploaded as a folder under
273
+ `<item file name>/` with the paths it was uploaded with.
274
+
204
275
  ### Data Units and project lifecycle
205
276
 
206
277
  ```python
@@ -312,6 +383,72 @@ batch = project.import_cloud(
312
383
  status = batch.wait()
313
384
  ```
314
385
 
386
+ ### Import annotation JSON
387
+
388
+ Media can come from S3 while the annotation JSON stays on your computer:
389
+
390
+ ```python
391
+ project = client.projects.get("PROJECT_ID")
392
+ media = project.import_cloud("CLOUD_STORAGE_ID", ["images/"])
393
+ status = media.wait()
394
+ if status.failed:
395
+ raise RuntimeError("Some media failed processing; inspect the Batch Queue.")
396
+
397
+ batch = project.annotation_imports.create(
398
+ "./predictions.json",
399
+ source_name="my-model", # Reuse this namespace for the same prediction source.
400
+ )
401
+ print(batch.preview["counts"], batch.preview["issues"])
402
+ # Inspect the preview before saving labels. Approval is a separate review step.
403
+ report = batch.commit()
404
+ print(report)
405
+ ```
406
+
407
+ Supported inputs are COCO JSON, Unitlab exports (UUEF item JSON, Audio JSON and
408
+ GeoJSON, as `.json` or `.geojson`), `unitlab-image/1` and `unitlab-video/1`. They
409
+ cover image, video, medical, document, audio, time series, text, HTML, geospatial
410
+ and pathology items. A UUEF file holds one item: import each file as its own batch.
411
+ Each JSON file is limited to 600 MiB. The backend matches original media paths/IDs,
412
+ with a unique-filename fallback; ambiguous names require an explicit mapping.
413
+ YOLO text/ZIP files and other tools' project files are not accepted by this importer.
414
+
415
+ For COCO video, pass `frame_mapping_file="./frames.json"` in
416
+ `unitlab-frame-map/1` format. Use `track_profile="attributes.track_id"` only for
417
+ COCO exports with that explicit track-ID convention. Import video media at its
418
+ original frame rate; annotations for frames omitted by sampling are rejected.
419
+
420
+ `project.annotation_imports.sources()` lists saved prediction sources, and
421
+ `project.annotation_imports.media(search="road.jpg")` finds media IDs for mappings.
422
+ Pass `source_id=` instead of `source_name=` to reuse a source by ID. Set
423
+ `options={"stable_ids": True}` only when your exporter keeps annotation IDs stable
424
+ between prediction runs. Otherwise the default protects existing objects from
425
+ unconfirmed ID reuse.
426
+
427
+ After processing completes or you supply mappings, call `batch.refresh()` to
428
+ rebuild the preview. `batch.refresh(options={...})` replaces all import options;
429
+ omitting `options` preserves them. Supported options are `stable_ids`,
430
+ `media_mapping`, `class_mapping`, `object_mapping`, and `review_mapping`.
431
+
432
+ `batch.commit()` preserves conflicts and human deletions by default. To resolve
433
+ a specific preview annotation, pass `decisions={annotation_key: "keep"}` (or
434
+ `"incoming"` / `"skip"`). Invalid records prevent the whole commit unless you
435
+ explicitly choose `allow_partial=True`. The report includes created, updated,
436
+ kept, unchanged and skipped counts when applicable. A stale preview raises
437
+ `ConflictError`; inspect a refreshed preview before committing again. If a
438
+ commit response is lost, repeat the same batch's `commit()` with the same
439
+ decisions; the server returns the saved report without duplicating labels.
440
+
441
+ For annotations already in connected storage, omit the local file and pass
442
+ `cloud_storage=storage`, `annotation_key="labels/predictions.json"`, and optionally
443
+ `frame_mapping_key="labels/frames.json"`. Keys are relative to that connection's
444
+ prefix. Media and annotations must resolve within the API key's workspace and
445
+ the selected project.
446
+
447
+ This requires a backend with the `/api/sdk/projects/{id}/annotation-imports/`
448
+ routes and the annotation-import database migration installed. Commit requires
449
+ a user-created API key so saved labels have an audit author. Legacy keys without
450
+ a creator can preview but receive `PermissionDeniedError` when committing.
451
+
315
452
  ### Data Groups
316
453
 
317
454
  Let Unitlab suggest a filename pattern:
@@ -390,6 +527,12 @@ unitlab project sources PROJECT_ID
390
527
  unitlab project detach-source PROJECT_ID SOURCE_LINK_ID --preview
391
528
  unitlab project upload PROJECT_ID --source ./data
392
529
  unitlab project upload PROJECT_ID --source ./maps
530
+ unitlab project upload PROJECT_ID --source ./recordings --type pointcloud --dry-run
531
+ unitlab project upload PROJECT_ID --source ./recordings --type pointcloud
532
+ unitlab project upload PROJECT_ID --source ./rig --type pointcloud \
533
+ --split-sensor-folders
534
+ unitlab project scene-status PROJECT_ID ITEM_ID
535
+ unitlab project scene-retry PROJECT_ID ITEM_ID
393
536
  unitlab batch-queue list PROJECT_ID
394
537
  unitlab batch-queue detail PROJECT_ID BATCH_QUEUE_ID
395
538
  unitlab batch-queue status PROJECT_ID BATCH_QUEUE_ID
@@ -405,6 +548,9 @@ unitlab project attach PROJECT_ID --dataset-version DATASET_ID:2
405
548
  # Assets
406
549
  unitlab assets upload ./data --folder "Raw data" --tag incoming
407
550
  unitlab assets upload ./slides --folder "Slides"
551
+ unitlab assets upload ./recordings --folder "Lidar" --type pointcloud
552
+ unitlab assets status ASSET_ID
553
+ unitlab assets retry ASSET_ID
408
554
 
409
555
  # Folders
410
556
  unitlab folders create "Raw data"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "unitlab"
7
- version = "3.0.4"
7
+ version = "3.1.0"
8
8
  description = "Python SDK for the Unitlab.ai data annotation platform"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
24
24
  ]
25
25
  dependencies = [
26
- "httpx>=0.27",
26
+ "httpx[socks]>=0.27",
27
27
  "tqdm>=4.60",
28
28
  "typer>=0.9",
29
29
  ]
@@ -1,6 +1,7 @@
1
1
  from importlib.metadata import version
2
2
 
3
3
  from ._grouping import tiles_from_template
4
+ from ._scene_uploader import detect_scenes
4
5
  from .client import UnitlabClient
5
6
  from .exceptions import (
6
7
  AmbiguousUploadCompletionError,
@@ -26,6 +27,7 @@ from .ontology import (
26
27
  TextAttribute,
27
28
  )
28
29
  from .resources import (
30
+ AnnotationImport,
29
31
  Asset,
30
32
  AttachedSource,
31
33
  BatchQueue,
@@ -58,6 +60,7 @@ __version__ = version("unitlab")
58
60
 
59
61
  __all__ = [
60
62
  "AmbiguousUploadCompletionError",
63
+ "AnnotationImport",
61
64
  "Asset",
62
65
  "AssetUploadResult",
63
66
  "AttachPreview",
@@ -102,5 +105,6 @@ __all__ = [
102
105
  "WorkflowStage",
103
106
  "WorkflowTask",
104
107
  "__version__",
108
+ "detect_scenes",
105
109
  "tiles_from_template",
106
110
  ]
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ import time
4
5
  from typing import Any
5
6
 
6
7
  import httpx
@@ -16,6 +17,11 @@ from .exceptions import (
16
17
  ValidationError,
17
18
  )
18
19
 
20
+ # Match HTTPTransport(retries=3): one initial attempt, then three retries for
21
+ # connection failures only. Leave transport construction to HTTPX so that its
22
+ # standard HTTP_PROXY/HTTPS_PROXY/ALL_PROXY and NO_PROXY handling stays active.
23
+ _CONNECT_RETRY_DELAYS = (0.0, 0.5, 1.0)
24
+
19
25
 
20
26
  def _extract_error_message(response: httpx.Response) -> str:
21
27
  try:
@@ -90,7 +96,6 @@ class HttpApi:
90
96
  self.client = httpx.Client(
91
97
  base_url=api_url,
92
98
  headers={"Authorization": f"Api-Key {api_key}"},
93
- transport=httpx.HTTPTransport(retries=3),
94
99
  timeout=60.0,
95
100
  )
96
101
 
@@ -124,7 +129,14 @@ class HttpApi:
124
129
  }
125
130
  if timeout is not None:
126
131
  kwargs["timeout"] = timeout
127
- response = self.client.request(method, endpoint, **kwargs)
132
+ for attempt in range(len(_CONNECT_RETRY_DELAYS) + 1):
133
+ try:
134
+ response = self.client.request(method, endpoint, **kwargs)
135
+ break
136
+ except (httpx.ConnectError, httpx.ConnectTimeout):
137
+ if attempt == len(_CONNECT_RETRY_DELAYS):
138
+ raise
139
+ time.sleep(_CONNECT_RETRY_DELAYS[attempt])
128
140
  except httpx.TimeoutException as exc:
129
141
  raise RequestTimeoutError(str(exc), exc) from exc
130
142
  except httpx.HTTPError as exc: