x4d-devkit 0.48.0rc5__tar.gz → 0.48.0rc7__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.0rc5 → x4d_devkit-0.48.0rc7}/PKG-INFO +1 -1
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/pyproject.toml +1 -1
- x4d_devkit-0.48.0rc7/tests/test_accounts.py +198 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_export.py +25 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_sdk_api.py +1 -1
- x4d_devkit-0.48.0rc7/x4d_devkit/accounts.py +147 -0
- x4d_devkit-0.48.0rc7/x4d_devkit/client/accounts.py +111 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/client.py +2 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_export.py +17 -2
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit.egg-info/PKG-INFO +1 -1
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit.egg-info/SOURCES.txt +3 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/LICENSE +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/README.md +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/setup.cfg +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_box.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_category_audit.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_clean_basemap_client.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_clip.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_geometry.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_preannotation.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_reference.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_work_session.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_loader.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_matching.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_models.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_preannotation_client.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_roadmark_preview.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_semantic_scene_independent.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_source_attributes.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_token.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_transform.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_validation.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_vector_map.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_vector_map_geometry.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_vector_map_session.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_work_session_validation_reuse.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/_image_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/audit.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/image_clips.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/image_exports.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/image_preannotations.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/image_work_sessions.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/preannotation.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/reconstruction.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_clip.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_geometry.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_preannotation.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_reference.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_schema.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_work_session.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/roadmark_preview.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/vector_map.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/vector_map_geometry.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/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.0rc7"
|
|
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,198 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from x4d_devkit.accounts import (
|
|
8
|
+
ACCOUNT_CONTRACT,
|
|
9
|
+
ACCOUNT_PAGE_CONTRACT,
|
|
10
|
+
parse_account_page,
|
|
11
|
+
parse_account_record,
|
|
12
|
+
validate_account_password,
|
|
13
|
+
validate_project_types,
|
|
14
|
+
)
|
|
15
|
+
from x4d_devkit.client import X4DClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def record(**changes):
|
|
19
|
+
return {
|
|
20
|
+
"contract_version": ACCOUNT_CONTRACT,
|
|
21
|
+
"id": 7,
|
|
22
|
+
"username": "annotator",
|
|
23
|
+
"role": "visitor",
|
|
24
|
+
"is_active": True,
|
|
25
|
+
"project_types": ["2d"],
|
|
26
|
+
"revision": 3,
|
|
27
|
+
**changes,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize(
|
|
32
|
+
"change",
|
|
33
|
+
[
|
|
34
|
+
{"id": True},
|
|
35
|
+
{"id": 0},
|
|
36
|
+
{"revision": -1},
|
|
37
|
+
{"revision": True},
|
|
38
|
+
{"is_active": 1},
|
|
39
|
+
{"username": " user"},
|
|
40
|
+
{"username": "user\x00"},
|
|
41
|
+
{"role": "editor"},
|
|
42
|
+
{"project_types": ["2D"]},
|
|
43
|
+
{"project_types": ["2d", "2d"]},
|
|
44
|
+
{"project_types": ["3d", "2d"]},
|
|
45
|
+
{"project_types": "2d"},
|
|
46
|
+
{"password": "must-never-appear"},
|
|
47
|
+
{"contract_version": "x4d-account-v0"},
|
|
48
|
+
],
|
|
49
|
+
)
|
|
50
|
+
def test_account_rejects_noncanonical_data_without_secret_echo(change):
|
|
51
|
+
with pytest.raises(ValueError) as error:
|
|
52
|
+
parse_account_record(record(**change))
|
|
53
|
+
assert "must-never-appear" not in str(error.value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.parametrize("grants", [[], ["2d"], ["3d"], ["2d", "3d"]])
|
|
57
|
+
def test_account_roundtrip_detaches_grants(grants):
|
|
58
|
+
value = record(project_types=grants)
|
|
59
|
+
parsed = parse_account_record(value)
|
|
60
|
+
detached = parsed.to_dict()
|
|
61
|
+
assert detached == value
|
|
62
|
+
detached["project_types"].append("mutated")
|
|
63
|
+
assert parsed.to_dict() == value
|
|
64
|
+
assert validate_project_types(grants) == grants
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.parametrize(
|
|
68
|
+
"password", ["short", " " * 12, "x" * 73, "密" * 25, "secret\ud800"]
|
|
69
|
+
)
|
|
70
|
+
def test_password_limits_are_bytes_and_do_not_echo_credentials(password):
|
|
71
|
+
with pytest.raises(ValueError) as error:
|
|
72
|
+
validate_account_password(password)
|
|
73
|
+
assert password not in str(error.value)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_page_rejects_duplicates_excess_and_private_fields():
|
|
77
|
+
page = {
|
|
78
|
+
"contract_version": ACCOUNT_PAGE_CONTRACT,
|
|
79
|
+
"page": 1,
|
|
80
|
+
"page_size": 10,
|
|
81
|
+
"total": 1,
|
|
82
|
+
"items": [record()],
|
|
83
|
+
}
|
|
84
|
+
assert parse_account_page(page) == page
|
|
85
|
+
for changes in (
|
|
86
|
+
{"items": [record(), record()]},
|
|
87
|
+
{"page_size": 101},
|
|
88
|
+
{"secret": "hidden"},
|
|
89
|
+
):
|
|
90
|
+
with pytest.raises(ValueError):
|
|
91
|
+
parse_account_page({**page, **changes})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_typed_client_uses_only_account_capabilities_and_clears_changed_credentials():
|
|
95
|
+
requests = []
|
|
96
|
+
current = record()
|
|
97
|
+
|
|
98
|
+
def handler(request):
|
|
99
|
+
requests.append(request)
|
|
100
|
+
path = request.url.path
|
|
101
|
+
if request.method == "GET" and path == "/api/v1/users":
|
|
102
|
+
return httpx.Response(
|
|
103
|
+
200,
|
|
104
|
+
json={
|
|
105
|
+
"contract_version": ACCOUNT_PAGE_CONTRACT,
|
|
106
|
+
"page": 1,
|
|
107
|
+
"page_size": 10,
|
|
108
|
+
"total": 1,
|
|
109
|
+
"items": [current],
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
if path.endswith("/update"):
|
|
113
|
+
body = json.loads(request.content)
|
|
114
|
+
for key in ("role", "project_types", "is_active"):
|
|
115
|
+
current[key] = body[key]
|
|
116
|
+
current["revision"] += 1
|
|
117
|
+
elif path.endswith("/reset-password") or path.endswith("/me/password"):
|
|
118
|
+
current["revision"] += 1
|
|
119
|
+
return httpx.Response(200, json=deepcopy(current))
|
|
120
|
+
|
|
121
|
+
with X4DClient(api_url="http://platform.test", token="private-token") as client:
|
|
122
|
+
client._http.close()
|
|
123
|
+
client._http = httpx.Client(
|
|
124
|
+
base_url=client.api_url,
|
|
125
|
+
headers={"Authorization": "Bearer private-token"},
|
|
126
|
+
transport=httpx.MockTransport(handler),
|
|
127
|
+
)
|
|
128
|
+
assert client.accounts.list(page_size=10)["items"][0]["id"] == 7
|
|
129
|
+
assert client.accounts.get(7).id == client.accounts.me().id == 7
|
|
130
|
+
assert (
|
|
131
|
+
client.accounts.create(
|
|
132
|
+
username="annotator",
|
|
133
|
+
password="private-password",
|
|
134
|
+
role="visitor",
|
|
135
|
+
project_types=["2d"],
|
|
136
|
+
).id
|
|
137
|
+
== 7
|
|
138
|
+
)
|
|
139
|
+
assert (
|
|
140
|
+
client.accounts.update(
|
|
141
|
+
7,
|
|
142
|
+
expected_revision=3,
|
|
143
|
+
role="visitor",
|
|
144
|
+
project_types=["2d", "3d"],
|
|
145
|
+
is_active=True,
|
|
146
|
+
).revision
|
|
147
|
+
== 4
|
|
148
|
+
)
|
|
149
|
+
assert (
|
|
150
|
+
client.accounts.reset_password(
|
|
151
|
+
7, expected_revision=4, password="replacement-password"
|
|
152
|
+
).revision
|
|
153
|
+
== 5
|
|
154
|
+
)
|
|
155
|
+
assert (
|
|
156
|
+
client.accounts.change_password(
|
|
157
|
+
current_password="replacement-password", new_password="another-password"
|
|
158
|
+
).revision
|
|
159
|
+
== 6
|
|
160
|
+
)
|
|
161
|
+
assert "Authorization" not in client._http.headers
|
|
162
|
+
assert all(request.url.host == "platform.test" for request in requests)
|
|
163
|
+
assert all(
|
|
164
|
+
request.headers["Authorization"] == "Bearer private-token"
|
|
165
|
+
for request in requests
|
|
166
|
+
)
|
|
167
|
+
assert json.loads(requests[-1].content) == {
|
|
168
|
+
"current_password": "replacement-password",
|
|
169
|
+
"new_password": "another-password",
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_client_rejects_foreign_responses_and_invalid_requests_before_io():
|
|
174
|
+
with X4DClient(api_url="http://platform.test") as client:
|
|
175
|
+
client._http.close()
|
|
176
|
+
client._http = httpx.Client(
|
|
177
|
+
base_url=client.api_url,
|
|
178
|
+
transport=httpx.MockTransport(
|
|
179
|
+
lambda request: httpx.Response(200, json=record(id=8))
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
with pytest.raises(ValueError, match="another user"):
|
|
183
|
+
client.accounts.get(7)
|
|
184
|
+
with pytest.raises(ValueError, match="UTF-8"):
|
|
185
|
+
client.accounts.create(
|
|
186
|
+
username="user",
|
|
187
|
+
password="密" * 25,
|
|
188
|
+
role="visitor",
|
|
189
|
+
project_types=["2d"],
|
|
190
|
+
)
|
|
191
|
+
with pytest.raises(ValueError, match="boolean"):
|
|
192
|
+
client.accounts.update(
|
|
193
|
+
7,
|
|
194
|
+
expected_revision=3,
|
|
195
|
+
role="visitor",
|
|
196
|
+
project_types=["2d"],
|
|
197
|
+
is_active=1,
|
|
198
|
+
)
|
|
@@ -215,6 +215,9 @@ def job(status="queued"):
|
|
|
215
215
|
"created_at": NOW,
|
|
216
216
|
"completed_at": NOW if terminal else None,
|
|
217
217
|
"can_cancel": not terminal,
|
|
218
|
+
"completion_counts": {"unprocessed": 2, "complete": 0, "confirmed_empty": 0}
|
|
219
|
+
if status in {"packaging", "validating", "uploading", "completed"}
|
|
220
|
+
else None,
|
|
218
221
|
}
|
|
219
222
|
|
|
220
223
|
|
|
@@ -255,6 +258,14 @@ def test_invalid_job_states_fail(change):
|
|
|
255
258
|
value["completed_at"] = NOW
|
|
256
259
|
if change == "infinite":
|
|
257
260
|
value["progress"]["rate"] = float("inf")
|
|
261
|
+
if change == "counts":
|
|
262
|
+
value["completion_counts"] = {
|
|
263
|
+
"unprocessed": -1,
|
|
264
|
+
"complete": 0,
|
|
265
|
+
"confirmed_empty": 0,
|
|
266
|
+
}
|
|
267
|
+
if change == "unsealed":
|
|
268
|
+
value["status"] = value["progress"]["stage"] = "packaging"
|
|
258
269
|
with pytest.raises((ImageContractError, ValueError)):
|
|
259
270
|
parse_image_export_job(value)
|
|
260
271
|
|
|
@@ -276,6 +287,7 @@ def test_typed_client_scope_and_ephemeral_download_do_not_forward_credentials():
|
|
|
276
287
|
"clip_ids": ["clip_test"],
|
|
277
288
|
"excluded": [],
|
|
278
289
|
"scope_token": "opaque-token",
|
|
290
|
+
"completed_scope_count": 0,
|
|
279
291
|
"expires_at": NOW,
|
|
280
292
|
},
|
|
281
293
|
)
|
|
@@ -387,3 +399,16 @@ def test_archive_member_history_is_not_retained(tmp_path, monkeypatch):
|
|
|
387
399
|
monkeypatch.setattr(tarfile.TarFile, "next", measured)
|
|
388
400
|
validate_image_export_bundle(path)
|
|
389
401
|
assert max(history) <= 1
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def test_completed_job_reports_zero_authoritative_scopes_and_rejects_unknown_counts():
|
|
405
|
+
value = job("completed")
|
|
406
|
+
assert parse_image_export_job(value).to_dict()["completion_counts"]["complete"] == 0
|
|
407
|
+
for counts in (
|
|
408
|
+
None,
|
|
409
|
+
{"unprocessed": True, "complete": 0, "confirmed_empty": 0},
|
|
410
|
+
{"complete": 2},
|
|
411
|
+
):
|
|
412
|
+
value["completion_counts"] = counts
|
|
413
|
+
with pytest.raises(ImageContractError):
|
|
414
|
+
parse_image_export_job(value)
|
|
@@ -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.0rc7"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Strict managed-account documents; the platform owns authorization."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
import unicodedata
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from x4d_devkit.json_contract import ensure_i_json
|
|
10
|
+
|
|
11
|
+
ACCOUNT_CONTRACT = "x4d-account-v1"
|
|
12
|
+
ACCOUNT_PAGE_CONTRACT = "x4d-account-page-v1"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _require(condition: bool, message: str) -> None:
|
|
16
|
+
if not condition:
|
|
17
|
+
raise ValueError(message)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def account_integer(value: Any, name: str, *, minimum=0) -> int:
|
|
21
|
+
_require(
|
|
22
|
+
type(value) is int and minimum <= value <= 2**53 - 1,
|
|
23
|
+
f"{name} must be an exact integer >= {minimum}",
|
|
24
|
+
)
|
|
25
|
+
return value
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_username(value: Any) -> str:
|
|
29
|
+
_require(
|
|
30
|
+
type(value) is str
|
|
31
|
+
and 1 <= len(value) <= 64
|
|
32
|
+
and value == value.strip()
|
|
33
|
+
and not any(unicodedata.category(c).startswith("C") for c in value),
|
|
34
|
+
"username must be canonical text of 1–64 characters without controls",
|
|
35
|
+
)
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_account_password(value: Any) -> str:
|
|
40
|
+
_require(
|
|
41
|
+
type(value) is str and len(value) >= 8 and bool(value.strip()),
|
|
42
|
+
"password must contain at least 8 characters and not be blank",
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
encoded = value.encode("utf-8")
|
|
46
|
+
except UnicodeError:
|
|
47
|
+
raise ValueError("password must be valid UTF-8") from None
|
|
48
|
+
_require(len(encoded) <= 72, "password must not exceed 72 UTF-8 bytes")
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def validate_project_types(value: Any) -> list[str]:
|
|
53
|
+
_require(
|
|
54
|
+
type(value) is list
|
|
55
|
+
and all(type(v) is str and v in ("2d", "3d") for v in value)
|
|
56
|
+
and value == sorted(set(value)),
|
|
57
|
+
"project_types must be a sorted unique subset of ['2d', '3d']",
|
|
58
|
+
)
|
|
59
|
+
return list(value)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_account_role(value: Any) -> str:
|
|
63
|
+
_require(
|
|
64
|
+
type(value) is str and value in ("admin", "visitor"),
|
|
65
|
+
"role must be admin or visitor",
|
|
66
|
+
)
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class AccountRecord:
|
|
72
|
+
"""An immutable public account; contains no credentials."""
|
|
73
|
+
|
|
74
|
+
id: int
|
|
75
|
+
username: str
|
|
76
|
+
role: str
|
|
77
|
+
is_active: bool
|
|
78
|
+
project_types: tuple[str, ...]
|
|
79
|
+
revision: int
|
|
80
|
+
|
|
81
|
+
def to_dict(self) -> dict:
|
|
82
|
+
value = asdict(self)
|
|
83
|
+
value["contract_version"] = ACCOUNT_CONTRACT
|
|
84
|
+
value["project_types"] = list(self.project_types)
|
|
85
|
+
return value
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def parse_account_record(value: Any) -> AccountRecord:
|
|
89
|
+
ensure_i_json(value, path="account")
|
|
90
|
+
_require(
|
|
91
|
+
type(value) is dict
|
|
92
|
+
and set(value)
|
|
93
|
+
== {
|
|
94
|
+
"contract_version",
|
|
95
|
+
"id",
|
|
96
|
+
"username",
|
|
97
|
+
"role",
|
|
98
|
+
"is_active",
|
|
99
|
+
"project_types",
|
|
100
|
+
"revision",
|
|
101
|
+
},
|
|
102
|
+
"account fields do not match the current contract",
|
|
103
|
+
)
|
|
104
|
+
_require(
|
|
105
|
+
value["contract_version"] == ACCOUNT_CONTRACT, "unsupported account contract"
|
|
106
|
+
)
|
|
107
|
+
_require(type(value["is_active"]) is bool, "is_active must be boolean")
|
|
108
|
+
return AccountRecord(
|
|
109
|
+
account_integer(value["id"], "id", minimum=1),
|
|
110
|
+
validate_username(value["username"]),
|
|
111
|
+
validate_account_role(value["role"]),
|
|
112
|
+
value["is_active"],
|
|
113
|
+
tuple(validate_project_types(value["project_types"])),
|
|
114
|
+
account_integer(value["revision"], "revision"),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def parse_account_page(value: Any) -> dict:
|
|
119
|
+
ensure_i_json(value, path="account_page")
|
|
120
|
+
_require(
|
|
121
|
+
type(value) is dict
|
|
122
|
+
and set(value) == {"contract_version", "page", "page_size", "total", "items"},
|
|
123
|
+
"account page fields do not match the current contract",
|
|
124
|
+
)
|
|
125
|
+
_require(
|
|
126
|
+
value["contract_version"] == ACCOUNT_PAGE_CONTRACT,
|
|
127
|
+
"unsupported account page contract",
|
|
128
|
+
)
|
|
129
|
+
page = account_integer(value["page"], "page", minimum=1)
|
|
130
|
+
size = account_integer(value["page_size"], "page_size", minimum=1)
|
|
131
|
+
total = account_integer(value["total"], "total")
|
|
132
|
+
_require(
|
|
133
|
+
size <= 100 and type(value["items"]) is list, "invalid account page size/items"
|
|
134
|
+
)
|
|
135
|
+
items = [parse_account_record(row).to_dict() for row in value["items"]]
|
|
136
|
+
_require(
|
|
137
|
+
len(items) <= min(size, total)
|
|
138
|
+
and len({row["id"] for row in items}) == len(items),
|
|
139
|
+
"account page has duplicate or excess items",
|
|
140
|
+
)
|
|
141
|
+
return {
|
|
142
|
+
"contract_version": ACCOUNT_PAGE_CONTRACT,
|
|
143
|
+
"page": page,
|
|
144
|
+
"page_size": size,
|
|
145
|
+
"total": total,
|
|
146
|
+
"items": items,
|
|
147
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Typed adapters for platform-owned account capabilities."""
|
|
2
|
+
|
|
3
|
+
from x4d_devkit.accounts import (
|
|
4
|
+
account_integer,
|
|
5
|
+
parse_account_page,
|
|
6
|
+
parse_account_record,
|
|
7
|
+
validate_account_password,
|
|
8
|
+
validate_account_role,
|
|
9
|
+
validate_project_types,
|
|
10
|
+
validate_username,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AccountsAPI:
|
|
15
|
+
def __init__(self, client):
|
|
16
|
+
self._client = client
|
|
17
|
+
|
|
18
|
+
def me(self):
|
|
19
|
+
return parse_account_record(self._client.get("/users/me"))
|
|
20
|
+
|
|
21
|
+
def get(self, user_id):
|
|
22
|
+
account_integer(user_id, "user_id", minimum=1)
|
|
23
|
+
return self._record(self._client.get(f"/users/{user_id}"), user_id)
|
|
24
|
+
|
|
25
|
+
def list(self, *, page=1, page_size=50):
|
|
26
|
+
account_integer(page, "page", minimum=1)
|
|
27
|
+
account_integer(page_size, "page_size", minimum=1)
|
|
28
|
+
if page_size > 100:
|
|
29
|
+
raise ValueError("page_size exceeds 100")
|
|
30
|
+
result = parse_account_page(
|
|
31
|
+
self._client.get("/users", params={"page": page, "page_size": page_size})
|
|
32
|
+
)
|
|
33
|
+
if result["page"] != page or result["page_size"] != page_size:
|
|
34
|
+
raise ValueError("account page belongs to another request")
|
|
35
|
+
return result
|
|
36
|
+
|
|
37
|
+
def create(self, *, username, password, role, project_types):
|
|
38
|
+
body = {
|
|
39
|
+
"username": validate_username(username),
|
|
40
|
+
"password": validate_account_password(password),
|
|
41
|
+
"role": validate_account_role(role),
|
|
42
|
+
"project_types": validate_project_types(project_types),
|
|
43
|
+
}
|
|
44
|
+
result = parse_account_record(self._client.post("/users", json=body))
|
|
45
|
+
if (
|
|
46
|
+
result.username != username
|
|
47
|
+
or result.role != role
|
|
48
|
+
or list(result.project_types) != project_types
|
|
49
|
+
):
|
|
50
|
+
raise ValueError("created account belongs to another request")
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
def update(self, user_id, *, expected_revision, role, project_types, is_active):
|
|
54
|
+
account_integer(user_id, "user_id", minimum=1)
|
|
55
|
+
account_integer(expected_revision, "expected_revision")
|
|
56
|
+
if type(is_active) is not bool:
|
|
57
|
+
raise ValueError("is_active must be boolean")
|
|
58
|
+
body = {
|
|
59
|
+
"expected_revision": expected_revision,
|
|
60
|
+
"role": validate_account_role(role),
|
|
61
|
+
"project_types": validate_project_types(project_types),
|
|
62
|
+
"is_active": is_active,
|
|
63
|
+
}
|
|
64
|
+
result = self._record(
|
|
65
|
+
self._client.post(f"/users/{user_id}/update", json=body), user_id
|
|
66
|
+
)
|
|
67
|
+
if result.revision < expected_revision or any(
|
|
68
|
+
result.to_dict()[key] != body[key]
|
|
69
|
+
for key in ("role", "project_types", "is_active")
|
|
70
|
+
):
|
|
71
|
+
raise ValueError("updated account belongs to another request")
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
def reset_password(self, user_id, *, expected_revision, password):
|
|
75
|
+
account_integer(user_id, "user_id", minimum=1)
|
|
76
|
+
account_integer(expected_revision, "expected_revision")
|
|
77
|
+
result = self._record(
|
|
78
|
+
self._client.post(
|
|
79
|
+
f"/users/{user_id}/reset-password",
|
|
80
|
+
json={
|
|
81
|
+
"expected_revision": expected_revision,
|
|
82
|
+
"password": validate_account_password(password),
|
|
83
|
+
},
|
|
84
|
+
),
|
|
85
|
+
user_id,
|
|
86
|
+
)
|
|
87
|
+
if result.revision <= expected_revision:
|
|
88
|
+
raise ValueError("password reset did not advance the account revision")
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
def change_password(self, *, current_password, new_password):
|
|
92
|
+
if type(current_password) is not str or not current_password:
|
|
93
|
+
raise ValueError("current_password must be explicit text")
|
|
94
|
+
result = parse_account_record(
|
|
95
|
+
self._client.post(
|
|
96
|
+
"/users/me/password",
|
|
97
|
+
json={
|
|
98
|
+
"current_password": current_password,
|
|
99
|
+
"new_password": validate_account_password(new_password),
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
self._client.set_token(None)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _record(value, user_id):
|
|
108
|
+
result = parse_account_record(value)
|
|
109
|
+
if result.id != user_id:
|
|
110
|
+
raise ValueError("account response belongs to another user")
|
|
111
|
+
return result
|
|
@@ -15,6 +15,7 @@ from typing import TYPE_CHECKING
|
|
|
15
15
|
import httpx
|
|
16
16
|
|
|
17
17
|
from x4d_devkit.client._origin import validated_origin
|
|
18
|
+
from x4d_devkit.client.accounts import AccountsAPI
|
|
18
19
|
from x4d_devkit.client.assets import AssetsAPI
|
|
19
20
|
from x4d_devkit.client.checkpoints import CheckpointsAPI
|
|
20
21
|
from x4d_devkit.client.bags import BagsAPI
|
|
@@ -67,6 +68,7 @@ class X4DClient:
|
|
|
67
68
|
base_url=self._api_url, headers=headers, timeout=timeout
|
|
68
69
|
)
|
|
69
70
|
self.assets = AssetsAPI(self)
|
|
71
|
+
self.accounts = AccountsAPI(self)
|
|
70
72
|
self.bags = BagsAPI(self)
|
|
71
73
|
self.capabilities = CapabilitiesAPI(self)
|
|
72
74
|
self.clip_work_sessions = ClipWorkSessionsAPI(self)
|
|
@@ -29,8 +29,8 @@ from x4d_devkit.json_contract import parse_i_json
|
|
|
29
29
|
EXPORT_CONTRACT = "x4d-image-export-v1"
|
|
30
30
|
INDEX_CONTRACT = "x4d-image-export-index-v1"
|
|
31
31
|
SCOPES_CONTRACT = "x4d-image-human-scopes-v1"
|
|
32
|
-
PREVIEW_CONTRACT = "x4d-image-export-preview-
|
|
33
|
-
JOB_CONTRACT = "x4d-image-export-job-
|
|
32
|
+
PREVIEW_CONTRACT = "x4d-image-export-preview-v2"
|
|
33
|
+
JOB_CONTRACT = "x4d-image-export-job-v2"
|
|
34
34
|
PAGE_CONTRACT = "x4d-image-export-page-v1"
|
|
35
35
|
DOWNLOAD_CONTRACT = "x4d-image-export-download-v1"
|
|
36
36
|
COMPONENTS = (*IMAGE_CLIP_COMPONENTS, "identity")
|
|
@@ -409,6 +409,7 @@ def parse_image_export_preview(value):
|
|
|
409
409
|
"excluded",
|
|
410
410
|
"scope_token",
|
|
411
411
|
"expires_at",
|
|
412
|
+
"completed_scope_count",
|
|
412
413
|
},
|
|
413
414
|
"export_preview",
|
|
414
415
|
)
|
|
@@ -418,6 +419,8 @@ def parse_image_export_preview(value):
|
|
|
418
419
|
"contract_version",
|
|
419
420
|
)
|
|
420
421
|
_scope(value)
|
|
422
|
+
if value["completed_scope_count"] is not None:
|
|
423
|
+
integer(value["completed_scope_count"], "completed_scope_count")
|
|
421
424
|
_ids(value["clip_ids"])
|
|
422
425
|
if value["clip_id"] is not None:
|
|
423
426
|
require(
|
|
@@ -467,6 +470,7 @@ def parse_image_export_job(value):
|
|
|
467
470
|
"created_at",
|
|
468
471
|
"completed_at",
|
|
469
472
|
"can_cancel",
|
|
473
|
+
"completion_counts",
|
|
470
474
|
},
|
|
471
475
|
"export_job",
|
|
472
476
|
)
|
|
@@ -528,6 +532,17 @@ def parse_image_export_job(value):
|
|
|
528
532
|
"failure needs exactly one explicit error",
|
|
529
533
|
"error",
|
|
530
534
|
)
|
|
535
|
+
counts = value["completion_counts"]
|
|
536
|
+
if counts is not None:
|
|
537
|
+
fields(counts, set(COMPLETION_STATES), "completion_counts")
|
|
538
|
+
for count in counts.values():
|
|
539
|
+
integer(count, "completion_counts")
|
|
540
|
+
require(
|
|
541
|
+
value["status"] not in {"packaging", "validating", "uploading", "completed"}
|
|
542
|
+
or counts is not None,
|
|
543
|
+
"sealed export must report authoritative completion",
|
|
544
|
+
"completion_counts",
|
|
545
|
+
)
|
|
531
546
|
if value["status"] == "completed":
|
|
532
547
|
_artifact(value["artifact"], value["export_id"])
|
|
533
548
|
else:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
|
+
tests/test_accounts.py
|
|
4
5
|
tests/test_annotation_contract.py
|
|
5
6
|
tests/test_annotation_reference.py
|
|
6
7
|
tests/test_assets_client.py
|
|
@@ -65,6 +66,7 @@ tests/test_work_session.py
|
|
|
65
66
|
tests/test_work_session_validation_reuse.py
|
|
66
67
|
x4d_devkit/__init__.py
|
|
67
68
|
x4d_devkit/_image_contract.py
|
|
69
|
+
x4d_devkit/accounts.py
|
|
68
70
|
x4d_devkit/annotation_contract.py
|
|
69
71
|
x4d_devkit/annotation_reference.py
|
|
70
72
|
x4d_devkit/audit.py
|
|
@@ -105,6 +107,7 @@ x4d_devkit.egg-info/requires.txt
|
|
|
105
107
|
x4d_devkit.egg-info/top_level.txt
|
|
106
108
|
x4d_devkit/client/__init__.py
|
|
107
109
|
x4d_devkit/client/_origin.py
|
|
110
|
+
x4d_devkit/client/accounts.py
|
|
108
111
|
x4d_devkit/client/annotation_reference.py
|
|
109
112
|
x4d_devkit/client/assets.py
|
|
110
113
|
x4d_devkit/client/bags.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_current_state_identity_consumers_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|