x4d-devkit 0.41.0rc1__tar.gz → 0.41.0rc2__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 (121) hide show
  1. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/PKG-INFO +1 -1
  2. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/pyproject.toml +1 -1
  3. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_preannotation_client.py +102 -5
  4. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_sdk_api.py +1 -1
  5. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/preannotation.py +104 -7
  6. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/PKG-INFO +1 -1
  7. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/LICENSE +0 -0
  8. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/README.md +0 -0
  9. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/setup.cfg +0 -0
  10. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_annotation_contract.py +0 -0
  11. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_annotation_reference.py +0 -0
  12. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_assets_client.py +0 -0
  13. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_boundary.py +0 -0
  14. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_box.py +0 -0
  15. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_build_info.py +0 -0
  16. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_cli_auth.py +0 -0
  17. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_client_json_contract.py +0 -0
  18. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_client_projects.py +0 -0
  19. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_clip_archives.py +0 -0
  20. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_contract_validation.py +0 -0
  21. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_coordinate_frames.py +0 -0
  22. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_current_state.py +0 -0
  23. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_current_state_identity_consumers_example.py +0 -0
  24. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_dataset_contract.py +0 -0
  25. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_dataset_identity.py +0 -0
  26. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_detection_eval.py +0 -0
  27. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_evaluate.py +0 -0
  28. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_export_bundle.py +0 -0
  29. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_image_review.py +0 -0
  30. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_inference_input.py +0 -0
  31. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_inference_schemas.py +0 -0
  32. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_inference_service.py +0 -0
  33. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_label_schemas.py +0 -0
  34. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_lidar_materialization.py +0 -0
  35. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_loader.py +0 -0
  36. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_matching.py +0 -0
  37. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_metrics.py +0 -0
  38. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_models.py +0 -0
  39. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_platform_pose.py +0 -0
  40. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_reconstruction_client.py +0 -0
  41. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_release_gate.py +0 -0
  42. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_authoring.py +0 -0
  43. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_client.py +0 -0
  44. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_contract.py +0 -0
  45. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_native_clip.py +0 -0
  46. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_time_contract.py +0 -0
  47. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_token.py +0 -0
  48. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_tracking_eval.py +0 -0
  49. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_training_identity.py +0 -0
  50. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_transform.py +0 -0
  51. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_transform_chain.py +0 -0
  52. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_validation.py +0 -0
  53. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_work_session.py +0 -0
  54. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/__init__.py +0 -0
  55. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/annotation_contract.py +0 -0
  56. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/annotation_reference.py +0 -0
  57. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/build_info.py +0 -0
  58. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/cli.py +0 -0
  59. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/__init__.py +0 -0
  60. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/_origin.py +0 -0
  61. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/annotation_reference.py +0 -0
  62. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/assets.py +0 -0
  63. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/bags.py +0 -0
  64. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/capabilities.py +0 -0
  65. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/checkpoints.py +0 -0
  66. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/client.py +0 -0
  67. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/clip_work_sessions.py +0 -0
  68. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/clips.py +0 -0
  69. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/dataset_identity.py +0 -0
  70. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/image_review.py +0 -0
  71. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/label_schemas.py +0 -0
  72. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/projects.py +0 -0
  73. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/reconstruction.py +0 -0
  74. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/semantic_scene.py +0 -0
  75. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  76. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/services.py +0 -0
  77. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/test_sets.py +0 -0
  78. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/consumer_identity.py +0 -0
  79. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/coordinate_contract.py +0 -0
  80. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/__init__.py +0 -0
  81. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/loader.py +0 -0
  82. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/models.py +0 -0
  83. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/token.py +0 -0
  84. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/transform.py +0 -0
  85. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/current_state.py +0 -0
  86. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/dataset_contract.py +0 -0
  87. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/dataset_identity.py +0 -0
  88. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/__init__.py +0 -0
  89. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/api.py +0 -0
  90. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/box.py +0 -0
  91. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/detection.py +0 -0
  92. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/matching.py +0 -0
  93. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/metrics.py +0 -0
  94. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/tracking.py +0 -0
  95. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/export_bundle.py +0 -0
  96. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/image_review.py +0 -0
  97. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/__init__.py +0 -0
  98. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/input.py +0 -0
  99. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/models.py +0 -0
  100. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/schemas.py +0 -0
  101. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/service.py +0 -0
  102. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/tasks.py +0 -0
  103. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/json_contract.py +0 -0
  104. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/label_schema.py +0 -0
  105. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/lidar_materialization.py +0 -0
  106. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/materialize.py +0 -0
  107. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/semantic_scene_authoring.py +0 -0
  108. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/semantic_scene_contract.py +0 -0
  109. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/time_contract.py +0 -0
  110. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/training_identity.py +0 -0
  111. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/__init__.py +0 -0
  112. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/contract.py +0 -0
  113. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/platform_pose.py +0 -0
  114. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/report.py +0 -0
  115. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/validator.py +0 -0
  116. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/work_session.py +0 -0
  117. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/SOURCES.txt +0 -0
  118. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  119. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/entry_points.txt +0 -0
  120. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/requires.txt +0 -0
  121. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/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.41.0rc1
