x4d-devkit 0.49.0__tar.gz → 0.51.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/PKG-INFO +1 -1
  2. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/pyproject.toml +1 -1
  3. x4d_devkit-0.51.0/tests/test_annotation_document_hash.py +111 -0
  4. x4d_devkit-0.51.0/tests/test_image_groups.py +249 -0
  5. x4d_devkit-0.51.0/tests/test_parse_equivalence.py +978 -0
  6. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_sdk_api.py +1 -1
  7. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_work_session.py +13 -0
  8. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/_image_contract.py +13 -3
  9. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/annotation_contract.py +111 -15
  10. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/core/loader.py +31 -17
  11. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/image_clip.py +9 -0
  12. x4d_devkit-0.51.0/x4d_devkit/image_groups.py +83 -0
  13. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/image_schema.py +7 -0
  14. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/image_work_session.py +4 -0
  15. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/json_contract.py +41 -0
  16. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/work_session.py +184 -45
  17. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit.egg-info/PKG-INFO +1 -1
  18. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit.egg-info/SOURCES.txt +4 -0
  19. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/LICENSE +0 -0
  20. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/README.md +0 -0
  21. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/setup.cfg +0 -0
  22. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_accounts.py +0 -0
  23. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_annotation_contract.py +0 -0
  24. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_annotation_reference.py +0 -0
  25. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_assets_client.py +0 -0
  26. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_boundary.py +0 -0
  27. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_box.py +0 -0
  28. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_build_info.py +0 -0
  29. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_category_audit.py +0 -0
  30. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_clean_basemap.py +0 -0
  31. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_clean_basemap_client.py +0 -0
  32. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_cli_auth.py +0 -0
  33. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_client_json_contract.py +0 -0
  34. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_client_projects.py +0 -0
  35. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_clip_archives.py +0 -0
  36. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_contract_validation.py +0 -0
  37. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_coordinate_frames.py +0 -0
  38. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_current_state.py +0 -0
  39. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_current_state_identity_consumers_example.py +0 -0
  40. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_dataset_contract.py +0 -0
  41. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_dataset_identity.py +0 -0
  42. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_detection_eval.py +0 -0
  43. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_evaluate.py +0 -0
  44. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_export_bundle.py +0 -0
  45. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_image_clip.py +0 -0
  46. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_image_export.py +0 -0
  47. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_image_geometry.py +0 -0
  48. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_image_preannotation.py +0 -0
  49. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_image_reference.py +0 -0
  50. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_image_review.py +0 -0
  51. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_image_schema_upgrade.py +0 -0
  52. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_image_work_session.py +0 -0
  53. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_inference_input.py +0 -0
  54. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_inference_schemas.py +0 -0
  55. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_inference_service.py +0 -0
  56. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_label_schemas.py +0 -0
  57. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_lidar_materialization.py +0 -0
  58. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_loader.py +0 -0
  59. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_matching.py +0 -0
  60. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_metrics.py +0 -0
  61. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_models.py +0 -0
  62. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_platform_pose.py +0 -0
  63. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_preannotation_client.py +0 -0
  64. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_project_discovery.py +0 -0
  65. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_reconstruction_client.py +0 -0
  66. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_release_gate.py +0 -0
  67. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_roadmark_preview.py +0 -0
  68. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_semantic_scene_authoring.py +0 -0
  69. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_semantic_scene_client.py +0 -0
  70. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_semantic_scene_contract.py +0 -0
  71. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_semantic_scene_independent.py +0 -0
  72. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_semantic_scene_native_clip.py +0 -0
  73. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_source_attributes.py +0 -0
  74. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_time_contract.py +0 -0
  75. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_token.py +0 -0
  76. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_tracking_eval.py +0 -0
  77. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_training_identity.py +0 -0
  78. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_transform.py +0 -0
  79. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_transform_chain.py +0 -0
  80. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_validation.py +0 -0
  81. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_vector_map.py +0 -0
  82. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_vector_map_geometry.py +0 -0
  83. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_vector_map_session.py +0 -0
  84. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/tests/test_work_session_validation_reuse.py +0 -0
  85. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/__init__.py +0 -0
  86. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/accounts.py +0 -0
  87. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/annotation_reference.py +0 -0
  88. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/audit.py +0 -0
  89. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/build_info.py +0 -0
  90. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/clean_basemap.py +0 -0
  91. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/cli.py +0 -0
  92. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/__init__.py +0 -0
  93. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/_origin.py +0 -0
  94. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/accounts.py +0 -0
  95. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/annotation_reference.py +0 -0
  96. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/assets.py +0 -0
  97. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/bags.py +0 -0
  98. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/capabilities.py +0 -0
  99. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/checkpoints.py +0 -0
  100. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/clean_basemap.py +0 -0
  101. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/client.py +0 -0
  102. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/clip_work_sessions.py +0 -0
  103. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/clips.py +0 -0
  104. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/dataset_identity.py +0 -0
  105. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/image_clips.py +0 -0
  106. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/image_exports.py +0 -0
  107. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/image_preannotations.py +0 -0
  108. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/image_review.py +0 -0
  109. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/image_schemas.py +0 -0
  110. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/image_work_sessions.py +0 -0
  111. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/label_schemas.py +0 -0
  112. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/preannotation.py +0 -0
  113. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/projects.py +0 -0
  114. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/reconstruction.py +0 -0
  115. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/semantic_scene.py +0 -0
  116. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  117. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/services.py +0 -0
  118. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/client/test_sets.py +0 -0
  119. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/consumer_identity.py +0 -0
  120. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/coordinate_contract.py +0 -0
  121. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/core/__init__.py +0 -0
  122. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/core/models.py +0 -0
  123. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/core/token.py +0 -0
  124. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/core/transform.py +0 -0
  125. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/current_state.py +0 -0
  126. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/dataset_contract.py +0 -0
  127. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/dataset_identity.py +0 -0
  128. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/eval/__init__.py +0 -0
  129. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/eval/api.py +0 -0
  130. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/eval/box.py +0 -0
  131. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/eval/detection.py +0 -0
  132. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/eval/matching.py +0 -0
  133. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/eval/metrics.py +0 -0
  134. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/eval/tracking.py +0 -0
  135. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/export_bundle.py +0 -0
  136. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/image_export.py +0 -0
  137. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/image_geometry.py +0 -0
  138. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/image_preannotation.py +0 -0
  139. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/image_reference.py +0 -0
  140. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/image_review.py +0 -0
  141. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/image_schema_upgrade.py +0 -0
  142. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/inference/__init__.py +0 -0
  143. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/inference/input.py +0 -0
  144. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/inference/models.py +0 -0
  145. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/inference/schemas.py +0 -0
  146. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/inference/service.py +0 -0
  147. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/inference/tasks.py +0 -0
  148. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/label_schema.py +0 -0
  149. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/lidar_materialization.py +0 -0
  150. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/materialize.py +0 -0
  151. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/project_discovery.py +0 -0
  152. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/roadmark_preview.py +0 -0
  153. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/semantic_scene_authoring.py +0 -0
  154. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/semantic_scene_contract.py +0 -0
  155. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/time_contract.py +0 -0
  156. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/training_identity.py +0 -0
  157. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/validation/__init__.py +0 -0
  158. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/validation/contract.py +0 -0
  159. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/validation/platform_pose.py +0 -0
  160. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/validation/report.py +0 -0
  161. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/validation/validator.py +0 -0
  162. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/vector_map.py +0 -0
  163. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit/vector_map_geometry.py +0 -0
  164. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  165. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
  166. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/x4d_devkit.egg-info/requires.txt +0 -0
  167. {x4d_devkit-0.49.0 → x4d_devkit-0.51.0}/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.49.0
