x4d-devkit 0.52.0rc1__tar.gz → 0.52.0rc2__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.52.0rc1 → x4d_devkit-0.52.0rc2}/PKG-INFO +1 -1
  2. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/pyproject.toml +1 -1
  3. x4d_devkit-0.52.0rc2/tests/test_image_conditional_rules.py +328 -0
  4. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_sdk_api.py +1 -1
  5. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_clip.py +97 -1
  6. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_schema.py +136 -3
  7. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit.egg-info/PKG-INFO +1 -1
  8. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit.egg-info/SOURCES.txt +1 -0
  9. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/LICENSE +0 -0
  10. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/README.md +0 -0
  11. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/setup.cfg +0 -0
  12. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_accounts.py +0 -0
  13. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_annotation_contract.py +0 -0
  14. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_annotation_document_hash.py +0 -0
  15. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_annotation_reference.py +0 -0
  16. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_assets_client.py +0 -0
  17. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_boundary.py +0 -0
  18. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_box.py +0 -0
  19. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_build_info.py +0 -0
  20. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_category_audit.py +0 -0
  21. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_clean_basemap.py +0 -0
  22. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_clean_basemap_client.py +0 -0
  23. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_cli_auth.py +0 -0
  24. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_client_json_contract.py +0 -0
  25. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_client_projects.py +0 -0
  26. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_clip_archives.py +0 -0
  27. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_contract_validation.py +0 -0
  28. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_coordinate_frames.py +0 -0
  29. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_current_state.py +0 -0
  30. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_current_state_identity_consumers_example.py +0 -0
  31. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_dataset_contract.py +0 -0
  32. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_dataset_identity.py +0 -0
  33. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_detection_eval.py +0 -0
  34. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_evaluate.py +0 -0
  35. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_export_bundle.py +0 -0
  36. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_clip.py +0 -0
  37. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_enum_set.py +0 -0
  38. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_export.py +0 -0
  39. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_geometry.py +0 -0
  40. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_groups.py +0 -0
  41. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_preannotation.py +0 -0
  42. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_reference.py +0 -0
  43. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_review.py +0 -0
  44. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_schema_upgrade.py +0 -0
  45. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_image_work_session.py +0 -0
  46. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_inference_input.py +0 -0
  47. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_inference_schemas.py +0 -0
  48. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_inference_service.py +0 -0
  49. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_label_schemas.py +0 -0
  50. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_lidar_materialization.py +0 -0
  51. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_loader.py +0 -0
  52. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_matching.py +0 -0
  53. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_metrics.py +0 -0
  54. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_models.py +0 -0
  55. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_parse_equivalence.py +0 -0
  56. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_platform_pose.py +0 -0
  57. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_preannotation_client.py +0 -0
  58. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_project_discovery.py +0 -0
  59. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_reconstruction_client.py +0 -0
  60. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_release_gate.py +0 -0
  61. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_roadmark_preview.py +0 -0
  62. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_semantic_scene_authoring.py +0 -0
  63. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_semantic_scene_client.py +0 -0
  64. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_semantic_scene_contract.py +0 -0
  65. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_semantic_scene_independent.py +0 -0
  66. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_semantic_scene_native_clip.py +0 -0
  67. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_source_attributes.py +0 -0
  68. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_time_contract.py +0 -0
  69. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_token.py +0 -0
  70. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_tracking_eval.py +0 -0
  71. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_training_identity.py +0 -0
  72. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_transform.py +0 -0
  73. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_transform_chain.py +0 -0
  74. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_validation.py +0 -0
  75. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_vector_map.py +0 -0
  76. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_vector_map_geometry.py +0 -0
  77. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_vector_map_session.py +0 -0
  78. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_work_session.py +0 -0
  79. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/tests/test_work_session_validation_reuse.py +0 -0
  80. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/__init__.py +0 -0
  81. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/_image_contract.py +0 -0
  82. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/accounts.py +0 -0
  83. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/annotation_contract.py +0 -0
  84. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/annotation_reference.py +0 -0
  85. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/audit.py +0 -0
  86. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/build_info.py +0 -0
  87. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/clean_basemap.py +0 -0
  88. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/cli.py +0 -0
  89. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/__init__.py +0 -0
  90. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/_origin.py +0 -0
  91. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/accounts.py +0 -0
  92. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/annotation_reference.py +0 -0
  93. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/assets.py +0 -0
  94. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/bags.py +0 -0
  95. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/capabilities.py +0 -0
  96. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/checkpoints.py +0 -0
  97. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/clean_basemap.py +0 -0
  98. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/client.py +0 -0
  99. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/clip_work_sessions.py +0 -0
  100. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/clips.py +0 -0
  101. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/dataset_identity.py +0 -0
  102. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_clips.py +0 -0
  103. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_exports.py +0 -0
  104. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_preannotations.py +0 -0
  105. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_review.py +0 -0
  106. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_schemas.py +0 -0
  107. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/image_work_sessions.py +0 -0
  108. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/label_schemas.py +0 -0
  109. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/preannotation.py +0 -0
  110. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/projects.py +0 -0
  111. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/reconstruction.py +0 -0
  112. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/semantic_scene.py +0 -0
  113. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  114. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/services.py +0 -0
  115. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/client/test_sets.py +0 -0
  116. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/consumer_identity.py +0 -0
  117. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/coordinate_contract.py +0 -0
  118. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/core/__init__.py +0 -0
  119. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/core/loader.py +0 -0
  120. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/core/models.py +0 -0
  121. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/core/token.py +0 -0
  122. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/core/transform.py +0 -0
  123. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/current_state.py +0 -0
  124. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/dataset_contract.py +0 -0
  125. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/dataset_identity.py +0 -0
  126. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/__init__.py +0 -0
  127. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/api.py +0 -0
  128. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/box.py +0 -0
  129. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/detection.py +0 -0
  130. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/matching.py +0 -0
  131. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/metrics.py +0 -0
  132. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/eval/tracking.py +0 -0
  133. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/export_bundle.py +0 -0
  134. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_export.py +0 -0
  135. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_geometry.py +0 -0
  136. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_groups.py +0 -0
  137. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_preannotation.py +0 -0
  138. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_reference.py +0 -0
  139. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_review.py +0 -0
  140. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_schema_upgrade.py +0 -0
  141. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/image_work_session.py +0 -0
  142. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/__init__.py +0 -0
  143. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/input.py +0 -0
  144. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/models.py +0 -0
  145. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/schemas.py +0 -0
  146. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/service.py +0 -0
  147. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/inference/tasks.py +0 -0
  148. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/json_contract.py +0 -0
  149. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/label_schema.py +0 -0
  150. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/lidar_materialization.py +0 -0
  151. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/materialize.py +0 -0
  152. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/project_discovery.py +0 -0
  153. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/roadmark_preview.py +0 -0
  154. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/semantic_scene_authoring.py +0 -0
  155. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/semantic_scene_contract.py +0 -0
  156. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/time_contract.py +0 -0
  157. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/training_identity.py +0 -0
  158. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/validation/__init__.py +0 -0
  159. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/validation/contract.py +0 -0
  160. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/validation/platform_pose.py +0 -0
  161. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/validation/report.py +0 -0
  162. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/validation/validator.py +0 -0
  163. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/vector_map.py +0 -0
  164. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/vector_map_geometry.py +0 -0
  165. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit/work_session.py +0 -0
  166. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  167. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit.egg-info/entry_points.txt +0 -0
  168. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/x4d_devkit.egg-info/requires.txt +0 -0
  169. {x4d_devkit-0.52.0rc1 → x4d_devkit-0.52.0rc2}/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.52.0rc1
