x4d-devkit 0.48.0rc9__tar.gz → 0.50.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 (165) hide show
  1. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/PKG-INFO +1 -1
  2. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/pyproject.toml +1 -1
  3. x4d_devkit-0.50.0/tests/test_image_groups.py +249 -0
  4. x4d_devkit-0.50.0/tests/test_project_discovery.py +264 -0
  5. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_sdk_api.py +1 -1
  6. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/_image_contract.py +13 -3
  7. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/__init__.py +2 -0
  8. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/projects.py +23 -2
  9. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/image_clip.py +9 -0
  10. x4d_devkit-0.50.0/x4d_devkit/image_groups.py +83 -0
  11. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/image_schema.py +7 -0
  12. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/image_work_session.py +4 -0
  13. x4d_devkit-0.50.0/x4d_devkit/project_discovery.py +361 -0
  14. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit.egg-info/PKG-INFO +1 -1
  15. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit.egg-info/SOURCES.txt +4 -0
  16. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/LICENSE +0 -0
  17. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/README.md +0 -0
  18. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/setup.cfg +0 -0
  19. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_accounts.py +0 -0
  20. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_annotation_contract.py +0 -0
  21. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_annotation_reference.py +0 -0
  22. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_assets_client.py +0 -0
  23. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_boundary.py +0 -0
  24. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_box.py +0 -0
  25. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_build_info.py +0 -0
  26. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_category_audit.py +0 -0
  27. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_clean_basemap.py +0 -0
  28. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_clean_basemap_client.py +0 -0
  29. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_cli_auth.py +0 -0
  30. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_client_json_contract.py +0 -0
  31. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_client_projects.py +0 -0
  32. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_clip_archives.py +0 -0
  33. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_contract_validation.py +0 -0
  34. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_coordinate_frames.py +0 -0
  35. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_current_state.py +0 -0
  36. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_current_state_identity_consumers_example.py +0 -0
  37. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_dataset_contract.py +0 -0
  38. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_dataset_identity.py +0 -0
  39. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_detection_eval.py +0 -0
  40. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_evaluate.py +0 -0
  41. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_export_bundle.py +0 -0
  42. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_image_clip.py +0 -0
  43. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_image_export.py +0 -0
  44. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_image_geometry.py +0 -0
  45. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_image_preannotation.py +0 -0
  46. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_image_reference.py +0 -0
  47. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_image_review.py +0 -0
  48. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_image_schema_upgrade.py +0 -0
  49. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_image_work_session.py +0 -0
  50. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_inference_input.py +0 -0
  51. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_inference_schemas.py +0 -0
  52. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_inference_service.py +0 -0
  53. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_label_schemas.py +0 -0
  54. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_lidar_materialization.py +0 -0
  55. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_loader.py +0 -0
  56. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_matching.py +0 -0
  57. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_metrics.py +0 -0
  58. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_models.py +0 -0
  59. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_platform_pose.py +0 -0
  60. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_preannotation_client.py +0 -0
  61. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_reconstruction_client.py +0 -0
  62. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_release_gate.py +0 -0
  63. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_roadmark_preview.py +0 -0
  64. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_semantic_scene_authoring.py +0 -0
  65. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_semantic_scene_client.py +0 -0
  66. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_semantic_scene_contract.py +0 -0
  67. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_semantic_scene_independent.py +0 -0
  68. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_semantic_scene_native_clip.py +0 -0
  69. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_source_attributes.py +0 -0
  70. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_time_contract.py +0 -0
  71. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_token.py +0 -0
  72. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_tracking_eval.py +0 -0
  73. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_training_identity.py +0 -0
  74. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_transform.py +0 -0
  75. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_transform_chain.py +0 -0
  76. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_validation.py +0 -0
  77. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_vector_map.py +0 -0
  78. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_vector_map_geometry.py +0 -0
  79. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_vector_map_session.py +0 -0
  80. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_work_session.py +0 -0
  81. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/tests/test_work_session_validation_reuse.py +0 -0
  82. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/__init__.py +0 -0
  83. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/accounts.py +0 -0
  84. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/annotation_contract.py +0 -0
  85. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/annotation_reference.py +0 -0
  86. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/audit.py +0 -0
  87. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/build_info.py +0 -0
  88. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/clean_basemap.py +0 -0
  89. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/cli.py +0 -0
  90. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/_origin.py +0 -0
  91. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/accounts.py +0 -0
  92. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/annotation_reference.py +0 -0
  93. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/assets.py +0 -0
  94. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/bags.py +0 -0
  95. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/capabilities.py +0 -0
  96. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/checkpoints.py +0 -0
  97. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/clean_basemap.py +0 -0
  98. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/client.py +0 -0
  99. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/clip_work_sessions.py +0 -0
  100. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/clips.py +0 -0
  101. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/dataset_identity.py +0 -0
  102. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/image_clips.py +0 -0
  103. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/image_exports.py +0 -0
  104. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/image_preannotations.py +0 -0
  105. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/image_review.py +0 -0
  106. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/image_schemas.py +0 -0
  107. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/image_work_sessions.py +0 -0
  108. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/label_schemas.py +0 -0
  109. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/preannotation.py +0 -0
  110. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/reconstruction.py +0 -0
  111. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/semantic_scene.py +0 -0
  112. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  113. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/services.py +0 -0
  114. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/client/test_sets.py +0 -0
  115. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/consumer_identity.py +0 -0
  116. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/coordinate_contract.py +0 -0
  117. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/core/__init__.py +0 -0
  118. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/core/loader.py +0 -0
  119. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/core/models.py +0 -0
  120. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/core/token.py +0 -0
  121. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/core/transform.py +0 -0
  122. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/current_state.py +0 -0
  123. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/dataset_contract.py +0 -0
  124. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/dataset_identity.py +0 -0
  125. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/eval/__init__.py +0 -0
  126. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/eval/api.py +0 -0
  127. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/eval/box.py +0 -0
  128. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/eval/detection.py +0 -0
  129. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/eval/matching.py +0 -0
  130. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/eval/metrics.py +0 -0
  131. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/eval/tracking.py +0 -0
  132. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/export_bundle.py +0 -0
  133. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/image_export.py +0 -0
  134. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/image_geometry.py +0 -0
  135. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/image_preannotation.py +0 -0
  136. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/image_reference.py +0 -0
  137. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/image_review.py +0 -0
  138. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/image_schema_upgrade.py +0 -0
  139. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/inference/__init__.py +0 -0
  140. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/inference/input.py +0 -0
  141. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/inference/models.py +0 -0
  142. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/inference/schemas.py +0 -0
  143. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/inference/service.py +0 -0
  144. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/inference/tasks.py +0 -0
  145. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/json_contract.py +0 -0
  146. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/label_schema.py +0 -0
  147. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/lidar_materialization.py +0 -0
  148. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/materialize.py +0 -0
  149. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/roadmark_preview.py +0 -0
  150. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/semantic_scene_authoring.py +0 -0
  151. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/semantic_scene_contract.py +0 -0
  152. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/time_contract.py +0 -0
  153. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/training_identity.py +0 -0
  154. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/validation/__init__.py +0 -0
  155. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/validation/contract.py +0 -0
  156. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/validation/platform_pose.py +0 -0
  157. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/validation/report.py +0 -0
  158. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/validation/validator.py +0 -0
  159. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/vector_map.py +0 -0
  160. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/vector_map_geometry.py +0 -0
  161. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit/work_session.py +0 -0
  162. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  163. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
  164. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.0}/x4d_devkit.egg-info/requires.txt +0 -0
  165. {x4d_devkit-0.48.0rc9 → x4d_devkit-0.50.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.48.0rc9
3
+ Version: 0.50.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.48.0rc9"
7
+ version = "0.50.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,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)
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+
5
+ import pytest
6
+
7
+ from x4d_devkit.client.projects import ProjectsAPI
8
+ from x4d_devkit.project_discovery import (
9
+ PROJECT_DISCOVERY_VERSION,
10
+ ProjectDiscoveryError,
11
+ project_discovery_query,
12
+ validate_project_discovery_page,
13
+ validate_project_image_task,
14
+ )
15
+
16
+
17
+ def discovery_page(task_id="a_new_task", *, revision=None, page=1):
18
+ return {
19
+ "items": [
20
+ {
21
+ "id": 7,
22
+ "name": "Project",
23
+ "project_type": "2d",
24
+ "image_task": {
25
+ "status": "registered",
26
+ "id": task_id,
27
+ "revision": revision or 1,
28
+ "title": "Registered title",
29
+ },
30
+ }
31
+ ]
32
+ if page <= 2
33
+ else [],
34
+ "total": 1 if revision else 2,
35
+ "page": page,
36
+ "page_size": 1,
37
+ "discovery": {
38
+ "contract_version": PROJECT_DISCOVERY_VERSION,
39
+ "applied_filters": {
40
+ "image_task_id": task_id,
41
+ "image_task_revision": revision,
42
+ },
43
+ "facets": {
44
+ "image_task": {
45
+ "options": [
46
+ {
47
+ "id": task_id,
48
+ "title": "Registered title",
49
+ "project_count": 2,
50
+ "revisions": [
51
+ {
52
+ "revision": 1,
53
+ "title": "Registered title",
54
+ "project_count": 1,
55
+ },
56
+ {
57
+ "revision": 2,
58
+ "title": "Registered title",
59
+ "project_count": 1,
60
+ },
61
+ ],
62
+ }
63
+ ],
64
+ "unresolved_project_count": 1,
65
+ }
66
+ },
67
+ },
68
+ }
69
+
70
+
71
+ class Client:
72
+ def __init__(self, value):
73
+ self.value, self.calls = value, []
74
+
75
+ def get(self, path, params=None):
76
+ self.calls.append((path, params))
77
+ return deepcopy(self.value)
78
+
79
+
80
+ def test_new_task_needs_no_business_enum_and_retains_old_call_shape():
81
+ client = Client(discovery_page("new_after_sdk_release"))
82
+ result = ProjectsAPI(client).list(
83
+ 1,
84
+ 1,
85
+ None,
86
+ "2d",
87
+ image_task_id="new_after_sdk_release",
88
+ include_facets=["image_task", "image_task"],
89
+ )
90
+ assert result["items"][0]["image_task"]["id"] == "new_after_sdk_release"
91
+ assert client.calls == [
92
+ (
93
+ "/projects",
94
+ {
95
+ "page": 1,
96
+ "page_size": 1,
97
+ "project_type": "2d",
98
+ "image_task_id": "new_after_sdk_release",
99
+ "include_facets": ["image_task"],
100
+ },
101
+ )
102
+ ]
103
+ old = {"items": [], "total": 0, "page": 1, "page_size": 50}
104
+ client = Client(old)
105
+ assert ProjectsAPI(client).list() == old
106
+ assert client.calls == [("/projects", {"page": 1, "page_size": 50})]
107
+
108
+
109
+ @pytest.mark.parametrize(
110
+ "kwargs",
111
+ [
112
+ {"image_task_id": "task"},
113
+ {"project_type": "3d", "include_facets": ["image_task"]},
114
+ {"project_type": "2d", "image_task_revision": 1},
115
+ {"project_type": "2d", "image_task_id": " task"},
116
+ {"project_type": "2d", "image_task_id": ""},
117
+ {"project_type": "2d", "image_task_id": "x" * 1025},
118
+ {"project_type": "2d", "image_task_id": "task", "image_task_revision": True},
119
+ {"project_type": "2d", "image_task_id": "task", "image_task_revision": 0},
120
+ {"project_type": "2d", "include_facets": "image_task"},
121
+ {"project_type": "2d", "include_facets": [""]},
122
+ {"project_type": "2d", "include_facets": ["private_json"]},
123
+ ],
124
+ )
125
+ def test_bad_query_is_rejected_before_transport(kwargs):
126
+ client = Client({})
127
+ with pytest.raises(ProjectDiscoveryError) as exc:
128
+ ProjectsAPI(client).list(**kwargs)
129
+ assert exc.value.code == "invalid_project_discovery_query"
130
+ assert client.calls == []
131
+
132
+
133
+ def test_exact_revision_and_out_of_range_pages():
134
+ assert (
135
+ ProjectsAPI(Client(discovery_page(revision=2))).list(
136
+ page_size=1,
137
+ project_type="2d",
138
+ image_task_id="a_new_task",
139
+ image_task_revision=2,
140
+ )["total"]
141
+ == 1
142
+ )
143
+ assert (
144
+ ProjectsAPI(Client(discovery_page(page=3))).list(
145
+ page=3, page_size=1, project_type="2d", image_task_id="a_new_task"
146
+ )["items"]
147
+ == []
148
+ )
149
+
150
+
151
+ def test_facet_scope_ignores_current_task_filter_and_counts_unresolved():
152
+ value = discovery_page()
153
+ value["discovery"]["applied_filters"]["image_task_id"] = None
154
+ value["total"], value["page_size"] = 3, 3
155
+ value["items"].extend(
156
+ [
157
+ {
158
+ "id": 8,
159
+ "name": "Another",
160
+ "project_type": "2d",
161
+ "image_task": {
162
+ "status": "registered",
163
+ "id": "a_new_task",
164
+ "revision": 2,
165
+ "title": "Registered title",
166
+ },
167
+ },
168
+ {
169
+ "id": 9,
170
+ "name": "Missing",
171
+ "project_type": "2d",
172
+ "image_task": {
173
+ "status": "missing",
174
+ "id": None,
175
+ "revision": None,
176
+ "title": None,
177
+ },
178
+ },
179
+ ]
180
+ )
181
+ assert (
182
+ ProjectsAPI(Client(value)).list(
183
+ page_size=3, project_type="2d", include_facets=["image_task"]
184
+ )["total"]
185
+ == 3
186
+ )
187
+ value["items"], value["total"] = [], 0
188
+ value["discovery"]["applied_filters"]["image_task_id"] = "not_registered"
189
+ assert (
190
+ ProjectsAPI(Client(value)).list(
191
+ page_size=3,
192
+ project_type="2d",
193
+ image_task_id="not_registered",
194
+ include_facets=["image_task"],
195
+ )["items"]
196
+ == []
197
+ )
198
+
199
+
200
+ @pytest.mark.parametrize(
201
+ "mutation",
202
+ [
203
+ lambda value: value.pop("discovery"),
204
+ lambda value: value["discovery"].update(contract_version="unknown"),
205
+ lambda value: value["discovery"]["applied_filters"].update(image_task_id=None),
206
+ lambda value: value["discovery"]["applied_filters"].update(
207
+ image_task_revision=True
208
+ ),
209
+ lambda value: value["discovery"].update(facets={}),
210
+ lambda value: value.update(page=2),
211
+ lambda value: value.update(total=True),
212
+ lambda value: value.update(total=3),
213
+ lambda value: value.update(items=[]),
214
+ lambda value: value["items"][0].update(project_type="3d"),
215
+ lambda value: value["items"][0]["image_task"].update(id="other"),
216
+ lambda value: value["items"][0]["image_task"].update(revision=4),
217
+ lambda value: value["items"][0]["image_task"].update(title="invented"),
218
+ lambda value: value["discovery"]["facets"]["image_task"]["options"][0].update(
219
+ project_count=3
220
+ ),
221
+ lambda value: value["discovery"]["facets"]["image_task"]["options"][0][
222
+ "revisions"
223
+ ].reverse(),
224
+ lambda value: value["discovery"]["facets"]["image_task"].update(
225
+ unresolved_project_count=-1
226
+ ),
227
+ ],
228
+ )
229
+ def test_old_ignored_filters_and_invalid_response_never_escape_to_ui(mutation):
230
+ value = discovery_page()
231
+ mutation(value)
232
+ with pytest.raises(ProjectDiscoveryError):
233
+ ProjectsAPI(Client(value)).list(
234
+ page_size=1,
235
+ project_type="2d",
236
+ image_task_id="a_new_task",
237
+ include_facets=["image_task"],
238
+ )
239
+
240
+
241
+ @pytest.mark.parametrize("status", ["registered", "missing", "invalid", "unavailable"])
242
+ def test_binding_statuses_do_not_fill_defaults(status):
243
+ value = {"status": status, "id": None, "revision": None, "title": None}
244
+ if status in {"registered", "unavailable"}:
245
+ value.update(id="task", revision=1)
246
+ if status == "registered":
247
+ value["title"] = "Title"
248
+ assert validate_project_image_task(value) is value
249
+ broken = {**value, "revision": False}
250
+ with pytest.raises(ProjectDiscoveryError):
251
+ validate_project_image_task(broken)
252
+
253
+
254
+ def test_optional_response_extensions_do_not_force_new_clients():
255
+ value = discovery_page()
256
+ value["discovery"]["future_optional"] = {"anything": True}
257
+ value["items"][0]["future_optional"] = "retained"
258
+ query = project_discovery_query(
259
+ project_type="2d", image_task_id="a_new_task", include_facets=["image_task"]
260
+ )
261
+ assert (
262
+ validate_project_discovery_page(value, query=query, page=1, page_size=1)
263
+ is value
264
+ )
@@ -11,7 +11,7 @@ from x4d_devkit.eval.matching import MetricData
11
11
 
12
12
 
13
13
  def test_package_version_is_current_release():
14
- assert x4d_devkit.__version__ == "0.48.0rc9"
14
+ assert x4d_devkit.__version__ == "0.50.0"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -26,10 +26,20 @@ def require(condition: bool, message: str, path: str) -> None:
26
26
  )