3
+ Version: 0.41.0rc2
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.41.0rc1"
7
+ version = "0.41.0rc2"
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"
@@ -50,7 +50,7 @@ def _service() -> dict:
50
50
 
51
51
  def _run(*, status: str = "queued", reused: bool = False) -> dict:
52
52
  succeeded = status == "succeeded"
53
- return {
53
+ result = {
54
54
  "run_id": "preann-run-1",
55
55
  "request_identity": _digest("a"),
56
56
  "project_id": 7,
@@ -76,7 +76,34 @@ def _run(*, status: str = "queued", reused: bool = False) -> dict:
76
76
  "created_at": "2026-08-18T08:00:00Z",
77
77
  "updated_at": "2026-08-18T08:02:00Z",
78
78
  "reused": reused,
79
+ "attempt_number": 1,
80
+ "attempt_count": 1,
81
+ "latest_attempt_id": "preann-run-1",
82
+ "is_latest_attempt": True,
83
+ "retry_of_run_id": None,
79
84
  }
85
+ result["attempts"] = [
86
+ {
87
+ key: copy.deepcopy(result[key])
88
+ for key in (
89
+ "run_id",
90
+ "attempt_number",
91
+ "retry_of_run_id",
92
+ "status",
93
+ "progress",
94
+ "error",
95
+ "candidate_id",
96
+ "temporal_workflow_id",
97
+ "actor_id",
98
+ "started_at",
99
+ "completed_at",
100
+ "cancellation_requested_at",
101
+ "created_at",
102
+ "updated_at",
103
+ )
104
+ }
105
+ ]
106
+ return result
80
107
 
81
108
 
82
109
  def _portable_candidate() -> dict:
@@ -144,7 +171,10 @@ def test_control_plane_uses_typed_service_run_cancel_and_candidate_contracts():
144
171
  requests.append(request)
145
172
  if request.url.path == "/api/v1/preannotation/services":
146
173
  return httpx.Response(200, json=[_service()])
147
- if request.url.path == "/api/v1/preannotation/runs" and request.method == "POST":
174
+ if (
175
+ request.url.path == "/api/v1/preannotation/runs"
176
+ and request.method == "POST"
177
+ ):
148
178
  return httpx.Response(202, json=_run())
149
179
  if request.url.path == "/api/v1/preannotation/runs" and request.method == "GET":
150
180
  return httpx.Response(200, json=[_run(status="succeeded")])
@@ -172,9 +202,7 @@ def test_control_plane_uses_typed_service_run_cancel_and_candidate_contracts():
172
202
  profile_id="centerpoint-track-v1",
173
203
  config={"score_threshold": 0.4},
174
204
  )
