x4d-devkit 0.51.0rc3__tar.gz → 0.52.0__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.51.0rc3 → x4d_devkit-0.52.0}/PKG-INFO +1 -1
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/pyproject.toml +1 -1
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_annotation_document_hash.py +23 -0
- x4d_devkit-0.52.0/tests/test_image_conditional_rules.py +328 -0
- x4d_devkit-0.52.0/tests/test_image_enum_set.py +178 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_parse_equivalence.py +3 -2
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_sdk_api.py +1 -1
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/annotation_contract.py +12 -2
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/image_clip.py +123 -1
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/image_schema.py +167 -5
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/image_schema_upgrade.py +7 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/work_session.py +2 -1
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit.egg-info/PKG-INFO +1 -1
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit.egg-info/SOURCES.txt +2 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/LICENSE +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/README.md +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/setup.cfg +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_accounts.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_annotation_reference.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_box.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_category_audit.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_clean_basemap.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_clean_basemap_client.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_image_clip.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_image_export.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_image_geometry.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_image_groups.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_image_preannotation.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_image_reference.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_image_schema_upgrade.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_image_work_session.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_loader.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_matching.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_models.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_preannotation_client.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_project_discovery.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_roadmark_preview.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_semantic_scene_independent.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_source_attributes.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_token.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_transform.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_validation.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_vector_map.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_vector_map_geometry.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_vector_map_session.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/tests/test_work_session_validation_reuse.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/_image_contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/accounts.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/annotation_reference.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/audit.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/clean_basemap.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/accounts.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/annotation_reference.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/clean_basemap.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/image_clips.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/image_exports.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/image_preannotations.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/image_schemas.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/image_work_sessions.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/preannotation.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/reconstruction.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/image_export.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/image_geometry.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/image_groups.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/image_preannotation.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/image_reference.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/image_work_session.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/project_discovery.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/roadmark_preview.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/vector_map.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit/vector_map_geometry.py +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.51.0rc3 → x4d_devkit-0.52.0}/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.52.0"
|
|
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"
|
|
@@ -4,11 +4,13 @@ import dataclasses
|
|
|
4
4
|
import hashlib
|
|
5
5
|
import random
|
|
6
6
|
|
|
7
|
+
import pytest
|
|
7
8
|
import rfc8785
|
|
8
9
|
|
|
9
10
|
from tests.test_annotation_contract import _annotation
|
|
10
11
|
from x4d_devkit.annotation_contract import (
|
|
11
12
|
ANNOTATION_CONTRACT_VERSION,
|
|
13
|
+
AnnotationContractError,
|
|
12
14
|
ClipAnnotationDocument,
|
|
13
15
|
build_clip_annotations,
|
|
14
16
|
parse_clip_annotations,
|
|
@@ -86,3 +88,24 @@ def test_document_hash_is_not_part_of_document_equality():
|
|
|
86
88
|
assert built == parsed == plain
|
|
87
89
|
assert "_document_hash" not in {field.name for field in dataclasses.fields(built)}
|
|
88
90
|
assert built.to_dict()["contract_version"] == ANNOTATION_CONTRACT_VERSION
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_parse_reuses_only_documents_this_module_verified():
|
|
94
|
+
built = build_clip_annotations("clip-a", "base_link", _random_rows(random.Random(3), 4))
|
|
95
|
+
assert parse_clip_annotations(built) is built
|
|
96
|
+
assert parse_clip_annotations(parse_clip_annotations(built.to_dict())) is not built
|
|
97
|
+
|
|
98
|
+
forged = ClipAnnotationDocument(
|
|
99
|
+
clip_id=built.clip_id,
|
|
100
|
+
annotation_frame_id=built.annotation_frame_id,
|
|
101
|
+
annotation_revision="sha256:" + "0" * 64,
|
|
102
|
+
annotations=built.annotations,
|
|
103
|
+
)
|
|
104
|
+
forged.document_hash() # caching the hash must not mark the document verified
|
|
105
|
+
with pytest.raises(AnnotationContractError) as error:
|
|
106
|
+
parse_clip_annotations(forged)
|
|
107
|
+
assert error.value.code == "annotation_revision_mismatch"
|
|
108
|
+
|
|
109
|
+
replaced = dataclasses.replace(built, annotation_revision="sha256:" + "1" * 64)
|
|
110
|
+
with pytest.raises(AnnotationContractError):
|
|
111
|
+
parse_clip_annotations(replaced)
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Closed conditional attribute rules, English terms and description coverage (#220)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from x4d_devkit._image_contract import ImageContractError, fingerprint
|
|
10
|
+
from x4d_devkit.image_clip import build_image_identity, parse_image_clip
|
|
11
|
+
from x4d_devkit.image_schema import parse_image_schema_body
|
|
12
|
+
from tests.image_fixtures import clip_payload, schema_body
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def enum(attribute_id, values, **extra):
|
|
16
|
+
return {
|
|
17
|
+
"id": attribute_id,
|
|
18
|
+
"display_name": attribute_id,
|
|
19
|
+
"type": "enum",
|
|
20
|
+
"required": False,
|
|
21
|
+
"scopes": ["object"],
|
|
22
|
+
"values": [{"id": value, "display_name": value} for value in values],
|
|
23
|
+
**extra,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def boolean(attribute_id):
|
|
28
|
+
return {
|
|
29
|
+
"id": attribute_id,
|
|
30
|
+
"display_name": attribute_id,
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"required": False,
|
|
33
|
+
"scopes": ["object"],
|
|
34
|
+
"values": [],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def directions():
|
|
39
|
+
return {
|
|
40
|
+
"id": "occlusion_directions",
|
|
41
|
+
"display_name": "occlusion directions",
|
|
42
|
+
"type": "enum_set",
|
|
43
|
+
"required": False,
|
|
44
|
+
"scopes": ["object"],
|
|
45
|
+
"values": [
|
|
46
|
+
{"id": side, "display_name": side} for side in ("top", "bottom", "left", "right")
|
|
47
|
+
],
|
|
48
|
+
"min_items": 1,
|
|
49
|
+
"max_items": 4,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
SIGNAL_RULES = [
|
|
54
|
+
{
|
|
55
|
+
"id": "head_facts",
|
|
56
|
+
"attributes": ["head_type", "occlusion"],
|
|
57
|
+
"required": ["head_type", "occlusion"],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"id": "arrow",
|
|
61
|
+
"when": {"attribute": "head_type", "equals": "arrow"},
|
|
62
|
+
"attributes": ["arrow_direction"],
|
|
63
|
+
"required": ["arrow_direction"],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "damaged",
|
|
67
|
+
"when": {"attribute": "head_type", "in": ["countdown", "countdown_damaged"]},
|
|
68
|
+
"attributes": ["digit_unclear"],
|
|
69
|
+
"required": ["digit_unclear"],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "digit_countdown",
|
|
73
|
+
"when": {"attribute": "head_type", "equals": "countdown"},
|
|
74
|
+
"attributes": ["digit"],
|
|
75
|
+
"required": ["digit"],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "digit_damaged_known",
|
|
79
|
+
"when": {
|
|
80
|
+
"all": [
|
|
81
|
+
{"attribute": "head_type", "equals": "countdown_damaged"},
|
|
82
|
+
{"attribute": "digit_unclear", "equals": False},
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
"attributes": ["digit"],
|
|
86
|
+
"required": ["digit"],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "digit_optional_when_unclear",
|
|
90
|
+
"when": {
|
|
91
|
+
"all": [
|
|
92
|
+
{"attribute": "head_type", "equals": "countdown_damaged"},
|
|
93
|
+
{"attribute": "digit_unclear", "equals": True},
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
"attributes": ["digit"],
|
|
97
|
+
"required": [],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "occluded",
|
|
101
|
+
"when": {"attribute": "occlusion", "equals": True},
|
|
102
|
+
"attributes": ["occlusion_directions", "occlusion_ratio"],
|
|
103
|
+
"required": ["occlusion_directions", "occlusion_ratio"],
|
|
104
|
+
},
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def signal_category(rules=None):
|
|
109
|
+
return {
|
|
110
|
+
"id": "box",
|
|
111
|
+
"display_name": "Signal head",
|
|
112
|
+
"geometry_type": "box_2d",
|
|
113
|
+
"color": "#112233",
|
|
114
|
+
"attributes": [
|
|
115
|
+
enum("head_type", ["round", "arrow", "countdown", "countdown_damaged"]),
|
|
116
|
+
enum("arrow_direction", ["up", "left"]),
|
|
117
|
+
enum("digit", [str(digit) for digit in range(10)]),
|
|
118
|
+
boolean("digit_unclear"),
|
|
119
|
+
boolean("occlusion"),
|
|
120
|
+
directions(),
|
|
121
|
+
enum("occlusion_ratio", ["low", "medium", "high"]),
|
|
122
|
+
],
|
|
123
|
+
"rules": copy.deepcopy(SIGNAL_RULES if rules is None else rules),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def body(category=None, description_fields=None):
|
|
128
|
+
value = schema_body()
|
|
129
|
+
value["categories"][1] = category or signal_category()
|
|
130
|
+
if description_fields is not None:
|
|
131
|
+
value["task_views"][1]["category_ids"] = ["box"]
|
|
132
|
+
value["task_views"][1]["description_fields"] = description_fields
|
|
133
|
+
return value
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def box(object_id, attributes):
|
|
137
|
+
return {
|
|
138
|
+
"id": object_id,
|
|
139
|
+
"image_id": "image_a",
|
|
140
|
+
"view_id": "objects",
|
|
141
|
+
"category_id": "box",
|
|
142
|
+
"geometry": {
|
|
143
|
+
"geometry_type": "box_2d",
|
|
144
|
+
"box": {"x_min": 10, "y_min": 20, "x_max": 30, "y_max": 40},
|
|
145
|
+
},
|
|
146
|
+
"attributes": attributes,
|
|
147
|
+
"attribute_ranges": [],
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def clip(schema, objects, *, objects_state="complete", descriptions=(), text_state="unprocessed"):
|
|
152
|
+
value = clip_payload()
|
|
153
|
+
value["label_schema"]["body"] = schema
|
|
154
|
+
value["label_schema"]["identity"]["fingerprint"] = fingerprint(schema)
|
|
155
|
+
value["image_annotation"]["objects"] = objects
|
|
156
|
+
value["image_annotation"]["descriptions"] = list(descriptions)
|
|
157
|
+
states = {"objects": objects_state, "semantics": text_state}
|
|
158
|
+
for row in value["image_annotation"]["completion"]:
|
|
159
|
+
row["state"] = states[row["view_id"]]
|
|
160
|
+
value["identity"] = build_image_identity(value)
|
|
161
|
+
return parse_image_clip(value)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def head(**attributes):
|
|
165
|
+
return box("head_a", {"occlusion": False, **attributes})
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pytest.mark.parametrize(
|
|
169
|
+
"attributes",
|
|
170
|
+
[
|
|
171
|
+
{"head_type": "round"},
|
|
172
|
+
{"head_type": "arrow", "arrow_direction": "left"},
|
|
173
|
+
{"head_type": "countdown", "digit_unclear": False, "digit": "4"},
|
|
174
|
+
{"head_type": "countdown_damaged", "digit_unclear": True},
|
|
175
|
+
{"head_type": "countdown_damaged", "digit_unclear": True, "digit": "9"},
|
|
176
|
+
{"head_type": "countdown_damaged", "digit_unclear": False, "digit": "9"},
|
|
177
|
+
{
|
|
178
|
+
"head_type": "round",
|
|
179
|
+
"occlusion": True,
|
|
180
|
+
"occlusion_directions": ["top", "left"],
|
|
181
|
+
"occlusion_ratio": "medium",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
)
|
|
185
|
+
def test_completed_heads_satisfy_conditional_rules(attributes):
|
|
186
|
+
clip(body(), [head(**attributes)])
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@pytest.mark.parametrize(
|
|
190
|
+
("attributes", "code", "attribute"),
|
|
191
|
+
[
|
|
192
|
+
({}, "attribute_required_on_completion", "head_type"),
|
|
193
|
+
({"head_type": "arrow"}, "attribute_required_on_completion", "arrow_direction"),
|
|
194
|
+
({"head_type": "round", "arrow_direction": "up"}, "attribute_not_applicable", "arrow_direction"),
|
|
195
|
+
({"head_type": "countdown", "digit_unclear": False}, "attribute_required_on_completion", "digit"),
|
|
196
|
+
({"head_type": "countdown_damaged", "digit_unclear": False}, "attribute_required_on_completion", "digit"),
|
|
197
|
+
({"head_type": "round", "digit": "3"}, "attribute_not_applicable", "digit"),
|
|
198
|
+
({"head_type": "round", "occlusion": True, "occlusion_ratio": "low"}, "attribute_required_on_completion", "occlusion_directions"),
|
|
199
|
+
({"head_type": "round", "occlusion_directions": ["top"]}, "attribute_not_applicable", "occlusion_directions"),
|
|
200
|
+
],
|
|
201
|
+
)
|
|
202
|
+
def test_completed_heads_reject_missing_or_leftover_facts(attributes, code, attribute):
|
|
203
|
+
with pytest.raises(ImageContractError) as error:
|
|
204
|
+
clip(body(), [head(**attributes)])
|
|
205
|
+
assert error.value.code == code
|
|
206
|
+
assert error.value.path == f"image_annotation.objects.head_a.attributes.{attribute}"
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_drafts_may_pause_with_missing_or_leftover_facts_but_values_stay_typed():
|
|
210
|
+
clip(body(), [head(head_type="round", arrow_direction="up")], objects_state="unprocessed")
|
|
211
|
+
clip(body(), [box("head_a", {})], objects_state="unprocessed")
|
|
212
|
+
with pytest.raises(ImageContractError):
|
|
213
|
+
clip(body(), [head(head_type="round", arrow_direction="sideways")], objects_state="unprocessed")
|
|
214
|
+
with pytest.raises(ImageContractError): # enum_set bounds apply to drafts too
|
|
215
|
+
clip(
|
|
216
|
+
body(),
|
|
217
|
+
[head(head_type="round", occlusion=True, occlusion_directions=[])],
|
|
218
|
+
objects_state="unprocessed",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_rules_change_the_schema_fingerprint():
|
|
223
|
+
plain = parse_image_schema_body(body(signal_category(rules=SIGNAL_RULES[:1])))
|
|
224
|
+
full = parse_image_schema_body(body())
|
|
225
|
+
assert plain.identity.fingerprint != full.identity.fingerprint
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _rule(**change):
|
|
229
|
+
rule = copy.deepcopy(SIGNAL_RULES[1])
|
|
230
|
+
rule.update(change)
|
|
231
|
+
return rule
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@pytest.mark.parametrize(
|
|
235
|
+
"rules",
|
|
236
|
+
[
|
|
237
|
+
[],
|
|
238
|
+
[_rule(attributes=["missing"], required=[])],
|
|
239
|
+
[_rule(required=["head_type"])],
|
|
240
|
+
[_rule(attributes=["arrow_direction", "arrow_direction"])],
|
|
241
|
+
[_rule(when={"attribute": "missing", "equals": "arrow"})],
|
|
242
|
+
[_rule(when={"attribute": "head_type", "equals": "triangle"})],
|
|
243
|
+
[_rule(when={"attribute": "head_type", "equals": True})],
|
|
244
|
+
[_rule(when={"attribute": "occlusion", "equals": "true"})],
|
|
245
|
+
[_rule(when={"attribute": "head_type", "in": []})],
|
|
246
|
+
[_rule(when={"attribute": "head_type", "in": ["arrow", "arrow"]})],
|
|
247
|
+
[_rule(when={"attribute": "head_type", "contains": "arrow"})],
|
|
248
|
+
[_rule(when={"attribute": "occlusion_directions", "equals": "top"})],
|
|
249
|
+
[_rule(when={"attribute": "head_type", "equals": "arrow", "in": ["arrow"]})],
|
|
250
|
+
[_rule(when={"all": [{"any": [{"attribute": "head_type", "equals": "arrow"}]}]})],
|
|
251
|
+
[_rule(when={"all": []})],
|
|
252
|
+
[_rule(when="head_type == 'arrow'")],
|
|
253
|
+
[_rule(), _rule()],
|
|
254
|
+
[
|
|
255
|
+
_rule(id="a", when={"attribute": "digit", "equals": "1"}, attributes=["arrow_direction"]),
|
|
256
|
+
_rule(id="b", when={"attribute": "arrow_direction", "equals": "up"}, attributes=["digit"], required=[]),
|
|
257
|
+
],
|
|
258
|
+
],
|
|
259
|
+
)
|
|
260
|
+
def test_schema_rejects_open_or_inconsistent_rules(rules):
|
|
261
|
+
with pytest.raises(ImageContractError):
|
|
262
|
+
parse_image_schema_body(body(signal_category(rules=rules)))
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_governed_attribute_cannot_also_be_always_required():
|
|
266
|
+
category = signal_category()
|
|
267
|
+
category["attributes"][1]["required"] = True
|
|
268
|
+
with pytest.raises(ImageContractError):
|
|
269
|
+
parse_image_schema_body(body(category))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_enum_values_may_declare_english_terms():
|
|
273
|
+
category = signal_category()
|
|
274
|
+
category["attributes"][0]["values"][0]["term"] = {"singular": "round light", "plural": "round lights"}
|
|
275
|
+
category["attributes"][2]["values"][0]["term"] = {"singular": "zero"}
|
|
276
|
+
parse_image_schema_body(body(category))
|
|
277
|
+
for term in ({"singular": " "}, {"plural": "lights"}, {"singular": "x", "plural": ""}, {"singular": "x", "other": "y"}):
|
|
278
|
+
category["attributes"][0]["values"][0]["term"] = term
|
|
279
|
+
with pytest.raises(ImageContractError):
|
|
280
|
+
parse_image_schema_body(body(category))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
TEXT_FIELDS = [
|
|
284
|
+
{"id": "phrase", "display_name": "Phrase", "required": True, "target": "object"},
|
|
285
|
+
{"id": "caption", "display_name": "Caption", "required": True, "target": "image"},
|
|
286
|
+
]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def text(text_id, target, field, value="text"):
|
|
290
|
+
return {
|
|
291
|
+
"id": text_id,
|
|
292
|
+
"image_id": "image_a",
|
|
293
|
+
"view_id": "semantics",
|
|
294
|
+
"target_id": target,
|
|
295
|
+
"fields": {field: value},
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def test_completed_description_scope_covers_every_object_and_the_image():
|
|
300
|
+
schema = body(description_fields=TEXT_FIELDS)
|
|
301
|
+
objects = [head(head_type="round"), {**head(head_type="round"), "id": "head_b"}]
|
|
302
|
+
full = [
|
|
303
|
+
text("p_a", "head_a", "phrase"),
|
|
304
|
+
text("p_b", "head_b", "phrase"),
|
|
305
|
+
text("caption", None, "caption"),
|
|
306
|
+
]
|
|
307
|
+
clip(schema, objects, descriptions=full, text_state="complete")
|
|
308
|
+
clip(schema, objects, descriptions=full[:1], text_state="unprocessed")
|
|
309
|
+
for missing in range(3):
|
|
310
|
+
partial = full[:missing] + full[missing + 1 :]
|
|
311
|
+
with pytest.raises(ImageContractError) as error:
|
|
312
|
+
clip(schema, objects, descriptions=partial, text_state="complete")
|
|
313
|
+
assert error.value.code == "description_coverage_incomplete"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_description_targets_are_unique_and_fields_match_their_target():
|
|
317
|
+
schema = body(description_fields=TEXT_FIELDS)
|
|
318
|
+
objects = [head(head_type="round")]
|
|
319
|
+
with pytest.raises(ImageContractError): # two phrases for one object
|
|
320
|
+
clip(schema, objects, descriptions=[text("p1", "head_a", "phrase"), text("p2", "head_a", "phrase")])
|
|
321
|
+
with pytest.raises(ImageContractError): # caption field on an object description
|
|
322
|
+
clip(schema, objects, descriptions=[text("p1", "head_a", "caption")])
|
|
323
|
+
with pytest.raises(ImageContractError): # phrase on the image-level description
|
|
324
|
+
clip(schema, objects, descriptions=[text("c", None, "phrase")])
|
|
325
|
+
with pytest.raises(ImageContractError):
|
|
326
|
+
parse_image_schema_body(
|
|
327
|
+
body(description_fields=[{**TEXT_FIELDS[0], "target": "region"}])
|
|
328
|
+
)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Controlled multi-select (`enum_set`) image attributes (x4d-devkit #223)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from x4d_devkit._image_contract import ImageContractError, fingerprint
|
|
10
|
+
from x4d_devkit.image_clip import build_image_identity, parse_image_clip
|
|
11
|
+
from x4d_devkit.image_schema import parse_image_schema_body
|
|
12
|
+
from x4d_devkit.image_schema_upgrade import additive_image_schema_changes
|
|
13
|
+
from tests.image_fixtures import clip_payload, schema_body
|
|
14
|
+
|
|
15
|
+
DIRECTIONS = [
|
|
16
|
+
{"id": "top", "display_name": "上"},
|
|
17
|
+
{"id": "bottom", "display_name": "下"},
|
|
18
|
+
{"id": "left", "display_name": "左"},
|
|
19
|
+
{"id": "right", "display_name": "右"},
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def directions_attribute(**overrides):
|
|
24
|
+
attribute = {
|
|
25
|
+
"id": "occlusion_directions",
|
|
26
|
+
"display_name": "遮挡方向",
|
|
27
|
+
"type": "enum_set",
|
|
28
|
+
"required": False,
|
|
29
|
+
"scopes": ["object"],
|
|
30
|
+
"values": copy.deepcopy(DIRECTIONS),
|
|
31
|
+
"min_items": 0,
|
|
32
|
+
"max_items": 4,
|
|
33
|
+
}
|
|
34
|
+
attribute.update(overrides)
|
|
35
|
+
return attribute
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def body_with(attribute):
|
|
39
|
+
body = schema_body()
|
|
40
|
+
body["categories"][1]["attributes"] = [attribute]
|
|
41
|
+
return body
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def box_object(attributes):
|
|
45
|
+
return {
|
|
46
|
+
"id": "box_a",
|
|
47
|
+
"image_id": "image_a",
|
|
48
|
+
"view_id": "objects",
|
|
49
|
+
"category_id": "box",
|
|
50
|
+
"geometry": {
|
|
51
|
+
"geometry_type": "box_2d",
|
|
52
|
+
"box": {"x_min": 10, "y_min": 20, "x_max": 30, "y_max": 40},
|
|
53
|
+
},
|
|
54
|
+
"attributes": attributes,
|
|
55
|
+
"attribute_ranges": [],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def clip_with(attribute, attributes):
|
|
60
|
+
value = clip_payload()
|
|
61
|
+
body = body_with(attribute)
|
|
62
|
+
value["label_schema"]["body"] = body
|
|
63
|
+
value["label_schema"]["identity"]["fingerprint"] = fingerprint(body)
|
|
64
|
+
value["image_annotation"]["objects"] = [box_object(attributes)]
|
|
65
|
+
value["identity"] = build_image_identity(value)
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_schema_accepts_bounded_enum_set():
|
|
70
|
+
parsed = parse_image_schema_body(body_with(directions_attribute()))
|
|
71
|
+
attribute = parsed.to_dict()["categories"][1]["attributes"][0]
|
|
72
|
+
assert (attribute["type"], attribute["min_items"], attribute["max_items"]) == ("enum_set", 0, 4)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.mark.parametrize(
|
|
76
|
+
"change",
|
|
77
|
+
[
|
|
78
|
+
{"values": []},
|
|
79
|
+
{"values": [DIRECTIONS[0], DIRECTIONS[0]]},
|
|
80
|
+
{"min_items": 2, "max_items": 1},
|
|
81
|
+
{"max_items": 5},
|
|
82
|
+
{"max_items": 0},
|
|
83
|
+
{"min_items": -1},
|
|
84
|
+
{"min_items": True},
|
|
85
|
+
{"max_items": 2.0},
|
|
86
|
+
{"scopes": ["object", "segment"]},
|
|
87
|
+
"missing-bounds",
|
|
88
|
+
"bounds-on-enum",
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
def test_schema_rejects_invalid_enum_set_declarations(change):
|
|
92
|
+
if change == "missing-bounds":
|
|
93
|
+
attribute = directions_attribute()
|
|
94
|
+
del attribute["max_items"]
|
|
95
|
+
elif change == "bounds-on-enum":
|
|
96
|
+
attribute = directions_attribute(type="enum")
|
|
97
|
+
else:
|
|
98
|
+
attribute = directions_attribute(**change)
|
|
99
|
+
body = body_with(attribute)
|
|
100
|
+
if change == {"scopes": ["object", "segment"]}:
|
|
101
|
+
body["categories"][0]["attributes"].append(attribute)
|
|
102
|
+
body["categories"][1]["attributes"] = []
|
|
103
|
+
with pytest.raises(ImageContractError):
|
|
104
|
+
parse_image_schema_body(body)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.parametrize(
|
|
108
|
+
"value",
|
|
109
|
+
[[], ["top"], ["top", "left"], ["top", "bottom", "left", "right"]],
|
|
110
|
+
)
|
|
111
|
+
def test_clip_accepts_declared_order_sets_and_keeps_them_exactly(value):
|
|
112
|
+
clip = parse_image_clip(clip_with(directions_attribute(), {"occlusion_directions": value}))
|
|
113
|
+
stored = clip.to_dict()["image_annotation"]["objects"][0]["attributes"]
|
|
114
|
+
assert stored == {"occlusion_directions": value}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_missing_and_empty_set_are_distinct_results():
|
|
118
|
+
missing = parse_image_clip(clip_with(directions_attribute(), {}))
|
|
119
|
+
empty = parse_image_clip(clip_with(directions_attribute(), {"occlusion_directions": []}))
|
|
120
|
+
assert (
|
|
121
|
+
missing.to_dict()["identity"]["annotation_revision"]
|
|
122
|
+
!= empty.to_dict()["identity"]["annotation_revision"]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.mark.parametrize(
|
|
127
|
+
"value",
|
|
128
|
+
[
|
|
129
|
+
["left", "top"], # not Schema declaration order: never re-sorted silently
|
|
130
|
+
["top", "top"],
|
|
131
|
+
["up"],
|
|
132
|
+
"top",
|
|
133
|
+
"top,left",
|
|
134
|
+
[1],
|
|
135
|
+
[None],
|
|
136
|
+
[["top"]],
|
|
137
|
+
{"top": True},
|
|
138
|
+
None,
|
|
139
|
+
True,
|
|
140
|
+
],
|
|
141
|
+
)
|
|
142
|
+
def test_clip_rejects_invalid_set_values_without_coercion(value):
|
|
143
|
+
with pytest.raises(ImageContractError):
|
|
144
|
+
parse_image_clip(clip_with(directions_attribute(), {"occlusion_directions": value}))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_clip_enforces_item_bounds_and_required_presence():
|
|
148
|
+
bounded = directions_attribute(min_items=1, max_items=2)
|
|
149
|
+
with pytest.raises(ImageContractError):
|
|
150
|
+
parse_image_clip(clip_with(bounded, {"occlusion_directions": []}))
|
|
151
|
+
with pytest.raises(ImageContractError):
|
|
152
|
+
parse_image_clip(clip_with(bounded, {"occlusion_directions": ["top", "bottom", "left"]}))
|
|
153
|
+
parse_image_clip(clip_with(bounded, {"occlusion_directions": ["bottom", "right"]}))
|
|
154
|
+
|
|
155
|
+
required = directions_attribute(required=True)
|
|
156
|
+
with pytest.raises(ImageContractError):
|
|
157
|
+
parse_image_clip(clip_with(required, {}))
|
|
158
|
+
parse_image_clip(clip_with(required, {"occlusion_directions": []}))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_schema_upgrade_may_add_values_but_must_keep_existing_order():
|
|
162
|
+
before = body_with(directions_attribute())
|
|
163
|
+
after = copy.deepcopy(before)
|
|
164
|
+
after["version"] = "1.1.0"
|
|
165
|
+
after["categories"][1]["attributes"][0]["values"].insert(
|
|
166
|
+
2, {"id": "front", "display_name": "前"}
|
|
167
|
+
)
|
|
168
|
+
changes = additive_image_schema_changes(before, after)
|
|
169
|
+
assert {"category_id": "box", "attribute_id": "occlusion_directions", "value_id": "front"} in changes[
|
|
170
|
+
"added_enum_values"
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
reordered = copy.deepcopy(before)
|
|
174
|
+
reordered["version"] = "1.1.0"
|
|
175
|
+
values = reordered["categories"][1]["attributes"][0]["values"]
|
|
176
|
+
values[0], values[1] = values[1], values[0]
|
|
177
|
+
with pytest.raises(ImageContractError):
|
|
178
|
+
additive_image_schema_changes(before, reordered)
|
|
@@ -836,9 +836,10 @@ def test_annotation_entry_points_canonicalize_rows_only_once(monkeypatch, entry)
|
|
|
836
836
|
assert annotation_revision(raw["clip_id"], raw["annotation_frame_id"],
|
|
837
837
|
raw["annotations"]) == raw["annotation_revision"]
|
|
838
838
|
# Revision and document hash share one serialization of every row; no call
|
|
839
|
-
# re-serializes the row list or a complete payload.
|
|
839
|
+
# re-serializes the row list or a complete payload. A document this module
|
|
840
|
+
# already verified is returned without serializing anything.
|
|
840
841
|
row_calls = [value for value in calls if isinstance(value, dict)]
|
|
841
|
-
assert row_calls == raw["annotations"]
|
|
842
|
+
assert row_calls == ([] if entry == "parsed-document" else raw["annotations"])
|
|
842
843
|
assert all(isinstance(value, str) for value in calls if not isinstance(value, dict))
|
|
843
844
|
|
|
844
845
|
|
|
@@ -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.52.0"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
|
|
@@ -38,6 +38,13 @@ _DOCUMENT_FIELDS = {
|
|
|
38
38
|
"annotations",
|
|
39
39
|
}
|
|
40
40
|
_REVISION_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
41
|
+
# Set only by build/parse after canonical verification; never by document_hash().
|
|
42
|
+
_VERIFIED_MARK = "_canonical_verified"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _mark_verified(document: "ClipAnnotationDocument", document_hash: str) -> None:
|
|
46
|
+
object.__setattr__(document, "_document_hash", document_hash)
|
|
47
|
+
object.__setattr__(document, _VERIFIED_MARK, True)
|
|
41
48
|
_FRAME_ID_RE = re.compile(r"^[a-z][a-z0-9_]{0,63}$")
|
|
42
49
|
|
|
43
50
|
|
|
@@ -148,7 +155,7 @@ def build_clip_annotations(
|
|
|
148
155
|
annotation_revision=revision,
|
|
149
156
|
annotations=tuple(canonical),
|
|
150
157
|
)
|
|
151
|
-
|
|
158
|
+
_mark_verified(document, document_hash)
|
|
152
159
|
return document
|
|
153
160
|
|
|
154
161
|
|
|
@@ -159,6 +166,9 @@ def parse_clip_annotations(
|
|
|
159
166
|
|
|
160
167
|
i_json_checked = False
|
|
161
168
|
if isinstance(value, ClipAnnotationDocument):
|
|
169
|
+
if value.__dict__.get(_VERIFIED_MARK) is True:
|
|
170
|
+
# Built or parsed by this module: already canonical and verified.
|
|
171
|
+
return value
|
|
162
172
|
data = value.to_dict()
|
|
163
173
|
elif isinstance(value, Mapping):
|
|
164
174
|
data = dict(value)
|
|
@@ -244,7 +254,7 @@ def parse_clip_annotations(
|
|
|
244
254
|
annotation_revision=revision,
|
|
245
255
|
annotations=tuple(annotations),
|
|
246
256
|
)
|
|
247
|
-
|
|
257
|
+
_mark_verified(document, document_hash)
|
|
248
258
|
return document
|
|
249
259
|
|
|
250
260
|
|