27
27
 
28
28
 
29
- def fields(value: Any, expected: set[str], path: str) -> None:
29
+ def fields(
30
+ value: Any,
31
+ expected: set[str],
32
+ path: str,
33
+ *,
34
+ optional: frozenset[str] | set[str] = frozenset(),
35
+ ) -> None:
30
36
  require(
31
- type(value) is dict and set(value) == expected,
32
- f"expected exactly {sorted(expected)}",
37
+ type(value) is dict and expected <= set(value) <= expected | optional,
38
+ (
39
+ f"expected exactly {sorted(expected)}"
40
+ if not optional
41
+ else f"expected required {sorted(expected)} and only optional {sorted(optional)}"
42
+ ),
33
43
  path,
34
44
  )
35
45
 
@@ -30,6 +30,7 @@ from x4d_devkit.client.semantic_scene_authoring import (
30
30
  SemanticSceneAuthoringWorkSessionsAPI,
31
31
  )
32
32
  from x4d_devkit.client.projects import ProjectResolutionError, ProjectsAPI
33
+ from x4d_devkit.project_discovery import ProjectDiscoveryError
33
34
  from x4d_devkit.client.reconstruction import (
34
35
  ReconstructionAPI,
35
36
  StaticRgbMapGeneration,
@@ -67,6 +68,7 @@ __all__ = [
67
68
  "PlatformAsset",
68
69
  "PlatformAssetReference",
69
70
  "ProjectResolutionError",
71
+ "ProjectDiscoveryError",
70
72
  "ProjectsAPI",
71
73
  "ReconstructionAPI",
72
74
  "RESOLVED_LABEL_SCHEMA_CONTEXT_VERSION",
@@ -2,8 +2,14 @@
2
2
  from __future__ import annotations
3
3
 
4
4
  from dataclasses import dataclass
5
+ from collections.abc import Sequence
5
6
  from typing import TYPE_CHECKING
6
7
 
8
+ from x4d_devkit.project_discovery import (
9
+ project_discovery_query,
10
+ validate_project_discovery_page,
11
+ )
12
+
7
13
  if TYPE_CHECKING:
8
14
  from x4d_devkit.client.client import X4DClient
9
15
 
@@ -27,8 +33,14 @@ class ProjectsAPI:
27
33
  def list(
28
34
  self, page: int = 1, page_size: int = 50, search: str | None = None,
29
35
  project_type: str | None = None,
36
+ *, image_task_id: str | None = None, image_task_revision: int | None = None,
37
+ include_facets: Sequence[str] | None = None,
30
38
  ) -> dict:
31
- """List all projects."""
39
+ """List accessible Projects, optionally discovering registered image tasks."""
40
+ query = project_discovery_query(
41
+ project_type=project_type, image_task_id=image_task_id,
42
+ image_task_revision=image_task_revision, include_facets=include_facets,
43
+ )
32
44
  params = {"page": page, "page_size": page_size}
33
45
  if search:
34
46
  params["search"] = search
@@ -36,7 +48,16 @@ class ProjectsAPI:
36
48
  if project_type not in {"2d", "3d"}:
37
49
  raise ValueError("project_type must be 2d or 3d")
38
50
  params["project_type"] = project_type
39
- return self._client.get("/projects", params=params)
51
+ if query.image_task_id is not None:
52
+ params["image_task_id"] = query.image_task_id
53
+ if query.image_task_revision is not None:
54
+ params["image_task_revision"] = query.image_task_revision
55
+ if query.include_facets:
56
+ params["include_facets"] = list(query.include_facets)
57
+ value = self._client.get("/projects", params=params)
58
+ if query.enabled:
59
+ validate_project_discovery_page(value, query=query, page=page, page_size=page_size)
60
+ return value
40
61
 
41
62
  def get(self, project_id: int) -> dict:
42
63
  """Get a project by ID."""
@@ -25,6 +25,7 @@ from x4d_devkit._image_contract import (
25
25
  unique_ids,
26
26
  )
27
27
  from x4d_devkit.image_geometry import parse_image_attribute_ranges, parse_image_geometry
28
+ from x4d_devkit.image_groups import validate_image_object_groups
28
29
  from x4d_devkit.image_schema import parse_image_schema_snapshot
29
30
 
30
31
 
@@ -243,6 +244,7 @@ def validate_image_annotations(
243
244
  value,
244
245
  {"contract_version", "clip_id", "objects", "descriptions", "completion"},
245
246
  "image_annotation",
247
+ optional={"object_groups"},
246
248
  )
247
249
  require(
248
250
  value["contract_version"] == IMAGE_ANNOTATION_CONTRACT_VERSION
@@ -328,6 +330,13 @@ def validate_image_annotations(
328
330
  path + ".attribute_ranges",
329
331
  )
330
332
  occupied.add((row["image_id"], row["view_id"]))
333
+ if "object_groups" in value:
334
+ validate_image_object_groups(
335
+ value["object_groups"],
336
+ objects=objects,
337
+ categories=categories,
338
+ result_ids=set(objects) | set(descriptions),
339
+ )
331
340
  for name, row in descriptions.items():
332
341
  path = "image_annotation.descriptions." + name
333
342
  fields(row, {"id", "image_id", "view_id", "target_id", "fields"}, path)