x4d-devkit 0.48.0rc8__tar.gz → 0.49.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 (163) hide show
  1. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/PKG-INFO +1 -1
  2. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/pyproject.toml +1 -1
  3. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_image_clip.py +34 -0
  4. x4d_devkit-0.49.0/tests/test_project_discovery.py +264 -0
  5. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_sdk_api.py +1 -1
  6. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/__init__.py +2 -0
  7. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/image_clips.py +26 -11
  8. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/projects.py +23 -2
  9. x4d_devkit-0.49.0/x4d_devkit/project_discovery.py +361 -0
  10. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit.egg-info/PKG-INFO +1 -1
  11. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit.egg-info/SOURCES.txt +2 -0
  12. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/LICENSE +0 -0
  13. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/README.md +0 -0
  14. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/setup.cfg +0 -0
  15. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_accounts.py +0 -0
  16. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_annotation_contract.py +0 -0
  17. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_annotation_reference.py +0 -0
  18. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_assets_client.py +0 -0
  19. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_boundary.py +0 -0
  20. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_box.py +0 -0
  21. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_build_info.py +0 -0
  22. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_category_audit.py +0 -0
  23. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_clean_basemap.py +0 -0
  24. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_clean_basemap_client.py +0 -0
  25. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_cli_auth.py +0 -0
  26. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_client_json_contract.py +0 -0
  27. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_client_projects.py +0 -0
  28. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_clip_archives.py +0 -0
  29. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_contract_validation.py +0 -0
  30. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_coordinate_frames.py +0 -0
  31. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_current_state.py +0 -0
  32. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_current_state_identity_consumers_example.py +0 -0
  33. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_dataset_contract.py +0 -0
  34. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_dataset_identity.py +0 -0
  35. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_detection_eval.py +0 -0
  36. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_evaluate.py +0 -0
  37. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_export_bundle.py +0 -0
  38. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_image_export.py +0 -0
  39. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_image_geometry.py +0 -0
  40. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_image_preannotation.py +0 -0
  41. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_image_reference.py +0 -0
  42. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_image_review.py +0 -0
  43. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_image_schema_upgrade.py +0 -0
  44. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_image_work_session.py +0 -0
  45. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_inference_input.py +0 -0
  46. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_inference_schemas.py +0 -0
  47. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_inference_service.py +0 -0
  48. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_label_schemas.py +0 -0
  49. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_lidar_materialization.py +0 -0
  50. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_loader.py +0 -0
  51. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_matching.py +0 -0
  52. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_metrics.py +0 -0
  53. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_models.py +0 -0
  54. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_platform_pose.py +0 -0
  55. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_preannotation_client.py +0 -0
  56. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_reconstruction_client.py +0 -0
  57. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_release_gate.py +0 -0
  58. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_roadmark_preview.py +0 -0
  59. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_semantic_scene_authoring.py +0 -0
  60. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_semantic_scene_client.py +0 -0
  61. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_semantic_scene_contract.py +0 -0
  62. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_semantic_scene_independent.py +0 -0
  63. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_semantic_scene_native_clip.py +0 -0
  64. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_source_attributes.py +0 -0
  65. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_time_contract.py +0 -0
  66. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_token.py +0 -0
  67. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_tracking_eval.py +0 -0
  68. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_training_identity.py +0 -0
  69. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_transform.py +0 -0
  70. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_transform_chain.py +0 -0
  71. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_validation.py +0 -0
  72. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_vector_map.py +0 -0
  73. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_vector_map_geometry.py +0 -0
  74. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_vector_map_session.py +0 -0
  75. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_work_session.py +0 -0
  76. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/tests/test_work_session_validation_reuse.py +0 -0
  77. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/__init__.py +0 -0
  78. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/_image_contract.py +0 -0
  79. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/accounts.py +0 -0
  80. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/annotation_contract.py +0 -0
  81. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/annotation_reference.py +0 -0
  82. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/audit.py +0 -0
  83. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/build_info.py +0 -0
  84. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/clean_basemap.py +0 -0
  85. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/cli.py +0 -0
  86. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/_origin.py +0 -0
  87. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/accounts.py +0 -0
  88. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/annotation_reference.py +0 -0
  89. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/assets.py +0 -0
  90. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/bags.py +0 -0
  91. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/capabilities.py +0 -0
  92. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/checkpoints.py +0 -0
  93. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/clean_basemap.py +0 -0
  94. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/client.py +0 -0
  95. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/clip_work_sessions.py +0 -0
  96. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/clips.py +0 -0
  97. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/dataset_identity.py +0 -0
  98. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/image_exports.py +0 -0
  99. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/image_preannotations.py +0 -0
  100. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/image_review.py +0 -0
  101. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/image_schemas.py +0 -0
  102. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/image_work_sessions.py +0 -0
  103. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/label_schemas.py +0 -0
  104. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/preannotation.py +0 -0
  105. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/reconstruction.py +0 -0
  106. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/semantic_scene.py +0 -0
  107. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  108. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/services.py +0 -0
  109. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/client/test_sets.py +0 -0
  110. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/consumer_identity.py +0 -0
  111. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/coordinate_contract.py +0 -0
  112. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/core/__init__.py +0 -0
  113. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/core/loader.py +0 -0
  114. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/core/models.py +0 -0
  115. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/core/token.py +0 -0
  116. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/core/transform.py +0 -0
  117. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/current_state.py +0 -0
  118. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/dataset_contract.py +0 -0
  119. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/dataset_identity.py +0 -0
  120. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/eval/__init__.py +0 -0
  121. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/eval/api.py +0 -0
  122. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/eval/box.py +0 -0
  123. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/eval/detection.py +0 -0
  124. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/eval/matching.py +0 -0
  125. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/eval/metrics.py +0 -0
  126. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/eval/tracking.py +0 -0
  127. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/export_bundle.py +0 -0
  128. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/image_clip.py +0 -0
  129. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/image_export.py +0 -0
  130. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/image_geometry.py +0 -0
  131. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/image_preannotation.py +0 -0
  132. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/image_reference.py +0 -0
  133. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/image_review.py +0 -0
  134. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/image_schema.py +0 -0
  135. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/image_schema_upgrade.py +0 -0
  136. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/image_work_session.py +0 -0
  137. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/inference/__init__.py +0 -0
  138. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/inference/input.py +0 -0
  139. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/inference/models.py +0 -0
  140. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/inference/schemas.py +0 -0
  141. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/inference/service.py +0 -0
  142. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/inference/tasks.py +0 -0
  143. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/json_contract.py +0 -0
  144. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/label_schema.py +0 -0
  145. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/lidar_materialization.py +0 -0
  146. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/materialize.py +0 -0
  147. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/roadmark_preview.py +0 -0
  148. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/semantic_scene_authoring.py +0 -0
  149. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/semantic_scene_contract.py +0 -0
  150. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/time_contract.py +0 -0
  151. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/training_identity.py +0 -0
  152. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/validation/__init__.py +0 -0
  153. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/validation/contract.py +0 -0
  154. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/validation/platform_pose.py +0 -0
  155. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/validation/report.py +0 -0
  156. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/validation/validator.py +0 -0
  157. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/vector_map.py +0 -0
  158. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/vector_map_geometry.py +0 -0
  159. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit/work_session.py +0 -0
  160. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  161. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
  162. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.0}/x4d_devkit.egg-info/requires.txt +0 -0
  163. {x4d_devkit-0.48.0rc8 → x4d_devkit-0.49.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.0rc8
3
+ Version: 0.49.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.0rc8"
7
+ version = "0.49.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"
@@ -308,6 +308,40 @@ def test_typed_client_validates_metadata_then_reads_one_authorized_verified_imag
308
308
  assert len(requests) == 3
309
309
 
310
310
 
311
+ @pytest.mark.parametrize("failure", [None, "source", "descriptor", "origin", "contract"])
312
+ def test_reference_only_read_validates_context_without_requesting_media(failure):
313
+ value, requests = clip_payload(), []
314
+
315
+ def handler(request):
316
+ requests.append(request)
317
+ assert request.url.path.endswith("/images/image_a")
318
+ result = {
319
+ "contract_version": "x4d-image-asset-reference-v1",
320
+ "clip_id": "clip_test", "image_id": "image_a",
321
+ "source_revision": value["identity"]["source_revision"],
322
+ "asset": dict(value["sample_data"][0]["asset"]),
323
+ "uri": "/api/v1/files/clips/clip_test/samples/cam_a/10000000020.jpg",
324
+ }
325
+ if failure == "source":
326
+ result["source_revision"] = "sha256:" + "f" * 64
327
+ elif failure == "descriptor":
328
+ result["asset"]["size"] += 1
329
+ elif failure == "origin":
330
+ result["uri"] = "https://untrusted.example/api/v1/files/private"
331
+ elif failure == "contract":
332
+ result["contract_version"] = "other"
333
+ return httpx.Response(200, json=result)
334
+
335
+ with client_for(handler) as client:
336
+ clip = parse_image_clip(value)
337
+ if failure:
338
+ with pytest.raises(ValueError):
339
+ client.image_clips.get_asset(clip, "image_a")
340
+ else:
341
+ assert client.image_clips.get_asset(clip, "image_a")["asset"] == clip.images[0]["asset"]
342
+ assert len(requests) == 1
343
+
344
+
311
345
  @pytest.mark.parametrize(
312
346
  "failure",
313
347
  [
@@ -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.0rc8"
14
+ assert x4d_devkit.__version__ == "0.49.0"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -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",
@@ -7,6 +7,7 @@ from urllib.parse import quote
7
7
 
8
8
  from x4d_devkit._image_contract import fields, identifier, integer, require
9
9
  from x4d_devkit.client.assets import PlatformAsset
10
+ from x4d_devkit.client.assets import _validated_file_uri
10
11
  from x4d_devkit.image_clip import ImageClip, parse_image_clip
11
12
 
12
13
 
@@ -47,22 +48,15 @@ class ImageClipsAPI:
47
48
  value = self._client.get(f"/image-clips/{quote(clip_id, safe='')}")
48
49
  return parse_image_clip(value, expected_clip_id=clip_id)
49
50
 
50
- def read_image(
51
- self, clip: ImageClip, image_id: str, *, max_bytes: int = 32 * 1024 * 1024
52
- ) -> PlatformAsset:
51
+ def get_asset(self, clip: ImageClip, image_id: str) -> dict:
52
+ """Validate an authorized reference without downloading original bytes."""
53
53
  require(
54
54
  type(clip) is ImageClip, "expected a validated native image Clip", "clip"
55
55
  )
56
56
  identifier(image_id, "image_id")
57
- integer(max_bytes, "max_bytes", minimum=1)
58
57
  image = next((item for item in clip.images if item["id"] == image_id), None)
59
58
  require(image is not None, "image does not belong to this Clip", "image_id")
60
59
  expected = image["asset"]
61
- require(
62
- expected["size"] <= max_bytes,
63
- "image exceeds the configured read budget",
64
- "image.asset.size",
65
- )
66
60
  value = self._client.get(
67
61
  f"/image-clips/{quote(clip.clip_id, safe='')}/images/{quote(image_id, safe='')}"
68
62
  )
@@ -82,12 +76,33 @@ class ImageClipsAPI:
82
76
  value["contract_version"] == IMAGE_ASSET_REFERENCE_VERSION
83
77
  and value["clip_id"] == clip.clip_id
84
78
  and value["image_id"] == image_id
85
- and value["source_revision"]
86
- == clip.to_dict()["identity"]["source_revision"]
87
79
  and value["asset"] == expected,
88
80
  "image asset context changed",
89
81
  "image_asset",
90
82
  )
83
+ require(
84
+ value["source_revision"] == clip.to_dict()["identity"]["source_revision"],
85
+ "image source revision changed",
86
+ "image_asset.source_revision",
87
+ )
88
+ _validated_file_uri(value["uri"], api_url=self._client.api_url)
89
+ return value
90
+
91
+ def read_image(
92
+ self, clip: ImageClip, image_id: str, *, max_bytes: int = 32 * 1024 * 1024
93
+ ) -> PlatformAsset:
94
+ require(type(clip) is ImageClip, "expected a validated native image Clip", "clip")
95
+ identifier(image_id, "image_id")
96
+ integer(max_bytes, "max_bytes", minimum=1)
97
+ image = next((item for item in clip.images if item["id"] == image_id), None)
98
+ require(image is not None, "image does not belong to this Clip", "image_id")
99
+ expected = image["asset"]
100
+ require(
101
+ expected["size"] <= max_bytes,
102
+ "image exceeds the configured read budget",
103
+ "image.asset.size",
104
+ )
105
+ value = self.get_asset(clip, image_id)
91
106
  asset = self._client.assets.fetch(value["uri"], max_bytes=expected["size"])
92
107
  require(
93
108
  len(asset.content) == expected["size"]
@@ -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."""