x4d-devkit 0.48.0rc4__tar.gz → 0.48.0rc5__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 (155) hide show
  1. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/PKG-INFO +1 -1
  2. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/pyproject.toml +1 -1
  3. x4d_devkit-0.48.0rc5/tests/test_image_export.py +389 -0
  4. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_sdk_api.py +1 -1
  5. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/cli.py +17 -0
  6. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/__init__.py +2 -0
  7. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/client.py +2 -0
  8. x4d_devkit-0.48.0rc5/x4d_devkit/client/image_exports.py +141 -0
  9. x4d_devkit-0.48.0rc5/x4d_devkit/image_export.py +606 -0
  10. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/PKG-INFO +1 -1
  11. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/SOURCES.txt +3 -0
  12. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/LICENSE +0 -0
  13. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/README.md +0 -0
  14. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/setup.cfg +0 -0
  15. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_annotation_contract.py +0 -0
  16. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_annotation_reference.py +0 -0
  17. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_assets_client.py +0 -0
  18. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_boundary.py +0 -0
  19. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_box.py +0 -0
  20. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_build_info.py +0 -0
  21. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_category_audit.py +0 -0
  22. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_clean_basemap.py +0 -0
  23. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_clean_basemap_client.py +0 -0
  24. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_cli_auth.py +0 -0
  25. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_client_json_contract.py +0 -0
  26. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_client_projects.py +0 -0
  27. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_clip_archives.py +0 -0
  28. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_contract_validation.py +0 -0
  29. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_coordinate_frames.py +0 -0
  30. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_current_state.py +0 -0
  31. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_current_state_identity_consumers_example.py +0 -0
  32. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_dataset_contract.py +0 -0
  33. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_dataset_identity.py +0 -0
  34. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_detection_eval.py +0 -0
  35. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_evaluate.py +0 -0
  36. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_export_bundle.py +0 -0
  37. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_clip.py +0 -0
  38. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_geometry.py +0 -0
  39. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_preannotation.py +0 -0
  40. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_reference.py +0 -0
  41. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_review.py +0 -0
  42. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_image_work_session.py +0 -0
  43. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_inference_input.py +0 -0
  44. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_inference_schemas.py +0 -0
  45. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_inference_service.py +0 -0
  46. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_label_schemas.py +0 -0
  47. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_lidar_materialization.py +0 -0
  48. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_loader.py +0 -0
  49. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_matching.py +0 -0
  50. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_metrics.py +0 -0
  51. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_models.py +0 -0
  52. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_platform_pose.py +0 -0
  53. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_preannotation_client.py +0 -0
  54. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_reconstruction_client.py +0 -0
  55. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_release_gate.py +0 -0
  56. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_roadmark_preview.py +0 -0
  57. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_authoring.py +0 -0
  58. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_client.py +0 -0
  59. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_contract.py +0 -0
  60. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_independent.py +0 -0
  61. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_native_clip.py +0 -0
  62. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_source_attributes.py +0 -0
  63. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_time_contract.py +0 -0
  64. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_token.py +0 -0
  65. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_tracking_eval.py +0 -0
  66. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_training_identity.py +0 -0
  67. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_transform.py +0 -0
  68. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_transform_chain.py +0 -0
  69. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_validation.py +0 -0
  70. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_vector_map.py +0 -0
  71. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_vector_map_geometry.py +0 -0
  72. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_vector_map_session.py +0 -0
  73. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_work_session.py +0 -0
  74. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/tests/test_work_session_validation_reuse.py +0 -0
  75. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/__init__.py +0 -0
  76. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/_image_contract.py +0 -0
  77. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/annotation_contract.py +0 -0
  78. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/annotation_reference.py +0 -0
  79. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/audit.py +0 -0
  80. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/build_info.py +0 -0
  81. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/clean_basemap.py +0 -0
  82. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/_origin.py +0 -0
  83. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/annotation_reference.py +0 -0
  84. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/assets.py +0 -0
  85. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/bags.py +0 -0
  86. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/capabilities.py +0 -0
  87. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/checkpoints.py +0 -0
  88. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clean_basemap.py +0 -0
  89. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clip_work_sessions.py +0 -0
  90. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clips.py +0 -0
  91. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/dataset_identity.py +0 -0
  92. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_clips.py +0 -0
  93. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_preannotations.py +0 -0
  94. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_review.py +0 -0
  95. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_work_sessions.py +0 -0
  96. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/label_schemas.py +0 -0
  97. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/preannotation.py +0 -0
  98. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/projects.py +0 -0
  99. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/reconstruction.py +0 -0
  100. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/semantic_scene.py +0 -0
  101. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  102. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/services.py +0 -0
  103. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/test_sets.py +0 -0
  104. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/consumer_identity.py +0 -0
  105. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/coordinate_contract.py +0 -0
  106. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/__init__.py +0 -0
  107. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/loader.py +0 -0
  108. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/models.py +0 -0
  109. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/token.py +0 -0
  110. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/transform.py +0 -0
  111. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/current_state.py +0 -0
  112. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/dataset_contract.py +0 -0
  113. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/dataset_identity.py +0 -0
  114. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/__init__.py +0 -0
  115. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/api.py +0 -0
  116. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/box.py +0 -0
  117. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/detection.py +0 -0
  118. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/matching.py +0 -0
  119. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/metrics.py +0 -0
  120. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/tracking.py +0 -0
  121. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/export_bundle.py +0 -0
  122. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_clip.py +0 -0
  123. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_geometry.py +0 -0
  124. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_preannotation.py +0 -0
  125. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_reference.py +0 -0
  126. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_review.py +0 -0
  127. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_schema.py +0 -0
  128. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_work_session.py +0 -0
  129. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/__init__.py +0 -0
  130. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/input.py +0 -0
  131. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/models.py +0 -0
  132. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/schemas.py +0 -0
  133. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/service.py +0 -0
  134. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/tasks.py +0 -0
  135. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/json_contract.py +0 -0
  136. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/label_schema.py +0 -0
  137. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/lidar_materialization.py +0 -0
  138. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/materialize.py +0 -0
  139. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/roadmark_preview.py +0 -0
  140. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/semantic_scene_authoring.py +0 -0
  141. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/semantic_scene_contract.py +0 -0
  142. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/time_contract.py +0 -0
  143. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/training_identity.py +0 -0
  144. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/__init__.py +0 -0
  145. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/contract.py +0 -0
  146. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/platform_pose.py +0 -0
  147. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/report.py +0 -0
  148. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/validator.py +0 -0
  149. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/vector_map.py +0 -0
  150. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/vector_map_geometry.py +0 -0
  151. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit/work_session.py +0 -0
  152. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  153. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/entry_points.txt +0 -0
  154. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/requires.txt +0 -0
  155. {x4d_devkit-0.48.0rc4 → x4d_devkit-0.48.0rc5}/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.0rc4
