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.
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/PKG-INFO +1 -1
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/pyproject.toml +1 -1
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_preannotation_client.py +102 -5
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_sdk_api.py +1 -1
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/preannotation.py +104 -7
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/PKG-INFO +1 -1
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/LICENSE +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/README.md +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/setup.cfg +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_annotation_reference.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_box.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_dataset_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_image_review.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_inference_input.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_label_schemas.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_lidar_materialization.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_loader.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_matching.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_models.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_reconstruction_client.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_client.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_semantic_scene_native_clip.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_time_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_token.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_tracking_eval.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_transform.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_validation.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_work_session.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/annotation_reference.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/cli.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/annotation_reference.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/image_review.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/label_schemas.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/reconstruction.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/semantic_scene.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/dataset_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/eval/tracking.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/image_review.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/input.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/lidar_materialization.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/materialize.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/semantic_scene_authoring.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/semantic_scene_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/time_contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/SOURCES.txt +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/x4d_devkit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "x4d-devkit"
|
|
7
|
-
version = "0.41.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
224
|
+
raise ValueError(
|
|
225
|
+
"succeeded preannotation run must contain candidate_id"
|
|
226
|
+
)
|
|
173
227
|
elif self.candidate_id is not None:
|
|
174
|
-
raise ValueError(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{x4d_devkit-0.41.0rc1 → x4d_devkit-0.41.0rc2}/tests/test_current_state_identity_consumers_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|