175
- runs = client.preannotation.list_runs(
176
- project_id=7, clip_id="clip-1", limit=10
177
- )
205
+ runs = client.preannotation.list_runs(project_id=7, clip_id="clip-1", limit=10)
178
206
  fetched = client.preannotation.get_run("preann-run-1")
179
207
  cancellation = client.preannotation.cancel_run("preann-run-1")
180
208
  candidate = client.preannotation.get_candidate(_digest("d"))
@@ -260,3 +288,72 @@ def test_create_run_rejects_non_i_json_config_before_request():
260
288
  profile_id="centerpoint-track-v1",
261
289
  config={"threshold": float("nan")},
262
290
  )
291
+
292
+
293
+ def _retried_run():
294
+ previous = _run(status="failed")["attempts"][0]
295
+ result = _run(status="succeeded")
296
+ result.update(
297
+ run_id="preann-run-2",
298
+ attempt_number=2,
299
+ attempt_count=2,
300
+ latest_attempt_id="preann-run-2",
301
+ retry_of_run_id="preann-run-1",
302
+ )
303
+ result["attempts"] = [
304
+ previous,
305
+ {key: copy.deepcopy(result[key]) for key in previous},
306
+ ]
307
+ return result
308
+
309
+
310
+ def test_retry_history_round_trip_and_historical_run():
311
+ payload = _retried_run()
312
+ parsed = PlatformPreannotationRun.model_validate(payload)
313
+ assert parsed.attempt_count == 2
314
+ assert parsed.attempts[0].status == PlatformPreannotationRunStatus.FAILED
315
+ assert parsed.attempts[1].candidate_id == parsed.candidate_id
316
+ assert (
317
+ PlatformPreannotationRun.model_validate_json(parsed.model_dump_json()) == parsed
318
+ )
319
+ payload.update(payload["attempts"][0])
320
+ payload["is_latest_attempt"] = False
321
+ old = PlatformPreannotationRun.model_validate(payload)
322
+ assert old.run_id == "preann-run-1" and old.latest_attempt_id == "preann-run-2"
323
+ payload["attempts"] = []
324
+ assert PlatformPreannotationRun.model_validate(payload).attempt_count == 2
325
+
326
+
327
+ @pytest.mark.parametrize(
328
+ "field,value",
329
+ [
330
+ ("attempt_count", 1),
331
+ ("attempt_number", 1),
332
+ ("attempt_number", True),
333
+ ("latest_attempt_id", "preann-missing"),
334
+ ("is_latest_attempt", False),
335
+ ("is_latest_attempt", 1),
336
+ ("retry_of_run_id", None),
337
+ ("attempts", [_run(status="failed")["attempts"][0]]),
338
+ ],
339
+ )
340
+ def test_inconsistent_attempt_identity_is_rejected(field, value):
341
+ payload = _retried_run()
342
+ payload[field] = value
343
+ with pytest.raises(ValidationError):
344
+ PlatformPreannotationRun.model_validate(payload)
345
+
346
+
347
+ @pytest.mark.parametrize("mutation", ["unknown", "link", "order", "state"])
348
+ def test_malformed_attempt_history_is_rejected(mutation):
349
+ payload = _retried_run()
350
+ if mutation == "unknown":
351
+ payload["attempts"][0]["unknown_field"] = True
352
+ elif mutation == "link":
353
+ payload["attempts"][1]["retry_of_run_id"] = "another-run"
354
+ elif mutation == "order":
355
+ payload["attempts"].reverse()
356
+ else:
357
+ payload["attempts"][1]["progress"]["stage"] = "different-state"
358
+ with pytest.raises(ValidationError):
359
+ PlatformPreannotationRun.model_validate(payload)
@@ -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.41.0rc1"
14
+ assert x4d_devkit.__version__ == "0.41.0rc2"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -86,7 +86,53 @@ class PlatformPreannotationService(_ControlPlaneModel):
86
86
  return value
