x4d-devkit 0.47.0rc1__tar.gz → 0.48.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 (161) hide show
  1. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/PKG-INFO +4 -1
  2. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/README.md +3 -0
  3. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/pyproject.toml +1 -1
  4. x4d_devkit-0.48.0/tests/test_accounts.py +198 -0
  5. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_boundary.py +6 -0
  6. x4d_devkit-0.48.0/tests/test_category_audit.py +272 -0
  7. x4d_devkit-0.48.0/tests/test_image_clip.py +391 -0
  8. x4d_devkit-0.48.0/tests/test_image_export.py +414 -0
  9. x4d_devkit-0.48.0/tests/test_image_geometry.py +295 -0
  10. x4d_devkit-0.48.0/tests/test_image_preannotation.py +380 -0
  11. x4d_devkit-0.48.0/tests/test_image_reference.py +211 -0
  12. x4d_devkit-0.48.0/tests/test_image_schema_upgrade.py +239 -0
  13. x4d_devkit-0.48.0/tests/test_image_work_session.py +228 -0
  14. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_sdk_api.py +1 -1
  15. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/__init__.py +14 -0
  16. x4d_devkit-0.48.0/x4d_devkit/_image_contract.py +118 -0
  17. x4d_devkit-0.48.0/x4d_devkit/accounts.py +147 -0
  18. x4d_devkit-0.48.0/x4d_devkit/audit.py +302 -0
  19. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/cli.py +82 -0
  20. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/__init__.py +7 -0
  21. x4d_devkit-0.48.0/x4d_devkit/client/accounts.py +111 -0
  22. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/assets.py +16 -1
  23. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/client.py +12 -0
  24. x4d_devkit-0.48.0/x4d_devkit/client/image_clips.py +122 -0
  25. x4d_devkit-0.48.0/x4d_devkit/client/image_exports.py +141 -0
  26. x4d_devkit-0.48.0/x4d_devkit/client/image_preannotations.py +117 -0
  27. x4d_devkit-0.48.0/x4d_devkit/client/image_schemas.py +115 -0
  28. x4d_devkit-0.48.0/x4d_devkit/client/image_work_sessions.py +80 -0
  29. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/projects.py +6 -1
  30. x4d_devkit-0.48.0/x4d_devkit/image_clip.py +689 -0
  31. x4d_devkit-0.48.0/x4d_devkit/image_export.py +621 -0
  32. x4d_devkit-0.48.0/x4d_devkit/image_geometry.py +355 -0
  33. x4d_devkit-0.48.0/x4d_devkit/image_preannotation.py +330 -0
  34. x4d_devkit-0.48.0/x4d_devkit/image_reference.py +438 -0
  35. x4d_devkit-0.48.0/x4d_devkit/image_schema.py +350 -0
  36. x4d_devkit-0.48.0/x4d_devkit/image_schema_upgrade.py +347 -0
  37. x4d_devkit-0.48.0/x4d_devkit/image_work_session.py +161 -0
  38. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit.egg-info/PKG-INFO +4 -1
  39. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit.egg-info/SOURCES.txt +26 -0
  40. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/LICENSE +0 -0
  41. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/setup.cfg +0 -0
  42. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_annotation_contract.py +0 -0
  43. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_annotation_reference.py +0 -0
  44. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_assets_client.py +0 -0
  45. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_box.py +0 -0
  46. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_build_info.py +0 -0
  47. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_clean_basemap.py +0 -0
  48. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_clean_basemap_client.py +0 -0
  49. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_cli_auth.py +0 -0
  50. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_client_json_contract.py +0 -0
  51. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_client_projects.py +0 -0
  52. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_clip_archives.py +0 -0
  53. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_contract_validation.py +0 -0
  54. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_coordinate_frames.py +0 -0
  55. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_current_state.py +0 -0
  56. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_current_state_identity_consumers_example.py +0 -0
  57. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_dataset_contract.py +0 -0
  58. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_dataset_identity.py +0 -0
  59. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_detection_eval.py +0 -0
  60. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_evaluate.py +0 -0
  61. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_export_bundle.py +0 -0
  62. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_image_review.py +0 -0
  63. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_inference_input.py +0 -0
  64. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_inference_schemas.py +0 -0
  65. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_inference_service.py +0 -0
  66. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_label_schemas.py +0 -0
  67. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_lidar_materialization.py +0 -0
  68. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_loader.py +0 -0
  69. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_matching.py +0 -0
  70. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_metrics.py +0 -0
  71. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_models.py +0 -0
  72. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_platform_pose.py +0 -0
  73. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_preannotation_client.py +0 -0
  74. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_reconstruction_client.py +0 -0
  75. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_release_gate.py +0 -0
  76. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_roadmark_preview.py +0 -0
  77. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_semantic_scene_authoring.py +0 -0
  78. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_semantic_scene_client.py +0 -0
  79. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_semantic_scene_contract.py +0 -0
  80. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_semantic_scene_independent.py +0 -0
  81. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_semantic_scene_native_clip.py +0 -0
  82. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_source_attributes.py +0 -0
  83. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_time_contract.py +0 -0
  84. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_token.py +0 -0
  85. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_tracking_eval.py +0 -0
  86. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_training_identity.py +0 -0
  87. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_transform.py +0 -0
  88. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_transform_chain.py +0 -0
  89. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_validation.py +0 -0
  90. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_vector_map.py +0 -0
  91. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_vector_map_geometry.py +0 -0
  92. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_vector_map_session.py +0 -0
  93. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_work_session.py +0 -0
  94. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/tests/test_work_session_validation_reuse.py +0 -0
  95. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/annotation_contract.py +0 -0
  96. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/annotation_reference.py +0 -0
  97. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/build_info.py +0 -0
  98. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/clean_basemap.py +0 -0
  99. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/_origin.py +0 -0
  100. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/annotation_reference.py +0 -0
  101. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/bags.py +0 -0
  102. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/capabilities.py +0 -0
  103. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/checkpoints.py +0 -0
  104. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/clean_basemap.py +0 -0
  105. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/clip_work_sessions.py +0 -0
  106. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/clips.py +0 -0
  107. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/dataset_identity.py +0 -0
  108. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/image_review.py +0 -0
  109. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/label_schemas.py +0 -0
  110. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/preannotation.py +0 -0
  111. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/reconstruction.py +0 -0
  112. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/semantic_scene.py +0 -0
  113. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  114. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/services.py +0 -0
  115. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/client/test_sets.py +0 -0
  116. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/consumer_identity.py +0 -0
  117. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/coordinate_contract.py +0 -0
  118. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/core/__init__.py +0 -0
  119. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/core/loader.py +0 -0
  120. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/core/models.py +0 -0
  121. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/core/token.py +0 -0
  122. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/core/transform.py +0 -0
  123. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/current_state.py +0 -0
  124. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/dataset_contract.py +0 -0
  125. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/dataset_identity.py +0 -0
  126. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/eval/__init__.py +0 -0
  127. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/eval/api.py +0 -0
  128. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/eval/box.py +0 -0
  129. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/eval/detection.py +0 -0
  130. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/eval/matching.py +0 -0
  131. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/eval/metrics.py +0 -0
  132. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/eval/tracking.py +0 -0
  133. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/export_bundle.py +0 -0
  134. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/image_review.py +0 -0
  135. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/inference/__init__.py +0 -0
  136. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/inference/input.py +0 -0
  137. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/inference/models.py +0 -0
  138. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/inference/schemas.py +0 -0
  139. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/inference/service.py +0 -0
  140. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/inference/tasks.py +0 -0
  141. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/json_contract.py +0 -0
  142. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/label_schema.py +0 -0
  143. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/lidar_materialization.py +0 -0
  144. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/materialize.py +0 -0
  145. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/roadmark_preview.py +0 -0
  146. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/semantic_scene_authoring.py +0 -0
  147. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/semantic_scene_contract.py +0 -0
  148. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/time_contract.py +0 -0
  149. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/training_identity.py +0 -0
  150. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/validation/__init__.py +0 -0
  151. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/validation/contract.py +0 -0
  152. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/validation/platform_pose.py +0 -0
  153. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/validation/report.py +0 -0
  154. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/validation/validator.py +0 -0
  155. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/vector_map.py +0 -0
  156. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/vector_map_geometry.py +0 -0
  157. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit/work_session.py +0 -0
  158. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  159. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
  160. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.0}/x4d_devkit.egg-info/requires.txt +0 -0
  161. {x4d_devkit-0.47.0rc1 → x4d_devkit-0.48.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.47.0rc1
3
+ Version: 0.48.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
@@ -549,6 +549,9 @@ boundary and complete workflow.
549
549
 
550
550
  ## Modules
551
551
 
552
+ Native 2D image contracts and the read-only typed image client are documented in
553
+ [Native image Clip v1](docs/image-clip-contract.md). Dataset 0.18 3D readers remain strict.
554
+
552
555
  | Module | Description |
553
556
  |--------|-------------|
554
557
  | `core` | Data models, token generation, coordinate transforms, clip loader |
@@ -506,6 +506,9 @@ boundary and complete workflow.
506
506
 
507
507
  ## Modules
508
508
 
509
+ Native 2D image contracts and the read-only typed image client are documented in
510
+ [Native image Clip v1](docs/image-clip-contract.md). Dataset 0.18 3D readers remain strict.
511
+
509
512
  | Module | Description |
510
513
  |--------|-------------|
511
514
  | `core` | Data models, token generation, coordinate transforms, clip loader |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "x4d-devkit"
7
- version = "0.47.0rc1"
7
+ version = "0.48.0"
8
8
  description = "Official X-4D external SDK and CLI for datasets, platform APIs, training bridges, and automation"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -0,0 +1,198 @@
1
+ from copy import deepcopy
2
+ import json
3
+
4
+ import httpx
5
+ import pytest
6
+
7
+ from x4d_devkit.accounts import (
8
+ ACCOUNT_CONTRACT,
9
+ ACCOUNT_PAGE_CONTRACT,
10
+ parse_account_page,
11
+ parse_account_record,
12
+ validate_account_password,
13
+ validate_project_types,
14
+ )
15
+ from x4d_devkit.client import X4DClient
16
+
17
+
18
+ def record(**changes):
19
+ return {
20
+ "contract_version": ACCOUNT_CONTRACT,
21
+ "id": 7,
22
+ "username": "annotator",
23
+ "role": "visitor",
24
+ "is_active": True,
25
+ "project_types": ["2d"],
26
+ "revision": 3,
27
+ **changes,
28
+ }
29
+
30
+
31
+ @pytest.mark.parametrize(
32
+ "change",
33
+ [
34
+ {"id": True},
35
+ {"id": 0},
36
+ {"revision": -1},
37
+ {"revision": True},
38
+ {"is_active": 1},
39
+ {"username": " user"},
40
+ {"username": "user\x00"},
41
+ {"role": "editor"},
42
+ {"project_types": ["2D"]},
43
+ {"project_types": ["2d", "2d"]},
44
+ {"project_types": ["3d", "2d"]},
45
+ {"project_types": "2d"},
46
+ {"password": "must-never-appear"},
47
+ {"contract_version": "x4d-account-v0"},
48
+ ],
49
+ )
50
+ def test_account_rejects_noncanonical_data_without_secret_echo(change):
51
+ with pytest.raises(ValueError) as error:
52
+ parse_account_record(record(**change))
53
+ assert "must-never-appear" not in str(error.value)
54
+
55
+
56
+ @pytest.mark.parametrize("grants", [[], ["2d"], ["3d"], ["2d", "3d"]])
57
+ def test_account_roundtrip_detaches_grants(grants):
58
+ value = record(project_types=grants)
59
+ parsed = parse_account_record(value)
60
+ detached = parsed.to_dict()
61
+ assert detached == value
62
+ detached["project_types"].append("mutated")
63
+ assert parsed.to_dict() == value
64
+ assert validate_project_types(grants) == grants
65
+
66
+
67
+ @pytest.mark.parametrize(
68
+ "password", ["short", " " * 12, "x" * 73, "密" * 25, "secret\ud800"]
69
+ )
70
+ def test_password_limits_are_bytes_and_do_not_echo_credentials(password):
71
+ with pytest.raises(ValueError) as error:
72
+ validate_account_password(password)
73
+ assert password not in str(error.value)
74
+
75
+
76
+ def test_page_rejects_duplicates_excess_and_private_fields():
77
+ page = {
78
+ "contract_version": ACCOUNT_PAGE_CONTRACT,
79
+ "page": 1,
80
+ "page_size": 10,
81
+ "total": 1,
82
+ "items": [record()],
83
+ }
84
+ assert parse_account_page(page) == page
85
+ for changes in (
86
+ {"items": [record(), record()]},
87
+ {"page_size": 101},
88
+ {"secret": "hidden"},
89
+ ):
90
+ with pytest.raises(ValueError):
91
+ parse_account_page({**page, **changes})
92
+
93
+
94
+ def test_typed_client_uses_only_account_capabilities_and_clears_changed_credentials():
95
+ requests = []
96
+ current = record()
97
+
98
+ def handler(request):
99
+ requests.append(request)
100
+ path = request.url.path
101
+ if request.method == "GET" and path == "/api/v1/users":
102
+ return httpx.Response(
103
+ 200,
104
+ json={
105
+ "contract_version": ACCOUNT_PAGE_CONTRACT,
106
+ "page": 1,
107
+ "page_size": 10,
108
+ "total": 1,
109
+ "items": [current],
110
+ },
111
+ )
112
+ if path.endswith("/update"):
113
+ body = json.loads(request.content)
114
+ for key in ("role", "project_types", "is_active"):
115
+ current[key] = body[key]
116
+ current["revision"] += 1
117
+ elif path.endswith("/reset-password") or path.endswith("/me/password"):
118
+ current["revision"] += 1
119
+ return httpx.Response(200, json=deepcopy(current))
120
+
121
+ with X4DClient(api_url="http://platform.test", token="private-token") as client:
122
+ client._http.close()
123
+ client._http = httpx.Client(
124
+ base_url=client.api_url,
125
+ headers={"Authorization": "Bearer private-token"},
126
+ transport=httpx.MockTransport(handler),
127
+ )
128
+ assert client.accounts.list(page_size=10)["items"][0]["id"] == 7
129
+ assert client.accounts.get(7).id == client.accounts.me().id == 7
130
+ assert (
131
+ client.accounts.create(
132
+ username="annotator",
133
+ password="private-password",
134
+ role="visitor",
135
+ project_types=["2d"],
136
+ ).id
137
+ == 7
138
+ )
139
+ assert (
140
+ client.accounts.update(
141
+ 7,
142
+ expected_revision=3,
143
+ role="visitor",
144
+ project_types=["2d", "3d"],
145
+ is_active=True,
146
+ ).revision
147
+ == 4
148
+ )
149
+ assert (
150
+ client.accounts.reset_password(
151
+ 7, expected_revision=4, password="replacement-password"
152
+ ).revision
153
+ == 5
154
+ )
155
+ assert (
156
+ client.accounts.change_password(
157
+ current_password="replacement-password", new_password="another-password"
158
+ ).revision
159
+ == 6
160
+ )
161
+ assert "Authorization" not in client._http.headers
162
+ assert all(request.url.host == "platform.test" for request in requests)
163
+ assert all(
164
+ request.headers["Authorization"] == "Bearer private-token"
165
+ for request in requests
166
+ )
167
+ assert json.loads(requests[-1].content) == {
168
+ "current_password": "replacement-password",
169
+ "new_password": "another-password",
170
+ }
171
+
172
+
173
+ def test_client_rejects_foreign_responses_and_invalid_requests_before_io():
174
+ with X4DClient(api_url="http://platform.test") as client:
175
+ client._http.close()
176
+ client._http = httpx.Client(
177
+ base_url=client.api_url,
178
+ transport=httpx.MockTransport(
179
+ lambda request: httpx.Response(200, json=record(id=8))
180
+ ),
181
+ )
182
+ with pytest.raises(ValueError, match="another user"):
183
+ client.accounts.get(7)
184
+ with pytest.raises(ValueError, match="UTF-8"):
185
+ client.accounts.create(
186
+ username="user",
187
+ password="密" * 25,
188
+ role="visitor",
189
+ project_types=["2d"],
190
+ )
191
+ with pytest.raises(ValueError, match="boolean"):
192
+ client.accounts.update(
193
+ 7,
194
+ expected_revision=3,
195
+ role="visitor",
196
+ project_types=["2d"],
197
+ is_active=1,
198
+ )
@@ -76,6 +76,12 @@ def test_top_level_exports_only_stable_sdk_surface():
76
76
  import x4d_devkit
77
77
 
78
78
  assert set(x4d_devkit.__all__) == {
79
+ "IMAGE_GEOMETRY_CONTRACT_VERSION",
80
+ "ImageAttributeRanges",
81
+ "ImageGeometry",
82
+ "ImageGeometryError",
83
+ "parse_image_attribute_ranges",
84
+ "parse_image_geometry",
79
85
  "__version__",
80
86
  "VECTOR_MAP_CONTRACT_VERSION",
81
87
  "VectorMapDocument",
@@ -0,0 +1,272 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import json
5
+ from pathlib import Path
6
+ import sys
7
+
8
+ import pytest
9
+
10
+ from tests.current_clip_fixtures import current_clip_tables
11
+ from x4d_devkit import cli
12
+ from x4d_devkit.annotation_contract import build_clip_annotations
13
+ from x4d_devkit.audit import audit_category_mapping, collect_category_stats
14
+ from x4d_devkit.training_identity import build_training_identity_manifest
15
+
16
+
17
+ def _clip(path: Path, categories: list[tuple[int, str]]) -> Path:
18
+ """Write valid metadata only: sensor payloads deliberately do not exist."""
19
+ path.mkdir()
20
+ tables = current_clip_tables(clip_id=path.name, sample_count=2)
21
+ template = tables["annotation.json"]["annotations"][0]
22
+ rows = []
23
+ for index, (frame, category) in enumerate(categories):
24
+ row = copy.deepcopy(template)
25
+ row.update(
26
+ annotation_token=f"annotation-{index}", sample_token=f"sample-{frame}",
27
+ instance_token=f"instance-{index}", category=category,
28
+ attributes={} if category == "pedestrian" else {"vehicle.with_driver": True},
29
+ )
30
+ rows.append(row)
31
+ tables["annotation.json"] = build_clip_annotations(path.name, "base_link", rows).to_dict()
32
+ for name, content in tables.items():
33
+ (path / name).write_text(json.dumps(content))
34
+ return path
35
+
36
+
37
+ @pytest.fixture
38
+ def clips(tmp_path):
39
+ return [
40
+ _clip(tmp_path / "a", [(0, "car"), (0, "car"), (1, "car"), (0, "truck"), (1, "pedestrian")]),
41
+ _clip(tmp_path / "b", [(0, "car")]),
42
+ ]
43
+
44
+
45
+ def _manifest(path, clips):
46
+ entries = []
47
+ for index, clip in enumerate(clips):
48
+ annotations = json.loads((clip / "annotation.json").read_text())
49
+ meta = json.loads((clip / "meta.json").read_text())
50
+ entries.append({
51
+ "project_id": 10 + index, "clip_id": clip.name, "source_revision": "source",
52
+ "samples_revision": "sha256:" + "1" * 64,
53
+ "sweeps_revision": "sha256:" + "2" * 64,
54
+ "calibration_revision": "calibration", "pose_revision": "pose",
55
+ "annotation_revision": annotations["annotation_revision"],
56
+ "semantic_scene_revision": None, "label_schema": meta["label_schema"],
57
+ "clip_schema_version": "0.18", "materialization_policy": "full",
58
+ "clip_dir": clip.name, "split": "train" if index == 0 else "validation",
59
+ })
60
+ value = build_training_identity_manifest(
61
+ projects=[{"project_id": 10 + index, "project_name": f"project-{index}"}
62
+ for index in range(len(clips))],
63
+ clips=entries,
64
+ )
65
+ path.write_text(json.dumps(value))
66
+ return path
67
+
68
+
69
+ def test_counts_use_occurrences_and_distinct_samples_without_media(clips):
70
+ report = collect_category_stats(clip_dirs=clips).to_dict()
71
+
72
+ assert report["summary"] == {"clip_count": 2, "sample_count": 4, "annotation_count": 6}
73
+ assert report["categories"] == [
74
+ {"category": "car", "annotation_count": 4, "sample_count": 3},
75
+ {"category": "pedestrian", "annotation_count": 1, "sample_count": 1},
76
+ {"category": "truck", "annotation_count": 1, "sample_count": 1},
77
+ ]
78
+ assert len(report["clips"]) == 2
79
+ assert report["clips"][1]["summary"]["sample_count"] == 2
80
+ source = report["clips"][0]["source"]
81
+ assert source["annotation_revision"].startswith("sha256:")
82
+ assert source["sample_index_revision"].startswith("sha256:")
83
+ assert source["sample_ids"] is None
84
+ assert source["label_schema"]["schema_id"] == "synthetic_road_scene"
85
+ assert not list(clips[0].rglob("*.bin"))
86
+
87
+
88
+ def test_reports_are_deterministic_and_return_isolated_data(clips):
89
+ stats = collect_category_stats(clip_dirs=clips)
90
+ report = stats.to_dict()
91
+ assert report == collect_category_stats(clip_dirs=list(reversed(clips))).to_dict()
92
+ report["clips"][0]["source"]["label_schema"]["schema_id"] = "tampered"
93
+ assert stats.to_dict()["clips"][0]["source"]["label_schema"]["schema_id"] == "synthetic_road_scene"
94
+
95
+
96
+ def test_many_to_one_mapping_unions_frames_and_separates_explicit_drops(clips):
97
+ stats = collect_category_stats(clip_dirs=clips)
98
+ result = audit_category_mapping(stats, {"truck": "Vehicle", "car": "Vehicle", "pedestrian": None, "unused": "Other"})
99
+
100
+ assert result["mapped_categories"] == [{
101
+ "category": "Vehicle", "annotation_count": 5, "sample_count": 3,
102
+ "raw_categories": ["car", "truck"],
103
+ }]
104
+ assert result["dropped_categories"] == [{"category": "pedestrian", "annotation_count": 1, "sample_count": 1}]
105
+ assert result["unmapped_categories"] == []
106
+ assert result["unused_mapping_keys"] == ["unused"]
107
+ assert result["summary"] == {
108
+ "mapped_annotation_count": 5, "dropped_annotation_count": 1, "unmapped_annotation_count": 0,
109
+ }
110
+
111
+
112
+ def test_case_sensitive_mapping_does_not_infer_unknown_category_policy(clips):
113
+ stats = collect_category_stats(clip_dirs=clips)
114
+ result = audit_category_mapping(stats, {"Car": "Vehicle", "pedestrian": None})
115
+ assert result["summary"] == {
116
+ "mapped_annotation_count": 0, "dropped_annotation_count": 1, "unmapped_annotation_count": 5,
117
+ }
118
+ assert result["unused_mapping_keys"] == ["Car"]
119
+ assert [row["category"] for row in result["unmapped_categories"]] == ["car", "truck"]
120
+
121
+
122
+ def test_empty_annotations_include_selected_empty_frames(tmp_path):
123
+ clip = _clip(tmp_path / "empty", [])
124
+ stats = collect_category_stats(clip_dirs=[clip])
125
+ assert stats.to_dict()["summary"] == {"clip_count": 1, "sample_count": 2, "annotation_count": 0}
126
+ assert stats.to_dict()["categories"] == []
127
+ assert audit_category_mapping(stats, {})["mapped_categories"] == []
128
+
129
+
130
+ def test_sample_selection_is_exact_and_counts_empty_selected_frames(clips):
131
+ report = collect_category_stats(
132
+ clip_dirs=clips, sample_ids={"a": ["sample-1"], "b": ["sample-1"]}
133
+ ).to_dict()
134
+ assert report["summary"] == {"clip_count": 2, "sample_count": 2, "annotation_count": 2}
135
+ assert report["clips"][0]["source"]["sample_ids"] == ["sample-1"]
136
+ assert report["clips"][1]["summary"]["annotation_count"] == 0
137
+
138
+
139
+ @pytest.mark.parametrize("selection, message", [
140
+ ({"a": ["sample-0"]}, "missing Clip"),
141
+ ({"a": ["sample-0"], "b": ["sample-0"], "extra": ["sample-0"]}, "outside"),
142
+ ({"a": ["absent"], "b": ["sample-0"]}, "unknown keyframe"),
143
+ ({"a": ["sample-0", "sample-0"], "b": ["sample-0"]}, "duplicate"),
144
+ ({"a": [], "b": ["sample-0"]}, "non-empty"),
145
+ ({"a": "sample-0", "b": ["sample-0"]}, "token list"),
146
+ ({"a": [False], "b": ["sample-0"]}, "strings"),
147
+ ])
148
+ def test_invalid_sample_selection_fails_instead_of_partial_report(clips, selection, message):
149
+ with pytest.raises(ValueError, match=message):
150
+ collect_category_stats(clip_dirs=clips, sample_ids=selection)
151
+
152
+
153
+ def test_manifest_resolves_relative_paths_and_exact_split_with_provenance(clips, tmp_path, monkeypatch):
154
+ manifest = _manifest(tmp_path / "training.json", clips)
155
+ monkeypatch.chdir("/tmp")
156
+ report = collect_category_stats(manifest=manifest, split="train").to_dict()
157
+ assert report["summary"] == {"clip_count": 1, "sample_count": 2, "annotation_count": 5}
158
+ assert report["manifest"]["path"] == str(manifest)
159
+ assert report["manifest"]["sha256"].startswith("sha256:")
160
+ assert report["groups"][0]["project_id"] == 10
161
+ assert report["groups"][0]["split"] == "train"
162
+ assert report["clips"][0]["source"]["clip_dir"] == str(clips[0])
163
+ with pytest.raises(ValueError, match="no Clips matched"):
164
+ collect_category_stats(manifest=manifest, split="Train")
165
+
166
+
167
+ @pytest.mark.parametrize("field,value", [
168
+ ("annotation_revision", "sha256:" + "0" * 64),
169
+ ("clip_id", "different-clip"),
170
+ ])
171
+ def test_stale_manifest_identity_fails(clips, tmp_path, field, value):
172
+ manifest = _manifest(tmp_path / "training.json", clips)
173
+ data = json.loads(manifest.read_text())
174
+ data["clips"][0][field] = value
175
+ manifest.write_text(json.dumps(data))
176
+ with pytest.raises(ValueError, match=field + " does not match"):
177
+ collect_category_stats(manifest=manifest)
178
+
179
+
180
+ def test_stale_manifest_schema_identity_fails(clips, tmp_path):
181
+ manifest = _manifest(tmp_path / "training.json", clips)
182
+ data = json.loads(manifest.read_text())
183
+ data["clips"][0]["label_schema"]["fingerprint"] = "sha256:" + "0" * 64
184
+ manifest.write_text(json.dumps(data))
185
+ with pytest.raises(ValueError, match="label_schema does not match"):
186
+ collect_category_stats(manifest=manifest)
187
+
188
+
189
+ @pytest.mark.parametrize("mutation,message", [
190
+ (lambda data: data.update(manifest_version="x4d-training-identity-v4"), "invalid"),
191
+ (lambda data: data["clips"][0].pop("clip_dir"), "requires clip_dir"),
192
+ (lambda data: data["clips"][0].update(ready=False), "not ready"),
193
+ ])
194
+ def test_manifest_rejects_legacy_missing_and_unready_inputs(clips, tmp_path, mutation, message):
195
+ manifest = _manifest(tmp_path / "training.json", clips)
196
+ data = json.loads(manifest.read_text())
197
+ mutation(data)
198
+ manifest.write_text(json.dumps(data))
199
+ with pytest.raises(ValueError, match=message):
200
+ collect_category_stats(manifest=manifest)
201
+
202
+
203
+ def test_invalid_duplicate_missing_and_legacy_clips_fail(clips, tmp_path):
204
+ with pytest.raises(ValueError, match="duplicate Clip ID"):
205
+ collect_category_stats(clip_dirs=[clips[0], clips[0]])
206
+ with pytest.raises(FileNotFoundError):
207
+ collect_category_stats(clip_dirs=[tmp_path / "absent"])
208
+ meta_path = clips[0] / "meta.json"
209
+ meta = json.loads(meta_path.read_text())
210
+ meta["schema_version"] = "0.17"
211
+ meta_path.write_text(json.dumps(meta))
212
+ with pytest.raises(ValueError, match="contract validation failed"):
213
+ collect_category_stats(clip_dirs=clips)
214
+
215
+
216
+ def test_annotation_schema_validation_cannot_be_bypassed(clips):
217
+ path = clips[0] / "annotation.json"
218
+ data = json.loads(path.read_text())
219
+ data["annotations"][0]["attributes"] = {"not_a_declared_attribute": True}
220
+ path.write_text(json.dumps(build_clip_annotations("a", "base_link", data["annotations"]).to_dict()))
221
+ with pytest.raises(ValueError, match="contract validation failed"):
222
+ collect_category_stats(clip_dirs=clips)
223
+
224
+
225
+ @pytest.mark.parametrize("mapping", [{"": "Vehicle"}, {"car ": "Vehicle"}, {"car": ""}, {"car": False}, {"car": 1}, {"car": ["Vehicle"]}, []])
226
+ def test_mapping_rejects_coercions_and_ambiguous_strings(clips, mapping):
227
+ with pytest.raises((ValueError, TypeError)):
228
+ audit_category_mapping(collect_category_stats(clip_dirs=clips), mapping)
229
+
230
+
231
+ def test_mapping_cannot_reconstruct_sample_overlap_from_aggregate_json(clips):
232
+ with pytest.raises(TypeError, match="result of collect_category_stats"):
233
+ audit_category_mapping(collect_category_stats(clip_dirs=clips).to_dict(), {})
234
+
235
+
236
+ @pytest.mark.parametrize("kwargs", [{}, {"clip_dirs": []}, {"clip_dirs": "directory"}, {"clip_dirs": [], "manifest": "file"}, {"clip_dirs": [], "split": "train"}])
237
+ def test_input_selection_is_explicit(kwargs):
238
+ with pytest.raises(ValueError):
239
+ collect_category_stats(**kwargs)
240
+
241
+
242
+ def test_cli_matches_api_for_categories_and_mapping(clips, tmp_path, monkeypatch, capsys):
243
+ monkeypatch.setattr(sys, "argv", ["x4d-devkit", "dataset", "categories", "--clips", *map(str, clips), "--format", "json"])
244
+ cli.main()
245
+ stats = collect_category_stats(clip_dirs=clips)
246
+ assert json.loads(capsys.readouterr().out) == stats.to_dict()
247
+ mapping = {"car": "Vehicle", "truck": "Vehicle", "pedestrian": None}
248
+ path = tmp_path / "mapping.json"
249
+ path.write_text(json.dumps(mapping))
250
+ monkeypatch.setattr(sys, "argv", ["x4d-devkit", "dataset", "audit-mapping", "--clips", *map(str, clips), "--mapping", str(path), "--format", "json"])
251
+ cli.main()
252
+ assert json.loads(capsys.readouterr().out) == audit_category_mapping(stats, mapping)
253
+
254
+
255
+ def test_cli_manifest_sample_selection_and_table(clips, tmp_path, monkeypatch, capsys):
256
+ manifest = _manifest(tmp_path / "training.json", clips)
257
+ samples = tmp_path / "samples.json"
258
+ samples.write_text(json.dumps({"a": ["sample-1"]}))
259
+ monkeypatch.setattr(sys, "argv", ["x4d-devkit", "dataset", "categories", "--manifest", str(manifest), "--split", "train", "--samples", str(samples)])
260
+ cli.main()
261
+ output = capsys.readouterr().out
262
+ assert "1 Clips, 1 samples, 2 annotations" in output
263
+ assert "pedestrian" in output
264
+
265
+
266
+ def test_cli_rejects_duplicate_json_mapping_keys_without_partial_output(clips, tmp_path, monkeypatch, capsys):
267
+ path = tmp_path / "mapping.json"
268
+ path.write_text('{"car":"Vehicle","car":null}')
269
+ monkeypatch.setattr(sys, "argv", ["x4d-devkit", "dataset", "audit-mapping", "--clips", str(clips[0]), "--mapping", str(path), "--format", "json"])
270
+ with pytest.raises(SystemExit, match="duplicate JSON field"):
271
+ cli.main()
272
+ assert capsys.readouterr().out == ""