x4d-devkit 0.23.0__tar.gz → 0.24.0rc1__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.23.0 → x4d_devkit-0.24.0rc1}/PKG-INFO +7 -5
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/README.md +6 -4
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/pyproject.toml +1 -1
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_dataset_identity.py +298 -70
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_label_schemas.py +143 -1
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_sdk_api.py +1 -1
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_work_session.py +27 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/cli.py +14 -14
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/__init__.py +9 -1
- x4d_devkit-0.24.0rc1/x4d_devkit/client/label_schemas.py +423 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/dataset_identity.py +382 -193
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/materialize.py +99 -19
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit.egg-info/PKG-INFO +7 -5
- x4d_devkit-0.23.0/x4d_devkit/client/label_schemas.py +0 -130
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/LICENSE +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/setup.cfg +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_annotation_contract.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_assets_client.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_boundary.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_box.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_build_info.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_cli_auth.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_client_json_contract.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_client_projects.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_clip_archives.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_contract_validation.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_coordinate_frames.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_current_state.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_current_state_identity_consumers_example.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_dataset_contract.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_detection_eval.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_evaluate.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_export_bundle.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_inference_builders.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_inference_schemas.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_inference_service.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_loader.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_matching.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_metrics.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_models.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_nuscenes_converter.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_platform_pose.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_release_gate.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_token.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_training_identity.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_transform.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_transform_chain.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/tests/test_validation.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/__init__.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/annotation_contract.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/build_info.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/_origin.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/assets.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/bags.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/capabilities.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/checkpoints.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/client.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/clip_work_sessions.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/clips.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/dataset_identity.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/projects.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/services.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/client/test_sets.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/consumer_identity.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/converters/__init__.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/converters/nuscenes.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/coordinate_contract.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/core/__init__.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/core/loader.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/core/models.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/core/token.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/core/transform.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/current_state.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/dataset_contract.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/eval/__init__.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/eval/api.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/eval/box.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/eval/detection.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/eval/matching.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/eval/metrics.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/export_bundle.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/inference/__init__.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/inference/builders.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/inference/models.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/inference/schemas.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/inference/service.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/inference/tasks.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/json_contract.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/label_schema.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/training_identity.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/validation/__init__.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/validation/contract.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/validation/platform_pose.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/validation/report.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/validation/validator.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit/work_session.py +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit.egg-info/SOURCES.txt +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit.egg-info/entry_points.txt +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/x4d_devkit.egg-info/requires.txt +0 -0
- {x4d_devkit-0.23.0 → x4d_devkit-0.24.0rc1}/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.
|
|
3
|
+
Version: 0.24.0rc1
|
|
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
|
|
@@ -66,7 +66,7 @@ Design decisions for this boundary are tracked in GitHub issues, starting with:
|
|
|
66
66
|
## Installation
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
pip install x4d-devkit==0.
|
|
69
|
+
pip install x4d-devkit==0.24.0rc1
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
Devkit is released independently as an immutable wheel. X-4D, XPoints, and
|
|
@@ -255,9 +255,11 @@ identity manifests, and update plans use one schema field only:
|
|
|
255
255
|
without the complete identity is also rejected.
|
|
256
256
|
|
|
257
257
|
`x4d-devkit dataset inspect` writes `x4d-remote-identity-v2`, and
|
|
258
|
-
`x4d-devkit dataset diff` writes `x4d-update-plan-
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
`x4d-devkit dataset diff` writes `x4d-update-plan-v3`. The v3 plan records the
|
|
259
|
+
exact materialization policy and each changed clip's reviewed target and local
|
|
260
|
+
predecessor identities. Applying it is fail-closed and idempotent: completed
|
|
261
|
+
clips are reported as already satisfied, while local or remote identity drift
|
|
262
|
+
requires a new plan. Older plan contracts are rejected rather than migrated.
|
|
261
263
|
|
|
262
264
|
### Load a clip
|
|
263
265
|
|
|
@@ -23,7 +23,7 @@ Design decisions for this boundary are tracked in GitHub issues, starting with:
|
|
|
23
23
|
## Installation
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
pip install x4d-devkit==0.
|
|
26
|
+
pip install x4d-devkit==0.24.0rc1
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Devkit is released independently as an immutable wheel. X-4D, XPoints, and
|
|
@@ -212,9 +212,11 @@ identity manifests, and update plans use one schema field only:
|
|
|
212
212
|
without the complete identity is also rejected.
|
|
213
213
|
|
|
214
214
|
`x4d-devkit dataset inspect` writes `x4d-remote-identity-v2`, and
|
|
215
|
-
`x4d-devkit dataset diff` writes `x4d-update-plan-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
`x4d-devkit dataset diff` writes `x4d-update-plan-v3`. The v3 plan records the
|
|
216
|
+
exact materialization policy and each changed clip's reviewed target and local
|
|
217
|
+
predecessor identities. Applying it is fail-closed and idempotent: completed
|
|
218
|
+
clips are reported as already satisfied, while local or remote identity drift
|
|
219
|
+
requires a new plan. Older plan contracts are rejected rather than migrated.
|
|
218
220
|
|
|
219
221
|
### Load a clip
|
|
220
222
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "x4d-devkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.24.0rc1"
|
|
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"
|
|
@@ -10,6 +10,7 @@ from tests.schema_contract_fixtures import schema_identity
|
|
|
10
10
|
from x4d_devkit import cli
|
|
11
11
|
from x4d_devkit.client.dataset_identity import DatasetIdentityAPI
|
|
12
12
|
from x4d_devkit.consumer_identity import empty_consumer_identity_diff
|
|
13
|
+
from x4d_devkit.current_state import identity_from_work_session
|
|
13
14
|
from x4d_devkit.dataset_identity import (
|
|
14
15
|
DATASET_IDENTITY_BUCKETS,
|
|
15
16
|
build_remote_identity_manifest,
|
|
@@ -82,6 +83,10 @@ def _diff_record(bucket, **overrides):
|
|
|
82
83
|
pose_revision="sha256:pose",
|
|
83
84
|
)
|
|
84
85
|
)
|
|
86
|
+
if bucket != "new_clips":
|
|
87
|
+
record["local_identity"] = _local_identity(
|
|
88
|
+
clip_id=record["clip_id"],
|
|
89
|
+
)
|
|
85
90
|
elif bucket == "removed_clips":
|
|
86
91
|
record["local_identity"] = _local_identity(clip_id=record["clip_id"])
|
|
87
92
|
elif bucket == "errors":
|
|
@@ -450,6 +455,7 @@ def test_null_remote_pose_revision_roundtrips_through_manifest_and_plan():
|
|
|
450
455
|
plan = build_update_plan(
|
|
451
456
|
diff,
|
|
452
457
|
consumer_identity_diff=empty_consumer_identity_diff(),
|
|
458
|
+
materialization_policy="full",
|
|
453
459
|
)
|
|
454
460
|
assert plan["clips"][0]["pose_revision"] is None
|
|
455
461
|
assert load_update_plan(plan)["clips"][0]["pose_revision"] is None
|
|
@@ -500,6 +506,7 @@ def test_build_update_plan_maps_actions_and_exit_code():
|
|
|
500
506
|
plan = build_update_plan(
|
|
501
507
|
diff,
|
|
502
508
|
consumer_identity_diff=empty_consumer_identity_diff(),
|
|
509
|
+
materialization_policy="full",
|
|
503
510
|
local_manifest="m.json",
|
|
504
511
|
)
|
|
505
512
|
|
|
@@ -510,7 +517,8 @@ def test_build_update_plan_maps_actions_and_exit_code():
|
|
|
510
517
|
assert plan["actions"]["reuse_source_cache"] == ["same", "ann"]
|
|
511
518
|
assert plan["actions"]["rebuild_infos"] is True
|
|
512
519
|
assert update_plan_exit_code(plan) == 13
|
|
513
|
-
assert plan["schema_version"] == "x4d-update-plan-
|
|
520
|
+
assert plan["schema_version"] == "x4d-update-plan-v3"
|
|
521
|
+
assert plan["materialization_policy"] == "full"
|
|
514
522
|
assert "platform_diff" not in plan
|
|
515
523
|
|
|
516
524
|
|
|
@@ -523,6 +531,7 @@ def test_build_update_plan_refreshes_schema_only_and_clip_format_changes():
|
|
|
523
531
|
plan = build_update_plan(
|
|
524
532
|
diff,
|
|
525
533
|
consumer_identity_diff=empty_consumer_identity_diff(),
|
|
534
|
+
materialization_policy="full",
|
|
526
535
|
)
|
|
527
536
|
|
|
528
537
|
assert plan["actions"]["download_annotation_only"] == ["schema"]
|
|
@@ -553,7 +562,11 @@ def test_build_update_plan_includes_consumer_identity_changes():
|
|
|
553
562
|
}],
|
|
554
563
|
}
|
|
555
564
|
|
|
556
|
-
plan = build_update_plan(
|
|
565
|
+
plan = build_update_plan(
|
|
566
|
+
diff,
|
|
567
|
+
consumer_identity_diff=consumer_diff,
|
|
568
|
+
materialization_policy="full",
|
|
569
|
+
)
|
|
557
570
|
|
|
558
571
|
assert plan["summary"]["consumer_changed"] is True
|
|
559
572
|
assert plan["consumer_identity"]["changed_fields"] == ["split", "class_mapping"]
|
|
@@ -576,12 +589,13 @@ def test_build_update_plan_rejects_partial_consumer_identity_diff():
|
|
|
576
589
|
build_update_plan(
|
|
577
590
|
diff,
|
|
578
591
|
consumer_identity_diff={"changed": True},
|
|
592
|
+
materialization_policy="full",
|
|
579
593
|
)
|
|
580
594
|
|
|
581
595
|
|
|
582
596
|
def test_build_update_plan_requires_explicit_consumer_identity_diff():
|
|
583
597
|
with pytest.raises(TypeError, match="consumer_identity_diff"):
|
|
584
|
-
build_update_plan(_diff_response())
|
|
598
|
+
build_update_plan(_diff_response(), materialization_policy="full")
|
|
585
599
|
|
|
586
600
|
|
|
587
601
|
def test_build_update_plan_distinguishes_pose_and_calibration_changes():
|
|
@@ -595,6 +609,7 @@ def test_build_update_plan_distinguishes_pose_and_calibration_changes():
|
|
|
595
609
|
plan = build_update_plan(
|
|
596
610
|
diff,
|
|
597
611
|
consumer_identity_diff=empty_consumer_identity_diff(),
|
|
612
|
+
materialization_policy="full",
|
|
598
613
|
)
|
|
599
614
|
|
|
600
615
|
assert plan["summary"]["calibration_changed"] == 1
|
|
@@ -661,6 +676,7 @@ def test_dataset_diff_cli_writes_plan_and_exits(monkeypatch, tmp_path, capsys):
|
|
|
661
676
|
project_name=None,
|
|
662
677
|
clip_ids=None,
|
|
663
678
|
include_asset_revision=False,
|
|
679
|
+
materialization_policy="full",
|
|
664
680
|
out=str(out),
|
|
665
681
|
format="table",
|
|
666
682
|
api_url=None,
|
|
@@ -742,6 +758,7 @@ def test_dataset_diff_cli_compares_training_identity(monkeypatch, tmp_path, caps
|
|
|
742
758
|
project_name=None,
|
|
743
759
|
clip_ids=None,
|
|
744
760
|
include_asset_revision=False,
|
|
761
|
+
materialization_policy="full",
|
|
745
762
|
out=str(out),
|
|
746
763
|
format="table",
|
|
747
764
|
api_url=None,
|
|
@@ -820,10 +837,11 @@ def test_dataset_inspect_cli_writes_remote_manifest(monkeypatch, tmp_path, capsy
|
|
|
820
837
|
|
|
821
838
|
def _plan(**overrides):
|
|
822
839
|
data = {
|
|
823
|
-
"schema_version": "x4d-update-plan-
|
|
840
|
+
"schema_version": "x4d-update-plan-v3",
|
|
824
841
|
"generated_at": "2026-06-30T00:00:00Z",
|
|
825
842
|
"x4d_devkit_version": "0.19.0",
|
|
826
843
|
"local_manifest": None,
|
|
844
|
+
"materialization_policy": "full",
|
|
827
845
|
"summary": {},
|
|
828
846
|
"clips": [
|
|
829
847
|
{
|
|
@@ -848,6 +866,7 @@ def _plan(**overrides):
|
|
|
848
866
|
"actions": {
|
|
849
867
|
"reuse_source_cache": ["clip-a"],
|
|
850
868
|
"download_source": ["clip-b"],
|
|
869
|
+
"refresh_source_metadata": [],
|
|
851
870
|
"download_annotation_only": ["clip-a"],
|
|
852
871
|
"invalidate_mapped_cache": ["clip-a", "clip-b"],
|
|
853
872
|
"rebuild_infos": True,
|
|
@@ -860,9 +879,88 @@ def _plan(**overrides):
|
|
|
860
879
|
return data
|
|
861
880
|
|
|
862
881
|
|
|
863
|
-
def
|
|
864
|
-
|
|
865
|
-
|
|
882
|
+
def _portable_work_session_identity(payload):
|
|
883
|
+
identity = identity_from_work_session(payload).to_dict()
|
|
884
|
+
return {
|
|
885
|
+
key: identity[key]
|
|
886
|
+
for key in (
|
|
887
|
+
"project_id",
|
|
888
|
+
"clip_id",
|
|
889
|
+
"source_revision",
|
|
890
|
+
"calibration_revision",
|
|
891
|
+
"pose_revision",
|
|
892
|
+
"annotation_revision",
|
|
893
|
+
"label_schema",
|
|
894
|
+
"clip_schema_version",
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _write_cache_marker(clip_dir, identity, *, policy="full"):
|
|
900
|
+
clip_dir.mkdir(parents=True, exist_ok=True)
|
|
901
|
+
(clip_dir / ".cache_identity.json").write_text(json.dumps({
|
|
902
|
+
**identity,
|
|
903
|
+
"ready": True,
|
|
904
|
+
"blockers": [],
|
|
905
|
+
"asset_revision": {},
|
|
906
|
+
"materialization_policy": policy,
|
|
907
|
+
"has_sweeps": policy == "full",
|
|
908
|
+
"x4d_devkit_version": "0.23.0",
|
|
909
|
+
"materialized_at": "2026-07-20T00:00:00Z",
|
|
910
|
+
}))
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
class _FakeStream:
|
|
914
|
+
def __init__(self, payload=b"asset"):
|
|
915
|
+
self.payload = payload
|
|
916
|
+
|
|
917
|
+
def __enter__(self):
|
|
918
|
+
return self
|
|
919
|
+
|
|
920
|
+
def __exit__(self, *args):
|
|
921
|
+
return None
|
|
922
|
+
|
|
923
|
+
def raise_for_status(self):
|
|
924
|
+
return None
|
|
925
|
+
|
|
926
|
+
def iter_bytes(self, chunk_size):
|
|
927
|
+
yield self.payload
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
class _FakeHTTP:
|
|
931
|
+
def __init__(self):
|
|
932
|
+
self.urls = []
|
|
933
|
+
|
|
934
|
+
def stream(self, method, url):
|
|
935
|
+
self.urls.append((method, url))
|
|
936
|
+
return _FakeStream()
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
def _sync_plan_for(
|
|
940
|
+
clips,
|
|
941
|
+
*,
|
|
942
|
+
download_source=(),
|
|
943
|
+
refresh_source_metadata=(),
|
|
944
|
+
download_annotation_only=(),
|
|
945
|
+
materialization_policy="full",
|
|
946
|
+
):
|
|
947
|
+
return _plan(
|
|
948
|
+
clips=list(clips),
|
|
949
|
+
materialization_policy=materialization_policy,
|
|
950
|
+
actions={
|
|
951
|
+
**_plan()["actions"],
|
|
952
|
+
"reuse_source_cache": [],
|
|
953
|
+
"download_source": list(download_source),
|
|
954
|
+
"refresh_source_metadata": list(refresh_source_metadata),
|
|
955
|
+
"download_annotation_only": list(download_annotation_only),
|
|
956
|
+
"invalidate_mapped_cache": [],
|
|
957
|
+
},
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def test_load_update_plan_rejects_old_v2_contract():
|
|
962
|
+
with pytest.raises(ValueError, match="x4d-update-plan-v3"):
|
|
963
|
+
load_update_plan({**_plan(), "schema_version": "x4d-update-plan-v2"})
|
|
866
964
|
|
|
867
965
|
|
|
868
966
|
def test_load_update_plan_rejects_old_consumer_identity_diff():
|
|
@@ -895,21 +993,30 @@ def test_load_update_plan_rejects_removed_schema_fields(field):
|
|
|
895
993
|
|
|
896
994
|
|
|
897
995
|
def test_sync_update_plan_dry_run_reports_actions(tmp_path):
|
|
996
|
+
plan = _plan()
|
|
997
|
+
_write_cache_marker(
|
|
998
|
+
tmp_path / "cache" / "clips" / "clip-a",
|
|
999
|
+
plan["clips"][0]["local_identity"],
|
|
1000
|
+
)
|
|
898
1001
|
result = sync_update_plan(
|
|
899
1002
|
None,
|
|
900
|
-
plan=
|
|
1003
|
+
plan=plan,
|
|
901
1004
|
data_root=tmp_path / "cache",
|
|
902
1005
|
dry_run=True,
|
|
903
1006
|
mapped_cache_dir=tmp_path / "mapped",
|
|
904
1007
|
)
|
|
905
1008
|
|
|
906
1009
|
assert result["dry_run"] is True
|
|
907
|
-
assert result["
|
|
908
|
-
"
|
|
909
|
-
"
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1010
|
+
assert [item["clip_id"] for item in result["planned_actions"]] == [
|
|
1011
|
+
"clip-b",
|
|
1012
|
+
"clip-a",
|
|
1013
|
+
]
|
|
1014
|
+
assert [item["clip_id"] for item in result["actions_to_execute"]] == [
|
|
1015
|
+
"clip-b",
|
|
1016
|
+
"clip-a",
|
|
1017
|
+
]
|
|
1018
|
+
assert result["executed_actions"] == []
|
|
1019
|
+
assert result["already_satisfied_actions"] == []
|
|
913
1020
|
assert result["invalidated_mapped_cache"] == [
|
|
914
1021
|
{"clip_id": "clip-a", "dry_run": True},
|
|
915
1022
|
{"clip_id": "clip-b", "dry_run": True},
|
|
@@ -919,48 +1026,62 @@ def test_sync_update_plan_dry_run_reports_actions(tmp_path):
|
|
|
919
1026
|
|
|
920
1027
|
|
|
921
1028
|
def test_sync_update_plan_blocks_not_ready_by_default(tmp_path):
|
|
1029
|
+
plan = _sync_plan_for([
|
|
1030
|
+
{
|
|
1031
|
+
"bucket": "not_ready",
|
|
1032
|
+
**_diff_record(
|
|
1033
|
+
"not_ready",
|
|
1034
|
+
project_id=1812,
|
|
1035
|
+
clip_id="clip-a",
|
|
1036
|
+
),
|
|
1037
|
+
"blockers": [{"code": "missing_ego_pose", "message": "ego pose is absent"}],
|
|
1038
|
+
}
|
|
1039
|
+
])
|
|
922
1040
|
result = sync_update_plan(
|
|
923
1041
|
None,
|
|
924
|
-
plan=
|
|
925
|
-
clips=[
|
|
926
|
-
{
|
|
927
|
-
"bucket": "not_ready",
|
|
928
|
-
**_diff_record(
|
|
929
|
-
"not_ready",
|
|
930
|
-
project_id=1812,
|
|
931
|
-
clip_id="clip-a",
|
|
932
|
-
),
|
|
933
|
-
"blockers": [{"code": "missing_ego_pose", "message": "ego pose is absent"}],
|
|
934
|
-
}
|
|
935
|
-
]
|
|
936
|
-
),
|
|
1042
|
+
plan=plan,
|
|
937
1043
|
data_root=tmp_path,
|
|
938
1044
|
dry_run=True,
|
|
939
1045
|
)
|
|
940
1046
|
|
|
941
|
-
assert result["
|
|
942
|
-
|
|
943
|
-
"code": "
|
|
944
|
-
|
|
945
|
-
}]
|
|
1047
|
+
assert result["failures"][0]["code"] == "not_ready"
|
|
1048
|
+
assert result["failures"][0]["details"]["blockers"] == [
|
|
1049
|
+
{"code": "missing_ego_pose", "message": "ego pose is absent"}
|
|
1050
|
+
]
|
|
946
1051
|
assert sync_result_exit_code(result) == 20
|
|
947
1052
|
|
|
948
1053
|
|
|
949
|
-
def
|
|
1054
|
+
def test_sync_update_plan_annotation_only_first_apply_and_replay(tmp_path):
|
|
1055
|
+
target_payload = current_work_session_payload(clip_id="clip-a")
|
|
1056
|
+
target_identity = _portable_work_session_identity(target_payload)
|
|
1057
|
+
predecessor = {
|
|
1058
|
+
**target_identity,
|
|
1059
|
+
"annotation_revision": "sha256:" + "1" * 64,
|
|
1060
|
+
}
|
|
1061
|
+
record = {
|
|
1062
|
+
"bucket": "annotation_changed",
|
|
1063
|
+
**_diff_record(
|
|
1064
|
+
"annotation_changed",
|
|
1065
|
+
**target_identity,
|
|
1066
|
+
local_identity=predecessor,
|
|
1067
|
+
),
|
|
1068
|
+
}
|
|
1069
|
+
plan = _sync_plan_for(
|
|
1070
|
+
[record],
|
|
1071
|
+
download_annotation_only=["clip-a"],
|
|
1072
|
+
)
|
|
950
1073
|
clip_dir = tmp_path / "cache" / "clips" / "clip-a"
|
|
951
|
-
clip_dir
|
|
1074
|
+
_write_cache_marker(clip_dir, predecessor)
|
|
952
1075
|
(clip_dir / "meta.json").write_text("{}\n")
|
|
953
1076
|
|
|
954
1077
|
class WorkSessions:
|
|
1078
|
+
def __init__(self):
|
|
1079
|
+
self.calls = 0
|
|
1080
|
+
|
|
955
1081
|
def open(self, project_id, clip_id):
|
|
956
1082
|
assert project_id == 1812
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
source_revision="s2",
|
|
960
|
-
calibration_revision="c2",
|
|
961
|
-
pose_revision="p2",
|
|
962
|
-
)
|
|
963
|
-
return payload
|
|
1083
|
+
self.calls += 1
|
|
1084
|
+
return target_payload
|
|
964
1085
|
|
|
965
1086
|
class Client:
|
|
966
1087
|
def __init__(self):
|
|
@@ -968,46 +1089,154 @@ def test_sync_update_plan_writes_annotation_only_tables_and_stale_markers(tmp_pa
|
|
|
968
1089
|
self._http = None
|
|
969
1090
|
|
|
970
1091
|
client = Client()
|
|
971
|
-
|
|
1092
|
+
first = sync_update_plan(
|
|
972
1093
|
client,
|
|
973
|
-
plan=
|
|
974
|
-
clips=[
|
|
975
|
-
{
|
|
976
|
-
"bucket": "annotation_changed",
|
|
977
|
-
**_diff_record(
|
|
978
|
-
"annotation_changed",
|
|
979
|
-
project_id=1812,
|
|
980
|
-
clip_id="clip-a",
|
|
981
|
-
),
|
|
982
|
-
}
|
|
983
|
-
],
|
|
984
|
-
actions={
|
|
985
|
-
**_plan()["actions"],
|
|
986
|
-
"download_source": [],
|
|
987
|
-
"invalidate_mapped_cache": ["clip-a"],
|
|
988
|
-
},
|
|
989
|
-
),
|
|
1094
|
+
plan=plan,
|
|
990
1095
|
data_root=tmp_path / "cache",
|
|
991
|
-
mapped_cache_dir=tmp_path / "mapped",
|
|
992
1096
|
)
|
|
1097
|
+
replay = sync_update_plan(client, plan=plan, data_root=tmp_path / "cache")
|
|
993
1098
|
|
|
994
1099
|
annotation_document = json.loads((clip_dir / "annotation.json").read_text())
|
|
995
1100
|
assert annotation_document["annotations"][0]["annotation_token"] == "annotation-0"
|
|
996
1101
|
assert not (clip_dir / "instance.json").exists()
|
|
997
|
-
assert
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1102
|
+
assert first["failures"] == []
|
|
1103
|
+
assert first["executed_actions"][0]["action"] == "download_annotation_only"
|
|
1104
|
+
assert replay["executed_actions"] == []
|
|
1105
|
+
assert replay["already_satisfied_actions"][0]["clip_id"] == "clip-a"
|
|
1106
|
+
assert client.clip_work_sessions.calls == 1
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def test_sync_update_plan_partial_retry_preserves_completed_clips(tmp_path):
|
|
1110
|
+
payloads = {
|
|
1111
|
+
clip_id: current_work_session_payload(clip_id=clip_id)
|
|
1112
|
+
for clip_id in ("clip-a", "clip-b")
|
|
1113
|
+
}
|
|
1114
|
+
records = [
|
|
1115
|
+
{
|
|
1116
|
+
"bucket": "new_clips",
|
|
1117
|
+
**_diff_record(
|
|
1118
|
+
"new_clips",
|
|
1119
|
+
**_portable_work_session_identity(payloads[clip_id]),
|
|
1120
|
+
),
|
|
1121
|
+
}
|
|
1122
|
+
for clip_id in ("clip-a", "clip-b")
|
|
1123
|
+
]
|
|
1124
|
+
plan = _sync_plan_for(
|
|
1125
|
+
records,
|
|
1126
|
+
download_source=["clip-a", "clip-b"],
|
|
1002
1127
|
)
|
|
1003
|
-
|
|
1004
|
-
|
|
1128
|
+
|
|
1129
|
+
class WorkSessions:
|
|
1130
|
+
def __init__(self):
|
|
1131
|
+
self.calls = []
|
|
1132
|
+
self.fail_clip_b = True
|
|
1133
|
+
|
|
1134
|
+
def open(self, project_id, clip_id):
|
|
1135
|
+
self.calls.append(clip_id)
|
|
1136
|
+
if clip_id == "clip-b" and self.fail_clip_b:
|
|
1137
|
+
raise RuntimeError("transient open failure")
|
|
1138
|
+
return payloads[clip_id]
|
|
1139
|
+
|
|
1140
|
+
class Client:
|
|
1141
|
+
def __init__(self):
|
|
1142
|
+
self.clip_work_sessions = WorkSessions()
|
|
1143
|
+
self._http = _FakeHTTP()
|
|
1144
|
+
|
|
1145
|
+
client = Client()
|
|
1146
|
+
first = sync_update_plan(client, plan=plan, data_root=tmp_path / "cache", workers=1)
|
|
1147
|
+
first_download_count = len(client._http.urls)
|
|
1148
|
+
client.clip_work_sessions.fail_clip_b = False
|
|
1149
|
+
retry = sync_update_plan(client, plan=plan, data_root=tmp_path / "cache", workers=1)
|
|
1150
|
+
completed_download_count = len(client._http.urls)
|
|
1151
|
+
replay = sync_update_plan(client, plan=plan, data_root=tmp_path / "cache", workers=1)
|
|
1152
|
+
|
|
1153
|
+
assert [item["clip_id"] for item in first["executed_actions"]] == ["clip-a"]
|
|
1154
|
+
assert first["failures"][0]["clip_id"] == "clip-b"
|
|
1155
|
+
assert [item["clip_id"] for item in retry["already_satisfied_actions"]] == ["clip-a"]
|
|
1156
|
+
assert [item["clip_id"] for item in retry["executed_actions"]] == ["clip-b"]
|
|
1157
|
+
assert len(client._http.urls) == first_download_count * 2
|
|
1158
|
+
assert client.clip_work_sessions.calls.count("clip-a") == 1
|
|
1159
|
+
assert replay["executed_actions"] == []
|
|
1160
|
+
assert [item["clip_id"] for item in replay["already_satisfied_actions"]] == [
|
|
1161
|
+
"clip-a",
|
|
1162
|
+
"clip-b",
|
|
1163
|
+
]
|
|
1164
|
+
assert len(client._http.urls) == completed_download_count
|
|
1165
|
+
|
|
1166
|
+
|
|
1167
|
+
def test_sync_update_plan_rejects_policy_and_local_target_conflicts(tmp_path):
|
|
1168
|
+
plan = _plan()
|
|
1169
|
+
clip_dir = tmp_path / "cache" / "clips" / "clip-a"
|
|
1170
|
+
_write_cache_marker(
|
|
1171
|
+
clip_dir,
|
|
1172
|
+
plan["clips"][0]["local_identity"],
|
|
1173
|
+
policy="keyframes_only",
|
|
1174
|
+
)
|
|
1175
|
+
policy_result = sync_update_plan(
|
|
1176
|
+
None,
|
|
1177
|
+
plan=plan,
|
|
1178
|
+
data_root=tmp_path / "cache",
|
|
1179
|
+
dry_run=True,
|
|
1180
|
+
)
|
|
1181
|
+
assert policy_result["failures"][0]["code"] == "materialization_policy_mismatch"
|
|
1182
|
+
|
|
1183
|
+
conflicting = {
|
|
1184
|
+
**plan["clips"][0]["local_identity"],
|
|
1185
|
+
"annotation_revision": "sha256:" + "2" * 64,
|
|
1186
|
+
}
|
|
1187
|
+
_write_cache_marker(clip_dir, conflicting)
|
|
1188
|
+
conflict_result = sync_update_plan(
|
|
1189
|
+
None,
|
|
1190
|
+
plan=plan,
|
|
1191
|
+
data_root=tmp_path / "cache",
|
|
1192
|
+
dry_run=True,
|
|
1193
|
+
)
|
|
1194
|
+
assert conflict_result["failures"][0]["code"] == "update_plan_target_mismatch"
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
def test_sync_update_plan_rejects_remote_state_moving_after_review(tmp_path):
|
|
1198
|
+
target_payload = current_work_session_payload(clip_id="clip-a")
|
|
1199
|
+
target_identity = _portable_work_session_identity(target_payload)
|
|
1200
|
+
predecessor = {
|
|
1201
|
+
**target_identity,
|
|
1202
|
+
"annotation_revision": "sha256:" + "1" * 64,
|
|
1203
|
+
}
|
|
1204
|
+
plan = _sync_plan_for(
|
|
1205
|
+
[{
|
|
1206
|
+
"bucket": "annotation_changed",
|
|
1207
|
+
**_diff_record(
|
|
1208
|
+
"annotation_changed",
|
|
1209
|
+
**target_identity,
|
|
1210
|
+
local_identity=predecessor,
|
|
1211
|
+
),
|
|
1212
|
+
}],
|
|
1213
|
+
download_annotation_only=["clip-a"],
|
|
1214
|
+
)
|
|
1215
|
+
_write_cache_marker(tmp_path / "cache" / "clips" / "clip-a", predecessor)
|
|
1216
|
+
moved = {**target_payload, "source_revision": "source-moved"}
|
|
1217
|
+
|
|
1218
|
+
class WorkSessions:
|
|
1219
|
+
def open(self, project_id, clip_id):
|
|
1220
|
+
return moved
|
|
1221
|
+
|
|
1222
|
+
class Client:
|
|
1223
|
+
clip_work_sessions = WorkSessions()
|
|
1224
|
+
_http = _FakeHTTP()
|
|
1225
|
+
|
|
1226
|
+
result = sync_update_plan(Client(), plan=plan, data_root=tmp_path / "cache")
|
|
1227
|
+
assert result["executed_actions"] == []
|
|
1228
|
+
assert result["failures"][0]["code"] == "remote_update_plan_target_mismatch"
|
|
1005
1229
|
|
|
1006
1230
|
|
|
1007
1231
|
def test_dataset_sync_cli_writes_result_and_exits(monkeypatch, tmp_path, capsys):
|
|
1232
|
+
plan_data = _plan()
|
|
1008
1233
|
plan = tmp_path / "plan.json"
|
|
1009
|
-
plan.write_text(json.dumps(
|
|
1234
|
+
plan.write_text(json.dumps(plan_data))
|
|
1010
1235
|
out = tmp_path / "sync_result.json"
|
|
1236
|
+
_write_cache_marker(
|
|
1237
|
+
tmp_path / "cache" / "clips" / "clip-a",
|
|
1238
|
+
plan_data["clips"][0]["local_identity"],
|
|
1239
|
+
)
|
|
1011
1240
|
|
|
1012
1241
|
args = argparse.Namespace(
|
|
1013
1242
|
plan=str(plan),
|
|
@@ -1016,7 +1245,6 @@ def test_dataset_sync_cli_writes_result_and_exits(monkeypatch, tmp_path, capsys)
|
|
|
1016
1245
|
dry_run=True,
|
|
1017
1246
|
allow_not_ready_skip=False,
|
|
1018
1247
|
mapped_cache_dir=None,
|
|
1019
|
-
materialization_policy="full",
|
|
1020
1248
|
out=str(out),
|
|
1021
1249
|
format="table",
|
|
1022
1250
|
api_url=None,
|
|
@@ -1028,4 +1256,4 @@ def test_dataset_sync_cli_writes_result_and_exits(monkeypatch, tmp_path, capsys)
|
|
|
1028
1256
|
|
|
1029
1257
|
assert exc.value.code == 0
|
|
1030
1258
|
assert "Dataset sync dry-run completed" in capsys.readouterr().out
|
|
1031
|
-
assert json.loads(out.read_text())["schema_version"] == "x4d-sync-result-
|
|
1259
|
+
assert json.loads(out.read_text())["schema_version"] == "x4d-sync-result-v2"
|