x4d-devkit 0.51.0rc4__tar.gz → 0.52.0__tar.gz

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