x4d-devkit 0.48.0rc2__tar.gz → 0.48.0rc4__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.
Files changed (152) hide show
  1. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/PKG-INFO +1 -1
  2. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/pyproject.toml +1 -1
  3. x4d_devkit-0.48.0rc4/tests/test_image_preannotation.py +380 -0
  4. x4d_devkit-0.48.0rc4/tests/test_image_reference.py +211 -0
  5. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_sdk_api.py +1 -1
  6. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/client.py +2 -0
  7. x4d_devkit-0.48.0rc4/x4d_devkit/client/image_preannotations.py +117 -0
  8. x4d_devkit-0.48.0rc4/x4d_devkit/image_preannotation.py +330 -0
  9. x4d_devkit-0.48.0rc4/x4d_devkit/image_reference.py +438 -0
  10. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit.egg-info/PKG-INFO +1 -1
  11. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit.egg-info/SOURCES.txt +5 -0
  12. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/LICENSE +0 -0
  13. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/README.md +0 -0
  14. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/setup.cfg +0 -0
  15. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_annotation_contract.py +0 -0
  16. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_annotation_reference.py +0 -0
  17. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_assets_client.py +0 -0
  18. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_boundary.py +0 -0
  19. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_box.py +0 -0
  20. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_build_info.py +0 -0
  21. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_category_audit.py +0 -0
  22. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_clean_basemap.py +0 -0
  23. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_clean_basemap_client.py +0 -0
  24. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_cli_auth.py +0 -0
  25. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_client_json_contract.py +0 -0
  26. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_client_projects.py +0 -0
  27. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_clip_archives.py +0 -0
  28. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_contract_validation.py +0 -0
  29. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_coordinate_frames.py +0 -0
  30. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_current_state.py +0 -0
  31. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_current_state_identity_consumers_example.py +0 -0
  32. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_dataset_contract.py +0 -0
  33. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_dataset_identity.py +0 -0
  34. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_detection_eval.py +0 -0
  35. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_evaluate.py +0 -0
  36. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_export_bundle.py +0 -0
  37. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_image_clip.py +0 -0
  38. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_image_geometry.py +0 -0
  39. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_image_review.py +0 -0
  40. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_image_work_session.py +0 -0
  41. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_inference_input.py +0 -0
  42. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_inference_schemas.py +0 -0
  43. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_inference_service.py +0 -0
  44. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_label_schemas.py +0 -0
  45. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_lidar_materialization.py +0 -0
  46. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_loader.py +0 -0
  47. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_matching.py +0 -0
  48. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_metrics.py +0 -0
  49. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_models.py +0 -0
  50. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_platform_pose.py +0 -0
  51. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_preannotation_client.py +0 -0
  52. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_reconstruction_client.py +0 -0
  53. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_release_gate.py +0 -0
  54. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_roadmark_preview.py +0 -0
  55. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_semantic_scene_authoring.py +0 -0
  56. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_semantic_scene_client.py +0 -0
  57. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_semantic_scene_contract.py +0 -0
  58. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_semantic_scene_independent.py +0 -0
  59. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_semantic_scene_native_clip.py +0 -0
  60. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_source_attributes.py +0 -0
  61. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_time_contract.py +0 -0
  62. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_token.py +0 -0
  63. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_tracking_eval.py +0 -0
  64. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_training_identity.py +0 -0
  65. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_transform.py +0 -0
  66. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_transform_chain.py +0 -0
  67. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_validation.py +0 -0
  68. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_vector_map.py +0 -0
  69. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_vector_map_geometry.py +0 -0
  70. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_vector_map_session.py +0 -0
  71. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_work_session.py +0 -0
  72. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/tests/test_work_session_validation_reuse.py +0 -0
  73. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/__init__.py +0 -0
  74. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/_image_contract.py +0 -0
  75. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/annotation_contract.py +0 -0
  76. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/annotation_reference.py +0 -0
  77. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/audit.py +0 -0
  78. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/build_info.py +0 -0
  79. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/clean_basemap.py +0 -0
  80. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/cli.py +0 -0
  81. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/__init__.py +0 -0
  82. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/_origin.py +0 -0
  83. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/annotation_reference.py +0 -0
  84. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/assets.py +0 -0
  85. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/bags.py +0 -0
  86. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/capabilities.py +0 -0
  87. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/checkpoints.py +0 -0
  88. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/clean_basemap.py +0 -0
  89. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/clip_work_sessions.py +0 -0
  90. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/clips.py +0 -0
  91. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/dataset_identity.py +0 -0
  92. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/image_clips.py +0 -0
  93. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/image_review.py +0 -0
  94. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/image_work_sessions.py +0 -0
  95. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/label_schemas.py +0 -0
  96. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/preannotation.py +0 -0
  97. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/projects.py +0 -0
  98. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/reconstruction.py +0 -0
  99. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/semantic_scene.py +0 -0
  100. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  101. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/services.py +0 -0
  102. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/client/test_sets.py +0 -0
  103. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/consumer_identity.py +0 -0
  104. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/coordinate_contract.py +0 -0
  105. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/core/__init__.py +0 -0
  106. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/core/loader.py +0 -0
  107. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/core/models.py +0 -0
  108. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/core/token.py +0 -0
  109. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/core/transform.py +0 -0
  110. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/current_state.py +0 -0
  111. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/dataset_contract.py +0 -0
  112. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/dataset_identity.py +0 -0
  113. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/eval/__init__.py +0 -0
  114. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/eval/api.py +0 -0
  115. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/eval/box.py +0 -0
  116. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/eval/detection.py +0 -0
  117. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/eval/matching.py +0 -0
  118. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/eval/metrics.py +0 -0
  119. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/eval/tracking.py +0 -0
  120. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/export_bundle.py +0 -0
  121. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/image_clip.py +0 -0
  122. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/image_geometry.py +0 -0
  123. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/image_review.py +0 -0
  124. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/image_schema.py +0 -0
  125. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/image_work_session.py +0 -0
  126. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/inference/__init__.py +0 -0
  127. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/inference/input.py +0 -0
  128. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/inference/models.py +0 -0
  129. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/inference/schemas.py +0 -0
  130. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/inference/service.py +0 -0
  131. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/inference/tasks.py +0 -0
  132. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/json_contract.py +0 -0
  133. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/label_schema.py +0 -0
  134. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/lidar_materialization.py +0 -0
  135. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/materialize.py +0 -0
  136. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/roadmark_preview.py +0 -0
  137. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/semantic_scene_authoring.py +0 -0
  138. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/semantic_scene_contract.py +0 -0
  139. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/time_contract.py +0 -0
  140. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/training_identity.py +0 -0
  141. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/validation/__init__.py +0 -0
  142. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/validation/contract.py +0 -0
  143. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/validation/platform_pose.py +0 -0
  144. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/validation/report.py +0 -0
  145. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/validation/validator.py +0 -0
  146. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/vector_map.py +0 -0
  147. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/vector_map_geometry.py +0 -0
  148. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit/work_session.py +0 -0
  149. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  150. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit.egg-info/entry_points.txt +0 -0
  151. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit.egg-info/requires.txt +0 -0
  152. {x4d_devkit-0.48.0rc2 → x4d_devkit-0.48.0rc4}/x4d_devkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.48.0rc2