3
+ Version: 0.48.0rc5
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.0rc4"
7
+ version = "0.48.0rc5"
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,389 @@
1
+ """Synthetic offline export and typed-client tests; no platform or private media."""
2
+
3
+ from copy import deepcopy
4
+ from io import BytesIO
5
+ import json
6
+ import tarfile
7
+
8
+ import httpx
9
+ import pytest
10
+
11
+ from tests.image_fixtures import IMAGE_BYTES, clip_payload, line_object
12
+ from x4d_devkit import X4DClient
13
+ from x4d_devkit._image_contract import ImageContractError, canonical
14
+ from x4d_devkit.image_clip import build_image_identity
15
+ from x4d_devkit.image_export import (
16
+ COMPONENTS,
17
+ DOWNLOAD_CONTRACT,
18
+ EXPORT_CONTRACT,
19
+ EXPORT_README,
20
+ INDEX_CONTRACT,
21
+ JOB_CONTRACT,
22
+ PAGE_CONTRACT,
23
+ PREVIEW_CONTRACT,
24
+ clip_manifest_entry,
25
+ human_scope_index,
26
+ parse_image_export_job,
27
+ reference_path,
28
+ sha256,
29
+ validate_image_export_bundle,
30
+ )
31
+
32
+ EXPORT_ID = "imexp_" + "a" * 32
33
+ NOW = "2026-10-26T12:00:00+00:00"
34
+
35
+
36
+ def contents(*, references=False, state="unprocessed"):
37
+ clip = clip_payload()
38
+ if state != "confirmed_empty":
39
+ clip["image_annotation"]["objects"] = [line_object()]
40
+ clip["image_annotation"]["completion"][0]["state"] = state
41
+ clip["identity"] = build_image_identity(clip)
42
+ cid = clip["meta"]["clip_id"]
43
+ files = {f"clips/{cid}/{name}.json": canonical(clip[name]) for name in COMPONENTS}
44
+ files[f"consumption/{cid}/human_scopes.json"] = canonical(human_scope_index(clip))
45
+ if references:
46
+ for view in clip["meta"]["view_ids"]:
47
+ files[reference_path(cid, view)] = canonical(
48
+ {
49
+ "contract_version": "x4d-image-preannotation-current-v1",
50
+ "project_id": clip["meta"]["project_id"],
51
+ "clip_id": cid,
52
+ "view_id": view,
53
+ "source_revision": clip["identity"]["source_revision"],
54
+ "schema_fingerprint": clip["identity"]["schema_fingerprint"],
55
+ "availability": "none",
56
+ "published_at": None,
57
+ "result_digest": None,
58
+ "artifact": None,
59
+ }
60
+ )
61
+ for image in clip["sample_data"]:
62
+ files[f"clips/{cid}/{image['asset']['path']}"] = IMAGE_BYTES
63
+ return clip, files
64
+
65
+
66
+ def bundle(tmp_path, clip, files, *, references=False, append=None, override=None):
67
+ cid = clip["meta"]["clip_id"]
68
+ index = canonical(
69
+ {
70
+ "contract_version": INDEX_CONTRACT,
71
+ "clip_id": cid,
72
+ "files": [
73
+ {"path": name, "size": len(raw), "sha256": sha256(raw)}
74
+ for name, raw in files.items()
75
+ ],
76
+ }
77
+ )
78
+ manifest = {
79
+ "contract_version": EXPORT_CONTRACT,
80
+ "export_id": EXPORT_ID,
81
+ "project_id": clip["meta"]["project_id"],
82
+ "created_at": NOW,
83
+ "include_preannotations": references,
84
+ "clips": [
85
+ clip_manifest_entry(clip, captured_at=NOW, file_index_sha256=sha256(index))
86
+ ],
87
+ }
88
+ entries = {
89
+ "manifest.json": canonical(manifest),
90
+ "README.md": EXPORT_README,
91
+ f"indexes/{cid}.json": index,
92
+ **files,
93
+ }
94
+ path = tmp_path / "export.tar"
95
+ with tarfile.open(path, "w") as archive:
96
+ for name, raw in entries.items():
97
+ info = tarfile.TarInfo(f"{EXPORT_ID}/{name}")
98
+ if override:
99
+ raw = override(name, raw)
100
+ info.size = len(raw)
101
+ archive.addfile(info, BytesIO(raw))
102
+ if append:
103
+ info = tarfile.TarInfo(append)
104
+ info.size = 1
105
+ archive.addfile(info, BytesIO(b"x"))
106
+ return path
107
+
108
+
109
+ @pytest.mark.parametrize("state", ["unprocessed", "complete", "confirmed_empty"])
110
+ @pytest.mark.parametrize("references", [False, True])
111
+ def test_roundtrip_preserves_native_identity_and_explicit_completion(
112
+ tmp_path, state, references
113
+ ):
114
+ clip, files = contents(state=state, references=references)
115
+ result = validate_image_export_bundle(
116
+ bundle(tmp_path, clip, files, references=references)
117
+ )
118
+ assert result["images"] == 1
119
+ assert result["completion_counts"][state] >= 1
120
+ assert result["objects"] == (state != "confirmed_empty")
121
+ if state == "unprocessed":
122
+ assert result["completion_counts"]["complete"] == 0
123
+ assert result["completion_counts"]["confirmed_empty"] == 0
124
+
125
+
126
+ @pytest.mark.parametrize(
127
+ "change",
128
+ [
129
+ "scope",
130
+ "scope-extra",
131
+ "missing",
132
+ "path",
133
+ "duplicate",
134
+ "identity",
135
+ "prediction-context",
136
+ "extra",
137
+ ],
138
+ )
139
+ def test_fail_closed_even_when_attacker_recomputes_transport_hashes(tmp_path, change):
140
+ clip, files = contents(references=True)
141
+ cid = clip["meta"]["clip_id"]
142
+ scope = f"consumption/{cid}/human_scopes.json"
143
+ append = None
144
+ if change.startswith("scope"):
145
+ value = json.loads(files[scope])
146
+ if change == "scope":
147
+ value["scopes"][0]["state"] = "complete"
148
+ else:
149
+ value["unexpected"] = True
150
+ files[scope] = canonical(value)
151
+ if change == "missing":
152
+ files.pop(scope)
153
+ if change == "path":
154
+ files["../escape"] = b"x"
155
+ if change == "duplicate":
156
+ append = f"{EXPORT_ID}/{scope}"
157
+ if change == "identity":
158
+ value = json.loads(files[f"clips/{cid}/identity.json"])
159
+ value["annotation_revision"] = "sha256:" + "0" * 64
160
+ files[f"clips/{cid}/identity.json"] = canonical(value)
161
+ if change == "prediction-context":
162
+ name = reference_path(cid, clip["meta"]["view_ids"][0])
163
+ value = json.loads(files[name])
164
+ value["source_revision"] = "sha256:" + "0" * 64
165
+ files[name] = canonical(value)
166
+ if change == "extra":
167
+ files[f"clips/{cid}/password.json"] = b"{}"
168
+ with pytest.raises((ImageContractError, ValueError)):
169
+ validate_image_export_bundle(
170
+ bundle(tmp_path, clip, files, references=True, append=append)
171
+ )
172
+
173
+
174
+ def test_corrupt_bytes_and_budget_are_rejected_without_extraction(tmp_path):
175
+ clip, files = contents()
176
+ path = bundle(
177
+ tmp_path,
178
+ clip,
179
+ files,
180
+ override=lambda name, raw: b"z" * len(raw) if name.endswith(".jpg") else raw,
181
+ )
182
+ with pytest.raises(ImageContractError, match="SHA256"):
183
+ validate_image_export_bundle(path)
184
+ path = bundle(tmp_path, clip, files)
185
+ with pytest.raises(ImageContractError, match="budget"):
186
+ validate_image_export_bundle(path, max_json_bytes=10)
187
+ assert list(tmp_path.iterdir()) == [path]
188
+
189
+
190
+ def job(status="queued"):
191
+ completed = status == "completed"
192
+ terminal = status in {"completed", "failed", "cancelled"}
193
+ return {
194
+ "contract_version": JOB_CONTRACT,
195
+ "export_id": EXPORT_ID,
196
+ "project_id": 7,
197
+ "clip_ids": ["clip_test"],
198
+ "include_preannotations": False,
199
+ "status": status,
200
+ "progress": {
201
+ "stage": status,
202
+ "processed": 0,
203
+ "total": 1,
204
+ "bytes": 0,
205
+ "elapsed_s": 0.0,
206
+ "rate": 0.0,
207
+ "eta_s": None,
208
+ },
209
+ "error": {"code": "test_failure", "message": "Explicit failure"}
210
+ if status == "failed"
211
+ else None,
212
+ "artifact": {"filename": EXPORT_ID + ".tar", "size": 10240, "sha256": "b" * 64}
213
+ if completed
214
+ else None,
215
+ "created_at": NOW,
216
+ "completed_at": NOW if terminal else None,
217
+ "can_cancel": not terminal,
218
+ }
219
+
220
+
221
+ @pytest.mark.parametrize(
222
+ "status",
223
+ [
224
+ "queued",
225
+ "capturing",
226
+ "packaging",
227
+ "validating",
228
+ "uploading",
229
+ "completed",
230
+ "failed",
231
+ "cancelled",
232
+ ],
233
+ )
234
+ def test_job_states_are_strict(status):
235
+ assert parse_image_export_job(job(status)).to_dict()["status"] == status
236
+
237
+
238
+ @pytest.mark.parametrize(
239
+ "change",
240
+ ["error", "artifact", "processed", "bool", "unknown", "terminal", "infinite"],
241
+ )
242
+ def test_invalid_job_states_fail(change):
243
+ value = job()
244
+ if change == "error":
245
+ value["error"] = {"code": "oops", "message": "not failed"}
246
+ if change == "artifact":
247
+ value["artifact"] = job("completed")["artifact"]
248
+ if change == "processed":
249
+ value["progress"]["processed"] = 2
250
+ if change == "bool":
251
+ value["project_id"] = True
252
+ if change == "unknown":
253
+ value["model_version"] = "forbidden"
254
+ if change == "terminal":
255
+ value["completed_at"] = NOW
256
+ if change == "infinite":
257
+ value["progress"]["rate"] = float("inf")
258
+ with pytest.raises((ImageContractError, ValueError)):
259
+ parse_image_export_job(value)
260
+
261
+
262
+ def test_typed_client_scope_and_ephemeral_download_do_not_forward_credentials():
263
+ calls = []
264
+
265
+ def handler(request):
266
+ calls.append(request)
267
+ path = request.url.path
268
+ if path.endswith("/preview"):
269
+ return httpx.Response(
270
+ 200,
271
+ json={
272
+ "contract_version": PREVIEW_CONTRACT,
273
+ "project_id": 7,
274
+ "clip_id": "clip_test",
275
+ "include_preannotations": False,
276
+ "clip_ids": ["clip_test"],
277
+ "excluded": [],
278
+ "scope_token": "opaque-token",
279
+ "expires_at": NOW,
280
+ },
281
+ )
282
+ if path.endswith("/download-link"):
283
+ return httpx.Response(
284
+ 200,
285
+ json={
286
+ "contract_version": DOWNLOAD_CONTRACT,
287
+ "export_id": EXPORT_ID,
288
+ "url": "http://storage.test/bucket/object?signature=private",
289
+ "expires_at": NOW,
290
+ **job("completed")["artifact"],
291
+ },
292
+ )
293
+ if path == "/api/v1/image-exports" and request.method == "GET":
294
+ return httpx.Response(
295
+ 200,
296
+ json={
297
+ "contract_version": PAGE_CONTRACT,
298
+ "project_id": 7,
299
+ "page": 1,
300
+ "page_size": 50,
301
+ "total": 1,
302
+ "items": [job()],
303
+ },
304
+ )
305
+ return httpx.Response(200, json=job())
306
+
307
+ client = X4DClient("http://platform.test", token="platform-private-token")
308
+ client._http = httpx.Client(
309
+ base_url="http://platform.test",
310
+ headers={"Authorization": "Bearer platform-private-token"},
311
+ transport=httpx.MockTransport(handler),
312
+ )
313
+ preview = client.image_exports.preview(project_id=7, clip_id="clip_test")
314
+ created = client.image_exports.create(
315
+ project_id=7,
316
+ clip_id="clip_test",
317
+ scope_token=preview.to_dict()["scope_token"],
318
+ idempotency_key="key",
319
+ )
320
+ assert created.to_dict()["export_id"] == EXPORT_ID
321
+ assert client.image_exports.list(project_id=7).to_dict()["total"] == 1
322
+ for action in ("get", "cancel", "retry"):
323
+ assert (
324
+ getattr(client.image_exports, action)(EXPORT_ID).to_dict()["export_id"]
325
+ == EXPORT_ID
326
+ )
327
+ assert (
328
+ client.image_exports.download_link(EXPORT_ID)
329
+ .to_dict()["url"]
330
+ .startswith("http://storage.test/")
331
+ )
332
+ assert all(request.url.host == "platform.test" for request in calls)
333
+ assert json.loads(calls[1].content) == {
334
+ "project_id": 7,
335
+ "clip_id": "clip_test",
336
+ "include_preannotations": False,
337
+ "scope_token": "opaque-token",
338
+ "idempotency_key": "key",
339
+ }
340
+ with pytest.raises(ImageContractError):
341
+ client.image_exports.preview(project_id=True)
342
+ client.close()
343
+
344
+
345
+ def test_client_rejects_foreign_response():
346
+ client = X4DClient("http://platform.test")
347
+ other = deepcopy(job())
348
+ other["export_id"] = "imexp_" + "c" * 32
349
+ client._http = httpx.Client(
350
+ base_url="http://platform.test",
351
+ transport=httpx.MockTransport(lambda r: httpx.Response(200, json=other)),
352
+ )
353
+ with pytest.raises(ImageContractError, match="foreign"):
354
+ client.image_exports.get(EXPORT_ID)
355
+ client.close()
356
+
357
+
358
+ def test_offline_cli_reports_valid_bundle(tmp_path, capsys):
359
+ from types import SimpleNamespace
360
+ from x4d_devkit.cli import cmd_validate_image_export
361
+
362
+ clip, files = contents()
363
+ path = bundle(tmp_path, clip, files)
364
+ cmd_validate_image_export(
365
+ SimpleNamespace(path=path, max_json_bytes=64 * 1024 * 1024)
366
+ )
367
+ assert json.loads(capsys.readouterr().out)["ok"] is True
368
+ path.write_bytes(b"invalid tar")
369
+ with pytest.raises(SystemExit) as error:
370
+ cmd_validate_image_export(
371
+ SimpleNamespace(path=path, max_json_bytes=64 * 1024 * 1024)
372
+ )
373
+ assert error.value.code == 1
374
+ assert json.loads(capsys.readouterr().out)["ok"] is False
375
+
376
+
377
+ def test_archive_member_history_is_not_retained(tmp_path, monkeypatch):
378
+ clip, files = contents()
379
+ path = bundle(tmp_path, clip, files)
380
+ original = tarfile.TarFile.next
381
+ history = []
382
+
383
+ def measured(archive):
384
+ history.append(len(archive.members))
385
+ return original(archive)
386
+
387
+ monkeypatch.setattr(tarfile.TarFile, "next", measured)
388
+ validate_image_export_bundle(path)
389
+ assert max(history) <= 1
@@ -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.0rc4"
14
+ assert x4d_devkit.__version__ == "0.48.0rc5"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -414,6 +414,18 @@ def cmd_login(args):
414
414
  print(f"Logged in as {username}; token saved to {path}")
