x4d-devkit 0.48.0rc5__tar.gz → 0.48.0rc7__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 (158) hide show
  1. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/PKG-INFO +1 -1
  2. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/pyproject.toml +1 -1
  3. x4d_devkit-0.48.0rc7/tests/test_accounts.py +198 -0
  4. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_export.py +25 -0
  5. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_sdk_api.py +1 -1
  6. x4d_devkit-0.48.0rc7/x4d_devkit/accounts.py +147 -0
  7. x4d_devkit-0.48.0rc7/x4d_devkit/client/accounts.py +111 -0
  8. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/client.py +2 -0
  9. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_export.py +17 -2
  10. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit.egg-info/PKG-INFO +1 -1
  11. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit.egg-info/SOURCES.txt +3 -0
  12. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/LICENSE +0 -0
  13. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/README.md +0 -0
  14. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/setup.cfg +0 -0
  15. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_annotation_contract.py +0 -0
  16. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_annotation_reference.py +0 -0
  17. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_assets_client.py +0 -0
  18. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_boundary.py +0 -0
  19. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_box.py +0 -0
  20. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_build_info.py +0 -0
  21. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_category_audit.py +0 -0
  22. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_clean_basemap.py +0 -0
  23. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_clean_basemap_client.py +0 -0
  24. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_cli_auth.py +0 -0
  25. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_client_json_contract.py +0 -0
  26. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_client_projects.py +0 -0
  27. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_clip_archives.py +0 -0
  28. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_contract_validation.py +0 -0
  29. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_coordinate_frames.py +0 -0
  30. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_current_state.py +0 -0
  31. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_current_state_identity_consumers_example.py +0 -0
  32. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_dataset_contract.py +0 -0
  33. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_dataset_identity.py +0 -0
  34. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_detection_eval.py +0 -0
  35. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_evaluate.py +0 -0
  36. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_export_bundle.py +0 -0
  37. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_clip.py +0 -0
  38. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_geometry.py +0 -0
  39. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_preannotation.py +0 -0
  40. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_reference.py +0 -0
  41. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_review.py +0 -0
  42. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_image_work_session.py +0 -0
  43. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_inference_input.py +0 -0
  44. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_inference_schemas.py +0 -0
  45. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_inference_service.py +0 -0
  46. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_label_schemas.py +0 -0
  47. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_lidar_materialization.py +0 -0
  48. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_loader.py +0 -0
  49. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_matching.py +0 -0
  50. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_metrics.py +0 -0
  51. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_models.py +0 -0
  52. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_platform_pose.py +0 -0
  53. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_preannotation_client.py +0 -0
  54. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_reconstruction_client.py +0 -0
  55. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_release_gate.py +0 -0
  56. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_roadmark_preview.py +0 -0
  57. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_semantic_scene_authoring.py +0 -0
  58. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_semantic_scene_client.py +0 -0
  59. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_semantic_scene_contract.py +0 -0
  60. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_semantic_scene_independent.py +0 -0
  61. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_semantic_scene_native_clip.py +0 -0
  62. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_source_attributes.py +0 -0
  63. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_time_contract.py +0 -0
  64. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_token.py +0 -0
  65. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_tracking_eval.py +0 -0
  66. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_training_identity.py +0 -0
  67. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_transform.py +0 -0
  68. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_transform_chain.py +0 -0
  69. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_validation.py +0 -0
  70. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_vector_map.py +0 -0
  71. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_vector_map_geometry.py +0 -0
  72. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_vector_map_session.py +0 -0
  73. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_work_session.py +0 -0
  74. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/tests/test_work_session_validation_reuse.py +0 -0
  75. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/__init__.py +0 -0
  76. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/_image_contract.py +0 -0
  77. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/annotation_contract.py +0 -0
  78. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/annotation_reference.py +0 -0
  79. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/audit.py +0 -0
  80. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/build_info.py +0 -0
  81. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/clean_basemap.py +0 -0
  82. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/cli.py +0 -0
  83. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/__init__.py +0 -0
  84. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/_origin.py +0 -0
  85. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/annotation_reference.py +0 -0
  86. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/assets.py +0 -0
  87. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/bags.py +0 -0
  88. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/capabilities.py +0 -0
  89. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/checkpoints.py +0 -0
  90. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/clean_basemap.py +0 -0
  91. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/clip_work_sessions.py +0 -0
  92. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/clips.py +0 -0
  93. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/dataset_identity.py +0 -0
  94. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/image_clips.py +0 -0
  95. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/image_exports.py +0 -0
  96. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/image_preannotations.py +0 -0
  97. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/image_review.py +0 -0
  98. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/image_work_sessions.py +0 -0
  99. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/label_schemas.py +0 -0
  100. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/preannotation.py +0 -0
  101. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/projects.py +0 -0
  102. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/reconstruction.py +0 -0
  103. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/semantic_scene.py +0 -0
  104. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  105. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/services.py +0 -0
  106. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/client/test_sets.py +0 -0
  107. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/consumer_identity.py +0 -0
  108. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/coordinate_contract.py +0 -0
  109. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/core/__init__.py +0 -0
  110. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/core/loader.py +0 -0
  111. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/core/models.py +0 -0
  112. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/core/token.py +0 -0
  113. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/core/transform.py +0 -0
  114. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/current_state.py +0 -0
  115. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/dataset_contract.py +0 -0
  116. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/dataset_identity.py +0 -0
  117. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/__init__.py +0 -0
  118. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/api.py +0 -0
  119. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/box.py +0 -0
  120. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/detection.py +0 -0
  121. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/matching.py +0 -0
  122. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/metrics.py +0 -0
  123. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/eval/tracking.py +0 -0
  124. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/export_bundle.py +0 -0
  125. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_clip.py +0 -0
  126. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_geometry.py +0 -0
  127. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_preannotation.py +0 -0
  128. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_reference.py +0 -0
  129. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_review.py +0 -0
  130. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_schema.py +0 -0
  131. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/image_work_session.py +0 -0
  132. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/__init__.py +0 -0
  133. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/input.py +0 -0
  134. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/models.py +0 -0
  135. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/schemas.py +0 -0
  136. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/service.py +0 -0
  137. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/inference/tasks.py +0 -0
  138. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/json_contract.py +0 -0
  139. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/label_schema.py +0 -0
  140. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/lidar_materialization.py +0 -0
  141. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/materialize.py +0 -0
  142. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/roadmark_preview.py +0 -0
  143. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/semantic_scene_authoring.py +0 -0
  144. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/semantic_scene_contract.py +0 -0
  145. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/time_contract.py +0 -0
  146. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/training_identity.py +0 -0
  147. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/validation/__init__.py +0 -0
  148. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/validation/contract.py +0 -0
  149. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/validation/platform_pose.py +0 -0
  150. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/validation/report.py +0 -0
  151. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/validation/validator.py +0 -0
  152. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/vector_map.py +0 -0
  153. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/vector_map_geometry.py +0 -0
  154. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit/work_session.py +0 -0
  155. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  156. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit.egg-info/entry_points.txt +0 -0
  157. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/x4d_devkit.egg-info/requires.txt +0 -0
  158. {x4d_devkit-0.48.0rc5 → x4d_devkit-0.48.0rc7}/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.0rc5