3
+ Version: 0.48.0rc4
4
4
  Summary: Official X-4D external SDK and CLI for datasets, platform APIs, training bridges, and automation
5
5
  Author: windzu
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "x4d-devkit"
7
- version = "0.48.0rc2"
7
+ version = "0.48.0rc4"
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,380 @@
1
+ import copy
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, parse_image_clip
10
+ from x4d_devkit.image_preannotation import (
11
+ CURRENT_CONTRACT,
12
+ STATE_CONTRACT,
13
+ REQUEST_CONTRACT,
14
+ SERVICE_CONTRACT,
15
+ parse_image_preannotation_current,
16
+ parse_image_preannotation_state,
17
+ parse_image_preannotation_request,
18
+ parse_image_preannotation_service,
19
+ parse_preannotation_operation,
20
+ )
21
+ from x4d_devkit.image_reference import IMAGE_INFERENCE_PROTOCOL, image_reference_source
22
+ from tests.test_image_reference import artifact, clip_payload, profile
23
+ from tests.image_fixtures import line_object
24
+
25
+
26
+ def context():
27
+ value = clip_payload()
28
+ return {
29
+ "clip_id": value["meta"]["clip_id"],
30
+ "project_id": value["meta"]["project_id"],
31
+ "view_id": "objects",
32
+ "source_revision": value["identity"]["source_revision"],
33
+ "schema_fingerprint": value["identity"]["schema_fingerprint"],
34
+ }
35
+
36
+
37
+ def progress():
38
+ return {
39
+ "total": 1,
40
+ "processed": 0,
41
+ "predicted": 0,
42
+ "empty": 0,
43
+ "failed": 0,
44
+ "unprocessed": 1,
45
+ }
46
+
47
+
48
+ def operation():
49
+ return {
50
+ "operation_id": "op_1",
51
+ "status": "running",
52
+ "progress": progress(),
53
+ "error": None,
54
+ "created_at": "2026-10-26T00:00:00Z",
55
+ "completed_at": None,
56
+ "can_cancel": True,
57
+ }
58
+
59
+
60
+ def current():
61
+ result = artifact()
62
+ return {
63
+ **context(),
64
+ "contract_version": CURRENT_CONTRACT,
65
+ "availability": "ready",
66
+ "published_at": "2026-10-26T00:01:00Z",
67
+ "result_digest": fingerprint(result),
68
+ "artifact": result,
69
+ }
70
+
71
+
72
+ def state():
73
+ return {
74
+ **context(),
75
+ "contract_version": STATE_CONTRACT,
76
+ "service_state": "ready",
77
+ "result": {
78
+ "availability": "ready",
79
+ "digest": current()["result_digest"],
80
+ "published_at": current()["published_at"],
81
+ "coverage": {
82
+ "total": 1,
83
+ "processed": 1,
84
+ "predicted": 1,
85
+ "empty": 0,
86
+ "failed": 0,
87
+ "unprocessed": 0,
88
+ },
89
+ },
90
+ "operation": operation(),
91
+ }
92
+
93
+
94
+ def receipt():
95
+ return {
96
+ **context(),
97
+ "contract_version": REQUEST_CONTRACT,
98
+ "disposition": "created",
99
+ "operation": operation(),
100
+ }
101
+
102
+
103
+ def test_current_is_separate_from_human_revision_and_is_a_detached_snapshot():
104
+ value = current()
105
+ parsed = parse_image_preannotation_current(value)
106
+ value["artifact"]["coverage"][0]["objects"].clear()
107
+ assert parsed.to_dict()["artifact"]["coverage"][0]["objects"]
108
+ assert "annotation_revision" not in parsed.to_dict()
109
+ human = clip_payload()
110
+ human["image_annotation"]["objects"] = [line_object()]
111
+ human["identity"] = build_image_identity(human)
112
+ assert (
113
+ image_reference_source(parse_image_clip(human)).to_dict()
114
+ == artifact()["source"]
115
+ )
116
+
117
+
118
+ def test_successful_zero_detection_is_ready_and_never_human_completion():
119
+ value = current()
120
+ value["artifact"]["coverage"][0].update(status="empty", objects=[])
121
+ value["result_digest"] = fingerprint(value["artifact"])
122
+ result = parse_image_preannotation_current(value).to_dict()
123
+ assert result["availability"] == "ready"
124
+ assert result["artifact"]["coverage"][0]["status"] == "empty"
125
+ assert "completion" not in result["artifact"]
126
+
127
+
128
+ @pytest.mark.parametrize("availability", ["none", "stale"])
129
+ def test_absence_is_explicit_and_cannot_carry_an_old_artifact(availability):
130
+ value = {
131
+ **context(),
132
+ "contract_version": CURRENT_CONTRACT,
133
+ "availability": availability,
134
+ "published_at": None,
135
+ "result_digest": None,
136
+ "artifact": None,
137
+ }
138
+ assert parse_image_preannotation_current(value).to_dict() == value
139
+ value["artifact"] = artifact()
140
+ with pytest.raises(ValueError):
141
+ parse_image_preannotation_current(value)
142
+
143
+
144
+ def test_partial_output_has_complete_failed_scopes_without_old_fill():
145
+ payload = clip_payload()
146
+ sample = copy.deepcopy(payload["sample"][0])
147
+ sample.update(
148
+ id="sample_b",
149
+ timestamp={"sec": 11, "nanosec": 20},
150
+ image_ids={"cam_a": "image_b"},
151
+ )
152
+ payload["sample"].append(sample)
153
+ image = copy.deepcopy(payload["sample_data"][0])
154
+ image.update(
155
+ id="image_b", sample_id="sample_b", timestamp={"sec": 11, "nanosec": 20}
156
+ )
157
+ image["asset"]["path"] = "samples/cam_a/11000000020.jpg"
158
+ payload["sample_data"].append(image)
159
+ payload["image_annotation"]["completion"] += [
160
+ {"image_id": "image_b", "view_id": view, "state": "unprocessed"}
161
+ for view in payload["meta"]["view_ids"]
162
+ ]
163
+ payload["identity"] = build_image_identity(payload)
164
+ value = current()
165
+ value["artifact"]["source"] = image_reference_source(
166
+ parse_image_clip(payload)
167
+ ).to_dict()
168
+ value["source_revision"] = payload["identity"]["source_revision"]
169
+ value["artifact"]["coverage"].append(
170
+ {
171
+ "image_id": "image_b",
172
+ "status": "failed",
173
+ "objects": [],
174
+ "error": {"code": "invalid_image", "message": "Image failed"},
175
+ }
176
+ )
177
+ value["availability"] = "partial"
178
+ value["result_digest"] = fingerprint(value["artifact"])
179
+ parsed = parse_image_preannotation_current(value).to_dict()
180
+ assert parsed["artifact"]["coverage"][1]["objects"] == []
181
+ value["availability"] = "ready"
182
+ with pytest.raises(ValueError):
183
+ parse_image_preannotation_current(value)
184
+ value["availability"] = "partial"
185
+ value["artifact"]["coverage"][0].update(
186
+ status="failed", objects=[], error={"code": "failed", "message": "Image failed"}
187
+ )
188
+ value["result_digest"] = fingerprint(value["artifact"])
189
+ with pytest.raises(ValueError, match="successes"):
190
+ parse_image_preannotation_current(value)
191
+
192
+
193
+ @pytest.mark.parametrize(
194
+ "change",
195
+ [
196
+ "digest",
197
+ "source",
198
+ "view",
199
+ "project",
200
+ "missing_image",
201
+ "extra_choice",
202
+ "all_failed",
203
+ "unknown_availability",
204
+ ],
205
+ )
206
+ def test_reject_wrong_current_context_geometry_or_selectors(change):
207
+ value = current()
208
+ if change == "digest":
209
+ value["result_digest"] = "sha256:" + "0" * 64
210
+ elif change == "source":
211
+ value["source_revision"] = "sha256:" + "0" * 64
212
+ elif change == "view":
213
+ value["view_id"] = "semantics"
214
+ elif change == "project":
215
+ value["project_id"] = 99
216
+ elif change == "missing_image":
217
+ value["artifact"]["coverage"] = []
218
+ elif change == "extra_choice":
219
+ value["model_version"] = "latest"
220
+ elif change == "all_failed":
221
+ value["availability"] = "partial"
222
+ value["artifact"]["coverage"][0].update(
223
+ status="failed",
224
+ objects=[],
225
+ error={"code": "failed", "message": "Image failed"},
226
+ )
227
+ value["result_digest"] = fingerprint(value["artifact"])
228
+ else:
229
+ value["availability"] = {}
230
+ with pytest.raises(ValueError):
231
+ parse_image_preannotation_current(value)
232
+
233
+
234
+ @pytest.mark.parametrize(
235
+ "change",
236
+ [
237
+ "timestamp",
238
+ "boolean_count",
239
+ "bad_count",
240
+ "terminal_time",
241
+ "terminal_cancel",
242
+ "partial_without_success",
243
+ "unknown_state",
244
+ ],
245
+ )
246
+ def test_operation_state_is_strict(change):
247
+ value = operation()
248
+ if change == "timestamp":
249
+ value["created_at"] = "yesterday"
250
+ elif change == "boolean_count":
251
+ value["progress"]["processed"] = True
252
+ elif change == "bad_count":
253
+ value["progress"]["processed"] = 1
254
+ elif change == "terminal_time":
255
+ value["status"] = "failed"
256
+ elif change == "terminal_cancel":
257
+ value.update(status="cancelled", completed_at="2026-10-26T00:01:00Z")
258
+ elif change == "partial_without_success":
259
+ value.update(
260
+ status="partial", completed_at="2026-10-26T00:01:00Z", can_cancel=False
261
+ )
262
+ else:
263
+ value["status"] = {}
264
+ with pytest.raises(ValueError):
265
+ parse_preannotation_operation(value)
266
+
267
+
268
+ def test_state_keeps_existing_result_available_during_or_after_failed_rerun():
269
+ value = state()
270
+ assert parse_image_preannotation_state(value).to_dict() == value
271
+ value["operation"].update(
272
+ status="failed",
273
+ completed_at="2026-10-26T00:01:00Z",
274
+ can_cancel=False,
275
+ error={"code": "service_unavailable", "message": "Service failed"},
276
+ )
277
+ assert (
278
+ parse_image_preannotation_state(value).to_dict()["result"]["availability"]
279
+ == "ready"
280
+ )
281
+ value["result"]["availability"] = "none"
282
+ with pytest.raises(ValueError):
283
+ parse_image_preannotation_state(value)
284
+
285
+
286
+ def test_receipt_rejects_model_choice_and_available_creating_work():
287
+ value = receipt()
288
+ assert parse_image_preannotation_request(value).to_dict() == value
289
+ value["profile_id"] = profile().to_dict()["profile_id"]
290
+ with pytest.raises(ValueError):
291
+ parse_image_preannotation_request(value)
292
+ value = receipt()
293
+ value["disposition"] = "available"
294
+ with pytest.raises(ValueError):
295
+ parse_image_preannotation_request(value)
296
+
297
+
298
+ def test_service_is_ready_with_one_model_and_explicit_task_outputs():
299
+ value = {
300
+ "contract_version": SERVICE_CONTRACT,
301
+ "protocol_version": IMAGE_INFERENCE_PROTOCOL,
302
+ "status": "ready",
303
+ "service_name": "synthetic",
304
+ "service_version": "test",
305
+ "task_ids": ["test_objects"],
306
+ "model": profile().to_dict(),
307
+ "outputs": [{"category_id": "line", "geometry_type": "polyline_2d"}],
308
+ "max_concurrent_tasks": 1,
309
+ }
310
+ assert parse_image_preannotation_service(value).to_dict() == value
311
+ value["task_ids"] *= 2
312
+ with pytest.raises(ValueError):
313
+ parse_image_preannotation_service(value)
314
+ value["task_ids"] = ["test_objects"]
315
+ value["models"] = [profile().to_dict()]
316
+ with pytest.raises(ValueError):
317
+ parse_image_preannotation_service(value)
318
+
319
+
320
+ def test_typed_client_resolves_no_model_or_history_and_rejects_foreign_current():
321
+ calls = []
322
+ foreign = False
323
+
324
+ def handler(request):
325
+ calls.append(request)
326
+ if request.method == "POST" and request.url.path.endswith("/requests"):
327
+ body = json.loads(request.content)
328
+ assert set(body) == {
329
+ "clip_id",
330
+ "view_id",
331
+ "source_revision",
332
+ "schema_fingerprint",
333
+ "idempotency_key",
334
+ "recompute",
335
+ }
336
+ return httpx.Response(200, json=receipt())
337
+ if request.url.path.endswith("/current"):
338
+ value = current()
339
+ if foreign:
340
+ value["clip_id"] = "foreign"
341
+ return httpx.Response(200, json=value)
342
+ if request.url.path.endswith("/cancel"):
343
+ assert json.loads(request.content) == {"operation_id": "op_1"}
344
+ return httpx.Response(200, json=state())
345
+
346
+ with X4DClient("https://platform.test") as client:
347
+ client._http.close()
348
+ client._http = httpx.Client(
349
+ base_url="https://platform.test", transport=httpx.MockTransport(handler)
350
+ )
351
+ clip = parse_image_clip(clip_payload())
352
+ api = client.image_preannotations
353
+ assert not hasattr(client, "image_references")
354
+ assert (
355
+ api.request(
356
+ clip=clip, view_id="objects", idempotency_key="request_1"
357
+ ).to_dict()
358
+ == receipt()
359
+ )
360
+ assert api.current(clip=clip, view_id="objects").to_dict() == current()
361
+ assert api.status(clip=clip, view_id="objects").to_dict() == state()
362
+ assert (
363
+ api.cancel(
364
+ clip_id=clip.clip_id, view_id="objects", operation_id="op_1"
365
+ ).to_dict()
366
+ == state()
367
+ )
368
+ foreign = True
369
+ with pytest.raises(ValueError):
370
+ api.current(clip=clip, view_id="objects")
371
+ with pytest.raises(TypeError):
372
+ api.request(
373
+ clip=clip,
374
+ view_id="objects",
375
+ idempotency_key="request_1",
376
+ profile_id="forbidden",
377
+ )
378
+ for call in calls:
379
+ assert "image-references" not in call.url.path
380
+ assert "annotation_revision" not in str(call.url)
@@ -0,0 +1,211 @@
1
+ import copy
2
+
3
+ import pytest
4
+
5
+ import hashlib
6
+
7
+ from tests.image_fixtures import (
8
+ IMAGE_BYTES as FIXTURE_BYTES,
9
+ clip_payload as base_clip_payload,
10
+ line_object,
11
+ )
12
+ from x4d_devkit.image_clip import build_image_identity, parse_image_clip
13
+ from x4d_devkit.image_reference import (
14
+ IMAGE_INFERENCE_PROTOCOL,
15
+ IMAGE_REFERENCE_CONTRACT,
16
+ build_image_model_profile,
17
+ build_image_prediction_input,
18
+ image_reference_source,
19
+ parse_image_model_profile,
20
+ parse_image_prediction,
21
+ parse_image_prediction_input,
22
+ parse_image_reference,
23
+ parse_image_reference_source,
24
+ )
25
+
26
+
27
+ IMAGE_BYTES = b"\xff\xd8\xff" + FIXTURE_BYTES
28
+
29
+
30
+ def clip_payload():
31
+ value = base_clip_payload()
32
+ value["sample_data"][0]["asset"].update(
33
+ size=len(IMAGE_BYTES), sha256=hashlib.sha256(IMAGE_BYTES).hexdigest()
34
+ )
35
+ value["identity"] = build_image_identity(value)
36
+ return value
37
+
38
+
39
+ def profile():
40
+ return build_image_model_profile(
41
+ {
42
+ "model_name": "test",
43
+ "model_version": "1",
44
+ "artifact": {
45
+ "bucket": "models",
46
+ "object_key": "enet/1.tar.gz",
47
+ "object_version": None,
48
+ "archive_sha256": "a" * 64,
49
+ "weights_sha256": "b" * 64,
50
+ },
51
+ "adapter": "test-v1",
52
+ "parameters": {},
53
+ "runtime": "cpu-test",
54
+ }
55
+ )
56
+
57
+
58
+ def source():
59
+ return image_reference_source(parse_image_clip(clip_payload()))
60
+
61
+
62
+ def request():
63
+ return build_image_prediction_input(
64
+ source(),
65
+ image_id="image_a",
66
+ view_id="objects",
67
+ profile_id=profile().to_dict()["profile_id"],
68
+ image_bytes=IMAGE_BYTES,
69
+ )
70
+
71
+
72
+ def artifact():
73
+ return {
74
+ "contract_version": IMAGE_REFERENCE_CONTRACT,
75
+ "run_id": "ref_1",
76
+ "source": source().to_dict(),
77
+ "profile": profile().to_dict(),
78
+ "view_id": "objects",
79
+ "coverage": [
80
+ {
81
+ "image_id": "image_a",
82
+ "status": "predicted",
83
+ "objects": [line_object()],
84
+ "error": None,
85
+ }
86
+ ],
87
+ }
88
+
89
+
90
+ def test_source_and_prediction_never_include_human_data_or_its_revision():
91
+ value = clip_payload()
92
+ value["image_annotation"]["objects"] = [line_object()]
93
+ value["identity"] = build_image_identity(value)
94
+ with_labels = image_reference_source(parse_image_clip(value))
95
+ assert with_labels.to_dict() == source().to_dict()
96
+ assert (
97
+ not {"image_annotation", "annotation_revision", "completion"}
98
+ & with_labels.to_dict().keys()
99
+ )
100
+ encoded = request().to_dict()
101
+ assert "image_annotation" not in str(encoded)
102
+ assert "annotation_revision" not in str(encoded)
103
+
104
+
105
+ def test_profile_binds_all_weights_adapter_parameters_and_runtime():
106
+ value = profile().to_dict()
107
+ for key in ("model_version", "adapter", "runtime"):
108
+ changed = copy.deepcopy(value)
109
+ changed[key] += "new"
110
+ with pytest.raises(ValueError, match="fingerprint"):
111
+ parse_image_model_profile(changed)
112
+ value["parameters"]["threshold"] = 0.7
113
+ with pytest.raises(ValueError, match="fingerprint"):
114
+ parse_image_model_profile(value)
115
+
116
+
117
+ @pytest.mark.parametrize(
118
+ "change", ["gt", "digest", "bytes", "size", "schema", "calibration", "view"]
119
+ )
120
+ def test_reject_source_or_prediction_identity_drift(change):
121
+ value = request().to_dict()
122
+ if change == "gt":
123
+ value["objects"] = []
124
+ if change == "digest":
125
+ value["image"]["asset"]["sha256"] = "0" * 64
126
+ if change == "bytes":
127
+ value["image_base64"] = "!invalid"
128
+ if change == "size":
129
+ value["image"]["asset"]["size"] += 1
130
+ if change == "schema":
131
+ value["context"]["schema_fingerprint"] = "sha256:" + "0" * 64
132
+ if change == "calibration":
133
+ value["calibrated_sensor"]["width"] += 1
134
+ if change == "view":
135
+ value["context"]["view_id"] = "semantics"
136
+ with pytest.raises(ValueError):
137
+ parse_image_prediction_input(value)
138
+
139
+
140
+ def test_source_rejects_revision_drift_and_extra_annotation():
141
+ value = source().to_dict()
142
+ value["sample_data"][0]["asset"]["sha256"] = "0" * 64
143
+ with pytest.raises(ValueError, match="source identity"):
144
+ parse_image_reference_source(value)
145
+ value = source().to_dict()
146
+ value["image_annotation"] = {}
147
+ with pytest.raises(ValueError):
148
+ parse_image_reference_source(value)
149
+
150
+
151
+ def test_prediction_echo_and_native_geometry_schema_validation():
152
+ req = request()
153
+ body = req.to_dict()
154
+ output = {
155
+ "protocol_version": IMAGE_INFERENCE_PROTOCOL,
156
+ "context": body["context"],
157
+ "profile_id": body["profile_id"],
158
+ "image_id": "image_a",
159
+ "objects": [line_object()],
160
+ }
161
+ assert parse_image_prediction(output, request=req).to_dict() == output
162
+ output["objects"][0]["geometry"]["vertices"][0]["x"] = 9000
163
+ with pytest.raises(ValueError):
164
+ parse_image_prediction(output, request=req)
165
+ output["objects"] = []
166
+ output["profile_id"] = "sha256:" + "0" * 64
167
+ with pytest.raises(ValueError, match="context"):
168
+ parse_image_prediction(output, request=req)
169
+
170
+
171
+ @pytest.mark.parametrize("state", ["predicted", "empty", "failed", "unprocessed"])
172
+ def test_reference_states_are_explicit_and_not_human_completion(state):
173
+ value = artifact()
174
+ row = value["coverage"][0]
175
+ row["status"] = state
176
+ if state != "predicted":
177
+ row["objects"] = []
178
+ if state == "failed":
179
+ row["error"] = {"code": "model_failed", "message": "Model execution failed"}
180
+ parsed = parse_image_reference(value)
181
+ assert parsed.to_dict() == value
182
+ assert "completion" not in parsed.to_dict()
183
+
184
+
185
+ @pytest.mark.parametrize(
186
+ "change",
187
+ [
188
+ "missing",
189
+ "duplicate",
190
+ "foreign-image",
191
+ "wrong-occupancy",
192
+ "missing-error",
193
+ "extra-gt",
194
+ ],
195
+ )
196
+ def test_reference_rejects_incomplete_or_misleading_coverage(change):
197
+ value = artifact()
198
+ if change == "missing":
199
+ value["coverage"] = []
200
+ if change == "duplicate":
201
+ value["coverage"] *= 2
202
+ if change == "foreign-image":
203
+ value["coverage"][0]["image_id"] = "foreign"
204
+ if change == "wrong-occupancy":
205
+ value["coverage"][0]["status"] = "empty"
206
+ if change == "missing-error":
207
+ value["coverage"][0].update(status="failed", objects=[])
208
+ if change == "extra-gt":
209
+ value["completion"] = []
210
+ with pytest.raises(ValueError):
211
+ parse_image_reference(value)
@@ -11,7 +11,7 @@ from x4d_devkit.eval.matching import MetricData
11
11
 