415
415
 
416
416
 
417
+ def cmd_validate_image_export(args):
418
+ import tarfile
419
+ from x4d_devkit.image_export import validate_image_export_bundle
420
+
421
+ try:
422
+ summary = validate_image_export_bundle(args.path, max_json_bytes=args.max_json_bytes)
423
+ except (OSError, ValueError, tarfile.TarError) as exc:
424
+ print_json({"ok": False, "error": str(exc)})
425
+ raise SystemExit(1) from exc
426
+ print_json({"ok": True, "summary": summary})
427
+
428
+
417
429
  def cmd_validate(args):
418
430
  from x4d_devkit.validation import validate_clip
419
431
 
@@ -755,6 +767,11 @@ def main():
755
767
  p_schemas_project.add_argument("--format", choices=["table", "json"], default="json")
756
768
  p_schemas_project.set_defaults(func=cmd_label_schemas_project_get)
757
769
 
770
+ p_image_export = sub.add_parser("validate-image-export", help="Validate a native image export tar offline")
771
+ p_image_export.add_argument("path")
772
+ p_image_export.add_argument("--max-json-bytes", type=int, default=64 * 1024 * 1024)
773
+ p_image_export.set_defaults(func=cmd_validate_image_export)
774
+
758
775
  p_val = sub.add_parser("validate", help="Validate clip directory")