3
+ Version: 0.48.0rc7
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.0rc5"
7
+ version = "0.48.0rc7"
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
+ )
@@ -215,6 +215,9 @@ def job(status="queued"):
215
215
  "created_at": NOW,
216
216
  "completed_at": NOW if terminal else None,
217
217
  "can_cancel": not terminal,
218
+ "completion_counts": {"unprocessed": 2, "complete": 0, "confirmed_empty": 0}
219
+ if status in {"packaging", "validating", "uploading", "completed"}
220
+ else None,
218
221
  }
219
222
 
220
223
 
@@ -255,6 +258,14 @@ def test_invalid_job_states_fail(change):
255
258
  value["completed_at"] = NOW
256
259
  if change == "infinite":
257
260
  value["progress"]["rate"] = float("inf")
261
+ if change == "counts":
262
+ value["completion_counts"] = {
263
+ "unprocessed": -1,
264
+ "complete": 0,
265
+ "confirmed_empty": 0,
266
+ }
267
+ if change == "unsealed":
268
+ value["status"] = value["progress"]["stage"] = "packaging"
258
269
  with pytest.raises((ImageContractError, ValueError)):
259
270
  parse_image_export_job(value)
260
271
 
@@ -276,6 +287,7 @@ def test_typed_client_scope_and_ephemeral_download_do_not_forward_credentials():
276
287
  "clip_ids": ["clip_test"],