3
+ Version: 0.51.0
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.49.0"
7
+ version = "0.51.0"
8
8
  description = "Official X-4D external SDK and CLI for datasets, platform APIs, training bridges, and automation"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import hashlib
5
+ import random
6
+
7
+ import pytest
8
+ import rfc8785
9
+
10
+ from tests.test_annotation_contract import _annotation
11
+ from x4d_devkit.annotation_contract import (
12
+ ANNOTATION_CONTRACT_VERSION,
13
+ AnnotationContractError,
14
+ ClipAnnotationDocument,
15
+ build_clip_annotations,
16
+ parse_clip_annotations,
17
+ )
18
+
19
+ _EDGE_FLOATS = (0.0, -0.0, 1e21, 1e-7, 5e-324, 1.7976931348623157e308, 0.1 + 0.2, -123.456)
20
+ _EDGE_TEXT = ("", "é", "雪", "\U0001f697", "a\"b\\c", "
", "\x7f")
21
+
22
+
23
+ def _sha(payload) -> str:
24
+ return f"sha256:{hashlib.sha256(rfc8785.dumps(payload)).hexdigest()}"
25
+
26
+
27
+ def _random_rows(rng: random.Random, count: int) -> list[dict]:
28
+ rows = []
29
+ for index in range(count):
30
+ row = _annotation(
31
+ f"ann-{rng.randrange(10**9):09d}-{index}",
32
+ f"sample-{rng.randrange(5)}",
33
+ instance=f"instance-{rng.randrange(4)}",
34
+ )
35
+ row["bbox_3d"]["translation"]["x"] = rng.choice(_EDGE_FLOATS)
36
+ row["bbox_3d"]["size"]["width"] = rng.random() * 10
37
+ row["attributes"] = {rng.choice(_EDGE_TEXT) or "k": rng.choice(_EDGE_TEXT)}
38
+ if rng.random() < 0.5:
39
+ row["velocity"] = {"vx": rng.choice(_EDGE_FLOATS), "vy": 0.0, "vz": -1.5}
40
+ rows.append(row)
41
+ for instance in {row["instance_token"] for row in rows}:
42
+ motion = rng.choice(("auto", "static", "dynamic"))
43
+ for row in rows:
44
+ if row["instance_token"] == instance:
45
+ row["motion"] = motion
46
+ return rows
47
+
48
+
49
+ def test_single_pass_digests_match_full_rfc8785_canonicalization():
50
+ rng = random.Random(8785)
51
+ for count in (0, 1, 2, 17, 120):
52
+ document = build_clip_annotations("clip-a", "base_link", _random_rows(rng, count))
53
+ payload = document.to_dict()
54
+ revision_payload = {key: value for key, value in payload.items() if key != "annotation_revision"}
55
+ assert document.annotation_revision == _sha(revision_payload)
56
+ assert document.document_hash() == _sha(payload)
57
+
58
+ parsed = parse_clip_annotations(payload)
59
+ assert parsed.annotation_revision == document.annotation_revision
60
+ assert parsed.document_hash() == _sha(payload)
61
+
62
+
63
+ def test_document_hash_is_derived_for_directly_constructed_and_replaced_documents():
64
+ document = build_clip_annotations("clip-a", "base_link", _random_rows(random.Random(1), 9))
65
+ direct = ClipAnnotationDocument(
66
+ clip_id=document.clip_id,
67
+ annotation_frame_id=document.annotation_frame_id,
68
+ annotation_revision=document.annotation_revision,
69
+ annotations=document.annotations,
70
+ )
71
+ assert direct.document_hash() == document.document_hash()
72
+
73
+ replaced = dataclasses.replace(document, annotations=document.annotations[:3])
74
+ assert replaced.document_hash() == _sha(replaced.to_dict())
75
+ assert replaced.document_hash() != document.document_hash()
76
+
77
+
78
+ def test_document_hash_is_not_part_of_document_equality():
79
+ rows = _random_rows(random.Random(2), 5)
80
+ built = build_clip_annotations("clip-a", "base_link", rows)
81
+ parsed = parse_clip_annotations(built.to_dict())
82
+ plain = ClipAnnotationDocument(
83
+ clip_id=built.clip_id,
84
+ annotation_frame_id=built.annotation_frame_id,
85
+ annotation_revision=built.annotation_revision,
86
+ annotations=built.annotations,
87
+ )
88
+ assert built == parsed == plain
89
+ assert "_document_hash" not in {field.name for field in dataclasses.fields(built)}
90
+ assert built.to_dict()["contract_version"] == ANNOTATION_CONTRACT_VERSION
91
+
92
+
93
+ def test_parse_reuses_only_documents_this_module_verified():
94
+ built = build_clip_annotations("clip-a", "base_link", _random_rows(random.Random(3), 4))
95
+ assert parse_clip_annotations(built) is built
96
+ assert parse_clip_annotations(parse_clip_annotations(built.to_dict())) is not built
97
+
98
+ forged = ClipAnnotationDocument(
99
+ clip_id=built.clip_id,
100
+ annotation_frame_id=built.annotation_frame_id,
101
+ annotation_revision="sha256:" + "0" * 64,
102
+ annotations=built.annotations,
103
+ )
104
+ forged.document_hash() # caching the hash must not mark the document verified
105
+ with pytest.raises(AnnotationContractError) as error:
106
+ parse_clip_annotations(forged)
107
+ assert error.value.code == "annotation_revision_mismatch"
108
+
109
+ replaced = dataclasses.replace(built, annotation_revision="sha256:" + "1" * 64)
110
+ with pytest.raises(AnnotationContractError):
111
+ parse_clip_annotations(replaced)
@@ -0,0 +1,249 @@
1
+ from copy import deepcopy
2
+
3
+ import pytest
4
+
5
+ from tests.image_fixtures import clip_payload, line_object
6
+ from x4d_devkit._image_contract import canonical
7
+ from x4d_devkit.image_clip import build_image_identity, parse_image_clip
8
+ from x4d_devkit.image_groups import (
9
+ IMAGE_OBJECT_GROUPS_VERSION,
10
+ parse_image_object_groups,
11
+ )
12
+ from x4d_devkit.image_schema import parse_image_schema_body
13
+ from x4d_devkit.image_schema_upgrade import additive_image_schema_changes
14
+ from x4d_devkit.image_work_session import (
15
+ IMAGE_WORK_SESSION_VERSION,
16
+ build_image_submit,
17
+ image_candidate,
18
+ parse_image_submit,
19
+ parse_image_work_session,
20
+ )
21
+
22
+
23
+ def grouped_payload():
24
+ value = clip_payload()
25
+ body = value["label_schema"]["body"]
26
+ body["categories"][0]["groupable"] = True
27
+ value["label_schema"]["identity"] = parse_image_schema_body(body).identity.to_dict()
28
+ first = line_object()
29
+ second = deepcopy(first)
30
+ second["id"] = "line_b"
31
+ second["attributes"] = {"visible": True}
32
+ second["attribute_ranges"][0]["attributes"] = {"style": "b"}
33
+ value["image_annotation"]["objects"] = [first, second]
34
+ value["image_annotation"]["object_groups"] = {
35
+ "contract_version": IMAGE_OBJECT_GROUPS_VERSION,
36
+ "groups": [
37
+ {
38
+ "id": "group_1",
39
+ "image_id": "image_a",
40
+ "view_id": "objects",
41
+ "object_ids": ["line_a", "line_b"],
42
+ }
43
+ ],
44
+ }
45
+ value["identity"] = build_image_identity(value)
46
+ return value
47
+
48
+
49
+ def test_relations_preserve_independent_objects_and_all_source_components():
50
+ value = grouped_payload()
51
+ original = deepcopy(value)
52
+ clip = parse_image_clip(value)
53
+ assert clip.to_dict() == original
54
+ session = parse_image_work_session(
55
+ {"contract_version": IMAGE_WORK_SESSION_VERSION, "clip": value}
56
+ )
57
+ request = build_image_submit(session, value["image_annotation"]).to_dict()
58
+ assert (
59
+ parse_image_submit(request).to_dict()["image_annotation"]
60
+ == original["image_annotation"]
61
+ )
62
+ candidate = image_candidate(session.clip, request["image_annotation"]).to_dict()
63
+ assert candidate == original
64
+ value["image_annotation"]["object_groups"]["groups"][0]["object_ids"].clear()
65
+ assert clip.to_dict() == original
66
+
67
+
68
+ def test_relation_changes_enter_revision_without_changing_geometry_or_source():
69
+ value = grouped_payload()
70
+ before = parse_image_clip(value)
71
+ annotation = deepcopy(value["image_annotation"])
72
+ annotation["object_groups"]["groups"][0]["id"] = "changed_group_identity"
73
+ after = image_candidate(before, annotation).to_dict()
74
+ for name in ("source_revision", "schema_fingerprint"):
75
+ assert after["identity"][name] == value["identity"][name]
76
+ for name in ("annotation_revision", "clip_revision"):
77
+ assert after["identity"][name] != value["identity"][name]
78
+ assert after["image_annotation"]["objects"] == value["image_annotation"]["objects"]
79
+ assert (
80
+ after["image_annotation"]["completion"]
81
+ == value["image_annotation"]["completion"]
82
+ )
83
+ annotation["object_groups"]["groups"] = []
84
+ ungrouped = image_candidate(before, annotation).to_dict()
85
+ assert (
86
+ ungrouped["image_annotation"]["objects"] == value["image_annotation"]["objects"]
87
+ )
88
+ assert ungrouped["image_annotation"]["object_groups"]["groups"] == []
89
+
90
+
91
+ def test_existing_documents_remain_byte_identical_and_do_not_infer_groups():
92
+ old = clip_payload()
93
+ assert canonical(parse_image_clip(old).to_dict()) == canonical(old)
94
+ assert (
95
+ "groupable"
96
+ not in parse_image_schema_body(old["label_schema"]["body"]).to_dict()[
97
+ "categories"
98
+ ][0]
99
+ )
100
+ value = grouped_payload()
101
+ del value["image_annotation"]["object_groups"]
102
+ value["identity"] = build_image_identity(value)
103
+ parsed = parse_image_clip(value).to_dict()
104
+ assert "object_groups" not in parsed["image_annotation"]
105
+ assert (
106
+ parsed["image_annotation"]["objects"][0]["geometry"]
107
+ == parsed["image_annotation"]["objects"][1]["geometry"]
108
+ )
109
+ assert canonical(parsed) == canonical(value)
110
+
111
+
112
+ @pytest.mark.parametrize(
113
+ "change",
114
+ [
115
+ "unknown_member",
116
+ "one_member",
117
+ "empty_members",
118
+ "duplicate_member",
119
+ "duplicate_group",
120
+ "duplicate_membership",
121
+ "cross_image",
122
+ "cross_view",
123
+ "disallowed_category",
124
+ "missing_permission",
125
+ "false_permission",
126
+ "colliding_id",
127
+ "unknown_version",
128
+ "null_fragment",
129
+ "extra_fragment",
130
+ "extra_group",
131
+ "object_provenance",
132
+ "geometry_mismatch",
133
+ "removed_member",
134
+ "non_array_members",
135
+ ],
136
+ )
137
+ def test_invalid_relations_fail_with_paths_without_repair(change):
138
+ value = grouped_payload()
139
+ annotation = value["image_annotation"]
140
+ fragment = annotation["object_groups"]
141
+ group = fragment["groups"][0]
142
+ if change == "unknown_member":
143
+ group["object_ids"][1] = "missing"
144
+ elif change == "one_member":
145
+ group["object_ids"].pop()
146
+ elif change == "empty_members":
147
+ group["object_ids"] = []
148
+ elif change == "duplicate_member":
149
+ group["object_ids"][1] = "line_a"
150
+ elif change == "duplicate_group":
151
+ fragment["groups"].append(deepcopy(group))
152
+ elif change == "duplicate_membership":
153
+ fragment["groups"].append({**group, "id": "group_2"})
154
+ elif change == "cross_image":
155
+ group["image_id"] = "another_image"
156
+ elif change == "cross_view":
157
+ group["view_id"] = "semantics"
158
+ elif change == "disallowed_category":
159
+ annotation["objects"][1].update(
160
+ category_id="box",
161
+ attributes={},
162
+ attribute_ranges=[],
163
+ geometry={
164
+ "geometry_type": "box_2d",
165
+ "box": {"x_min": 1, "y_min": 2, "x_max": 10, "y_max": 20},
166
+ },
167
+ )
168
+ elif change == "missing_permission":
169
+ del value["label_schema"]["body"]["categories"][0]["groupable"]
170
+ elif change == "false_permission":
171
+ value["label_schema"]["body"]["categories"][0]["groupable"] = False
172
+ elif change == "colliding_id":
173
+ group["id"] = "line_a"
174
+ elif change == "unknown_version":
175
+ fragment["contract_version"] = "unknown"
176
+ elif change == "null_fragment":
177
+ annotation["object_groups"] = None
178
+ elif change == "extra_fragment":
179
+ fragment["origin"] = "split"
180
+ elif change == "extra_group":
181
+ group["origin"] = "split"
182
+ elif change == "object_provenance":
183
+ annotation["objects"][0]["split_from"] = "original"
184
+ elif change == "geometry_mismatch":
185
+ annotation["objects"][1]["geometry"]["geometry_type"] = "box_2d"
186
+ elif change == "removed_member":
187
+ annotation["objects"].pop()
188
+ elif change == "non_array_members":
189
+ group["object_ids"] = "line_a,line_b"
190
+ body = value["label_schema"]["body"]
191
+ value["label_schema"]["identity"] = parse_image_schema_body(body).identity.to_dict()
192
+ value["identity"] = build_image_identity(value)
193
+ original = deepcopy(value)
194
+ with pytest.raises(ValueError) as caught:
195
+ parse_image_clip(value)
196
+ assert caught.value.path
197
+ assert value == original
198
+
199
+
200
+ @pytest.mark.parametrize("value", [None, 0, 1, "true", [], {}])
201
+ def test_grouping_permission_is_an_explicit_boolean(value):
202
+ body = clip_payload()["label_schema"]["body"]
203
+ body["categories"][0]["groupable"] = value
204
+ with pytest.raises(ValueError) as caught:
205
+ parse_image_schema_body(body)
206
+ assert caught.value.path.endswith("groupable")
207
+
208
+
209
+ def test_unknown_business_category_can_use_groups_without_a_catalogue():
210
+ value = grouped_payload()
211
+ body = value["label_schema"]["body"]
212
+ body["categories"][0]["id"] = "new_business_after_sdk_release"
213
+ body["task_views"][0]["category_ids"][0] = "new_business_after_sdk_release"
214
+ for row in value["image_annotation"]["objects"]:
215
+ row["category_id"] = "new_business_after_sdk_release"
216
+ value["label_schema"]["identity"] = parse_image_schema_body(body).identity.to_dict()
217
+ value["identity"] = build_image_identity(value)
218
+ assert parse_image_clip(value).to_dict() == value
219
+
220
+
221
+ def test_empty_fragment_is_valid_and_is_not_injected_when_absent():
222
+ assert (
223
+ parse_image_object_groups(
224
+ {"contract_version": IMAGE_OBJECT_GROUPS_VERSION, "groups": []}
225
+ ).to_dict()["groups"]
226
+ == []
227
+ )
228
+ value = clip_payload()
229
+ annotation = deepcopy(value["image_annotation"])
230
+ annotation["object_groups"] = {
231
+ "contract_version": IMAGE_OBJECT_GROUPS_VERSION,
232
+ "groups": [],
233
+ }
234
+ assert (
235
+ image_candidate(parse_image_clip(value), annotation).to_dict()[
236
+ "image_annotation"
237
+ ]
238
+ == annotation
239
+ )
240
+ assert "object_groups" not in parse_image_clip(value).to_dict()["image_annotation"]
241
+
242
+
243
+ def test_grouping_permission_changes_are_not_silently_additive_upgrades():
244
+ old = clip_payload()["label_schema"]["body"]
245
+ new = deepcopy(old)
246
+ new["version"] = "2.0.0"
247
+ new["categories"][0]["groupable"] = True
248
+ with pytest.raises(ValueError):
249
+ additive_image_schema_changes(old, new)