x4d-devkit 0.48.0rc6__tar.gz → 0.48.0rc8__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.0rc6 → x4d_devkit-0.48.0rc8}/PKG-INFO +1 -1
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/pyproject.toml +1 -1
- x4d_devkit-0.48.0rc8/tests/test_accounts.py +198 -0
- x4d_devkit-0.48.0rc8/tests/test_image_schema_upgrade.py +239 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_sdk_api.py +1 -1
- x4d_devkit-0.48.0rc8/x4d_devkit/accounts.py +147 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/__init__.py +3 -0
- x4d_devkit-0.48.0rc8/x4d_devkit/client/accounts.py +111 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/client.py +4 -0
- x4d_devkit-0.48.0rc8/x4d_devkit/client/image_schemas.py +115 -0
- x4d_devkit-0.48.0rc8/x4d_devkit/image_schema_upgrade.py +347 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit.egg-info/PKG-INFO +1 -1
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit.egg-info/SOURCES.txt +6 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/LICENSE +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/README.md +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/setup.cfg +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_box.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_category_audit.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_clean_basemap_client.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_image_clip.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_image_export.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_image_geometry.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_image_preannotation.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_image_reference.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_image_work_session.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_loader.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_matching.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_models.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_preannotation_client.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_roadmark_preview.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_semantic_scene_independent.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_source_attributes.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_token.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_transform.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_validation.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_vector_map.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_vector_map_geometry.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_vector_map_session.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/tests/test_work_session_validation_reuse.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/_image_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/audit.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/annotation_reference.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/clean_basemap.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/image_clips.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/image_exports.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/image_preannotations.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/image_work_sessions.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/preannotation.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/reconstruction.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/image_clip.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/image_export.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/image_geometry.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/image_preannotation.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/image_reference.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/image_schema.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/image_work_session.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/roadmark_preview.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/vector_map.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/vector_map_geometry.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.48.0rc6 → x4d_devkit-0.48.0rc8}/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.0rc8"
|
|
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
|
+
)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from x4d_devkit import X4DClient
|
|
8
|
+
from x4d_devkit._image_contract import fingerprint
|
|
9
|
+
from x4d_devkit.image_clip import build_image_identity
|
|
10
|
+
from x4d_devkit.image_schema import (
|
|
11
|
+
parse_image_schema_body,
|
|
12
|
+
parse_image_schema_record,
|
|
13
|
+
build_image_schema_snapshot,
|
|
14
|
+
)
|
|
15
|
+
from x4d_devkit.image_schema_upgrade import (
|
|
16
|
+
RECEIPT_CONTRACT,
|
|
17
|
+
additive_image_schema_changes,
|
|
18
|
+
image_schema_upgrade_candidate,
|
|
19
|
+
build_image_schema_upgrade_preview,
|
|
20
|
+
build_image_schema_upgrade_request,
|
|
21
|
+
parse_image_schema_upgrade_preview,
|
|
22
|
+
parse_image_schema_upgrade_request,
|
|
23
|
+
parse_image_schema_upgrade_receipt,
|
|
24
|
+
)
|
|
25
|
+
from tests.image_fixtures import clip_payload, line_object
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def target(clip):
|
|
29
|
+
body = deepcopy(clip["label_schema"]["body"])
|
|
30
|
+
body["version"] = "1.1.0"
|
|
31
|
+
body["categories"][0]["attributes"].append(
|
|
32
|
+
{
|
|
33
|
+
"id": "optional",
|
|
34
|
+
"display_name": "Optional",
|
|
35
|
+
"type": "boolean",
|
|
36
|
+
"required": False,
|
|
37
|
+
"scopes": ["object", "segment"],
|
|
38
|
+
"values": [],
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
for category in body["categories"]:
|
|
42
|
+
for attribute in category["attributes"]:
|
|
43
|
+
if attribute["type"] == "enum":
|
|
44
|
+
attribute["values"].append({"id": "extra", "display_name": "Extra"})
|
|
45
|
+
return build_image_schema_snapshot(
|
|
46
|
+
parse_image_schema_record(
|
|
47
|
+
{
|
|
48
|
+
"contract_version": "x4d-image-label-schema-record-v1",
|
|
49
|
+
"status": "active",
|
|
50
|
+
"body": body,
|
|
51
|
+
"fingerprint": parse_image_schema_body(body).identity.fingerprint,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_additive_upgrade_preserves_complete_human_geometry_ranges_and_completion():
|
|
58
|
+
clip = clip_payload()
|
|
59
|
+
clip["image_annotation"]["objects"] = [line_object()]
|
|
60
|
+
clip["identity"] = build_image_identity(clip)
|
|
61
|
+
snapshot = target(clip)
|
|
62
|
+
candidate = image_schema_upgrade_candidate(clip, snapshot).to_dict()
|
|
63
|
+
assert all(
|
|
64
|
+
candidate[key] == clip[key]
|
|
65
|
+
for key in clip
|
|
66
|
+
if key not in {"label_schema", "identity"}
|
|
67
|
+
)
|
|
68
|
+
assert (
|
|
69
|
+
candidate["identity"]["source_revision"] == clip["identity"]["source_revision"]
|
|
70
|
+
)
|
|
71
|
+
assert (
|
|
72
|
+
candidate["identity"]["annotation_revision"]
|
|
73
|
+
== clip["identity"]["annotation_revision"]
|
|
74
|
+
)
|
|
75
|
+
assert (
|
|
76
|
+
candidate["identity"]["schema_fingerprint"]
|
|
77
|
+
!= clip["identity"]["schema_fingerprint"]
|
|
78
|
+
)
|
|
79
|
+
assert "optional" not in candidate["image_annotation"]["objects"][0]["attributes"]
|
|
80
|
+
preview = build_image_schema_upgrade_preview(clip, snapshot)
|
|
81
|
+
assert (
|
|
82
|
+
parse_image_schema_upgrade_preview(preview.to_dict()).to_dict()
|
|
83
|
+
== preview.to_dict()
|
|
84
|
+
)
|
|
85
|
+
request = build_image_schema_upgrade_request(preview, "explicit-test-key")
|
|
86
|
+
assert (
|
|
87
|
+
parse_image_schema_upgrade_request(request.to_dict()).to_dict()
|
|
88
|
+
== request.to_dict()
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.parametrize(
|
|
93
|
+
"change",
|
|
94
|
+
[
|
|
95
|
+
"same-version",
|
|
96
|
+
"backwards",
|
|
97
|
+
"schema-id",
|
|
98
|
+
"category",
|
|
99
|
+
"geometry",
|
|
100
|
+
"view",
|
|
101
|
+
"attribute-remove",
|
|
102
|
+
"required",
|
|
103
|
+
"scope",
|
|
104
|
+
"enum-meaning",
|
|
105
|
+
"new-required",
|
|
106
|
+
],
|
|
107
|
+
)
|
|
108
|
+
def test_incompatible_reinterpretations_require_a_separate_migration(change):
|
|
109
|
+
clip = clip_payload()
|
|
110
|
+
old, new = clip["label_schema"]["body"], target(clip)["body"]
|
|
111
|
+
if change == "same-version":
|
|
112
|
+
new["version"] = old["version"]
|
|
113
|
+
if change == "backwards":
|
|
114
|
+
new["version"] = "0.1.0"
|
|
115
|
+
if change == "schema-id":
|
|
116
|
+
new["schema_id"] = "another"
|
|
117
|
+
if change == "category":
|
|
118
|
+
new["categories"].pop()
|
|
119
|
+
if change == "geometry":
|
|
120
|
+
new["categories"][0]["geometry_type"] = "box_2d"
|
|
121
|
+
if change == "view":
|
|
122
|
+
new["task_views"][0]["display_name"] = "Reinterpreted"
|
|
123
|
+
if change == "attribute-remove":
|
|
124
|
+
new["categories"][0]["attributes"].pop(0)
|
|
125
|
+
if change == "required":
|
|
126
|
+
new["categories"][0]["attributes"][0]["required"] = True
|
|
127
|
+
if change == "scope":
|
|
128
|
+
new["categories"][0]["attributes"][0]["scopes"] = ["object"]
|
|
129
|
+
if change == "enum-meaning":
|
|
130
|
+
new["categories"][0]["attributes"][1]["values"][0]["display_name"] = (
|
|
131
|
+
"Different meaning"
|
|
132
|
+
)
|
|
133
|
+
if change == "new-required":
|
|
134
|
+
new["categories"][0]["attributes"][-1]["required"] = True
|
|
135
|
+
with pytest.raises(ValueError):
|
|
136
|
+
additive_image_schema_changes(old, new)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.mark.parametrize(
|
|
140
|
+
"part",
|
|
141
|
+
["digest", "source", "human", "schema", "changes", "foreign-clip", "unknown-field"],
|
|
142
|
+
)
|
|
143
|
+
def test_preview_rejects_tampered_transition_even_with_recomputed_fingerprint(part):
|
|
144
|
+
clip = clip_payload()
|
|
145
|
+
value = build_image_schema_upgrade_preview(clip, target(clip)).to_dict()
|
|
146
|
+
if part == "source":
|
|
147
|
+
value["after_identity"]["components"]["sample"] = "sha256:" + "a" * 64
|
|
148
|
+
if part == "human":
|
|
149
|
+
value["after_identity"]["annotation_revision"] = "sha256:" + "a" * 64
|
|
150
|
+
if part == "schema":
|
|
151
|
+
value["target_schema"]["body"]["version"] = "1.2.0"
|
|
152
|
+
if part == "changes":
|
|
153
|
+
value["changes"]["added_attributes"] = []
|
|
154
|
+
if part == "foreign-clip":
|
|
155
|
+
value["after_identity"]["clip_id"] = "foreign"
|
|
156
|
+
if part == "unknown-field":
|
|
157
|
+
value["automatic_repair"] = True
|
|
158
|
+
value["preview_fingerprint"] = (
|
|
159
|
+
"sha256:" + "0" * 64
|
|
160
|
+
if part == "digest"
|
|
161
|
+
else fingerprint({k: v for k, v in value.items() if k != "preview_fingerprint"})
|
|
162
|
+
)
|
|
163
|
+
with pytest.raises(ValueError):
|
|
164
|
+
parse_image_schema_upgrade_preview(value)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_typed_image_schema_reads_and_upgrade_verify_the_exact_request():
|
|
168
|
+
clip = clip_payload()
|
|
169
|
+
snapshot = target(clip)
|
|
170
|
+
preview = build_image_schema_upgrade_preview(clip, snapshot).to_dict()
|
|
171
|
+
record = {
|
|
172
|
+
"contract_version": "x4d-image-label-schema-record-v1",
|
|
173
|
+
"status": "active",
|
|
174
|
+
"body": snapshot["body"],
|
|
175
|
+
"fingerprint": snapshot["identity"]["fingerprint"],
|
|
176
|
+
}
|
|
177
|
+
requests, bad = [], {"enabled": False}
|
|
178
|
+
|
|
179
|
+
def handler(request):
|
|
180
|
+
requests.append(request)
|
|
181
|
+
path = request.url.path
|
|
182
|
+
if path == "/api/v1/label-schemas":
|
|
183
|
+
assert request.url.params["project_type"] == "2d"
|
|
184
|
+
return httpx.Response(200, json={"items": [record], "total": 1})
|
|
185
|
+
if path == "/api/v1/projects/7/label-schema":
|
|
186
|
+
return httpx.Response(
|
|
187
|
+
200,
|
|
188
|
+
json={
|
|
189
|
+
"contract_version": "x4d-image-project-label-schema-v1",
|
|
190
|
+
"project_id": 7,
|
|
191
|
+
"label_schema": record,
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
if path.startswith("/api/v1/label-schemas/"):
|
|
195
|
+
return httpx.Response(200, json=record)
|
|
196
|
+
if path.endswith("/preview"):
|
|
197
|
+
assert json.loads(request.content) == {
|
|
198
|
+
"project_id": 7,
|
|
199
|
+
"clip_id": "clip_test",
|
|
200
|
+
"target_schema": snapshot["identity"],
|
|
201
|
+
}
|
|
202
|
+
return httpx.Response(200, json=preview)
|
|
203
|
+
body = json.loads(request.content)
|
|
204
|
+
receipt = {
|
|
205
|
+
"contract_version": RECEIPT_CONTRACT,
|
|
206
|
+
"project_id": 7,
|
|
207
|
+
"clip_id": "clip_test",
|
|
208
|
+
"idempotency_key": body["idempotency_key"],
|
|
209
|
+
"preview_fingerprint": preview["preview_fingerprint"],
|
|
210
|
+
"identity": preview["after_identity"],
|
|
211
|
+
"changed": True,
|
|
212
|
+
}
|
|
213
|
+
if bad["enabled"]:
|
|
214
|
+
receipt["idempotency_key"] = "other-request"
|
|
215
|
+
return httpx.Response(200, json=receipt)
|
|
216
|
+
|
|
217
|
+
with X4DClient("http://x4d.test", token="test") as client:
|
|
218
|
+
client._http.close()
|
|
219
|
+
client._http = httpx.Client(
|
|
220
|
+
base_url="http://x4d.test", transport=httpx.MockTransport(handler)
|
|
221
|
+
)
|
|
222
|
+
assert client.image_schemas.list()["total"] == 1
|
|
223
|
+
assert (
|
|
224
|
+
client.image_schemas.get(
|
|
225
|
+
schema_id=snapshot["identity"]["schema_id"], version="1.1.0"
|
|
226
|
+
).body
|
|
227
|
+
== snapshot["body"]
|
|
228
|
+
)
|
|
229
|
+
assert client.image_schemas.get_project(7).to_dict()["project_id"] == 7
|
|
230
|
+
parsed = client.image_schemas.preview_upgrade(clip=clip, target_schema=snapshot)
|
|
231
|
+
request = client.image_schemas.upgrade_request(parsed, "explicit-request")
|
|
232
|
+
receipt = client.image_schemas.commit_upgrade(request)
|
|
233
|
+
assert (
|
|
234
|
+
parse_image_schema_upgrade_receipt(receipt.to_dict()).to_dict()["identity"]
|
|
235
|
+
== preview["after_identity"]
|
|
236
|
+
)
|
|
237
|
+
bad["enabled"] = True
|
|
238
|
+
with pytest.raises(ValueError, match="another request"):
|
|
239
|
+
client.image_schemas.commit_upgrade(request)
|
|
@@ -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.0rc8"
|
|
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
|
+
}
|
|
@@ -42,7 +42,10 @@ from x4d_devkit.client.reconstruction import (
|
|
|
42
42
|
from x4d_devkit.client.services import ServicesAPI
|
|
43
43
|
from x4d_devkit.client.test_sets import TestSetsAPI
|
|
44
44
|
|
|
45
|
+
from x4d_devkit.client.image_schemas import ImageSchemasAPI
|
|
46
|
+
|
|
45
47
|
__all__ = [
|
|
48
|
+
"ImageSchemasAPI",
|
|
46
49
|
"ImageWorkSessionsAPI",
|
|
47
50
|
"ImageExportsAPI",
|
|
48
51
|
"CleanBasemapState",
|