3
+ Version: 0.52.0rc2
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.52.0rc1"
7
+ version = "0.52.0rc2"
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
+ )
@@ -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.52.0rc1"
14
+ assert x4d_devkit.__version__ == "0.52.0rc2"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -261,6 +261,72 @@ def _attributes(values: Any, category: dict, scope: str, path: str) -> None:
261
261
  )
262
262
 
263
263
 
264
+ def _condition_holds(condition: dict, values: dict) -> bool:
265
+ for combinator, test in (("all", all), ("any", any)):
266
+ if combinator in condition:
267
+ return test(_condition_holds(item, values) for item in condition[combinator])
268
+ name = condition["attribute"]
269
+ if name not in values:
270
+ return False
271
+ if "equals" in condition:
272
+ return values[name] == condition["equals"] and type(values[name]) is type(
273
+ condition["equals"]
274
+ )
275
+ if "in" in condition:
276
+ return values[name] in condition["in"]
277
+ return condition["contains"] in values[name]
278
+
279
+
280
+ def _completed_rules(values: dict, category: dict, path: str) -> None:
281
+ """Completion-time requiredness and applicability from closed Schema rules."""
282
+
283
+ rules = category.get("rules", [])
284
+ governed = {name for rule in rules for name in rule["attributes"]}
285
+ applicable: set[str] = set()
286
+ for rule in rules:
287
+ if "when" not in rule or _condition_holds(rule["when"], values):
288
+ applicable.update(rule["attributes"])
289
+ for name in rule["required"]:
290
+ if name not in values:
291
+ raise ImageContractError(
292
+ code="attribute_required_on_completion",
293
+ message=f"rule {rule['id']} requires this attribute",
294
+ path=path + "." + name,
295
+ )
296
+ for name in sorted(governed - applicable):
297
+ if name in values:
298
+ raise ImageContractError(
299
+ code="attribute_not_applicable",
300
+ message="attribute is not applicable to the completed object",
301
+ path=path + "." + name,
302
+ )
303
+
304
+
305
+ def _description_coverage(
306
+ image_id: str, view: dict, objects: dict, described: set
307
+ ) -> None:
308
+ targets = {item.get("target") for item in view["description_fields"]}
309
+ path = f"image_annotation.completion.{image_id}.{view['id']}"
310
+ if "object" in targets:
311
+ for name, row in sorted(objects.items()):
312
+ if row["image_id"] != image_id or (
313
+ view["category_ids"] and row["category_id"] not in view["category_ids"]
314
+ ):
315
+ continue
316
+ if (view["id"], image_id, name) not in described:
317
+ raise ImageContractError(
318
+ code="description_coverage_incomplete",
319
+ message="every applicable object requires its description",
320
+ path=path + "." + name,
321
+ )
322
+ if "image" in targets and (view["id"], image_id, None) not in described:
323
+ raise ImageContractError(
324
+ code="description_coverage_incomplete",
325
+ message="the completed image requires its image description",
326
+ path=path,
327
+ )
328
+
329
+
264
330
  def validate_image_annotations(
265
331
  value: Any, *, clip_id: str, images: dict, schema: dict, view_ids: list[str]
266
332
  ) -> ImageDocument:
@@ -363,6 +429,7 @@ def validate_image_annotations(
363
429
  categories=categories,
364
430
  result_ids=set(objects) | set(descriptions),
365
431
  )
432
+ described: set[tuple] = set()
366
433
  for name, row in descriptions.items():
367
434
  path = "image_annotation.descriptions." + name
368
435
  fields(row, {"id", "image_id", "view_id", "target_id", "fields"}, path)
@@ -398,12 +465,25 @@ def validate_image_annotations(
398
465
  path + ".target_id",
399
466
  )
400
467
  values = row["fields"]
401
- definitions = {item["id"]: item for item in view["description_fields"]}
468
+ kind = "image" if target is None else "object"
469
+ definitions = {
470
+ item["id"]: item
471
+ for item in view["description_fields"]
472
+ if item.get("target", kind) == kind
473
+ }
402
474
  require(
403
475
  type(values) is dict and set(values) <= set(definitions),
404
476
  "unsupported description fields",
405
477
  path + ".fields",
406
478
  )
479
+ if any("target" in item for item in view["description_fields"]):
480
+ key = (row["view_id"], row["image_id"], target)
481
+ require(
482
+ key not in described,
483
+ "duplicate description for the same target",
484
+ path + ".target_id",
485
+ )
486
+ described.add(key)
407
487
  for key, item in values.items():
