x4d-devkit 0.48.0rc4__tar.gz → 0.48.0rc5__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.
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/PKG-INFO +1 -1
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/pyproject.toml +1 -1
- x4d_devkit-0.48.0rc5/tests/test_image_export.py +389 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_sdk_api.py +1 -1
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/cli.py +17 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/__init__.py +2 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/client.py +2 -0
- x4d_devkit-0.48.0rc5/x4d_devkit/client/image_exports.py +141 -0
- x4d_devkit-0.48.0rc5/x4d_devkit/image_export.py +606 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/PKG-INFO +1 -1
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/SOURCES.txt +3 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/LICENSE +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/README.md +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/setup.cfg +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_box.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_category_audit.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_clean_basemap_client.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_clip.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_geometry.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_preannotation.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_reference.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_work_session.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_loader.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_matching.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_models.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_preannotation_client.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_roadmark_preview.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_independent.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_source_attributes.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_token.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_transform.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_validation.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_vector_map.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_vector_map_geometry.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_vector_map_session.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_work_session_validation_reuse.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/_image_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/audit.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_clips.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_preannotations.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_work_sessions.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/preannotation.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/reconstruction.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_clip.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_geometry.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_preannotation.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_reference.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_schema.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_work_session.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/roadmark_preview.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/vector_map.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/vector_map_geometry.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "x4d-devkit"
|
|
7
|
-
version = "0.48.
|
|
7
|
+
version = "0.48.0rc5"
|
|
8
8
|
description = "Official X-4D external SDK and CLI for datasets, platform APIs, training bridges, and automation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
"""Synthetic offline export and typed-client tests; no platform or private media."""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
import json
|
|
6
|
+
import tarfile
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from tests.image_fixtures import IMAGE_BYTES, clip_payload, line_object
|
|
12
|
+
from x4d_devkit import X4DClient
|
|
13
|
+
from x4d_devkit._image_contract import ImageContractError, canonical
|
|
14
|
+
from x4d_devkit.image_clip import build_image_identity
|
|
15
|
+
from x4d_devkit.image_export import (
|
|
16
|
+
COMPONENTS,
|
|
17
|
+
DOWNLOAD_CONTRACT,
|
|
18
|
+
EXPORT_CONTRACT,
|
|
19
|
+
EXPORT_README,
|
|
20
|
+
INDEX_CONTRACT,
|
|
21
|
+
JOB_CONTRACT,
|
|
22
|
+
PAGE_CONTRACT,
|
|
23
|
+
PREVIEW_CONTRACT,
|
|
24
|
+
clip_manifest_entry,
|
|
25
|
+
human_scope_index,
|
|
26
|
+
parse_image_export_job,
|
|
27
|
+
reference_path,
|
|
28
|
+
sha256,
|
|
29
|
+
validate_image_export_bundle,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
EXPORT_ID = "imexp_" + "a" * 32
|
|
33
|
+
NOW = "2026-10-26T12:00:00+00:00"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def contents(*, references=False, state="unprocessed"):
|
|
37
|
+
clip = clip_payload()
|
|
38
|
+
if state != "confirmed_empty":
|
|
39
|
+
clip["image_annotation"]["objects"] = [line_object()]
|
|
40
|
+
clip["image_annotation"]["completion"][0]["state"] = state
|
|
41
|
+
clip["identity"] = build_image_identity(clip)
|
|
42
|
+
cid = clip["meta"]["clip_id"]
|
|
43
|
+
files = {f"clips/{cid}/{name}.json": canonical(clip[name]) for name in COMPONENTS}
|
|
44
|
+
files[f"consumption/{cid}/human_scopes.json"] = canonical(human_scope_index(clip))
|
|
45
|
+
if references:
|
|
46
|
+
for view in clip["meta"]["view_ids"]:
|
|
47
|
+
files[reference_path(cid, view)] = canonical(
|
|
48
|
+
{
|
|
49
|
+
"contract_version": "x4d-image-preannotation-current-v1",
|
|
50
|
+
"project_id": clip["meta"]["project_id"],
|
|
51
|
+
"clip_id": cid,
|
|
52
|
+
"view_id": view,
|
|
53
|
+
"source_revision": clip["identity"]["source_revision"],
|
|
54
|
+
"schema_fingerprint": clip["identity"]["schema_fingerprint"],
|
|
55
|
+
"availability": "none",
|
|
56
|
+
"published_at": None,
|
|
57
|
+
"result_digest": None,
|
|
58
|
+
"artifact": None,
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
for image in clip["sample_data"]:
|
|
62
|
+
files[f"clips/{cid}/{image['asset']['path']}"] = IMAGE_BYTES
|
|
63
|
+
return clip, files
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def bundle(tmp_path, clip, files, *, references=False, append=None, override=None):
|
|
67
|
+
cid = clip["meta"]["clip_id"]
|
|
68
|
+
index = canonical(
|
|
69
|
+
{
|
|
70
|
+
"contract_version": INDEX_CONTRACT,
|
|
71
|
+
"clip_id": cid,
|
|
72
|
+
"files": [
|
|
73
|
+
{"path": name, "size": len(raw), "sha256": sha256(raw)}
|
|
74
|
+
for name, raw in files.items()
|
|
75
|
+
],
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
manifest = {
|
|
79
|
+
"contract_version": EXPORT_CONTRACT,
|
|
80
|
+
"export_id": EXPORT_ID,
|
|
81
|
+
"project_id": clip["meta"]["project_id"],
|
|
82
|
+
"created_at": NOW,
|
|
83
|
+
"include_preannotations": references,
|
|
84
|
+
"clips": [
|
|
85
|
+
clip_manifest_entry(clip, captured_at=NOW, file_index_sha256=sha256(index))
|
|
86
|
+
],
|
|
87
|
+
}
|
|
88
|
+
entries = {
|
|
89
|
+
"manifest.json": canonical(manifest),
|
|
90
|
+
"README.md": EXPORT_README,
|
|
91
|
+
f"indexes/{cid}.json": index,
|
|
92
|
+
**files,
|
|
93
|
+
}
|
|
94
|
+
path = tmp_path / "export.tar"
|
|
95
|
+
with tarfile.open(path, "w") as archive:
|
|
96
|
+
for name, raw in entries.items():
|
|
97
|
+
info = tarfile.TarInfo(f"{EXPORT_ID}/{name}")
|
|
98
|
+
if override:
|
|
99
|
+
raw = override(name, raw)
|
|
100
|
+
info.size = len(raw)
|
|
101
|
+
archive.addfile(info, BytesIO(raw))
|
|
102
|
+
if append:
|
|
103
|
+
info = tarfile.TarInfo(append)
|
|
104
|
+
info.size = 1
|
|
105
|
+
archive.addfile(info, BytesIO(b"x"))
|
|
106
|
+
return path
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.mark.parametrize("state", ["unprocessed", "complete", "confirmed_empty"])
|
|
110
|
+
@pytest.mark.parametrize("references", [False, True])
|
|
111
|
+
def test_roundtrip_preserves_native_identity_and_explicit_completion(
|
|
112
|
+
tmp_path, state, references
|
|
113
|
+
):
|
|
114
|
+
clip, files = contents(state=state, references=references)
|
|
115
|
+
result = validate_image_export_bundle(
|
|
116
|
+
bundle(tmp_path, clip, files, references=references)
|
|
117
|
+
)
|
|
118
|
+
assert result["images"] == 1
|
|
119
|
+
assert result["completion_counts"][state] >= 1
|
|
120
|
+
assert result["objects"] == (state != "confirmed_empty")
|
|
121
|
+
if state == "unprocessed":
|
|
122
|
+
assert result["completion_counts"]["complete"] == 0
|
|
123
|
+
assert result["completion_counts"]["confirmed_empty"] == 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.mark.parametrize(
|
|
127
|
+
"change",
|
|
128
|
+
[
|
|
129
|
+
"scope",
|
|
130
|
+
"scope-extra",
|
|
131
|
+
"missing",
|
|
132
|
+
"path",
|
|
133
|
+
"duplicate",
|
|
134
|
+
"identity",
|
|
135
|
+
"prediction-context",
|
|
136
|
+
"extra",
|
|
137
|
+
],
|
|
138
|
+
)
|
|
139
|
+
def test_fail_closed_even_when_attacker_recomputes_transport_hashes(tmp_path, change):
|
|
140
|
+
clip, files = contents(references=True)
|
|
141
|
+
cid = clip["meta"]["clip_id"]
|
|
142
|
+
scope = f"consumption/{cid}/human_scopes.json"
|
|
143
|
+
append = None
|
|
144
|
+
if change.startswith("scope"):
|
|
145
|
+
value = json.loads(files[scope])
|
|
146
|
+
if change == "scope":
|
|
147
|
+
value["scopes"][0]["state"] = "complete"
|
|
148
|
+
else:
|
|
149
|
+
value["unexpected"] = True
|
|
150
|
+
files[scope] = canonical(value)
|
|
151
|
+
if change == "missing":
|
|
152
|
+
files.pop(scope)
|
|
153
|
+
if change == "path":
|
|
154
|
+
files["../escape"] = b"x"
|
|
155
|
+
if change == "duplicate":
|
|
156
|
+
append = f"{EXPORT_ID}/{scope}"
|
|
157
|
+
if change == "identity":
|
|
158
|
+
value = json.loads(files[f"clips/{cid}/identity.json"])
|
|
159
|
+
value["annotation_revision"] = "sha256:" + "0" * 64
|
|
160
|
+
files[f"clips/{cid}/identity.json"] = canonical(value)
|
|
161
|
+
if change == "prediction-context":
|
|
162
|
+
name = reference_path(cid, clip["meta"]["view_ids"][0])
|
|
163
|
+
value = json.loads(files[name])
|
|
164
|
+
value["source_revision"] = "sha256:" + "0" * 64
|
|
165
|
+
files[name] = canonical(value)
|
|
166
|
+
if change == "extra":
|
|
167
|
+
files[f"clips/{cid}/password.json"] = b"{}"
|
|
168
|
+
with pytest.raises((ImageContractError, ValueError)):
|
|
169
|
+
validate_image_export_bundle(
|
|
170
|
+
bundle(tmp_path, clip, files, references=True, append=append)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_corrupt_bytes_and_budget_are_rejected_without_extraction(tmp_path):
|
|
175
|
+
clip, files = contents()
|
|
176
|
+
path = bundle(
|
|
177
|
+
tmp_path,
|
|
178
|
+
clip,
|
|
179
|
+
files,
|
|
180
|
+
override=lambda name, raw: b"z" * len(raw) if name.endswith(".jpg") else raw,
|
|
181
|
+
)
|
|
182
|
+
with pytest.raises(ImageContractError, match="SHA256"):
|
|
183
|
+
validate_image_export_bundle(path)
|
|
184
|
+
path = bundle(tmp_path, clip, files)
|
|
185
|
+
with pytest.raises(ImageContractError, match="budget"):
|
|
186
|
+
validate_image_export_bundle(path, max_json_bytes=10)
|
|
187
|
+
assert list(tmp_path.iterdir()) == [path]
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def job(status="queued"):
|
|
191
|
+
completed = status == "completed"
|
|
192
|
+
terminal = status in {"completed", "failed", "cancelled"}
|
|
193
|
+
return {
|
|
194
|
+
"contract_version": JOB_CONTRACT,
|
|
195
|
+
"export_id": EXPORT_ID,
|
|
196
|
+
"project_id": 7,
|
|
197
|
+
"clip_ids": ["clip_test"],
|
|
198
|
+
"include_preannotations": False,
|
|
199
|
+
"status": status,
|
|
200
|
+
"progress": {
|
|
201
|
+
"stage": status,
|
|
202
|
+
"processed": 0,
|
|
203
|
+
"total": 1,
|
|
204
|
+
"bytes": 0,
|
|
205
|
+
"elapsed_s": 0.0,
|
|
206
|
+
"rate": 0.0,
|
|
207
|
+
"eta_s": None,
|
|
208
|
+
},
|
|
209
|
+
"error": {"code": "test_failure", "message": "Explicit failure"}
|
|
210
|
+
if status == "failed"
|
|
211
|
+
else None,
|
|
212
|
+
"artifact": {"filename": EXPORT_ID + ".tar", "size": 10240, "sha256": "b" * 64}
|
|
213
|
+
if completed
|
|
214
|
+
else None,
|
|
215
|
+
"created_at": NOW,
|
|
216
|
+
"completed_at": NOW if terminal else None,
|
|
217
|
+
"can_cancel": not terminal,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@pytest.mark.parametrize(
|
|
222
|
+
"status",
|
|
223
|
+
[
|
|
224
|
+
"queued",
|
|
225
|
+
"capturing",
|
|
226
|
+
"packaging",
|
|
227
|
+
"validating",
|
|
228
|
+
"uploading",
|
|
229
|
+
"completed",
|
|
230
|
+
"failed",
|
|
231
|
+
"cancelled",
|
|
232
|
+
],
|
|
233
|
+
)
|
|
234
|
+
def test_job_states_are_strict(status):
|
|
235
|
+
assert parse_image_export_job(job(status)).to_dict()["status"] == status
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@pytest.mark.parametrize(
|
|
239
|
+
"change",
|
|
240
|
+
["error", "artifact", "processed", "bool", "unknown", "terminal", "infinite"],
|
|
241
|
+
)
|
|
242
|
+
def test_invalid_job_states_fail(change):
|
|
243
|
+
value = job()
|
|
244
|
+
if change == "error":
|
|
245
|
+
value["error"] = {"code": "oops", "message": "not failed"}
|
|
246
|
+
if change == "artifact":
|
|
247
|
+
value["artifact"] = job("completed")["artifact"]
|
|
248
|
+
if change == "processed":
|
|
249
|
+
value["progress"]["processed"] = 2
|
|
250
|
+
if change == "bool":
|
|
251
|
+
value["project_id"] = True
|
|
252
|
+
if change == "unknown":
|
|
253
|
+
value["model_version"] = "forbidden"
|
|
254
|
+
if change == "terminal":
|
|
255
|
+
value["completed_at"] = NOW
|
|
256
|
+
if change == "infinite":
|
|
257
|
+
value["progress"]["rate"] = float("inf")
|
|
258
|
+
with pytest.raises((ImageContractError, ValueError)):
|
|
259
|
+
parse_image_export_job(value)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_typed_client_scope_and_ephemeral_download_do_not_forward_credentials():
|
|
263
|
+
calls = []
|
|
264
|
+
|
|
265
|
+
def handler(request):
|
|
266
|
+
calls.append(request)
|
|
267
|
+
path = request.url.path
|
|
268
|
+
if path.endswith("/preview"):
|
|
269
|
+
return httpx.Response(
|
|
270
|
+
200,
|
|
271
|
+
json={
|
|
272
|
+
"contract_version": PREVIEW_CONTRACT,
|
|
273
|
+
"project_id": 7,
|
|
274
|
+
"clip_id": "clip_test",
|
|
275
|
+
"include_preannotations": False,
|
|
276
|
+
"clip_ids": ["clip_test"],
|
|
277
|
+
"excluded": [],
|
|
278
|
+
"scope_token": "opaque-token",
|
|
279
|
+
"expires_at": NOW,
|
|
280
|
+
},
|
|
281
|
+
)
|
|
282
|
+
if path.endswith("/download-link"):
|
|
283
|
+
return httpx.Response(
|
|
284
|
+
200,
|
|
285
|
+
json={
|
|
286
|
+
"contract_version": DOWNLOAD_CONTRACT,
|
|
287
|
+
"export_id": EXPORT_ID,
|
|
288
|
+
"url": "http://storage.test/bucket/object?signature=private",
|
|
289
|
+
"expires_at": NOW,
|
|
290
|
+
**job("completed")["artifact"],
|
|
291
|
+
},
|
|
292
|
+
)
|
|
293
|
+
if path == "/api/v1/image-exports" and request.method == "GET":
|
|
294
|
+
return httpx.Response(
|
|
295
|
+
200,
|
|
296
|
+
json={
|
|
297
|
+
"contract_version": PAGE_CONTRACT,
|
|
298
|
+
"project_id": 7,
|
|
299
|
+
"page": 1,
|
|
300
|
+
"page_size": 50,
|
|
301
|
+
"total": 1,
|
|
302
|
+
"items": [job()],
|
|
303
|
+
},
|
|
304
|
+
)
|
|
305
|
+
return httpx.Response(200, json=job())
|
|
306
|
+
|
|
307
|
+
client = X4DClient("http://platform.test", token="platform-private-token")
|
|
308
|
+
client._http = httpx.Client(
|
|
309
|
+
base_url="http://platform.test",
|
|
310
|
+
headers={"Authorization": "Bearer platform-private-token"},
|
|
311
|
+
transport=httpx.MockTransport(handler),
|
|
312
|
+
)
|
|
313
|
+
preview = client.image_exports.preview(project_id=7, clip_id="clip_test")
|
|
314
|
+
created = client.image_exports.create(
|
|
315
|
+
project_id=7,
|
|
316
|
+
clip_id="clip_test",
|
|
317
|
+
scope_token=preview.to_dict()["scope_token"],
|
|
318
|
+
idempotency_key="key",
|
|
319
|
+
)
|
|
320
|
+
assert created.to_dict()["export_id"] == EXPORT_ID
|
|
321
|
+
assert client.image_exports.list(project_id=7).to_dict()["total"] == 1
|
|
322
|
+
for action in ("get", "cancel", "retry"):
|
|
323
|
+
assert (
|
|
324
|
+
getattr(client.image_exports, action)(EXPORT_ID).to_dict()["export_id"]
|
|
325
|
+
== EXPORT_ID
|
|
326
|
+
)
|
|
327
|
+
assert (
|
|
328
|
+
client.image_exports.download_link(EXPORT_ID)
|
|
329
|
+
.to_dict()["url"]
|
|
330
|
+
.startswith("http://storage.test/")
|
|
331
|
+
)
|
|
332
|
+
assert all(request.url.host == "platform.test" for request in calls)
|
|
333
|
+
assert json.loads(calls[1].content) == {
|
|
334
|
+
"project_id": 7,
|
|
335
|
+
"clip_id": "clip_test",
|
|
336
|
+
"include_preannotations": False,
|
|
337
|
+
"scope_token": "opaque-token",
|
|
338
|
+
"idempotency_key": "key",
|
|
339
|
+
}
|
|
340
|
+
with pytest.raises(ImageContractError):
|
|
341
|
+
client.image_exports.preview(project_id=True)
|
|
342
|
+
client.close()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_client_rejects_foreign_response():
|
|
346
|
+
client = X4DClient("http://platform.test")
|
|
347
|
+
other = deepcopy(job())
|
|
348
|
+
other["export_id"] = "imexp_" + "c" * 32
|
|
349
|
+
client._http = httpx.Client(
|
|
350
|
+
base_url="http://platform.test",
|
|
351
|
+
transport=httpx.MockTransport(lambda r: httpx.Response(200, json=other)),
|
|
352
|
+
)
|
|
353
|
+
with pytest.raises(ImageContractError, match="foreign"):
|
|
354
|
+
client.image_exports.get(EXPORT_ID)
|
|
355
|
+
client.close()
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_offline_cli_reports_valid_bundle(tmp_path, capsys):
|
|
359
|
+
from types import SimpleNamespace
|
|
360
|
+
from x4d_devkit.cli import cmd_validate_image_export
|
|
361
|
+
|
|
362
|
+
clip, files = contents()
|
|
363
|
+
path = bundle(tmp_path, clip, files)
|
|
364
|
+
cmd_validate_image_export(
|
|
365
|
+
SimpleNamespace(path=path, max_json_bytes=64 * 1024 * 1024)
|
|
366
|
+
)
|
|
367
|
+
assert json.loads(capsys.readouterr().out)["ok"] is True
|
|
368
|
+
path.write_bytes(b"invalid tar")
|
|
369
|
+
with pytest.raises(SystemExit) as error:
|
|
370
|
+
cmd_validate_image_export(
|
|
371
|
+
SimpleNamespace(path=path, max_json_bytes=64 * 1024 * 1024)
|
|
372
|
+
)
|
|
373
|
+
assert error.value.code == 1
|
|
374
|
+
assert json.loads(capsys.readouterr().out)["ok"] is False
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def test_archive_member_history_is_not_retained(tmp_path, monkeypatch):
|
|
378
|
+
clip, files = contents()
|
|
379
|
+
path = bundle(tmp_path, clip, files)
|
|
380
|
+
original = tarfile.TarFile.next
|
|
381
|
+
history = []
|
|
382
|
+
|
|
383
|
+
def measured(archive):
|
|
384
|
+
history.append(len(archive.members))
|
|
385
|
+
return original(archive)
|
|
386
|
+
|
|
387
|
+
monkeypatch.setattr(tarfile.TarFile, "next", measured)
|
|
388
|
+
validate_image_export_bundle(path)
|
|
389
|
+
assert max(history) <= 1
|
|
@@ -11,7 +11,7 @@ from x4d_devkit.eval.matching import MetricData
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def test_package_version_is_current_release():
|
|
14
|
-
assert x4d_devkit.__version__ == "0.48.
|
|
14
|
+
assert x4d_devkit.__version__ == "0.48.0rc5"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
|
|
@@ -414,6 +414,18 @@ def cmd_login(args):
|
|
|
414
414
|
print(f"Logged in as {username}; token saved to {path}")
|
|
415
415
|
|
|
416
416
|
|
|
417
|
+
def cmd_validate_image_export(args):
|
|
418
|
+
import tarfile
|
|
419
|
+
from x4d_devkit.image_export import validate_image_export_bundle
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
summary = validate_image_export_bundle(args.path, max_json_bytes=args.max_json_bytes)
|
|
423
|
+
except (OSError, ValueError, tarfile.TarError) as exc:
|
|
424
|
+
print_json({"ok": False, "error": str(exc)})
|
|
425
|
+
raise SystemExit(1) from exc
|
|
426
|
+
print_json({"ok": True, "summary": summary})
|
|
427
|
+
|
|
428
|
+
|
|
417
429
|
def cmd_validate(args):
|
|
418
430
|
from x4d_devkit.validation import validate_clip
|
|
419
431
|
|
|
@@ -755,6 +767,11 @@ def main():
|
|
|
755
767
|
p_schemas_project.add_argument("--format", choices=["table", "json"], default="json")
|
|
756
768
|
p_schemas_project.set_defaults(func=cmd_label_schemas_project_get)
|
|
757
769
|
|
|
770
|
+
p_image_export = sub.add_parser("validate-image-export", help="Validate a native image export tar offline")
|
|
771
|
+
p_image_export.add_argument("path")
|
|
772
|
+
p_image_export.add_argument("--max-json-bytes", type=int, default=64 * 1024 * 1024)
|
|
773
|
+
p_image_export.set_defaults(func=cmd_validate_image_export)
|
|
774
|
+
|
|
758
775
|
p_val = sub.add_parser("validate", help="Validate clip directory")
|
|
759
776
|
p_val.add_argument("path", help="Clip directory or parent directory")
|
|
760
777
|
p_val.set_defaults(func=cmd_validate)
|
|
@@ -10,6 +10,7 @@ from x4d_devkit.client.clip_work_sessions import (
|
|
|
10
10
|
)
|
|
11
11
|
from x4d_devkit.client.client import X4DClient
|
|
12
12
|
from x4d_devkit.client.image_work_sessions import ImageWorkSessionsAPI
|
|
13
|
+
from x4d_devkit.client.image_exports import ImageExportsAPI
|
|
13
14
|
from x4d_devkit.client.clips import ClipsAPI
|
|
14
15
|
from x4d_devkit.client.dataset_identity import DatasetIdentityAPI
|
|
15
16
|
from x4d_devkit.client.label_schemas import (
|
|
@@ -43,6 +44,7 @@ from x4d_devkit.client.test_sets import TestSetsAPI
|
|
|
43
44
|
|
|
44
45
|
__all__ = [
|
|
45
46
|
"ImageWorkSessionsAPI",
|
|
47
|
+
"ImageExportsAPI",
|
|
46
48
|
"CleanBasemapState",
|
|
47
49
|
"AssetsAPI",
|
|
48
50
|
"BagsAPI",
|
|
@@ -26,6 +26,7 @@ from x4d_devkit.client.label_schemas import LabelSchemasAPI
|
|
|
26
26
|
from x4d_devkit.client.image_clips import ImageClipsAPI
|
|
27
27
|
from x4d_devkit.client.image_work_sessions import ImageWorkSessionsAPI
|
|
28
28
|
from x4d_devkit.client.image_preannotations import ImagePreannotationsAPI
|
|
29
|
+
from x4d_devkit.client.image_exports import ImageExportsAPI
|
|
29
30
|
from x4d_devkit.client.semantic_scene import SemanticSceneAPI
|
|
30
31
|
from x4d_devkit.client.semantic_scene_authoring import (
|
|
31
32
|
SemanticSceneAuthoringWorkSessionsAPI,
|
|
@@ -76,6 +77,7 @@ class X4DClient:
|
|
|
76
77
|
self.image_clips = ImageClipsAPI(self)
|
|
77
78
|
self.image_work_sessions = ImageWorkSessionsAPI(self)
|
|
78
79
|
self.image_preannotations = ImagePreannotationsAPI(self)
|
|
80
|
+
self.image_exports = ImageExportsAPI(self)
|
|
79
81
|
self.label_schemas = LabelSchemasAPI(self)
|
|
80
82
|
self.semantic_scene = SemanticSceneAPI(self)
|
|
81
83
|
self.semantic_scene_authoring_work_sessions = (
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Typed invocation of platform-owned native image export capabilities."""
|
|
2
|
+
|
|
3
|
+
from x4d_devkit._image_contract import identifier, integer, require, text
|
|
4
|
+
from x4d_devkit.image_export import (
|
|
5
|
+
export_identifier,
|
|
6
|
+
parse_image_export_download,
|
|
7
|
+
parse_image_export_job,
|
|
8
|
+
parse_image_export_page,
|
|
9
|
+
parse_image_export_preview,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ImageExportsAPI:
|
|
14
|
+
def __init__(self, client):
|
|
15
|
+
self._client = client
|
|
16
|
+
|
|
17
|
+
def _scope(self, project_id, clip_id, include_preannotations):
|
|
18
|
+
integer(project_id, "project_id", minimum=1)
|
|
19
|
+
if clip_id is not None:
|
|
20
|
+
identifier(clip_id, "clip_id")
|
|
21
|
+
require(
|
|
22
|
+
type(include_preannotations) is bool,
|
|
23
|
+
"expected boolean",
|
|
24
|
+
"include_preannotations",
|
|
25
|
+
)
|
|
26
|
+
return {
|
|
27
|
+
"project_id": project_id,
|
|
28
|
+
"clip_id": clip_id,
|
|
29
|
+
"include_preannotations": include_preannotations,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def preview(self, *, project_id, clip_id=None, include_preannotations=False):
|
|
33
|
+
body = self._scope(project_id, clip_id, include_preannotations)
|
|
34
|
+
result = parse_image_export_preview(
|
|
35
|
+
self._client.post("/image-exports/preview", json=body)
|
|
36
|
+
)
|
|
37
|
+
value = result.to_dict()
|
|
38
|
+
require(
|
|
39
|
+
all(value[key] == item for key, item in body.items()),
|
|
40
|
+
"foreign preview context",
|
|
41
|
+
"preview",
|
|
42
|
+
)
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
def create(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
project_id,
|
|
49
|
+
scope_token,
|
|
50
|
+
idempotency_key,
|
|
51
|
+
clip_id=None,
|
|
52
|
+
include_preannotations=False,
|
|
53
|
+
):
|
|
54
|
+
body = self._scope(project_id, clip_id, include_preannotations)
|
|
55
|
+
text(scope_token, "scope_token")
|
|
56
|
+
text(idempotency_key, "idempotency_key")
|
|
57
|
+
result = parse_image_export_job(
|
|
58
|
+
self._client.post(
|
|
59
|
+
"/image-exports",
|
|
60
|
+
json={
|
|
61
|
+
**body,
|
|
62
|
+
"scope_token": scope_token,
|
|
63
|
+
"idempotency_key": idempotency_key,
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
value = result.to_dict()
|
|
68
|
+
require(
|
|
69
|
+
value["project_id"] == project_id
|
|
70
|
+
and value["include_preannotations"] == include_preannotations
|
|
71
|
+
and (clip_id is None or value["clip_ids"] == [clip_id]),
|
|
72
|
+
"foreign created job",
|
|
73
|
+
"job",
|
|
74
|
+
)
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
def list(self, *, project_id, page=1, page_size=50):
|
|
78
|
+
for key, value in {
|
|
79
|
+
"project_id": project_id,
|
|
80
|
+
"page": page,
|
|
81
|
+
"page_size": page_size,
|
|
82
|
+
}.items():
|
|
83
|
+
integer(value, key, minimum=1)
|
|
84
|
+
require(page_size <= 100, "page size exceeds 100", "page_size")
|
|
85
|
+
result = parse_image_export_page(
|
|
86
|
+
self._client.get(
|
|
87
|
+
"/image-exports",
|
|
88
|
+
params={"project_id": project_id, "page": page, "page_size": page_size},
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
value = result.to_dict()
|
|
92
|
+
require(
|
|
93
|
+
all(
|
|
94
|
+
value[key] == item
|
|
95
|
+
for key, item in {
|
|
96
|
+
"project_id": project_id,
|
|
97
|
+
"page": page,
|
|
98
|
+
"page_size": page_size,
|
|
99
|
+
}.items()
|
|
100
|
+
),
|
|
101
|
+
"foreign export page",
|
|
102
|
+
"page",
|
|
103
|
+
)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
def get(self, export_id):
|
|
107
|
+
return self._job(export_id, "get")
|
|
108
|
+
|
|
109
|
+
def cancel(self, export_id):
|
|
110
|
+
return self._job(export_id, "cancel")
|
|
111
|
+
|
|
112
|
+
def retry(self, export_id):
|
|
113
|
+
return self._job(export_id, "retry")
|
|
114
|
+
|
|
115
|
+
def _job(self, export_id, action):
|
|
116
|
+
export_identifier(export_id)
|
|
117
|
+
path = f"/image-exports/{export_id}"
|
|
118
|
+
payload = (
|
|
119
|
+
self._client.get(path)
|
|
120
|
+
if action == "get"
|
|
121
|
+
else self._client.post(f"{path}/{action}", json={})
|
|
122
|
+
)
|
|
123
|
+
result = parse_image_export_job(payload)
|
|
124
|
+
require(
|
|
125
|
+
result.to_dict()["export_id"] == export_id,
|
|
126
|
+
"foreign export job",
|
|
127
|
+
"export_id",
|
|
128
|
+
)
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
def download_link(self, export_id):
|
|
132
|
+
export_identifier(export_id)
|
|
133
|
+
result = parse_image_export_download(
|
|
134
|
+
self._client.post(f"/image-exports/{export_id}/download-link", json={})
|
|
135
|
+
)
|
|
136
|
+
require(
|
|
137
|
+
result.to_dict()["export_id"] == export_id,
|
|
138
|
+
"foreign export download",
|
|
139
|
+
"export_id",
|
|
140
|
+
)
|
|
141
|
+
return result
|