x4d-devkit 0.52.0rc2__tar.gz → 0.53.0rc2__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.52.0rc2 → x4d_devkit-0.53.0rc2}/PKG-INFO +1 -1
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/pyproject.toml +1 -1
- x4d_devkit-0.53.0rc2/tests/test_image_schema_migration.py +283 -0
- x4d_devkit-0.53.0rc2/tests/test_image_schema_registry_client.py +161 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_sdk_api.py +1 -1
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/__init__.py +7 -1
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/client.py +16 -0
- x4d_devkit-0.53.0rc2/x4d_devkit/client/image_schemas.py +317 -0
- x4d_devkit-0.53.0rc2/x4d_devkit/image_schema_migration.py +550 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit.egg-info/PKG-INFO +1 -1
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit.egg-info/SOURCES.txt +3 -0
- x4d_devkit-0.52.0rc2/x4d_devkit/client/image_schemas.py +0 -115
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/LICENSE +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/README.md +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/setup.cfg +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_accounts.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_annotation_document_hash.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_annotation_reference.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_box.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_category_audit.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_clean_basemap.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_clean_basemap_client.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_image_clip.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_image_conditional_rules.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_image_enum_set.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_image_export.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_image_geometry.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_image_groups.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_image_preannotation.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_image_reference.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_image_schema_upgrade.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_image_work_session.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_loader.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_matching.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_models.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_parse_equivalence.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_preannotation_client.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_project_discovery.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_roadmark_preview.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_semantic_scene_independent.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_source_attributes.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_token.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_transform.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_validation.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_vector_map.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_vector_map_geometry.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_vector_map_session.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/tests/test_work_session_validation_reuse.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/_image_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/accounts.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/annotation_reference.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/audit.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/clean_basemap.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/accounts.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/annotation_reference.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/clean_basemap.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/image_clips.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/image_exports.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/image_preannotations.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/image_work_sessions.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/preannotation.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/reconstruction.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/image_clip.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/image_export.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/image_geometry.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/image_groups.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/image_preannotation.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/image_reference.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/image_schema.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/image_schema_upgrade.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/image_work_session.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/project_discovery.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/roadmark_preview.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/vector_map.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/vector_map_geometry.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.52.0rc2 → x4d_devkit-0.53.0rc2}/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.
|
|
7
|
+
version = "0.53.0rc2"
|
|
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,283 @@
|
|
|
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
|
+
build_image_schema_snapshot,
|
|
12
|
+
parse_image_schema_body,
|
|
13
|
+
parse_image_schema_record,
|
|
14
|
+
)
|
|
15
|
+
from x4d_devkit.image_schema_migration import (
|
|
16
|
+
MAPPING_CONTRACT,
|
|
17
|
+
ImageSchemaMigrationConflict,
|
|
18
|
+
build_image_schema_migration_preview,
|
|
19
|
+
build_image_schema_migration_receipt,
|
|
20
|
+
build_image_schema_migration_request,
|
|
21
|
+
image_schema_migration_candidate,
|
|
22
|
+
migrate_image_annotation,
|
|
23
|
+
parse_image_schema_migration_mapping,
|
|
24
|
+
parse_image_schema_migration_preview,
|
|
25
|
+
parse_image_schema_migration_request,
|
|
26
|
+
)
|
|
27
|
+
from x4d_devkit.image_schema_upgrade import additive_image_schema_changes
|
|
28
|
+
from tests.image_fixtures import clip_payload, line_object
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def snapshot(body):
|
|
32
|
+
return build_image_schema_snapshot(
|
|
33
|
+
parse_image_schema_record(
|
|
34
|
+
{
|
|
35
|
+
"contract_version": "x4d-image-label-schema-record-v1",
|
|
36
|
+
"status": "active",
|
|
37
|
+
"body": body,
|
|
38
|
+
"fingerprint": parse_image_schema_body(body).identity.fingerprint,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def target(clip, **changes):
|
|
45
|
+
"""Non-additive 2.0.0: whole-line `visible` becomes segment `occluded`,
|
|
46
|
+
`style` becomes object-only `pattern`, and lines become groupable."""
|
|
47
|
+
body = deepcopy(clip["label_schema"]["body"])
|
|
48
|
+
body["version"] = changes.get("version", "2.0.0")
|
|
49
|
+
body["schema_id"] = changes.get("schema_id", body["schema_id"])
|
|
50
|
+
line = body["categories"][0]
|
|
51
|
+
line["groupable"] = True
|
|
52
|
+
line["attributes"] = [
|
|
53
|
+
{
|
|
54
|
+
"id": "occluded",
|
|
55
|
+
"display_name": "Occluded",
|
|
56
|
+
"type": "boolean",
|
|
57
|
+
"required": False,
|
|
58
|
+
"scopes": ["segment"],
|
|
59
|
+
"values": [],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "pattern",
|
|
63
|
+
"display_name": "Pattern",
|
|
64
|
+
"type": "enum",
|
|
65
|
+
"required": False,
|
|
66
|
+
"scopes": ["object"],
|
|
67
|
+
"values": [
|
|
68
|
+
{"id": "a", "display_name": "A"},
|
|
69
|
+
{"id": "b", "display_name": "B"},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
]
|
|
73
|
+
return snapshot(body)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def mapping(*rules):
|
|
77
|
+
return {"contract_version": MAPPING_CONTRACT, "rules": list(rules)}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def rule(scope, attribute_id, value, action):
|
|
81
|
+
return {
|
|
82
|
+
"match": {
|
|
83
|
+
"category_id": "line",
|
|
84
|
+
"scope": scope,
|
|
85
|
+
"attribute_id": attribute_id,
|
|
86
|
+
"value": value,
|
|
87
|
+
},
|
|
88
|
+
"action": action,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def with_line(obj=None):
|
|
93
|
+
clip = clip_payload()
|
|
94
|
+
clip["image_annotation"]["objects"] = [obj or line_object()]
|
|
95
|
+
clip["identity"] = build_image_identity(clip)
|
|
96
|
+
return clip
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
VISIBLE_TO_RANGE = rule(
|
|
100
|
+
"object", "visible", False, {"kind": "range", "attribute_id": "occluded", "value": True}
|
|
101
|
+
)
|
|
102
|
+
DISCARD_STYLE = rule("segment", "style", "a", {"kind": "discard"})
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_target_is_not_an_additive_upgrade():
|
|
106
|
+
clip = with_line()
|
|
107
|
+
with pytest.raises(ValueError):
|
|
108
|
+
additive_image_schema_changes(clip["label_schema"]["body"], target(clip)["body"])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_unused_vocabulary_needs_no_rule_and_keeps_the_annotation_bytes():
|
|
112
|
+
clip = with_line({**line_object(), "attributes": {}, "attribute_ranges": []})
|
|
113
|
+
candidate, report = image_schema_migration_candidate(clip, target(clip), mapping())
|
|
114
|
+
result = candidate.to_dict()
|
|
115
|
+
assert result["image_annotation"] == clip["image_annotation"]
|
|
116
|
+
assert result["identity"]["annotation_revision"] == clip["identity"]["annotation_revision"]
|
|
117
|
+
assert result["identity"]["source_revision"] == clip["identity"]["source_revision"]
|
|
118
|
+
assert result["identity"]["schema_fingerprint"] != clip["identity"]["schema_fingerprint"]
|
|
119
|
+
assert report == {"changes": [], "removed_ranges": [], "rule_usage": [], "carried_values": 0}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_every_unrepresentable_occurrence_is_listed_with_its_location():
|
|
123
|
+
clip = with_line()
|
|
124
|
+
with pytest.raises(ImageSchemaMigrationConflict) as caught:
|
|
125
|
+
image_schema_migration_candidate(clip, target(clip), mapping())
|
|
126
|
+
assert [
|
|
127
|
+
(row["code"], row["object_id"], row["range_id"], row["attribute_id"], row["value"])
|
|
128
|
+
for row in caught.value.conflicts
|
|
129
|
+
] == [
|
|
130
|
+
("value_unmapped", "line_a", None, "visible", False),
|
|
131
|
+
("value_unmapped", "line_a", "r0", "style", "a"),
|
|
132
|
+
]
|
|
133
|
+
assert caught.value.details["conflicts"] == caught.value.conflicts
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_explicit_rules_convert_and_discard_while_preserving_identity_and_geometry():
|
|
137
|
+
clip = with_line()
|
|
138
|
+
rules = mapping(VISIBLE_TO_RANGE, DISCARD_STYLE)
|
|
139
|
+
candidate, report = image_schema_migration_candidate(clip, target(clip), rules)
|
|
140
|
+
result = candidate.to_dict()
|
|
141
|
+
before, after = clip["image_annotation"]["objects"][0], result["image_annotation"]["objects"][0]
|
|
142
|
+
assert {k: v for k, v in after.items() if k not in ("attributes", "attribute_ranges")} == {
|
|
143
|
+
k: v for k, v in before.items() if k not in ("attributes", "attribute_ranges")
|
|
144
|
+
}
|
|
145
|
+
assert after["attributes"] == {}
|
|
146
|
+
[added] = after["attribute_ranges"]
|
|
147
|
+
assert added["start_vertex_id"] == "p0" and added["end_vertex_id"] == "p1"
|
|
148
|
+
assert added["attributes"] == {"occluded": True}
|
|
149
|
+
assert report["removed_ranges"] == [{"object_id": "line_a", "range_id": "r0"}]
|
|
150
|
+
assert report["rule_usage"] == [1, 1]
|
|
151
|
+
assert [(c["kind"], c["before"], c["after"]) for c in report["changes"]] == [
|
|
152
|
+
("range", False, {"attribute_id": "occluded", "value": True}),
|
|
153
|
+
("discard", "a", None),
|
|
154
|
+
]
|
|
155
|
+
for name in ("meta", "sample", "sample_data", "calibrated_sensor"):
|
|
156
|
+
assert result[name] == clip[name]
|
|
157
|
+
again, _ = image_schema_migration_candidate(clip, target(clip), rules)
|
|
158
|
+
assert again.to_dict() == result # deterministic range IDs
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_draft_annotations_migrate_with_the_same_function():
|
|
162
|
+
clip = with_line()
|
|
163
|
+
draft, report = migrate_image_annotation(
|
|
164
|
+
clip["image_annotation"],
|
|
165
|
+
target_schema=target(clip)["body"],
|
|
166
|
+
mapping=mapping(VISIBLE_TO_RANGE, DISCARD_STYLE),
|
|
167
|
+
)
|
|
168
|
+
full, _ = image_schema_migration_candidate(
|
|
169
|
+
clip, target(clip), mapping(VISIBLE_TO_RANGE, DISCARD_STYLE)
|
|
170
|
+
)
|
|
171
|
+
assert draft == full.to_dict()["image_annotation"]
|
|
172
|
+
assert report["rule_usage"] == [1, 1]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_a_mapped_value_must_be_valid_in_the_target_scope():
|
|
176
|
+
clip = with_line()
|
|
177
|
+
bad = rule("segment", "style", "a", {"kind": "set", "attribute_id": "pattern", "value": "a"})
|
|
178
|
+
with pytest.raises(ImageSchemaMigrationConflict) as caught:
|
|
179
|
+
image_schema_migration_candidate(clip, target(clip), mapping(VISIBLE_TO_RANGE, bad))
|
|
180
|
+
assert [row["code"] for row in caught.value.conflicts] == ["mapped_value_invalid"]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_two_values_cannot_land_on_the_same_attribute():
|
|
184
|
+
obj = {**line_object(), "attributes": {"visible": False, "style": "b"}, "attribute_ranges": []}
|
|
185
|
+
clip = with_line(obj)
|
|
186
|
+
rules = mapping(
|
|
187
|
+
rule("object", "visible", False, {"kind": "set", "attribute_id": "pattern", "value": "a"}),
|
|
188
|
+
rule("object", "style", "b", {"kind": "set", "attribute_id": "pattern", "value": "b"}),
|
|
189
|
+
)
|
|
190
|
+
with pytest.raises(ImageSchemaMigrationConflict) as caught:
|
|
191
|
+
image_schema_migration_candidate(clip, target(clip), rules)
|
|
192
|
+
assert [row["code"] for row in caught.value.conflicts] == ["attribute_collision"]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.mark.parametrize(
|
|
196
|
+
"rules",
|
|
197
|
+
[
|
|
198
|
+
[VISIBLE_TO_RANGE, VISIBLE_TO_RANGE],
|
|
199
|
+
[rule("segment", "style", "a", {"kind": "range", "attribute_id": "occluded", "value": True})],
|
|
200
|
+
[rule("object", "visible", False, {"kind": "rename"})],
|
|
201
|
+
[rule("object", "visible", False, {"kind": "discard", "attribute_id": "x"})],
|
|
202
|
+
[rule("image", "visible", False, {"kind": "discard"})],
|
|
203
|
+
[rule("object", "visible", 3, {"kind": "discard"})],
|
|
204
|
+
],
|
|
205
|
+
)
|
|
206
|
+
def test_mapping_grammar_is_closed(rules):
|
|
207
|
+
with pytest.raises(ValueError):
|
|
208
|
+
parse_image_schema_migration_mapping(mapping(*rules))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@pytest.mark.parametrize("change", [{"schema_id": "another"}, {"version": "1.0.0"}])
|
|
212
|
+
def test_migration_keeps_the_schema_id_and_advances_the_version(change):
|
|
213
|
+
clip = with_line({**line_object(), "attributes": {}, "attribute_ranges": []})
|
|
214
|
+
with pytest.raises(ValueError):
|
|
215
|
+
image_schema_migration_candidate(clip, target(clip, **change), mapping())
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@pytest.mark.parametrize("part", ["digest", "source", "report", "usage", "mapping", "unknown"])
|
|
219
|
+
def test_preview_rejects_tampering_even_with_a_recomputed_fingerprint(part):
|
|
220
|
+
clip = with_line()
|
|
221
|
+
value = build_image_schema_migration_preview(
|
|
222
|
+
clip, target(clip), mapping(VISIBLE_TO_RANGE, DISCARD_STYLE)
|
|
223
|
+
).to_dict()
|
|
224
|
+
if part == "source":
|
|
225
|
+
value["after_identity"]["components"]["sample"] = "sha256:" + "a" * 64
|
|
226
|
+
if part == "report":
|
|
227
|
+
value["report"]["changes"] = []
|
|
228
|
+
value["report"]["removed_ranges"] = []
|
|
229
|
+
value["report"]["rule_usage"] = [0, 0]
|
|
230
|
+
if part == "usage":
|
|
231
|
+
value["report"]["rule_usage"] = [1]
|
|
232
|
+
if part == "mapping":
|
|
233
|
+
value["mapping"]["rules"].append(VISIBLE_TO_RANGE)
|
|
234
|
+
if part == "unknown":
|
|
235
|
+
value["automatic_repair"] = True
|
|
236
|
+
value["preview_fingerprint"] = (
|
|
237
|
+
"sha256:" + "0" * 64
|
|
238
|
+
if part == "digest"
|
|
239
|
+
else fingerprint({k: v for k, v in value.items() if k != "preview_fingerprint"})
|
|
240
|
+
)
|
|
241
|
+
with pytest.raises(ValueError):
|
|
242
|
+
parse_image_schema_migration_preview(value)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_typed_client_verifies_the_exact_preview_and_receipt():
|
|
246
|
+
clip = with_line()
|
|
247
|
+
snap = target(clip)
|
|
248
|
+
rules = mapping(VISIBLE_TO_RANGE, DISCARD_STYLE)
|
|
249
|
+
preview = build_image_schema_migration_preview(clip, snap, rules).to_dict()
|
|
250
|
+
bad = {"enabled": False}
|
|
251
|
+
|
|
252
|
+
def handler(request):
|
|
253
|
+
body = json.loads(request.content)
|
|
254
|
+
if request.url.path.endswith("/image-schema-migrations/preview"):
|
|
255
|
+
assert body == {
|
|
256
|
+
"project_id": 7,
|
|
257
|
+
"clip_id": "clip_test",
|
|
258
|
+
"target_schema": snap["identity"],
|
|
259
|
+
"mapping": rules,
|
|
260
|
+
}
|
|
261
|
+
return httpx.Response(200, json=preview)
|
|
262
|
+
assert request.url.path.endswith("/image-schema-migrations/commit")
|
|
263
|
+
receipt = build_image_schema_migration_receipt(body).to_dict()
|
|
264
|
+
if bad["enabled"]:
|
|
265
|
+
receipt["idempotency_key"] = "other-request"
|
|
266
|
+
return httpx.Response(200, json=receipt)
|
|
267
|
+
|
|
268
|
+
with X4DClient("http://x4d.test", token="test") as client:
|
|
269
|
+
client._http.close()
|
|
270
|
+
client._http = httpx.Client(
|
|
271
|
+
base_url="http://x4d.test", transport=httpx.MockTransport(handler)
|
|
272
|
+
)
|
|
273
|
+
parsed = client.image_schemas.preview_migration(
|
|
274
|
+
clip=clip, target_schema=snap, mapping=rules
|
|
275
|
+
)
|
|
276
|
+
request = client.image_schemas.migration_request(parsed, "explicit-migration")
|
|
277
|
+
assert parse_image_schema_migration_request(request.to_dict())
|
|
278
|
+
receipt = client.image_schemas.commit_migration(request).to_dict()
|
|
279
|
+
assert receipt["identity"] == preview["after_identity"]
|
|
280
|
+
bad["enabled"] = True
|
|
281
|
+
with pytest.raises(ValueError, match="another request"):
|
|
282
|
+
client.image_schemas.commit_migration(request)
|
|
283
|
+
assert build_image_schema_migration_request(parsed, "k").to_dict()["preview"] == preview
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Typed image Schema authoring against a strict in-memory registry."""
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from x4d_devkit import X4DClient
|
|
11
|
+
from x4d_devkit._image_contract import canonical
|
|
12
|
+
from x4d_devkit.image_schema import parse_image_schema_body
|
|
13
|
+
from tests.image_fixtures import schema_body
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def etag(value):
|
|
17
|
+
return '"sha256:' + hashlib.sha256(canonical(value)).hexdigest() + '"'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Registry:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.rows = {}
|
|
23
|
+
self.project = None
|
|
24
|
+
self.tamper = None
|
|
25
|
+
|
|
26
|
+
def record(self, body, status):
|
|
27
|
+
return {
|
|
28
|
+
"contract_version": "x4d-image-label-schema-record-v1",
|
|
29
|
+
"status": status,
|
|
30
|
+
"body": body,
|
|
31
|
+
"fingerprint": parse_image_schema_body(body).identity.fingerprint,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def reply(self, value, status=200):
|
|
35
|
+
if self.tamper:
|
|
36
|
+
value = self.tamper(deepcopy(value))
|
|
37
|
+
return httpx.Response(status, json=value, headers={"ETag": etag(value)})
|
|
38
|
+
|
|
39
|
+
def __call__(self, request):
|
|
40
|
+
path = request.url.path.removeprefix("/api/v1")
|
|
41
|
+
body = json.loads(request.content) if request.content else None
|
|
42
|
+
match = request.headers.get("If-Match")
|
|
43
|
+
if path == "/image-schemas/validate":
|
|
44
|
+
return httpx.Response(200, json=self.record(body["body"], "draft"))
|
|
45
|
+
if path == "/label-schemas" and request.method == "POST":
|
|
46
|
+
key = (body["body"]["schema_id"], body["body"]["version"])
|
|
47
|
+
self.rows[key] = self.record(body["body"], "draft")
|
|
48
|
+
return self.reply(self.rows[key], 201)
|
|
49
|
+
if path.startswith("/label-schemas/"):
|
|
50
|
+
_, _, schema_id, _, version, *action = path.split("/")
|
|
51
|
+
row = self.rows[(schema_id, version)]
|
|
52
|
+
if request.method == "GET":
|
|
53
|
+
return self.reply(row)
|
|
54
|
+
if match != etag(row):
|
|
55
|
+
return httpx.Response(412, json={"detail": "changed"})
|
|
56
|
+
if request.method == "DELETE":
|
|
57
|
+
del self.rows[(schema_id, version)]
|
|
58
|
+
return httpx.Response(204)
|
|
59
|
+
if action == ["publish"]:
|
|
60
|
+
row["status"] = "active"
|
|
61
|
+
elif action == ["deprecate"]:
|
|
62
|
+
row["status"] = "deprecated"
|
|
63
|
+
else:
|
|
64
|
+
row["body"] = body["body"]
|
|
65
|
+
row["fingerprint"] = parse_image_schema_body(body["body"]).identity.fingerprint
|
|
66
|
+
return self.reply(row)
|
|
67
|
+
if path == "/projects/7/label-schema":
|
|
68
|
+
if request.method == "PUT":
|
|
69
|
+
if match != etag(self.project):
|
|
70
|
+
return httpx.Response(412, json={"detail": "changed"})
|
|
71
|
+
self.project = {**self.project, "label_schema": self.rows[(body["schema_id"], body["version"])]}
|
|
72
|
+
return self.reply(self.project)
|
|
73
|
+
raise AssertionError(path)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.fixture
|
|
77
|
+
def setup():
|
|
78
|
+
registry = Registry()
|
|
79
|
+
with X4DClient("http://x4d.test", token="test") as client:
|
|
80
|
+
client._http.close()
|
|
81
|
+
client._http = httpx.Client(base_url="http://x4d.test", transport=httpx.MockTransport(registry))
|
|
82
|
+
yield client.image_schemas, registry
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def draft_body(version="2.0.0"):
|
|
86
|
+
body = schema_body()
|
|
87
|
+
body["version"] = version
|
|
88
|
+
return body
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_draft_update_publish_and_bind_round_trip(setup):
|
|
92
|
+
api, registry = setup
|
|
93
|
+
body = draft_body()
|
|
94
|
+
assert api.validate(body).to_dict()["status"] == "draft"
|
|
95
|
+
entry = api.create_draft(body)
|
|
96
|
+
assert entry.record.to_dict()["status"] == "draft"
|
|
97
|
+
edited = deepcopy(body)
|
|
98
|
+
edited["description"] = "Edited draft"
|
|
99
|
+
entry = api.update_draft(edited, etag=entry.etag)
|
|
100
|
+
assert entry.record.body["description"] == "Edited draft"
|
|
101
|
+
assert api.get_entry(schema_id="test_image", version="2.0.0") == entry
|
|
102
|
+
entry = api.publish(entry)
|
|
103
|
+
assert entry.record.to_dict()["status"] == "active"
|
|
104
|
+
registry.project = {
|
|
105
|
+
"contract_version": "x4d-image-project-label-schema-v1",
|
|
106
|
+
"project_id": 7,
|
|
107
|
+
"label_schema": registry.record(schema_body(), "active"),
|
|
108
|
+
}
|
|
109
|
+
project = api.get_project_entry(7)
|
|
110
|
+
bound = api.bind_project(project, schema_id="test_image", version="2.0.0")
|
|
111
|
+
assert bound.value.to_dict()["label_schema"]["body"]["version"] == "2.0.0"
|
|
112
|
+
deprecated = api.deprecate(entry, reason="replaced")
|
|
113
|
+
assert deprecated.record.to_dict()["status"] == "deprecated"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_stale_etag_is_rejected_by_the_server(setup):
|
|
117
|
+
api, _ = setup
|
|
118
|
+
first = api.create_draft(draft_body())
|
|
119
|
+
api.update_draft({**draft_body(), "description": "one"}, etag=first.etag)
|
|
120
|
+
with pytest.raises(httpx.HTTPStatusError):
|
|
121
|
+
api.update_draft({**draft_body(), "description": "two"}, etag=first.etag)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_delete_draft_needs_the_current_etag(setup):
|
|
125
|
+
api, registry = setup
|
|
126
|
+
entry = api.create_draft(draft_body())
|
|
127
|
+
api.delete_draft(entry)
|
|
128
|
+
assert registry.rows == {}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_invalid_body_never_reaches_the_server(setup):
|
|
132
|
+
api, registry = setup
|
|
133
|
+
body = draft_body()
|
|
134
|
+
body["categories"][0]["geometry_type"] = "circle"
|
|
135
|
+
with pytest.raises(ValueError):
|
|
136
|
+
api.create_draft(body)
|
|
137
|
+
assert registry.rows == {}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@pytest.mark.parametrize("field", ["status", "body"])
|
|
141
|
+
def test_a_response_for_another_record_or_lifecycle_is_rejected(setup, field):
|
|
142
|
+
api, registry = setup
|
|
143
|
+
entry = api.create_draft(draft_body())
|
|
144
|
+
|
|
145
|
+
def tamper(value):
|
|
146
|
+
if field == "status":
|
|
147
|
+
value["status"] = "draft"
|
|
148
|
+
else:
|
|
149
|
+
value["body"]["description"] = "something else"
|
|
150
|
+
value["fingerprint"] = parse_image_schema_body(value["body"]).identity.fingerprint
|
|
151
|
+
return value
|
|
152
|
+
|
|
153
|
+
registry.tamper = tamper
|
|
154
|
+
with pytest.raises(ValueError, match="another Schema"):
|
|
155
|
+
api.publish(entry)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_a_weak_or_missing_etag_is_refused_locally(setup):
|
|
159
|
+
api, _ = setup
|
|
160
|
+
with pytest.raises(ValueError, match="strong ETag"):
|
|
161
|
+
api.update_draft(draft_body(), etag="W/\"x\"")
|
|
@@ -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.
|
|
14
|
+
assert x4d_devkit.__version__ == "0.53.0rc2"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
|
|
@@ -43,9 +43,15 @@ from x4d_devkit.client.reconstruction import (
|
|
|
43
43
|
from x4d_devkit.client.services import ServicesAPI
|
|
44
44
|
from x4d_devkit.client.test_sets import TestSetsAPI
|
|
45
45
|
|
|
46
|
-
from x4d_devkit.client.image_schemas import
|
|
46
|
+
from x4d_devkit.client.image_schemas import (
|
|
47
|
+
ImageProjectSchemaEntry,
|
|
48
|
+
ImageSchemaEntry,
|
|
49
|
+
ImageSchemasAPI,
|
|
50
|
+
)
|
|
47
51
|
|
|
48
52
|
__all__ = [
|
|
53
|
+
"ImageProjectSchemaEntry",
|
|
54
|
+
"ImageSchemaEntry",
|
|
49
55
|
"ImageSchemasAPI",
|
|
50
56
|
"ImageWorkSessionsAPI",
|
|
51
57
|
"ImageExportsAPI",
|
|
@@ -173,6 +173,22 @@ class X4DClient:
|
|
|
173
173
|
resp.raise_for_status()
|
|
174
174
|
return parse_i_json(resp.content, path=f"POST {self._url(path)} response")
|
|
175
175
|
|
|
176
|
+
def put(self, path: str, **kwargs):
|
|
177
|
+
resp = self._http.put(self._url(path), **kwargs)
|
|
178
|
+
resp.raise_for_status()
|
|
179
|
+
return parse_i_json(resp.content, path=f"PUT {self._url(path)} response")
|
|
180
|
+
|
|
181
|
+
def exchange(self, method: str, path: str, **kwargs):
|
|
182
|
+
"""One request returning ``(strict JSON or None for 204, ETag or None)``."""
|
|
183
|
+
resp = self._http.request(method, self._url(path), **kwargs)
|
|
184
|
+
resp.raise_for_status()
|
|
185
|
+
value = (
|
|
186
|
+
parse_i_json(resp.content, path=f"{method} {self._url(path)} response")
|
|
187
|
+
if resp.content
|
|
188
|
+
else None
|
|
189
|
+
)
|
|
190
|
+
return value, resp.headers.get("ETag")
|
|
191
|
+
|
|
176
192
|
def delete(self, path: str, **kwargs):
|
|
177
193
|
resp = self._http.delete(self._url(path), **kwargs)
|
|
178
194
|
resp.raise_for_status()
|