87
87
 
88
88
 
89
- class PlatformPreannotationRun(_ControlPlaneModel):
89
+ class PlatformPreannotationRunAttempt(_ControlPlaneModel):
90
+ """One immutable attempt identity and its current execution state."""
91
+
92
+ run_id: str
93
+ attempt_number: int = Field(ge=1, strict=True)
94
+ retry_of_run_id: str | None
95
+ status: PlatformPreannotationRunStatus
96
+ progress: PreannotationRunProgress
97
+ error: PreannotationRunError | None
98
+ candidate_id: str | None
99
+ temporal_workflow_id: str
100
+ actor_id: int | None = Field(gt=0, strict=True)
101
+ started_at: datetime | None
102
+ completed_at: datetime | None
103
+ cancellation_requested_at: datetime | None
104
+ created_at: datetime
105
+ updated_at: datetime
106
+
107
+ @field_validator("run_id", "retry_of_run_id", mode="before")
108
+ @classmethod
109
+ def validate_attempt_id(cls, value: Any, info: Any) -> str | None:
110
+ if value is None and info.field_name == "retry_of_run_id":
111
+ return None
112
+ return _protocol_token(value, path="preannotation attempt identifier")
113
+
114
+ @field_validator("candidate_id", mode="before")
115
+ @classmethod
116
+ def validate_attempt_candidate(cls, value: Any) -> str | None:
117
+ return None if value is None else _sha256(value, path="attempt.candidate_id")
118
+
119
+ @field_validator("temporal_workflow_id", mode="before")
120
+ @classmethod
121
+ def validate_attempt_workflow(cls, value: Any) -> str:
122
+ return _canonical_string(value, path="attempt.temporal_workflow_id")
123
+
124
+ @model_validator(mode="after")
125
+ def validate_attempt_result(self) -> "PlatformPreannotationRunAttempt":
126
+ if (self.status == PlatformPreannotationRunStatus.SUCCEEDED) != (
127
+ self.candidate_id is not None
128
+ ):
129
+ raise ValueError("only succeeded attempts must contain candidate_id")
130
+ if (self.attempt_number == 1) != (self.retry_of_run_id is None):
131
+ raise ValueError("only the first attempt has no retry predecessor")
132
+ return self
133
+
134
+
135
+ class PlatformPreannotationRun(PlatformPreannotationRunAttempt):
90
136
  """One durable X-4D preannotation run."""
91
137
 
92
138
  run_id: str
@@ -99,6 +145,10 @@ class PlatformPreannotationRun(_ControlPlaneModel):
99
145
  profile_id: str
100
146
  pipeline_identity: str
101
147
  checkpoint_identity: str
148
+ attempt_count: int = Field(ge=1, strict=True)
149
+ latest_attempt_id: str
150
+ is_latest_attempt: bool = Field(strict=True)
151
+ attempts: list[PlatformPreannotationRunAttempt]
102
152
  status: PlatformPreannotationRunStatus
103
153
  progress: PreannotationRunProgress
104
154
  error: PreannotationRunError | None
@@ -117,7 +167,9 @@ class PlatformPreannotationRun(_ControlPlaneModel):
117
167
  def validate_project_id(cls, value: Any) -> int:
118
168
  return _positive_int(value, path="preannotation run.project_id")
119
169
 
120
- @field_validator("run_id", "service_id", "profile_id", mode="before")
170
+ @field_validator(
171
+ "run_id", "service_id", "profile_id", "latest_attempt_id", mode="before"
172
+ )
121
173
  @classmethod
122
174
  def validate_protocol_id(cls, value: Any) -> str:
123
175
  return _protocol_token(value, path="preannotation run identifier")
@@ -169,9 +221,48 @@ class PlatformPreannotationRun(_ControlPlaneModel):
169
221
  def succeeded_run_has_candidate(self) -> "PlatformPreannotationRun":
170
222
  if self.status == PlatformPreannotationRunStatus.SUCCEEDED:
