x4d-devkit 0.48.0rc3__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.0rc3 → x4d_devkit-0.48.0rc5}/PKG-INFO +1 -1
- {x4d_devkit-0.48.0rc3 → 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.0rc5/tests/test_image_preannotation.py +380 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_image_reference.py +0 -56
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_sdk_api.py +1 -1
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/cli.py +17 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/__init__.py +2 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/client.py +4 -2
- x4d_devkit-0.48.0rc5/x4d_devkit/client/image_exports.py +141 -0
- x4d_devkit-0.48.0rc5/x4d_devkit/client/image_preannotations.py +117 -0
- x4d_devkit-0.48.0rc5/x4d_devkit/image_export.py +606 -0
- x4d_devkit-0.48.0rc5/x4d_devkit/image_preannotation.py +330 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/PKG-INFO +1 -1
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/SOURCES.txt +6 -1
- x4d_devkit-0.48.0rc3/x4d_devkit/client/image_references.py +0 -147
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/LICENSE +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/README.md +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/setup.cfg +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_box.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_category_audit.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_clean_basemap_client.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_image_clip.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_image_geometry.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_image_work_session.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_loader.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_matching.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_models.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_preannotation_client.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_roadmark_preview.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_independent.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_source_attributes.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_token.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_transform.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_validation.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_vector_map.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_vector_map_geometry.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_vector_map_session.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_work_session_validation_reuse.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/_image_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/audit.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_clips.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_work_sessions.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/preannotation.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/reconstruction.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_clip.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_geometry.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_reference.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_schema.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_work_session.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/roadmark_preview.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/vector_map.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/vector_map_geometry.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.48.0rc3 → 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
|