408
488
  require(
409
489
  type(item) is str and bool(item.strip()),
@@ -454,6 +534,22 @@ def validate_image_annotations(
454
534
  "completion must cover every applicable image and view",
455
535
  "image_annotation.completion",
456
536
  )
537
+ complete = {
538
+ (row["image_id"], row["view_id"])
539
+ for row in value["completion"]
540
+ if row["state"] == "complete"
541
+ }
542
+ for name, row in objects.items():
543
+ if (row["image_id"], row["view_id"]) in complete:
544
+ _completed_rules(
545
+ row["attributes"],
546
+ categories[row["category_id"]],
547
+ "image_annotation.objects." + name + ".attributes",
548
+ )
549
+ for image_id, view_id in sorted(complete):
550
+ view = views[view_id]
551
+ if view["kind"] == "descriptions":
552
+ _description_coverage(image_id, view, objects, described)
457
553
  return document(value)
458
554
 
459
555
 
@@ -128,6 +128,126 @@ class ImageSchemaRecord(ImageDocument):
128
128
  return parse_image_schema_body(self.body).task_view_by_id(view_id)
129
129
 
130
130
 
131
+ def _term(value: Any, path: str) -> None:
132
+ fields(value, {"singular"}, path, optional={"plural"})
133
+ text(value["singular"], path + ".singular")
134
+ if "plural" in value:
135
+ text(value["plural"], path + ".plural")
136
+
137
+
138
+ def _predicate(value: Any, attributes: dict, path: str) -> str:
139
+ require(type(value) is dict, "condition must be an object", path)
140
+ operators = [name for name in ("equals", "in", "contains") if name in value]
141
+ require(
142
+ len(operators) == 1 and set(value) == {"attribute", operators[0]},
143
+ "predicate requires attribute and exactly one of equals/in/contains",
144
+ path,
145
+ )
146
+ name, operator = value["attribute"], operators[0]
147
+ require(
148
+ type(name) is str and name in attributes and "object" in attributes[name]["scopes"],
149
+ "condition must reference an object attribute of this category",
150
+ path + ".attribute",
151
+ )
152
+ definition = attributes[name]
153
+ declared = {item["id"] for item in definition["values"]}
154
+ operand = value[operator]
155
+ kind = definition["type"]
156
+ if operator == "equals" and kind == "boolean":
157
+ ok = type(operand) is bool
158
+ elif operator == "equals" and kind == "enum":
159
+ ok = type(operand) is str and operand in declared
160
+ elif operator == "in" and kind == "enum":
161
+ ok = (
162
+ type(operand) is list
163
+ and len(operand) > 0
164
+ and all(type(item) is str and item in declared for item in operand)
165
+ and len(set(operand)) == len(operand)
166
+ )
167
+ elif operator == "contains" and kind == "enum_set":
168
+ ok = type(operand) is str and operand in declared
169
+ else:
170
+ ok = False
171
+ require(
172
+ ok,
173
+ "condition operand does not match the attribute type or values",
174
+ path + "." + operator,
175
+ )
176
+ return name
177
+
178
+
179
+ def _condition(value: Any, attributes: dict, path: str) -> set[str]:
180
+ require(type(value) is dict, "condition must be an object", path)
181
+ for combinator in ("all", "any"):
182
+ if combinator in value:
183
+ require(set(value) == {combinator}, "combinator must be alone", path)
184
+ items = value[combinator]
185
+ require(
186
+ type(items) is list and bool(items),
187
+ "combinator needs predicates",
188
+ path + "." + combinator,
189
+ )
190
+ return {
191
+ _predicate(item, attributes, f"{path}.{combinator}[{index}]")
192
+ for index, item in enumerate(items)
193
+ }
194
+ return {_predicate(value, attributes, path)}
195
+
196
+
197
+ def parse_attribute_rules(value: Any, attributes: dict, path: str) -> None:
198
+ """Closed conditional applicability/requiredness rules (x4d-devkit #220)."""
199
+
200
+ rules = unique_ids(value, path, nonempty=True)
201
+ depends: dict[str, set[str]] = {}
202
+ for rule_id, rule in rules.items():
203
+ target = f"{path}.{rule_id}"
204
+ fields(rule, {"id", "attributes", "required"}, target, optional={"when"})
205
+ governed = rows(rule["attributes"], target + ".attributes", nonempty=True)
206
+ require(
207
+ all(type(name) is str and name in attributes for name in governed)
208
+ and len(set(governed)) == len(governed),
209
+ "rule attributes must be unique attributes of this category",
210
+ target + ".attributes",
211
+ )
212
+ for name in governed:
213
+ definition = attributes[name]
214
+ require(
215
+ "object" in definition["scopes"] and not definition["required"],
216
+ "governed attributes must be optional object attributes",
217
+ target + ".attributes." + name,
218
+ )
219
+ required = rows(rule["required"], target + ".required")
220
+ require(
221
+ all(type(name) is str for name in required)
222
+ and len(set(required)) == len(required)
223
+ and set(required) <= set(governed),
224
+ "required must be a unique subset of attributes",
225
+ target + ".required",
226
+ )
227
+ reads = (
228
+ _condition(rule["when"], attributes, target + ".when")
229
+ if "when" in rule
230
+ else set()
231
+ )
232
+ for name in governed:
233
+ depends.setdefault(name, set()).update(reads)
234
+ visiting: set[str] = set()
235
+ done: set[str] = set()
236
+
237
+ def visit(name: str) -> None:
238
+ require(name not in visiting, "rule dependencies must be acyclic", path)
239
+ if name in done:
240
+ return
241
+ visiting.add(name)
242
+ for parent in sorted(depends.get(name, ())):
243
+ visit(parent)
244
+ visiting.discard(name)
245
+ done.add(name)
246
+
247
+ for name in sorted(depends):
248
+ visit(name)
249
+
250
+
131
251
  def parse_image_schema_body(value: Any) -> ImageSchemaBody:
132
252
  canonical(value)
133
253
  fields(
@@ -162,7 +282,7 @@ def parse_image_schema_body(value: Any) -> ImageSchemaBody:
162
282
  category,
163
283
  {"id", "display_name", "geometry_type", "color", "attributes"},
164
284
  path,
165
- optional={"groupable"},
285
+ optional={"groupable", "rules"},
166
286
  )
167
287
  if "groupable" in category:
168
288
  require(
@@ -233,8 +353,12 @@ def parse_image_schema_body(value: Any) -> ImageSchemaBody:
233
353
  target + ".values",
234
354
  )
235
355
  for item in values.values():
236
- fields(item, {"id", "display_name"}, target + ".values")
356
+ fields(item, {"id", "display_name"}, target + ".values", optional={"term"})
237
357
  text(item["display_name"], target + ".values.display_name")
358
+ if "term" in item:
359
+ _term(item["term"], target + ".values." + item["id"] + ".term")
360
+ if "rules" in category:
361
+ parse_attribute_rules(category["rules"], attributes, path + ".rules")
238
362
  views = unique_ids(value["task_views"], "schema.task_views")
239
363
  used = set()
240
364
  for name, view in views.items():
@@ -275,7 +399,16 @@ def parse_image_schema_body(value: Any) -> ImageSchemaBody:
275
399
  )
276
400
  for item in descriptions.values():
277
401
  fields(
278
- item, {"id", "display_name", "required"}, path + ".description_fields"
402
+ item,
403
+ {"id", "display_name", "required"},
404
+ path + ".description_fields",
405
+ optional={"target"},
406
+ )
407
+ require(
408
+ "target" not in item
409
+ or (type(item["target"]) is str and item["target"] in {"object", "image"}),
410
+ "description target must be object or image",
411
+ path + ".description_fields.target",
279
412
  )
280
413
  text(item["display_name"], path + ".description_fields.display_name")
281
414
  require(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.52.0rc1
3
+ Version: 0.52.0rc2
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
@@ -26,6 +26,7 @@ tests/test_detection_eval.py
26
26
  tests/test_evaluate.py
27
27
  tests/test_export_bundle.py
28
28
  tests/test_image_clip.py
29
+ tests/test_image_conditional_rules.py
29
30
  tests/test_image_enum_set.py
30
31
  tests/test_image_export.py
31
32
  tests/test_image_geometry.py
File without changes
File without changes
File without changes