759
776
  p_val.add_argument("path", help="Clip directory or parent directory")
760
777
  p_val.set_defaults(func=cmd_validate)
@@ -10,6 +10,7 @@ from x4d_devkit.client.clip_work_sessions import (
10
10
  )
11
11
  from x4d_devkit.client.client import X4DClient
12
12
  from x4d_devkit.client.image_work_sessions import ImageWorkSessionsAPI
13
+ from x4d_devkit.client.image_exports import ImageExportsAPI
13
14
  from x4d_devkit.client.clips import ClipsAPI
14
15
  from x4d_devkit.client.dataset_identity import DatasetIdentityAPI
15
16
  from x4d_devkit.client.label_schemas import (
@@ -43,6 +44,7 @@ from x4d_devkit.client.test_sets import TestSetsAPI
43
44
 
44
45
  __all__ = [
45
46
  "ImageWorkSessionsAPI",
47
+ "ImageExportsAPI",
46
48
  "CleanBasemapState",
47
49
  "AssetsAPI",
48
50
  "BagsAPI",
@@ -26,6 +26,7 @@ from x4d_devkit.client.label_schemas import LabelSchemasAPI
26
26
  from x4d_devkit.client.image_clips import ImageClipsAPI
27
27
  from x4d_devkit.client.image_work_sessions import ImageWorkSessionsAPI
28
28
  from x4d_devkit.client.image_preannotations import ImagePreannotationsAPI
29
+ from x4d_devkit.client.image_exports import ImageExportsAPI
29
30
  from x4d_devkit.client.semantic_scene import SemanticSceneAPI
30
31
  from x4d_devkit.client.semantic_scene_authoring import (
31
32
  SemanticSceneAuthoringWorkSessionsAPI,
@@ -76,6 +77,7 @@ class X4DClient:
76
77
  self.image_clips = ImageClipsAPI(self)
77
78
  self.image_work_sessions = ImageWorkSessionsAPI(self)
78
79
  self.image_preannotations = ImagePreannotationsAPI(self)
80
+ self.image_exports = ImageExportsAPI(self)
79
81
  self.label_schemas = LabelSchemasAPI(self)
80
82
  self.semantic_scene = SemanticSceneAPI(self)
81
83
  self.semantic_scene_authoring_work_sessions = (
@@ -0,0 +1,141 @@
1
+ """Typed invocation of platform-owned native image export capabilities."""
2
+
3
+ from x4d_devkit._image_contract import identifier, integer, require, text
4
+ from x4d_devkit.image_export import (
5
+ export_identifier,
6
+ parse_image_export_download,
7
+ parse_image_export_job,
8
+ parse_image_export_page,
9
+ parse_image_export_preview,
10
+ )
11
+
12
+
13
+ class ImageExportsAPI:
14
+ def __init__(self, client):
15
+ self._client = client
16
+
17
+ def _scope(self, project_id, clip_id, include_preannotations):
18
+ integer(project_id, "project_id", minimum=1)
19
+ if clip_id is not None:
20
+ identifier(clip_id, "clip_id")
21
+ require(
22
+ type(include_preannotations) is bool,
23
+ "expected boolean",
24
+ "include_preannotations",
25
+ )
26
+ return {
27
+ "project_id": project_id,
28
+ "clip_id": clip_id,
29
+ "include_preannotations": include_preannotations,
30
+ }
31
+
32
+ def preview(self, *, project_id, clip_id=None, include_preannotations=False):
33
+ body = self._scope(project_id, clip_id, include_preannotations)
34
+ result = parse_image_export_preview(
35
+ self._client.post("/image-exports/preview", json=body)
36
+ )
37
+ value = result.to_dict()
38
+ require(
39
+ all(value[key] == item for key, item in body.items()),
40
+ "foreign preview context",
41
+ "preview",
42
+ )
43
+ return result
44
+
45
+ def create(
46
+ self,
47
+ *,
48
+ project_id,
49
+ scope_token,
50
+ idempotency_key,
51
+ clip_id=None,
52
+ include_preannotations=False,
53
+ ):
54
+ body = self._scope(project_id, clip_id, include_preannotations)
55
+ text(scope_token, "scope_token")
56
+ text(idempotency_key, "idempotency_key")
57
+ result = parse_image_export_job(
58
+ self._client.post(
59
+ "/image-exports",
60
+ json={
61
+ **body,
62
+ "scope_token": scope_token,
63
+ "idempotency_key": idempotency_key,
64
+ },
65
+ )
66
+ )
67
+ value = result.to_dict()
68
+ require(
69
+ value["project_id"] == project_id
70
+ and value["include_preannotations"] == include_preannotations
71
+ and (clip_id is None or value["clip_ids"] == [clip_id]),
72
+ "foreign created job",
73
+ "job",
74
+ )
75
+ return result
76
+
77
+ def list(self, *, project_id, page=1, page_size=50):
78
+ for key, value in {
79
+ "project_id": project_id,
80
+ "page": page,
81
+ "page_size": page_size,
82
+ }.items():
83
+ integer(value, key, minimum=1)
84
+ require(page_size <= 100, "page size exceeds 100", "page_size")
85
+ result = parse_image_export_page(
86
+ self._client.get(
87
+ "/image-exports",
88
+ params={"project_id": project_id, "page": page, "page_size": page_size},
89
+ )
90
+ )
91
+ value = result.to_dict()
92
+ require(
93
+ all(
94
+ value[key] == item
95
+ for key, item in {
96
+ "project_id": project_id,
97
+ "page": page,
98
+ "page_size": page_size,
99
+ }.items()
100
+ ),
101
+ "foreign export page",
102
+ "page",
103
+ )
104
+ return result
105
+
106
+ def get(self, export_id):
107
+ return self._job(export_id, "get")
108
+
109
+ def cancel(self, export_id):
110
+ return self._job(export_id, "cancel")
111
+
112
+ def retry(self, export_id):
113
+ return self._job(export_id, "retry")
114
+
115
+ def _job(self, export_id, action):
116
+ export_identifier(export_id)
117
+ path = f"/image-exports/{export_id}"
118
+ payload = (
119
+ self._client.get(path)
120
+ if action == "get"
121
+ else self._client.post(f"{path}/{action}", json={})
122
+ )
123
+ result = parse_image_export_job(payload)
124
+ require(
125
+ result.to_dict()["export_id"] == export_id,
126
+ "foreign export job",
127
+ "export_id",
128
+ )
129
+ return result
130
+
131
+ def download_link(self, export_id):
132
+ export_identifier(export_id)
133
+ result = parse_image_export_download(
134
+ self._client.post(f"/image-exports/{export_id}/download-link", json={})
135
+ )
136
+ require(
137
+ result.to_dict()["export_id"] == export_id,
138
+ "foreign export download",
139
+ "export_id",
140
+ )
141
+ return result