x4d-devkit 0.48.0rc3__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 (156) hide show
  1. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/PKG-INFO +1 -1
  2. {x4d_devkit-0.48.0rc3 → 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.0rc5/tests/test_image_preannotation.py +380 -0
  5. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_image_reference.py +0 -56
  6. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_sdk_api.py +1 -1
  7. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/cli.py +17 -0
  8. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/__init__.py +2 -0
  9. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/client.py +4 -2
  10. x4d_devkit-0.48.0rc5/x4d_devkit/client/image_exports.py +141 -0
  11. x4d_devkit-0.48.0rc5/x4d_devkit/client/image_preannotations.py +117 -0
  12. x4d_devkit-0.48.0rc5/x4d_devkit/image_export.py +606 -0
  13. x4d_devkit-0.48.0rc5/x4d_devkit/image_preannotation.py +330 -0
  14. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/PKG-INFO +1 -1
  15. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/SOURCES.txt +6 -1
  16. x4d_devkit-0.48.0rc3/x4d_devkit/client/image_references.py +0 -147
  17. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/LICENSE +0 -0
  18. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/README.md +0 -0
  19. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/setup.cfg +0 -0
  20. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_annotation_contract.py +0 -0
  21. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_annotation_reference.py +0 -0
  22. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_assets_client.py +0 -0
  23. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_boundary.py +0 -0
  24. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_box.py +0 -0
  25. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_build_info.py +0 -0
  26. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_category_audit.py +0 -0
  27. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_clean_basemap.py +0 -0
  28. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_clean_basemap_client.py +0 -0
  29. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_cli_auth.py +0 -0
  30. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_client_json_contract.py +0 -0
  31. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_client_projects.py +0 -0
  32. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_clip_archives.py +0 -0
  33. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_contract_validation.py +0 -0
  34. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_coordinate_frames.py +0 -0
  35. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_current_state.py +0 -0
  36. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_current_state_identity_consumers_example.py +0 -0
  37. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_dataset_contract.py +0 -0
  38. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_dataset_identity.py +0 -0
  39. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_detection_eval.py +0 -0
  40. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_evaluate.py +0 -0
  41. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_export_bundle.py +0 -0
  42. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_image_clip.py +0 -0
  43. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_image_geometry.py +0 -0
  44. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_image_review.py +0 -0
  45. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_image_work_session.py +0 -0
  46. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_inference_input.py +0 -0
  47. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_inference_schemas.py +0 -0
  48. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_inference_service.py +0 -0
  49. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_label_schemas.py +0 -0
  50. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_lidar_materialization.py +0 -0
  51. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_loader.py +0 -0
  52. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_matching.py +0 -0
  53. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_metrics.py +0 -0
  54. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_models.py +0 -0
  55. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_platform_pose.py +0 -0
  56. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_preannotation_client.py +0 -0
  57. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_reconstruction_client.py +0 -0
  58. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_release_gate.py +0 -0
  59. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_roadmark_preview.py +0 -0
  60. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_authoring.py +0 -0
  61. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_client.py +0 -0
  62. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_contract.py +0 -0
  63. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_independent.py +0 -0
  64. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_semantic_scene_native_clip.py +0 -0
  65. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_source_attributes.py +0 -0
  66. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_time_contract.py +0 -0
  67. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_token.py +0 -0
  68. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_tracking_eval.py +0 -0
  69. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_training_identity.py +0 -0
  70. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_transform.py +0 -0
  71. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_transform_chain.py +0 -0
  72. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_validation.py +0 -0
  73. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_vector_map.py +0 -0
  74. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_vector_map_geometry.py +0 -0
  75. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_vector_map_session.py +0 -0
  76. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_work_session.py +0 -0
  77. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/tests/test_work_session_validation_reuse.py +0 -0
  78. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/__init__.py +0 -0
  79. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/_image_contract.py +0 -0
  80. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/annotation_contract.py +0 -0
  81. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/annotation_reference.py +0 -0
  82. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/audit.py +0 -0
  83. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/build_info.py +0 -0
  84. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/clean_basemap.py +0 -0
  85. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/_origin.py +0 -0
  86. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/annotation_reference.py +0 -0
  87. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/assets.py +0 -0
  88. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/bags.py +0 -0
  89. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/capabilities.py +0 -0
  90. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/checkpoints.py +0 -0
  91. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clean_basemap.py +0 -0
  92. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clip_work_sessions.py +0 -0
  93. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/clips.py +0 -0
  94. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/dataset_identity.py +0 -0
  95. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_clips.py +0 -0
  96. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_review.py +0 -0
  97. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/image_work_sessions.py +0 -0
  98. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/label_schemas.py +0 -0
  99. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/preannotation.py +0 -0
  100. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/projects.py +0 -0
  101. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/reconstruction.py +0 -0
  102. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/semantic_scene.py +0 -0
  103. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  104. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/services.py +0 -0
  105. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/client/test_sets.py +0 -0
  106. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/consumer_identity.py +0 -0
  107. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/coordinate_contract.py +0 -0
  108. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/__init__.py +0 -0
  109. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/loader.py +0 -0
  110. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/models.py +0 -0
  111. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/token.py +0 -0
  112. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/core/transform.py +0 -0
  113. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/current_state.py +0 -0
  114. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/dataset_contract.py +0 -0
  115. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/dataset_identity.py +0 -0
  116. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/__init__.py +0 -0
  117. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/api.py +0 -0
  118. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/box.py +0 -0
  119. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/detection.py +0 -0
  120. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/matching.py +0 -0
  121. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/metrics.py +0 -0
  122. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/eval/tracking.py +0 -0
  123. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/export_bundle.py +0 -0
  124. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_clip.py +0 -0
  125. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_geometry.py +0 -0
  126. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_reference.py +0 -0
  127. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_review.py +0 -0
  128. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_schema.py +0 -0
  129. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/image_work_session.py +0 -0
  130. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/__init__.py +0 -0
  131. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/input.py +0 -0
  132. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/models.py +0 -0
  133. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/schemas.py +0 -0
  134. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/service.py +0 -0
  135. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/inference/tasks.py +0 -0
  136. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/json_contract.py +0 -0
  137. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/label_schema.py +0 -0
  138. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/lidar_materialization.py +0 -0
  139. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/materialize.py +0 -0
  140. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/roadmark_preview.py +0 -0
  141. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/semantic_scene_authoring.py +0 -0
  142. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/semantic_scene_contract.py +0 -0
  143. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/time_contract.py +0 -0
  144. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/training_identity.py +0 -0
  145. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/__init__.py +0 -0
  146. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/contract.py +0 -0
  147. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/platform_pose.py +0 -0
  148. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/report.py +0 -0
  149. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/validation/validator.py +0 -0
  150. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/vector_map.py +0 -0
  151. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/vector_map_geometry.py +0 -0
  152. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit/work_session.py +0 -0
  153. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  154. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/entry_points.txt +0 -0
  155. {x4d_devkit-0.48.0rc3 → x4d_devkit-0.48.0rc5}/x4d_devkit.egg-info/requires.txt +0 -0
  156. {x4d_devkit-0.48.0rc3 → 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.0rc3
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.0rc3"
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