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.
Files changed (177) hide show
  1. {x4d_devkit-0.55.1/x4d_devkit.egg-info → x4d_devkit-0.56.0rc1}/PKG-INFO +1 -1
  2. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/pyproject.toml +1 -1
  3. x4d_devkit-0.56.0rc1/tests/test_image_external_predictions.py +529 -0
  4. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_preannotation.py +7 -2
  5. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_reference.py +5 -3
  6. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_sdk_api.py +1 -1
  7. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_preannotations.py +43 -0
  8. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_clip.py +40 -4
  9. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_preannotation.py +132 -0
  10. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_reference.py +101 -36
  11. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1/x4d_devkit.egg-info}/PKG-INFO +1 -1
  12. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit.egg-info/SOURCES.txt +1 -0
  13. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/LICENSE +0 -0
  14. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/README.md +0 -0
  15. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/setup.cfg +0 -0
  16. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_accounts.py +0 -0
  17. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_annotation_contract.py +0 -0
  18. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_annotation_document_hash.py +0 -0
  19. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_annotation_reference.py +0 -0
  20. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_assets_client.py +0 -0
  21. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_boundary.py +0 -0
  22. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_box.py +0 -0
  23. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_build_info.py +0 -0
  24. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_category_audit.py +0 -0
  25. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_clean_basemap.py +0 -0
  26. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_clean_basemap_client.py +0 -0
  27. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_cli_auth.py +0 -0
  28. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_client_json_contract.py +0 -0
  29. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_client_projects.py +0 -0
  30. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_clip_archives.py +0 -0
  31. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_contract_validation.py +0 -0
  32. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_coordinate_frames.py +0 -0
  33. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_current_state.py +0 -0
  34. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_current_state_identity_consumers_example.py +0 -0
  35. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_dataset_contract.py +0 -0
  36. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_dataset_identity.py +0 -0
  37. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_detection_eval.py +0 -0
  38. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_evaluate.py +0 -0
  39. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_export_bundle.py +0 -0
  40. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_clip.py +0 -0
  41. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_conditional_rules.py +0 -0
  42. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_enum_set.py +0 -0
  43. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_export.py +0 -0
  44. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_geometry.py +0 -0
  45. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_groups.py +0 -0
  46. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_review.py +0 -0
  47. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_schema_migration.py +0 -0
  48. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_schema_registry_client.py +0 -0
  49. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_schema_upgrade.py +0 -0
  50. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_source.py +0 -0
  51. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_image_work_session.py +0 -0
  52. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_inference_input.py +0 -0
  53. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_inference_schemas.py +0 -0
  54. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_inference_service.py +0 -0
  55. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_label_schemas.py +0 -0
  56. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_lidar_materialization.py +0 -0
  57. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_loader.py +0 -0
  58. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_matching.py +0 -0
  59. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_metrics.py +0 -0
  60. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_models.py +0 -0
  61. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_parse_equivalence.py +0 -0
  62. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_platform_pose.py +0 -0
  63. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_preannotation_client.py +0 -0
  64. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_project_discovery.py +0 -0
  65. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_reconstruction_client.py +0 -0
  66. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_release_gate.py +0 -0
  67. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_roadmark_preview.py +0 -0
  68. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_semantic_scene_authoring.py +0 -0
  69. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_semantic_scene_client.py +0 -0
  70. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_semantic_scene_contract.py +0 -0
  71. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_semantic_scene_independent.py +0 -0
  72. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_semantic_scene_native_clip.py +0 -0
  73. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_source_attributes.py +0 -0
  74. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_source_content.py +0 -0
  75. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_time_contract.py +0 -0
  76. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_token.py +0 -0
  77. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_tracking_eval.py +0 -0
  78. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_training_identity.py +0 -0
  79. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_transform.py +0 -0
  80. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_transform_chain.py +0 -0
  81. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_validation.py +0 -0
  82. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_vector_map.py +0 -0
  83. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_vector_map_geometry.py +0 -0
  84. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_vector_map_session.py +0 -0
  85. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_work_session.py +0 -0
  86. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/tests/test_work_session_validation_reuse.py +0 -0
  87. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/__init__.py +0 -0
  88. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/_image_contract.py +0 -0
  89. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/accounts.py +0 -0
  90. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/annotation_contract.py +0 -0
  91. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/annotation_reference.py +0 -0
  92. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/audit.py +0 -0
  93. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/build_info.py +0 -0
  94. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/clean_basemap.py +0 -0
  95. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/cli.py +0 -0
  96. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/__init__.py +0 -0
  97. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/_origin.py +0 -0
  98. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/accounts.py +0 -0
  99. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/annotation_reference.py +0 -0
  100. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/assets.py +0 -0
  101. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/bags.py +0 -0
  102. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/capabilities.py +0 -0
  103. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/checkpoints.py +0 -0
  104. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/clean_basemap.py +0 -0
  105. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/client.py +0 -0
  106. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/clip_work_sessions.py +0 -0
  107. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/clips.py +0 -0
  108. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/dataset_identity.py +0 -0
  109. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_clips.py +0 -0
  110. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_exports.py +0 -0
  111. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_review.py +0 -0
  112. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_schemas.py +0 -0
  113. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/image_work_sessions.py +0 -0
  114. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/label_schemas.py +0 -0
  115. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/preannotation.py +0 -0
  116. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/projects.py +0 -0
  117. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/reconstruction.py +0 -0
  118. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/semantic_scene.py +0 -0
  119. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  120. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/services.py +0 -0
  121. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/client/test_sets.py +0 -0
  122. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/consumer_identity.py +0 -0
  123. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/coordinate_contract.py +0 -0
  124. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/core/__init__.py +0 -0
  125. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/core/loader.py +0 -0
  126. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/core/models.py +0 -0
  127. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/core/token.py +0 -0
  128. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/core/transform.py +0 -0
  129. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/current_state.py +0 -0
  130. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/dataset_contract.py +0 -0
  131. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/dataset_identity.py +0 -0
  132. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/__init__.py +0 -0
  133. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/api.py +0 -0
  134. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/box.py +0 -0
  135. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/detection.py +0 -0
  136. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/matching.py +0 -0
  137. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/metrics.py +0 -0
  138. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/eval/tracking.py +0 -0
  139. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/export_bundle.py +0 -0
  140. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_export.py +0 -0
  141. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_geometry.py +0 -0
  142. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_groups.py +0 -0
  143. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_review.py +0 -0
  144. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_schema.py +0 -0
  145. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_schema_migration.py +0 -0
  146. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_schema_upgrade.py +0 -0
  147. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_source.py +0 -0
  148. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/image_work_session.py +0 -0
  149. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/__init__.py +0 -0
  150. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/input.py +0 -0
  151. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/models.py +0 -0
  152. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/schemas.py +0 -0
  153. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/service.py +0 -0
  154. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/inference/tasks.py +0 -0
  155. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/json_contract.py +0 -0
  156. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/label_schema.py +0 -0
  157. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/lidar_materialization.py +0 -0
  158. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/materialize.py +0 -0
  159. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/project_discovery.py +0 -0
  160. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/roadmark_preview.py +0 -0
  161. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/semantic_scene_authoring.py +0 -0
  162. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/semantic_scene_contract.py +0 -0
  163. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/source_content.py +0 -0
  164. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/time_contract.py +0 -0
  165. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/training_identity.py +0 -0
  166. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/validation/__init__.py +0 -0
  167. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/validation/contract.py +0 -0
  168. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/validation/platform_pose.py +0 -0
  169. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/validation/report.py +0 -0
  170. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/validation/validator.py +0 -0
  171. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/vector_map.py +0 -0
  172. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/vector_map_geometry.py +0 -0
  173. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit/work_session.py +0 -0
  174. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  175. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit.egg-info/entry_points.txt +0 -0
  176. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/x4d_devkit.egg-info/requires.txt +0 -0
  177. {x4d_devkit-0.55.1 → x4d_devkit-0.56.0rc1}/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.55.1
3
+ Version: 0.56.0rc1
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.55.1"
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", objects=[], error={"code": "failed", "message": "Image 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]["status"] = "empty"
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.55.1"
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(