x4d-devkit 0.55.1__tar.gz → 0.56.0rc1__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.55.1/x4d_devkit.egg-info → x4d_devkit-0.56.0rc1}/PKG-INFO +1 -1
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/pyproject.toml +1 -1
- x4d_devkit-0.56.0rc1/tests/test_image_external_predictions.py +529 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_preannotation.py +7 -2
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_reference.py +5 -3
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_sdk_api.py +1 -1
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_preannotations.py +43 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_clip.py +40 -4
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_preannotation.py +132 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_reference.py +101 -36
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1/x4d_devkit.egg-info}/PKG-INFO +1 -1
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit.egg-info/SOURCES.txt +1 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/LICENSE +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/README.md +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/setup.cfg +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_accounts.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_annotation_document_hash.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_annotation_reference.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_box.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_category_audit.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_clean_basemap.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_clean_basemap_client.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_clip.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_conditional_rules.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_enum_set.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_export.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_geometry.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_groups.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_schema_migration.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_schema_registry_client.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_schema_upgrade.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_source.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_work_session.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_loader.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_matching.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_models.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_parse_equivalence.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_preannotation_client.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_project_discovery.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_roadmark_preview.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_semantic_scene_independent.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_source_attributes.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_source_content.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_token.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_transform.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_validation.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_vector_map.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_vector_map_geometry.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_vector_map_session.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_work_session_validation_reuse.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/_image_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/accounts.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/annotation_reference.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/audit.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/clean_basemap.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/accounts.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/annotation_reference.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/clean_basemap.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_clips.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_exports.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_schemas.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_work_sessions.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/preannotation.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/reconstruction.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_export.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_geometry.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_groups.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_schema.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_schema_migration.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_schema_upgrade.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_source.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_work_session.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/project_discovery.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/roadmark_preview.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/source_content.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/vector_map.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/vector_map_geometry.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/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.56.0rc1"
|
|
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,529 @@
|
|
|
1
|
+
"""Reference v2 producers/scores and external prediction publishing (#245)."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from tests.image_fixtures import schema_record
|
|
12
|
+
from tests.test_image_reference import artifact, clip_payload, profile
|
|
13
|
+
from x4d_devkit import X4DClient
|
|
14
|
+
from x4d_devkit.image_clip import build_image_identity, parse_image_clip
|
|
15
|
+
from x4d_devkit.image_preannotation import (
|
|
16
|
+
EXTERNAL_CONTRACT,
|
|
17
|
+
EXTERNAL_RECEIPT_CONTRACT,
|
|
18
|
+
build_image_external_reference,
|
|
19
|
+
parse_image_preannotation_external,
|
|
20
|
+
parse_image_preannotation_external_receipt,
|
|
21
|
+
)
|
|
22
|
+
from x4d_devkit.image_reference import (
|
|
23
|
+
IMAGE_REFERENCE_CONTRACT,
|
|
24
|
+
image_reference_source,
|
|
25
|
+
parse_image_reference,
|
|
26
|
+
prediction_object_id,
|
|
27
|
+
)
|
|
28
|
+
from x4d_devkit.image_schema import (
|
|
29
|
+
build_image_schema_snapshot,
|
|
30
|
+
parse_image_schema_body,
|
|
31
|
+
parse_image_schema_record,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def required_clip_payload():
|
|
36
|
+
"""Box category with a human-required enum attribute and two images."""
|
|
37
|
+
payload = clip_payload()
|
|
38
|
+
record = schema_record()
|
|
39
|
+
box = next(row for row in record["body"]["categories"] if row["id"] == "box")
|
|
40
|
+
box["attributes"] = [
|
|
41
|
+
{
|
|
42
|
+
"id": "kind",
|
|
43
|
+
"display_name": "Kind",
|
|
44
|
+
"type": "enum",
|
|
45
|
+
"required": True,
|
|
46
|
+
"scopes": ["object"],
|
|
47
|
+
"values": [
|
|
48
|
+
{"id": "bin", "display_name": "Bin"},
|
|
49
|
+
{"id": "bag", "display_name": "Bag"},
|
|
50
|
+
],
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
record["fingerprint"] = parse_image_schema_body(
|
|
54
|
+
record["body"]
|
|
55
|
+
).identity.fingerprint
|
|
56
|
+
payload["label_schema"] = build_image_schema_snapshot(
|
|
57
|
+
parse_image_schema_record(record)
|
|
58
|
+
)
|
|
59
|
+
sample = copy.deepcopy(payload["sample"][0])
|
|
60
|
+
sample.update(
|
|
61
|
+
id="sample_b",
|
|
62
|
+
timestamp={"sec": 11, "nanosec": 20},
|
|
63
|
+
image_ids={"cam_a": "image_b"},
|
|
64
|
+
)
|
|
65
|
+
payload["sample"].append(sample)
|
|
66
|
+
image = copy.deepcopy(payload["sample_data"][0])
|
|
67
|
+
image.update(
|
|
68
|
+
id="image_b", sample_id="sample_b", timestamp={"sec": 11, "nanosec": 20}
|
|
69
|
+
)
|
|
70
|
+
image["asset"]["path"] = "samples/cam_a/11000000020.jpg"
|
|
71
|
+
payload["sample_data"].append(image)
|
|
72
|
+
payload["image_annotation"]["completion"] += [
|
|
73
|
+
{"image_id": "image_b", "view_id": view, "state": "unprocessed"}
|
|
74
|
+
for view in payload["meta"]["view_ids"]
|
|
75
|
+
]
|
|
76
|
+
payload["identity"] = build_image_identity(payload)
|
|
77
|
+
return payload
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def box(image_id, ordinal, attributes=None):
|
|
81
|
+
return {
|
|
82
|
+
"id": prediction_object_id(image_id, ordinal),
|
|
83
|
+
"image_id": image_id,
|
|
84
|
+
"view_id": "objects",
|
|
85
|
+
"category_id": "box",
|
|
86
|
+
"geometry": {
|
|
87
|
+
"geometry_type": "box_2d",
|
|
88
|
+
"box": {"x_min": 10, "y_min": 20, "x_max": 40.5, "y_max": 60},
|
|
89
|
+
},
|
|
90
|
+
"attributes": {} if attributes is None else attributes,
|
|
91
|
+
"attribute_ranges": [],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def coverage():
|
|
96
|
+
first = box("image_a", 0)
|
|
97
|
+
return [
|
|
98
|
+
{
|
|
99
|
+
"image_id": "image_a",
|
|
100
|
+
"status": "predicted",
|
|
101
|
+
"objects": [first, box("image_a", 1, {"kind": "bag"})],
|
|
102
|
+
"scores": {first["id"]: 0.91},
|
|
103
|
+
"error": None,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"image_id": "image_b",
|
|
107
|
+
"status": "empty",
|
|
108
|
+
"objects": [],
|
|
109
|
+
"scores": {},
|
|
110
|
+
"error": None,
|
|
111
|
+
},
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def required_context():
|
|
116
|
+
payload = required_clip_payload()
|
|
117
|
+
return {
|
|
118
|
+
"clip_id": payload["meta"]["clip_id"],
|
|
119
|
+
"project_id": payload["meta"]["project_id"],
|
|
120
|
+
"view_id": "objects",
|
|
121
|
+
"source_revision": payload["identity"]["source_revision"],
|
|
122
|
+
"schema_fingerprint": payload["identity"]["schema_fingerprint"],
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def external_request():
|
|
127
|
+
return {
|
|
128
|
+
**required_context(),
|
|
129
|
+
"contract_version": EXTERNAL_CONTRACT,
|
|
130
|
+
"idempotency_key": "publish_1",
|
|
131
|
+
"producer": {"kind": "external", "name": "litter-vlm", "version": "2026.11"},
|
|
132
|
+
"coverage": coverage(),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def required_source():
|
|
137
|
+
return image_reference_source(parse_image_clip(required_clip_payload()))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_prediction_object_id_is_deterministic_and_canonical():
|
|
141
|
+
first = prediction_object_id("image_a", 0)
|
|
142
|
+
assert first == prediction_object_id("image_a", 0)
|
|
143
|
+
assert first != prediction_object_id("image_a", 1)
|
|
144
|
+
assert first != prediction_object_id("image_b", 0)
|
|
145
|
+
assert re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}", first)
|
|
146
|
+
assert re.fullmatch(
|
|
147
|
+
r"[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}",
|
|
148
|
+
prediction_object_id("image_a", 2**53 - 1),
|
|
149
|
+
)
|
|
150
|
+
for image_id, ordinal in (("image_a", -1), ("image_a", True), ("", 0)):
|
|
151
|
+
with pytest.raises(ValueError):
|
|
152
|
+
prediction_object_id(image_id, ordinal)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_service_and_external_artifacts_are_valid_v2():
|
|
156
|
+
value = artifact()
|
|
157
|
+
assert IMAGE_REFERENCE_CONTRACT == "x4d-image-reference-v2"
|
|
158
|
+
assert parse_image_reference(value).to_dict() == value
|
|
159
|
+
value["producer"] = {"kind": "external", "name": "converter", "version": "1"}
|
|
160
|
+
assert parse_image_reference(value).to_dict() == value
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.mark.parametrize(
|
|
164
|
+
"change",
|
|
165
|
+
[
|
|
166
|
+
"v1_contract",
|
|
167
|
+
"v1_profile_field",
|
|
168
|
+
"unknown_kind",
|
|
169
|
+
"external_with_profile",
|
|
170
|
+
"service_with_name",
|
|
171
|
+
"long_name",
|
|
172
|
+
"blank_version",
|
|
173
|
+
"missing_scores",
|
|
174
|
+
],
|
|
175
|
+
)
|
|
176
|
+
def test_reference_rejects_v1_and_inexact_producers(change):
|
|
177
|
+
value = artifact()
|
|
178
|
+
if change == "v1_contract":
|
|
179
|
+
value["contract_version"] = "x4d-image-reference-v1"
|
|
180
|
+
elif change == "v1_profile_field":
|
|
181
|
+
value["profile"] = value.pop("producer")["profile"]
|
|
182
|
+
elif change == "unknown_kind":
|
|
183
|
+
value["producer"]["kind"] = "human"
|
|
184
|
+
elif change == "external_with_profile":
|
|
185
|
+
value["producer"] = {
|
|
186
|
+
"kind": "external",
|
|
187
|
+
"name": "x",
|
|
188
|
+
"version": "1",
|
|
189
|
+
"profile": profile().to_dict(),
|
|
190
|
+
}
|
|
191
|
+
elif change == "service_with_name":
|
|
192
|
+
value["producer"]["name"] = "x"
|
|
193
|
+
elif change == "long_name":
|
|
194
|
+
value["producer"] = {"kind": "external", "name": "n" * 129, "version": "1"}
|
|
195
|
+
elif change == "blank_version":
|
|
196
|
+
value["producer"] = {"kind": "external", "name": "n", "version": " "}
|
|
197
|
+
else:
|
|
198
|
+
del value["coverage"][0]["scores"]
|
|
199
|
+
with pytest.raises(ValueError):
|
|
200
|
+
parse_image_reference(value)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@pytest.mark.parametrize(
|
|
204
|
+
"scores",
|
|
205
|
+
[
|
|
206
|
+
{"line_b": 0.5},
|
|
207
|
+
{"line_a": True},
|
|
208
|
+
{"line_a": "0.5"},
|
|
209
|
+
{"line_a": None},
|
|
210
|
+
[],
|
|
211
|
+
],
|
|
212
|
+
)
|
|
213
|
+
def test_reference_rejects_invalid_scores(scores):
|
|
214
|
+
value = artifact()
|
|
215
|
+
value["coverage"][0]["scores"] = scores
|
|
216
|
+
with pytest.raises(ValueError):
|
|
217
|
+
parse_image_reference(value)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@pytest.mark.parametrize("score", [math.inf, -math.inf, math.nan])
|
|
221
|
+
def test_reference_rejects_non_finite_scores(score):
|
|
222
|
+
value = artifact()
|
|
223
|
+
value["coverage"][0]["scores"] = {"line_a": score}
|
|
224
|
+
with pytest.raises(ValueError):
|
|
225
|
+
parse_image_reference(value)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_reference_accepts_raw_integer_and_negative_scores_and_partial_keys():
|
|
229
|
+
value = artifact()
|
|
230
|
+
for scores in ({"line_a": -3}, {"line_a": 12}, {}):
|
|
231
|
+
value["coverage"][0]["scores"] = scores
|
|
232
|
+
assert parse_image_reference(value).to_dict() == value
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@pytest.mark.parametrize("state", ["empty", "failed", "unprocessed"])
|
|
236
|
+
def test_scores_are_only_for_predicted_rows(state):
|
|
237
|
+
value = artifact()
|
|
238
|
+
value["coverage"][0].update(status=state, objects=[], scores={"line_a": 0.5})
|
|
239
|
+
if state == "failed":
|
|
240
|
+
value["coverage"][0]["error"] = {"code": "failed", "message": "Failed"}
|
|
241
|
+
with pytest.raises(ValueError, match="scores"):
|
|
242
|
+
parse_image_reference(value)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_candidates_may_omit_human_required_attributes_only():
|
|
246
|
+
value = build_image_external_reference(
|
|
247
|
+
external_request(), source=required_source(), run_id="ext_1"
|
|
248
|
+
).to_dict()
|
|
249
|
+
assert value["coverage"][0]["objects"][0]["attributes"] == {}
|
|
250
|
+
# Human formal validation still requires the attribute.
|
|
251
|
+
human = required_clip_payload()
|
|
252
|
+
human["image_annotation"]["objects"] = [box("image_a", 0)]
|
|
253
|
+
human["image_annotation"]["completion"][0]["state"] = "complete"
|
|
254
|
+
human["identity"] = build_image_identity(human)
|
|
255
|
+
with pytest.raises(ValueError, match="required"):
|
|
256
|
+
parse_image_clip(human)
|
|
257
|
+
human["image_annotation"]["completion"][0]["state"] = "unprocessed"
|
|
258
|
+
human["identity"] = build_image_identity(human)
|
|
259
|
+
with pytest.raises(ValueError, match="required"):
|
|
260
|
+
parse_image_clip(human)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@pytest.mark.parametrize(
|
|
264
|
+
"attributes", [{"kind": "crate"}, {"kind": True}, {"colour": "red"}]
|
|
265
|
+
)
|
|
266
|
+
def test_candidates_keep_attribute_values_and_scopes_strict(attributes):
|
|
267
|
+
request = external_request()
|
|
268
|
+
request["coverage"][0]["objects"][0]["attributes"] = attributes
|
|
269
|
+
with pytest.raises(ValueError):
|
|
270
|
+
build_image_external_reference(
|
|
271
|
+
request, source=required_source(), run_id="ext_1"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_duplicate_object_ids_are_rejected_within_and_across_images():
|
|
276
|
+
request = external_request()
|
|
277
|
+
rows = request["coverage"]
|
|
278
|
+
rows[0]["objects"][1]["id"] = rows[0]["objects"][0]["id"]
|
|
279
|
+
with pytest.raises(ValueError, match="duplicate"):
|
|
280
|
+
parse_image_preannotation_external(request)
|
|
281
|
+
request = external_request()
|
|
282
|
+
rows = request["coverage"]
|
|
283
|
+
moved = copy.deepcopy(rows[0]["objects"][0])
|
|
284
|
+
moved["image_id"] = "image_b"
|
|
285
|
+
rows[1].update(status="predicted", objects=[moved])
|
|
286
|
+
with pytest.raises(ValueError, match="duplicate"):
|
|
287
|
+
parse_image_preannotation_external(request)
|
|
288
|
+
value = build_image_external_reference(
|
|
289
|
+
external_request(), source=required_source(), run_id="ext_1"
|
|
290
|
+
).to_dict()
|
|
291
|
+
value["coverage"][1].update(
|
|
292
|
+
status="predicted", objects=[moved], scores={}
|
|
293
|
+
)
|
|
294
|
+
with pytest.raises(ValueError, match="duplicate"):
|
|
295
|
+
parse_image_reference(value)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_external_request_and_build_round_trip():
|
|
299
|
+
request = external_request()
|
|
300
|
+
assert parse_image_preannotation_external(request).to_dict() == request
|
|
301
|
+
parsed = parse_image_preannotation_external(request)
|
|
302
|
+
built = build_image_external_reference(
|
|
303
|
+
parsed, source=required_source(), run_id="ext_1"
|
|
304
|
+
).to_dict()
|
|
305
|
+
assert built == {
|
|
306
|
+
"contract_version": IMAGE_REFERENCE_CONTRACT,
|
|
307
|
+
"run_id": "ext_1",
|
|
308
|
+
"source": required_source().to_dict(),
|
|
309
|
+
"producer": request["producer"],
|
|
310
|
+
"view_id": "objects",
|
|
311
|
+
"coverage": request["coverage"],
|
|
312
|
+
}
|
|
313
|
+
assert (
|
|
314
|
+
build_image_external_reference(
|
|
315
|
+
request, source=required_source().to_dict(), run_id="ext_1"
|
|
316
|
+
).to_dict()
|
|
317
|
+
== built
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@pytest.mark.parametrize(
|
|
322
|
+
"change",
|
|
323
|
+
[
|
|
324
|
+
"unprocessed",
|
|
325
|
+
"all_failed",
|
|
326
|
+
"service_producer",
|
|
327
|
+
"no_coverage",
|
|
328
|
+
"wrong_contract",
|
|
329
|
+
"bad_key",
|
|
330
|
+
"duplicate_image",
|
|
331
|
+
"extra_field",
|
|
332
|
+
"failed_without_error",
|
|
333
|
+
],
|
|
334
|
+
)
|
|
335
|
+
def test_external_request_is_strict(change):
|
|
336
|
+
value = external_request()
|
|
337
|
+
failed = {"code": "model_failed", "message": "Model failed"}
|
|
338
|
+
if change == "unprocessed":
|
|
339
|
+
value["coverage"][1]["status"] = "unprocessed"
|
|
340
|
+
elif change == "all_failed":
|
|
341
|
+
for row in value["coverage"]:
|
|
342
|
+
row.update(status="failed", objects=[], scores={}, error=failed)
|
|
343
|
+
elif change == "service_producer":
|
|
344
|
+
value["producer"] = {"kind": "service", "profile": profile().to_dict()}
|
|
345
|
+
elif change == "no_coverage":
|
|
346
|
+
value["coverage"] = []
|
|
347
|
+
elif change == "wrong_contract":
|
|
348
|
+
value["contract_version"] = "x4d-image-preannotation-external-v0"
|
|
349
|
+
elif change == "bad_key":
|
|
350
|
+
value["idempotency_key"] = "bad key"
|
|
351
|
+
elif change == "duplicate_image":
|
|
352
|
+
value["coverage"][1]["image_id"] = "image_a"
|
|
353
|
+
elif change == "extra_field":
|
|
354
|
+
value["run_id"] = "chosen"
|
|
355
|
+
else:
|
|
356
|
+
value["coverage"][1]["status"] = "failed"
|
|
357
|
+
with pytest.raises(ValueError):
|
|
358
|
+
parse_image_preannotation_external(value)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@pytest.mark.parametrize(
|
|
362
|
+
"change", ["clip", "project", "source", "schema", "view", "order", "missing"]
|
|
363
|
+
)
|
|
364
|
+
def test_build_rejects_context_mismatch_and_wrong_image_order(change):
|
|
365
|
+
value = external_request()
|
|
366
|
+
if change == "clip":
|
|
367
|
+
value["clip_id"] = "other_clip"
|
|
368
|
+
elif change == "project":
|
|
369
|
+
value["project_id"] = 99
|
|
370
|
+
elif change == "source":
|
|
371
|
+
value["source_revision"] = "sha256:" + "0" * 64
|
|
372
|
+
elif change == "schema":
|
|
373
|
+
value["schema_fingerprint"] = "sha256:" + "0" * 64
|
|
374
|
+
elif change == "view":
|
|
375
|
+
value["view_id"] = "other_view"
|
|
376
|
+
elif change == "order":
|
|
377
|
+
value["coverage"].reverse()
|
|
378
|
+
else:
|
|
379
|
+
value["coverage"].pop()
|
|
380
|
+
with pytest.raises(ValueError):
|
|
381
|
+
build_image_external_reference(
|
|
382
|
+
value, source=required_source(), run_id="ext_1"
|
|
383
|
+
)
|
|
384
|
+
with pytest.raises(ValueError):
|
|
385
|
+
build_image_external_reference(
|
|
386
|
+
external_request(), source=required_source(), run_id="bad run"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def operation(status="completed"):
|
|
391
|
+
return {
|
|
392
|
+
"operation_id": "op_ext",
|
|
393
|
+
"status": status,
|
|
394
|
+
"progress": progress(status),
|
|
395
|
+
"error": None,
|
|
396
|
+
"created_at": "2026-11-09T00:00:00Z",
|
|
397
|
+
"completed_at": "2026-11-09T00:00:01Z",
|
|
398
|
+
"can_cancel": False,
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def progress(status="completed"):
|
|
403
|
+
failed = 1 if status == "partial" else 0
|
|
404
|
+
return {
|
|
405
|
+
"total": 2,
|
|
406
|
+
"processed": 2,
|
|
407
|
+
"predicted": 1,
|
|
408
|
+
"empty": 1 - failed,
|
|
409
|
+
"failed": failed,
|
|
410
|
+
"unprocessed": 0,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def external_receipt(status="completed", disposition="published"):
|
|
415
|
+
return {
|
|
416
|
+
**required_context(),
|
|
417
|
+
"contract_version": EXTERNAL_RECEIPT_CONTRACT,
|
|
418
|
+
"disposition": disposition,
|
|
419
|
+
"operation": operation(status),
|
|
420
|
+
"result": {
|
|
421
|
+
"availability": "ready" if status == "completed" else "partial",
|
|
422
|
+
"digest": "sha256:" + "a" * 64,
|
|
423
|
+
"published_at": "2026-11-09T00:00:01Z",
|
|
424
|
+
"coverage": progress(status),
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
@pytest.mark.parametrize(
|
|
430
|
+
"status,disposition",
|
|
431
|
+
[("completed", "published"), ("partial", "replayed")],
|
|
432
|
+
)
|
|
433
|
+
def test_external_receipt_valid(status, disposition):
|
|
434
|
+
value = external_receipt(status, disposition)
|
|
435
|
+
assert parse_image_preannotation_external_receipt(value).to_dict() == value
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@pytest.mark.parametrize(
|
|
439
|
+
"change",
|
|
440
|
+
[
|
|
441
|
+
"disposition",
|
|
442
|
+
"contract",
|
|
443
|
+
"running",
|
|
444
|
+
"failed",
|
|
445
|
+
"cancellable",
|
|
446
|
+
"availability_mismatch",
|
|
447
|
+
"stale",
|
|
448
|
+
"coverage_mismatch",
|
|
449
|
+
"extra",
|
|
450
|
+
],
|
|
451
|
+
)
|
|
452
|
+
def test_external_receipt_rejects_inconsistent_states(change):
|
|
453
|
+
value = external_receipt()
|
|
454
|
+
if change == "disposition":
|
|
455
|
+
value["disposition"] = "created"
|
|
456
|
+
elif change == "contract":
|
|
457
|
+
value["contract_version"] = "x4d-image-preannotation-request-v1"
|
|
458
|
+
elif change == "running":
|
|
459
|
+
value["operation"].update(status="running", completed_at=None, can_cancel=True)
|
|
460
|
+
elif change == "failed":
|
|
461
|
+
value["operation"].update(
|
|
462
|
+
status="failed", error={"code": "x", "message": "Failed"}
|
|
463
|
+
)
|
|
464
|
+
elif change == "cancellable":
|
|
465
|
+
value["operation"]["can_cancel"] = True
|
|
466
|
+
elif change == "availability_mismatch":
|
|
467
|
+
value = external_receipt("partial")
|
|
468
|
+
value["result"]["availability"] = "ready"
|
|
469
|
+
value["result"]["coverage"] = progress("completed")
|
|
470
|
+
elif change == "stale":
|
|
471
|
+
value["result"] = {
|
|
472
|
+
"availability": "stale",
|
|
473
|
+
"digest": None,
|
|
474
|
+
"published_at": None,
|
|
475
|
+
"coverage": None,
|
|
476
|
+
}
|
|
477
|
+
elif change == "coverage_mismatch":
|
|
478
|
+
value["result"]["coverage"]["predicted"] = 2
|
|
479
|
+
value["result"]["coverage"]["empty"] = 0
|
|
480
|
+
else:
|
|
481
|
+
value["artifact"] = None
|
|
482
|
+
with pytest.raises(ValueError):
|
|
483
|
+
parse_image_preannotation_external_receipt(value)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def test_typed_client_publish_external_round_trip():
|
|
487
|
+
calls = []
|
|
488
|
+
foreign = False
|
|
489
|
+
|
|
490
|
+
def handler(request):
|
|
491
|
+
calls.append(request)
|
|
492
|
+
body = json.loads(request.content)
|
|
493
|
+
assert body == external_request()
|
|
494
|
+
value = external_receipt()
|
|
495
|
+
if foreign:
|
|
496
|
+
value["source_revision"] = "sha256:" + "0" * 64
|
|
497
|
+
return httpx.Response(200, json=value)
|
|
498
|
+
|
|
499
|
+
with X4DClient("https://platform.test") as client:
|
|
500
|
+
client._http.close()
|
|
501
|
+
client._http = httpx.Client(
|
|
502
|
+
base_url="https://platform.test", transport=httpx.MockTransport(handler)
|
|
503
|
+
)
|
|
504
|
+
clip = parse_image_clip(required_clip_payload())
|
|
505
|
+
api = client.image_preannotations
|
|
506
|
+
arguments = dict(
|
|
507
|
+
clip=clip,
|
|
508
|
+
view_id="objects",
|
|
509
|
+
idempotency_key="publish_1",
|
|
510
|
+
producer_name="litter-vlm",
|
|
511
|
+
producer_version="2026.11",
|
|
512
|
+
coverage=coverage(),
|
|
513
|
+
)
|
|
514
|
+
assert api.publish_external(**arguments).to_dict() == external_receipt()
|
|
515
|
+
foreign = True
|
|
516
|
+
with pytest.raises(ValueError, match="context"):
|
|
517
|
+
api.publish_external(**arguments)
|
|
518
|
+
sent = len(calls)
|
|
519
|
+
invalid = copy.deepcopy(arguments)
|
|
520
|
+
invalid["coverage"][1]["status"] = "unprocessed"
|
|
521
|
+
with pytest.raises(ValueError):
|
|
522
|
+
api.publish_external(**invalid)
|
|
523
|
+
with pytest.raises(ValueError):
|
|
524
|
+
api.publish_external(**{**arguments, "view_id": "semantics_x"})
|
|
525
|
+
assert len(calls) == sent
|
|
526
|
+
assert calls[0].method == "POST"
|
|
527
|
+
assert calls[0].url.path == (
|
|
528
|
+
"/api/v1/image-preannotations/clips/clip_test/views/objects/external"
|
|
529
|
+
)
|
|
@@ -117,7 +117,7 @@ def test_current_is_separate_from_human_revision_and_is_a_detached_snapshot():
|
|
|
117
117
|
|
|
118
118
|
def test_successful_zero_detection_is_ready_and_never_human_completion():
|
|
119
119
|
value = current()
|
|
120
|
-
value["artifact"]["coverage"][0].update(status="empty", objects=[])
|
|
120
|
+
value["artifact"]["coverage"][0].update(status="empty", objects=[], scores={})
|
|
121
121
|
value["result_digest"] = fingerprint(value["artifact"])
|
|
122
122
|
result = parse_image_preannotation_current(value).to_dict()
|
|
123
123
|
assert result["availability"] == "ready"
|
|
@@ -171,6 +171,7 @@ def test_partial_output_has_complete_failed_scopes_without_old_fill():
|
|
|
171
171
|
"image_id": "image_b",
|
|
172
172
|
"status": "failed",
|
|
173
173
|
"objects": [],
|
|
174
|
+
"scores": {},
|
|
174
175
|
"error": {"code": "invalid_image", "message": "Image failed"},
|
|
175
176
|
}
|
|
176
177
|
)
|
|
@@ -183,7 +184,10 @@ def test_partial_output_has_complete_failed_scopes_without_old_fill():
|
|
|
183
184
|
parse_image_preannotation_current(value)
|
|
184
185
|
value["availability"] = "partial"
|
|
185
186
|
value["artifact"]["coverage"][0].update(
|
|
186
|
-
status="failed",
|
|
187
|
+
status="failed",
|
|
188
|
+
objects=[],
|
|
189
|
+
scores={},
|
|
190
|
+
error={"code": "failed", "message": "Image failed"},
|
|
187
191
|
)
|
|
188
192
|
value["result_digest"] = fingerprint(value["artifact"])
|
|
189
193
|
with pytest.raises(ValueError, match="successes"):
|
|
@@ -222,6 +226,7 @@ def test_reject_wrong_current_context_geometry_or_selectors(change):
|
|
|
222
226
|
value["artifact"]["coverage"][0].update(
|
|
223
227
|
status="failed",
|
|
224
228
|
objects=[],
|
|
229
|
+
scores={},
|
|
225
230
|
error={"code": "failed", "message": "Image failed"},
|
|
226
231
|
)
|
|
227
232
|
value["result_digest"] = fingerprint(value["artifact"])
|
|
@@ -74,13 +74,14 @@ def artifact():
|
|
|
74
74
|
"contract_version": IMAGE_REFERENCE_CONTRACT,
|
|
75
75
|
"run_id": "ref_1",
|
|
76
76
|
"source": source().to_dict(),
|
|
77
|
-
"profile": profile().to_dict(),
|
|
77
|
+
"producer": {"kind": "service", "profile": profile().to_dict()},
|
|
78
78
|
"view_id": "objects",
|
|
79
79
|
"coverage": [
|
|
80
80
|
{
|
|
81
81
|
"image_id": "image_a",
|
|
82
82
|
"status": "predicted",
|
|
83
83
|
"objects": [line_object()],
|
|
84
|
+
"scores": {"line_a": 0.875},
|
|
84
85
|
"error": None,
|
|
85
86
|
}
|
|
86
87
|
],
|
|
@@ -175,6 +176,7 @@ def test_reference_states_are_explicit_and_not_human_completion(state):
|
|
|
175
176
|
row["status"] = state
|
|
176
177
|
if state != "predicted":
|
|
177
178
|
row["objects"] = []
|
|
179
|
+
row["scores"] = {}
|
|
178
180
|
if state == "failed":
|
|
179
181
|
row["error"] = {"code": "model_failed", "message": "Model execution failed"}
|
|
180
182
|
parsed = parse_image_reference(value)
|
|
@@ -202,9 +204,9 @@ def test_reference_rejects_incomplete_or_misleading_coverage(change):
|
|
|
202
204
|
if change == "foreign-image":
|
|
203
205
|
value["coverage"][0]["image_id"] = "foreign"
|
|
204
206
|
if change == "wrong-occupancy":
|
|
205
|
-
value["coverage"][0]
|
|
207
|
+
value["coverage"][0].update(status="empty", scores={})
|
|
206
208
|
if change == "missing-error":
|
|
207
|
-
value["coverage"][0].update(status="failed", objects=[])
|
|
209
|
+
value["coverage"][0].update(status="failed", objects=[], scores={})
|
|
208
210
|
if change == "extra-gt":
|
|
209
211
|
value["completion"] = []
|
|
210
212
|
with pytest.raises(ValueError):
|
|
@@ -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.56.0rc1"
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
|
|
@@ -5,7 +5,10 @@ from urllib.parse import quote
|
|
|
5
5
|
from x4d_devkit._image_contract import identifier, require
|
|
6
6
|
from x4d_devkit.image_clip import ImageClip
|
|
7
7
|
from x4d_devkit.image_preannotation import (
|
|
8
|
+
EXTERNAL_CONTRACT,
|
|
8
9
|
parse_image_preannotation_current,
|
|
10
|
+
parse_image_preannotation_external,
|
|
11
|
+
parse_image_preannotation_external_receipt,
|
|
9
12
|
parse_image_preannotation_request,
|
|
10
13
|
parse_image_preannotation_state,
|
|
11
14
|
)
|
|
@@ -69,6 +72,46 @@ class ImagePreannotationsAPI:
|
|
|
69
72
|
context,
|
|
70
73
|
)
|
|
71
74
|
|
|
75
|
+
def publish_external(
|
|
76
|
+
self,
|
|
77
|
+
*,
|
|
78
|
+
clip: ImageClip,
|
|
79
|
+
view_id: str,
|
|
80
|
+
idempotency_key: str,
|
|
81
|
+
producer_name: str,
|
|
82
|
+
producer_version: str,
|
|
83
|
+
coverage: list,
|
|
84
|
+
):
|
|
85
|
+
"""Publish external model predictions as the current preannotation.
|
|
86
|
+
|
|
87
|
+
``coverage`` has one row per Clip image in source order; see
|
|
88
|
+
``x4d-image-preannotation-external-v1``. The request is validated
|
|
89
|
+
locally before it is sent, and the platform replaces the current result
|
|
90
|
+
atomically.
|
|
91
|
+
"""
|
|
92
|
+
context = _context(clip, view_id)
|
|
93
|
+
body = parse_image_preannotation_external(
|
|
94
|
+
{
|
|
95
|
+
**context,
|
|
96
|
+
"contract_version": EXTERNAL_CONTRACT,
|
|
97
|
+
"idempotency_key": idempotency_key,
|
|
98
|
+
"producer": {
|
|
99
|
+
"kind": "external",
|
|
100
|
+
"name": producer_name,
|
|
101
|
+
"version": producer_version,
|
|
102
|
+
},
|
|
103
|
+
"coverage": coverage,
|
|
104
|
+
}
|
|
105
|
+
).to_dict()
|
|
106
|
+
return _check(
|
|
107
|
+
parse_image_preannotation_external_receipt(
|
|
108
|
+
self._client.post(
|
|
109
|
+
_path(clip.clip_id, view_id) + "/external", json=body
|
|
110
|
+
)
|
|
111
|
+
),
|
|
112
|
+
context,
|
|
113
|
+
)
|
|
114
|
+
|
|
72
115
|
def _read(self, *, clip, view_id, suffix, parser):
|
|
73
116
|
context = _context(clip, view_id)
|
|
74
117
|
value = self._client.get(
|