12
12
 
13
13
  def test_package_version_is_current_release():
14
- assert x4d_devkit.__version__ == "0.48.0rc2"
14
+ assert x4d_devkit.__version__ == "0.48.0rc4"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -25,6 +25,7 @@ from x4d_devkit.client.dataset_identity import DatasetIdentityAPI
25
25
  from x4d_devkit.client.label_schemas import LabelSchemasAPI
26
26
  from x4d_devkit.client.image_clips import ImageClipsAPI
27
27
  from x4d_devkit.client.image_work_sessions import ImageWorkSessionsAPI
28
+ from x4d_devkit.client.image_preannotations import ImagePreannotationsAPI
28
29
  from x4d_devkit.client.semantic_scene import SemanticSceneAPI
29
30
  from x4d_devkit.client.semantic_scene_authoring import (
30
31
  SemanticSceneAuthoringWorkSessionsAPI,
@@ -74,6 +75,7 @@ class X4DClient:
74
75
  self.clips = ClipsAPI(self)
75
76
  self.image_clips = ImageClipsAPI(self)
76
77
  self.image_work_sessions = ImageWorkSessionsAPI(self)
78
+ self.image_preannotations = ImagePreannotationsAPI(self)
77
79
  self.label_schemas = LabelSchemasAPI(self)
78
80
  self.semantic_scene = SemanticSceneAPI(self)
79
81
  self.semantic_scene_authoring_work_sessions = (