277
288
  "excluded": [],
278
289
  "scope_token": "opaque-token",
290
+ "completed_scope_count": 0,
279
291
  "expires_at": NOW,
280
292
  },
281
293
  )
@@ -387,3 +399,16 @@ def test_archive_member_history_is_not_retained(tmp_path, monkeypatch):
387
399
  monkeypatch.setattr(tarfile.TarFile, "next", measured)
388
400
  validate_image_export_bundle(path)
389
401
  assert max(history) <= 1
402
+
403
+
404
+ def test_completed_job_reports_zero_authoritative_scopes_and_rejects_unknown_counts():
405
+ value = job("completed")
406
+ assert parse_image_export_job(value).to_dict()["completion_counts"]["complete"] == 0
407
+ for counts in (
408
+ None,
409
+ {"unprocessed": True, "complete": 0, "confirmed_empty": 0},
410
+ {"complete": 2},
411
+ ):
412
+ value["completion_counts"] = counts
413
+ with pytest.raises(ImageContractError):
414
+ parse_image_export_job(value)
@@ -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.0rc5"
14
+ assert x4d_devkit.__version__ == "0.48.0rc7"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -0,0 +1,147 @@
1
+ """Strict managed-account documents; the platform owns authorization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import asdict, dataclass
6
+ import unicodedata
7
+ from typing import Any
8
+
9
+ from x4d_devkit.json_contract import ensure_i_json
10
+
11
+ ACCOUNT_CONTRACT = "x4d-account-v1"
12
+ ACCOUNT_PAGE_CONTRACT = "x4d-account-page-v1"
13
+
14
+
15
+ def _require(condition: bool, message: str) -> None:
16
+ if not condition:
17
+ raise ValueError(message)
18
+
19
+
20
+ def account_integer(value: Any, name: str, *, minimum=0) -> int:
21
+ _require(
22
+ type(value) is int and minimum <= value <= 2**53 - 1,
23
+ f"{name} must be an exact integer >= {minimum}",
24
+ )
25
+ return value
26
+
27
+
28
+ def validate_username(value: Any) -> str:
29
+ _require(
30
+ type(value) is str
31
+ and 1 <= len(value) <= 64
32
+ and value == value.strip()
33
+ and not any(unicodedata.category(c).startswith("C") for c in value),
34
+ "username must be canonical text of 1–64 characters without controls",
35
+ )
36
+ return value
37
+
38
+
39
+ def validate_account_password(value: Any) -> str:
40
+ _require(
41
+ type(value) is str and len(value) >= 8 and bool(value.strip()),
42
+ "password must contain at least 8 characters and not be blank",
43
+ )
44
+ try:
45
+ encoded = value.encode("utf-8")
46
+ except UnicodeError:
47
+ raise ValueError("password must be valid UTF-8") from None
48
+ _require(len(encoded) <= 72, "password must not exceed 72 UTF-8 bytes")
49
+ return value
50
+
51
+
52
+ def validate_project_types(value: Any) -> list[str]:
53
+ _require(
54
+ type(value) is list
55
+ and all(type(v) is str and v in ("2d", "3d") for v in value)
56
+ and value == sorted(set(value)),
57
+ "project_types must be a sorted unique subset of ['2d', '3d']",
58
+ )
59
+ return list(value)
60
+
61
+
62
+ def validate_account_role(value: Any) -> str:
63
+ _require(
64
+ type(value) is str and value in ("admin", "visitor"),
65
+ "role must be admin or visitor",
66
+ )
67
+ return value
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class AccountRecord:
72
+ """An immutable public account; contains no credentials."""
73
+
74
+ id: int
75
+ username: str
76
+ role: str
77
+ is_active: bool
78
+ project_types: tuple[str, ...]
79
+ revision: int
80
+
81
+ def to_dict(self) -> dict:
82
+ value = asdict(self)
83
+ value["contract_version"] = ACCOUNT_CONTRACT
84
+ value["project_types"] = list(self.project_types)
85
+ return value
86
+
87
+
88
+ def parse_account_record(value: Any) -> AccountRecord:
89
+ ensure_i_json(value, path="account")
90
+ _require(
91
+ type(value) is dict
92
+ and set(value)
93
+ == {
94
+ "contract_version",
95
+ "id",
96
+ "username",
97
+ "role",
98
+ "is_active",
99
+ "project_types",
100
+ "revision",
101
+ },
102
+ "account fields do not match the current contract",
103
+ )
104
+ _require(
105
+ value["contract_version"] == ACCOUNT_CONTRACT, "unsupported account contract"
106
+ )
107
+ _require(type(value["is_active"]) is bool, "is_active must be boolean")
108
+ return AccountRecord(
109
+ account_integer(value["id"], "id", minimum=1),
110
+ validate_username(value["username"]),
111
+ validate_account_role(value["role"]),
112
+ value["is_active"],
113
+ tuple(validate_project_types(value["project_types"])),
114
+ account_integer(value["revision"], "revision"),
115
+ )
116
+
117
+
118
+ def parse_account_page(value: Any) -> dict:
119
+ ensure_i_json(value, path="account_page")
120
+ _require(
121
+ type(value) is dict
122
+ and set(value) == {"contract_version", "page", "page_size", "total", "items"},
123
+ "account page fields do not match the current contract",
124
+ )
125
+ _require(
126
+ value["contract_version"] == ACCOUNT_PAGE_CONTRACT,
127
+ "unsupported account page contract",
128
+ )
129
+ page = account_integer(value["page"], "page", minimum=1)
130
+ size = account_integer(value["page_size"], "page_size", minimum=1)
131
+ total = account_integer(value["total"], "total")
132
+ _require(
133
+ size <= 100 and type(value["items"]) is list, "invalid account page size/items"
134
+ )
135
+ items = [parse_account_record(row).to_dict() for row in value["items"]]
136
+ _require(
137
+ len(items) <= min(size, total)
138
+ and len({row["id"] for row in items}) == len(items),
139
+ "account page has duplicate or excess items",
140
+ )
141
+ return {
142
+ "contract_version": ACCOUNT_PAGE_CONTRACT,
143
+ "page": page,
144
+ "page_size": size,
145
+ "total": total,
146
+ "items": items,
147
+ }
@@ -0,0 +1,111 @@
1
+ """Typed adapters for platform-owned account capabilities."""
2
+
3
+ from x4d_devkit.accounts import (
4
+ account_integer,
5
+ parse_account_page,
6
+ parse_account_record,
7
+ validate_account_password,
8
+ validate_account_role,
9
+ validate_project_types,
10
+ validate_username,
11
+ )
12
+
13
+
14
+ class AccountsAPI:
15
+ def __init__(self, client):
16
+ self._client = client
17
+
18
+ def me(self):
19
+ return parse_account_record(self._client.get("/users/me"))
20
+
21
+ def get(self, user_id):
22
+ account_integer(user_id, "user_id", minimum=1)
23
+ return self._record(self._client.get(f"/users/{user_id}"), user_id)
24
+
25
+ def list(self, *, page=1, page_size=50):
26
+ account_integer(page, "page", minimum=1)
27
+ account_integer(page_size, "page_size", minimum=1)
28
+ if page_size > 100:
29
+ raise ValueError("page_size exceeds 100")
30
+ result = parse_account_page(
31
+ self._client.get("/users", params={"page": page, "page_size": page_size})
32
+ )
33
+ if result["page"] != page or result["page_size"] != page_size:
34
+ raise ValueError("account page belongs to another request")
35
+ return result
36
+
37
+ def create(self, *, username, password, role, project_types):
38
+ body = {
39
+ "username": validate_username(username),
40
+ "password": validate_account_password(password),
41
+ "role": validate_account_role(role),
42
+ "project_types": validate_project_types(project_types),
43
+ }
44
+ result = parse_account_record(self._client.post("/users", json=body))
45
+ if (
46
+ result.username != username
47
+ or result.role != role
48
+ or list(result.project_types) != project_types
49
+ ):
50
+ raise ValueError("created account belongs to another request")
51
+ return result
52
+
53
+ def update(self, user_id, *, expected_revision, role, project_types, is_active):
54
+ account_integer(user_id, "user_id", minimum=1)
55
+ account_integer(expected_revision, "expected_revision")
56
+ if type(is_active) is not bool:
57
+ raise ValueError("is_active must be boolean")
58
+ body = {
59
+ "expected_revision": expected_revision,
60
+ "role": validate_account_role(role),
61
+ "project_types": validate_project_types(project_types),
62
+ "is_active": is_active,
63
+ }
64
+ result = self._record(
65
+ self._client.post(f"/users/{user_id}/update", json=body), user_id
66
+ )
67
+ if result.revision < expected_revision or any(
68
+ result.to_dict()[key] != body[key]
69
+ for key in ("role", "project_types", "is_active")
70
+ ):
71
+ raise ValueError("updated account belongs to another request")
72
+ return result
73
+
74
+ def reset_password(self, user_id, *, expected_revision, password):
75
+ account_integer(user_id, "user_id", minimum=1)
76
+ account_integer(expected_revision, "expected_revision")
77
+ result = self._record(
78
+ self._client.post(
79
+ f"/users/{user_id}/reset-password",
80
+ json={
81
+ "expected_revision": expected_revision,
82
+ "password": validate_account_password(password),
83
+ },
84
+ ),
85
+ user_id,
86
+ )
87
+ if result.revision <= expected_revision:
88
+ raise ValueError("password reset did not advance the account revision")
89
+ return result
90
+
91
+ def change_password(self, *, current_password, new_password):
92
+ if type(current_password) is not str or not current_password:
93
+ raise ValueError("current_password must be explicit text")
94
+ result = parse_account_record(
95
+ self._client.post(
96
+ "/users/me/password",
97
+ json={
98
+ "current_password": current_password,
99
+ "new_password": validate_account_password(new_password),
100
+ },
101
+ )
102
+ )
103
+ self._client.set_token(None)
104
+ return result
105
+
106
+ @staticmethod
107
+ def _record(value, user_id):
108
+ result = parse_account_record(value)
109
+ if result.id != user_id:
110
+ raise ValueError("account response belongs to another user")
111
+ return result
@@ -15,6 +15,7 @@ from typing import TYPE_CHECKING
15
15
  import httpx
16
16
 
17
17
  from x4d_devkit.client._origin import validated_origin
18
+ from x4d_devkit.client.accounts import AccountsAPI
18
19
  from x4d_devkit.client.assets import AssetsAPI
19
20
  from x4d_devkit.client.checkpoints import CheckpointsAPI
20
21
  from x4d_devkit.client.bags import BagsAPI
@@ -67,6 +68,7 @@ class X4DClient:
67
68
  base_url=self._api_url, headers=headers, timeout=timeout
68
69
  )
69
70
  self.assets = AssetsAPI(self)
71
+ self.accounts = AccountsAPI(self)
70
72
  self.bags = BagsAPI(self)
71
73
  self.capabilities = CapabilitiesAPI(self)
72
74
  self.clip_work_sessions = ClipWorkSessionsAPI(self)
@@ -29,8 +29,8 @@ from x4d_devkit.json_contract import parse_i_json
29
29
  EXPORT_CONTRACT = "x4d-image-export-v1"
30
30
  INDEX_CONTRACT = "x4d-image-export-index-v1"
31
31
  SCOPES_CONTRACT = "x4d-image-human-scopes-v1"
32
- PREVIEW_CONTRACT = "x4d-image-export-preview-v1"
33
- JOB_CONTRACT = "x4d-image-export-job-v1"
32
+ PREVIEW_CONTRACT = "x4d-image-export-preview-v2"
33
+ JOB_CONTRACT = "x4d-image-export-job-v2"
34
34
  PAGE_CONTRACT = "x4d-image-export-page-v1"
35
35
  DOWNLOAD_CONTRACT = "x4d-image-export-download-v1"
36
36
  COMPONENTS = (*IMAGE_CLIP_COMPONENTS, "identity")
@@ -409,6 +409,7 @@ def parse_image_export_preview(value):
409
409
  "excluded",
410
410
  "scope_token",
411
411
  "expires_at",
412
+ "completed_scope_count",
412
413
  },
413
414
  "export_preview",
414
415
  )
@@ -418,6 +419,8 @@ def parse_image_export_preview(value):
418
419
  "contract_version",
419
420
  )
420
421
  _scope(value)
422
+ if value["completed_scope_count"] is not None:
423
+ integer(value["completed_scope_count"], "completed_scope_count")
421
424
  _ids(value["clip_ids"])
422
425
  if value["clip_id"] is not None:
423
426
  require(
@@ -467,6 +470,7 @@ def parse_image_export_job(value):
467
470
  "created_at",
468
471
  "completed_at",
469
472
  "can_cancel",
473
+ "completion_counts",
470
474
  },
471
475
  "export_job",
472
476
  )
@@ -528,6 +532,17 @@ def parse_image_export_job(value):
528
532
  "failure needs exactly one explicit error",
529
533
  "error",
530
534
  )
535
+ counts = value["completion_counts"]
536
+ if counts is not None:
537
+ fields(counts, set(COMPLETION_STATES), "completion_counts")
538
+ for count in counts.values():
539
+ integer(count, "completion_counts")
540
+ require(
541
+ value["status"] not in {"packaging", "validating", "uploading", "completed"}
542
+ or counts is not None,
543
+ "sealed export must report authoritative completion",
544
+ "completion_counts",
545
+ )
531
546
  if value["status"] == "completed":
532
547
  _artifact(value["artifact"], value["export_id"])
533
548
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.48.0rc5
3
+ Version: 0.48.0rc7
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
@@ -1,6 +1,7 @@
1
1
  LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
+ tests/test_accounts.py
4
5
  tests/test_annotation_contract.py
5
6
  tests/test_annotation_reference.py
6
7
  tests/test_assets_client.py
@@ -65,6 +66,7 @@ tests/test_work_session.py
65
66
  tests/test_work_session_validation_reuse.py
66
67
  x4d_devkit/__init__.py
67
68
  x4d_devkit/_image_contract.py
69
+ x4d_devkit/accounts.py
68
70
  x4d_devkit/annotation_contract.py
69
71
  x4d_devkit/annotation_reference.py
70
72
  x4d_devkit/audit.py
@@ -105,6 +107,7 @@ x4d_devkit.egg-info/requires.txt
105
107
  x4d_devkit.egg-info/top_level.txt
106
108
  x4d_devkit/client/__init__.py
107
109
  x4d_devkit/client/_origin.py
110
+ x4d_devkit/client/accounts.py
108
111
  x4d_devkit/client/annotation_reference.py
109
112
  x4d_devkit/client/assets.py
110
113
  x4d_devkit/client/bags.py
File without changes
File without changes
File without changes