171
223
  if self.candidate_id is None:
172
- raise ValueError("succeeded preannotation run must contain candidate_id")
224
+ raise ValueError(
225
+ "succeeded preannotation run must contain candidate_id"
226
+ )
173
227
  elif self.candidate_id is not None:
174
- raise ValueError("only a succeeded preannotation run may contain candidate_id")
228
+ raise ValueError(
229
+ "only a succeeded preannotation run may contain candidate_id"
230
+ )
231
+ return self
232
+
233
+ @model_validator(mode="after")
234
+ def validate_attempt_history(self) -> "PlatformPreannotationRun":
235
+ if self.attempt_number > self.attempt_count:
236
+ raise ValueError("attempt_number must not exceed attempt_count")
237
+ if self.is_latest_attempt != (self.attempt_number == self.attempt_count):
238
+ raise ValueError("latest attempt flag disagrees with attempt count")
239
+ if self.is_latest_attempt != (self.run_id == self.latest_attempt_id):
240
+ raise ValueError("latest attempt flag disagrees with run identity")
241
+ # The list endpoint explicitly omits expansion; detail includes history.
242
+ if not self.attempts:
243
+ return self
244
+ if len(self.attempts) != self.attempt_count:
245
+ raise ValueError("attempt_count must match complete attempt history")
246
+ ids = [attempt.run_id for attempt in self.attempts]
247
+ if len(set(ids)) != len(ids) or self.run_id not in ids:
248
+ raise ValueError("attempt history must uniquely contain this run")
249
+ for number, attempt in enumerate(self.attempts, 1):
250
+ predecessor = None if number == 1 else ids[number - 2]
251
+ if (
252
+ attempt.attempt_number != number
253
+ or attempt.retry_of_run_id != predecessor
254
+ ):
255
+ raise ValueError(
256
+ "attempt history must be ordered with exact retry links"
257
+ )
258
+ if self.latest_attempt_id != ids[-1] or self.is_latest_attempt != (
259
+ self.run_id == self.latest_attempt_id
260
+ ):
261
+ raise ValueError("latest attempt identity disagrees with history")
262
+ current = self.attempts[ids.index(self.run_id)]
263
+ for field in PlatformPreannotationRunAttempt.model_fields:
264
+ if getattr(current, field) != getattr(self, field):
265
+ raise ValueError(f"current attempt disagrees with history: {field}")
175
266
  return self
176
267
 
177
268
 
@@ -384,13 +475,17 @@ class PreannotationAPI:
384
475
  payload = self._get("/preannotation/runs", params=params)
385
476
  if type(payload) is not list:
386
477
  raise TypeError("preannotation runs response must be a list")
387
- results = tuple(PlatformPreannotationRun.model_validate(item) for item in payload)
478
+ results = tuple(
479
+ PlatformPreannotationRun.model_validate(item) for item in payload
480
+ )
388
481
  if any(
389
482
  item.project_id != project_id
390
483
  or (clip_id is not None and item.clip_id != clip_id)
391
484
  for item in results
392
485
  ):
393
- raise ValueError("preannotation runs response is outside the requested scope")
486
+ raise ValueError(
487
+ "preannotation runs response is outside the requested scope"
488
+ )
394
489
  return results
395
490
 
396
491
  def get_run(self, run_id: str) -> PlatformPreannotationRun:
@@ -401,7 +496,9 @@ class PreannotationAPI:
401
496
  self._get(f"/preannotation/runs/{run_id}")
402
497
  )
403
498
  if result.run_id != run_id:
404
- raise ValueError("preannotation run response identity does not match request")
499
+ raise ValueError(
500
+ "preannotation run response identity does not match request"
501
+ )
405
502
  return result
406
503
 
407
504
  def cancel_run(self, run_id: str) -> PlatformPreannotationCancellation:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.41.0rc1
3
+ Version: 0.41.0rc2
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
File without changes
File without changes
File without changes