x4d-devkit 0.22.0rc2__tar.gz → 0.23.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.
Files changed (101) hide show
  1. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/PKG-INFO +8 -6
  2. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/README.md +7 -5
  3. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/pyproject.toml +1 -1
  4. x4d_devkit-0.23.0rc1/tests/test_annotation_contract.py +193 -0
  5. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_boundary.py +8 -0
  6. x4d_devkit-0.23.0rc1/tests/test_contract_validation.py +382 -0
  7. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_current_state.py +3 -3
  8. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_current_state_identity_consumers_example.py +5 -2
  9. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_dataset_identity.py +18 -13
  10. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_export_bundle.py +4 -4
  11. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_loader.py +2 -1
  12. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_models.py +2 -2
  13. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_nuscenes_converter.py +23 -9
  14. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_sdk_api.py +4 -4
  15. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_training_identity.py +3 -3
  16. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_validation.py +57 -7
  17. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_work_session.py +41 -33
  18. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/__init__.py +18 -0
  19. x4d_devkit-0.23.0rc1/x4d_devkit/annotation_contract.py +428 -0
  20. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/clip_work_sessions.py +1 -5
  21. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/converters/nuscenes.py +27 -54
  22. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/core/loader.py +39 -10
  23. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/dataset_contract.py +2 -2
  24. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/export_bundle.py +1 -1
  25. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/materialize.py +5 -1
  26. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/validation/contract.py +18 -365
  27. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/validation/platform_pose.py +1 -1
  28. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/validation/validator.py +54 -8
  29. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/work_session.py +17 -8
  30. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit.egg-info/PKG-INFO +8 -6
  31. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit.egg-info/SOURCES.txt +2 -0
  32. x4d_devkit-0.22.0rc2/tests/test_contract_validation.py +0 -443
  33. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/LICENSE +0 -0
  34. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/setup.cfg +0 -0
  35. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_assets_client.py +0 -0
  36. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_box.py +0 -0
  37. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_build_info.py +0 -0
  38. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_cli_auth.py +0 -0
  39. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_client_json_contract.py +0 -0
  40. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_client_projects.py +0 -0
  41. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_clip_archives.py +0 -0
  42. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_coordinate_frames.py +0 -0
  43. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_dataset_contract.py +0 -0
  44. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_detection_eval.py +0 -0
  45. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_evaluate.py +0 -0
  46. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_inference_builders.py +0 -0
  47. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_inference_schemas.py +0 -0
  48. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_inference_service.py +0 -0
  49. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_label_schemas.py +0 -0
  50. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_matching.py +0 -0
  51. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_metrics.py +0 -0
  52. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_platform_pose.py +0 -0
  53. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_release_gate.py +0 -0
  54. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_token.py +0 -0
  55. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_transform.py +0 -0
  56. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/tests/test_transform_chain.py +0 -0
  57. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/build_info.py +0 -0
  58. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/cli.py +0 -0
  59. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/__init__.py +0 -0
  60. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/_origin.py +0 -0
  61. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/assets.py +0 -0
  62. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/bags.py +0 -0
  63. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/capabilities.py +0 -0
  64. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/checkpoints.py +0 -0
  65. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/client.py +0 -0
  66. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/clips.py +0 -0
  67. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/dataset_identity.py +0 -0
  68. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/label_schemas.py +0 -0
  69. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/projects.py +0 -0
  70. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/services.py +0 -0
  71. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/client/test_sets.py +0 -0
  72. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/consumer_identity.py +0 -0
  73. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/converters/__init__.py +0 -0
  74. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/coordinate_contract.py +0 -0
  75. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/core/__init__.py +0 -0
  76. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/core/models.py +0 -0
  77. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/core/token.py +0 -0
  78. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/core/transform.py +0 -0
  79. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/current_state.py +0 -0
  80. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/dataset_identity.py +0 -0
  81. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/eval/__init__.py +0 -0
  82. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/eval/api.py +0 -0
  83. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/eval/box.py +0 -0
  84. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/eval/detection.py +0 -0
  85. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/eval/matching.py +0 -0
  86. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/eval/metrics.py +0 -0
  87. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/inference/__init__.py +0 -0
  88. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/inference/builders.py +0 -0
  89. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/inference/models.py +0 -0
  90. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/inference/schemas.py +0 -0
  91. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/inference/service.py +0 -0
  92. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/inference/tasks.py +0 -0
  93. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/json_contract.py +0 -0
  94. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/label_schema.py +0 -0
  95. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/training_identity.py +0 -0
  96. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/validation/__init__.py +0 -0
  97. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit/validation/report.py +0 -0
  98. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  99. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit.egg-info/entry_points.txt +0 -0
  100. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.0rc1}/x4d_devkit.egg-info/requires.txt +0 -0
  101. {x4d_devkit-0.22.0rc2 → x4d_devkit-0.23.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.22.0rc2
3
+ Version: 0.23.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.22.0rc2
69
+ pip install x4d-devkit==0.23.0rc1
70
70
  ```
71
71
 
72
72
  Devkit is released independently as an immutable wheel. X-4D, XPoints, and
@@ -105,7 +105,10 @@ validation boundary.
105
105
  Shared validation rules for platform submit, X-Points preflight, local clip
106
106
  validation, and training consumers are documented in
107
107
  [docs/shared-contract-validation.md](docs/shared-contract-validation.md).
108
- Portable export producers and consumers share the exact `0.11` manifest
108
+ The Dataset 0.12 single-file annotation authority, semantic revision, and
109
+ derived track views are documented in
110
+ [docs/annotation-contract.md](docs/annotation-contract.md).
111
+ Portable export producers and consumers share the exact `0.12` manifest
109
112
  parser documented in
110
113
  [docs/export-bundle-contract.md](docs/export-bundle-contract.md).
111
114
  `examples/current_state_identity_consumers.py` provides an offline downstream
@@ -119,7 +122,7 @@ Included:
119
122
  workflows such as clip listing/download, screening preview, model/checkpoint
120
123
  registration, status queries, and capability discovery.
121
124
  - Strict validation of `x4d-training-identity-v1` manifests.
122
- - Strict producer-option and manifest parsing for export bundle `0.11`.
125
+ - Strict producer-option and manifest parsing for export bundle `0.12`.
123
126
  - External inference service wrappers for third-party model-serving projects.
124
127
 
125
128
  Excluded:
@@ -233,7 +236,7 @@ comparison = compare_project_label_schema_snapshot(stored_snapshot, current)
233
236
  assert comparison.status == "same"
234
237
  ```
235
238
 
236
- The native `0.11` clip stores only the immutable identity in
239
+ The native `0.12` clip stores only the immutable identity in
237
240
  `meta.label_schema`. It must not contain `label_schema.json`. The complete
238
241
  registry record is explicit runtime context for `ClipLoader`, `validate_clip`,
239
242
  conversion, and work-session parsing; its identity must exactly match
@@ -296,7 +299,6 @@ try:
296
299
  response = client.clip_work_sessions.submit_annotations(
297
300
  manifest=manifest,
298
301
  annotations=manifest.annotations,
299
- instances=manifest.instances,
300
302
  )
301
303
  print(response["annotation_revision"])
302
304
  except WorkSessionConflictError as exc:
@@ -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.22.0rc2
26
+ pip install x4d-devkit==0.23.0rc1
27
27
  ```
28
28
 
29
29
  Devkit is released independently as an immutable wheel. X-4D, XPoints, and
@@ -62,7 +62,10 @@ validation boundary.
62
62
  Shared validation rules for platform submit, X-Points preflight, local clip
63
63
  validation, and training consumers are documented in
64
64
  [docs/shared-contract-validation.md](docs/shared-contract-validation.md).
65
- Portable export producers and consumers share the exact `0.11` manifest
65
+ The Dataset 0.12 single-file annotation authority, semantic revision, and
66
+ derived track views are documented in
67
+ [docs/annotation-contract.md](docs/annotation-contract.md).
68
+ Portable export producers and consumers share the exact `0.12` manifest
66
69
  parser documented in
67
70
  [docs/export-bundle-contract.md](docs/export-bundle-contract.md).
68
71
  `examples/current_state_identity_consumers.py` provides an offline downstream
@@ -76,7 +79,7 @@ Included:
76
79
  workflows such as clip listing/download, screening preview, model/checkpoint
77
80
  registration, status queries, and capability discovery.
78
81
  - Strict validation of `x4d-training-identity-v1` manifests.
79
- - Strict producer-option and manifest parsing for export bundle `0.11`.
82
+ - Strict producer-option and manifest parsing for export bundle `0.12`.
80
83
  - External inference service wrappers for third-party model-serving projects.
81
84
 
82
85
  Excluded:
@@ -190,7 +193,7 @@ comparison = compare_project_label_schema_snapshot(stored_snapshot, current)
190
193
  assert comparison.status == "same"
191
194
  ```
192
195
 
193
- The native `0.11` clip stores only the immutable identity in
196
+ The native `0.12` clip stores only the immutable identity in
194
197
  `meta.label_schema`. It must not contain `label_schema.json`. The complete
195
198
  registry record is explicit runtime context for `ClipLoader`, `validate_clip`,
196
199
  conversion, and work-session parsing; its identity must exactly match
@@ -253,7 +256,6 @@ try:
253
256
  response = client.clip_work_sessions.submit_annotations(
254
257
  manifest=manifest,
255
258
  annotations=manifest.annotations,
256
- instances=manifest.instances,
257
259
  )
258
260
  print(response["annotation_revision"])
259
261
  except WorkSessionConflictError as exc:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "x4d-devkit"
7
- version = "0.22.0rc2"
7
+ version = "0.23.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"
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+
5
+ import pytest
6
+
7
+ from x4d_devkit.annotation_contract import (
8
+ ANNOTATION_CONTRACT_VERSION,
9
+ AnnotationContractError,
10
+ annotation_revision,
11
+ build_clip_annotations,
12
+ derive_annotation_views,
13
+ parse_clip_annotations,
14
+ )
15
+
16
+
17
+ def _annotation(
18
+ token: str,
19
+ sample: str,
20
+ instance: str = "instance-0",
21
+ category: str = "car",
22
+ ) -> dict:
23
+ return {
24
+ "annotation_token": token,
25
+ "sample_token": sample,
26
+ "instance_token": instance,
27
+ "category": category,
28
+ "bbox_3d": {
29
+ "translation": {"x": 1.0, "y": 2.0, "z": 3.0},
30
+ "size": {"length": 4.0, "width": 2.0, "height": 1.5},
31
+ "rotation": {"qx": 0.0, "qy": 0.0, "qz": 0.0, "qw": 1.0},
32
+ },
33
+ "num_lidar_pts": 5,
34
+ "velocity": None,
35
+ "attributes": {},
36
+ }
37
+
38
+
39
+ def _samples() -> list[dict]:
40
+ return [
41
+ {"sample_token": "sample-0", "timestamp_us": 100},
42
+ {"sample_token": "sample-1", "timestamp_us": 200},
43
+ ]
44
+
45
+
46
+ def test_build_and_parse_annotation_document_is_canonical_and_order_independent():
47
+ later = _annotation("annotation-1", "sample-1")
48
+ earlier = _annotation("annotation-0", "sample-0")
49
+
50
+ first = build_clip_annotations("clip-0", [later, earlier])
51
+ second = build_clip_annotations("clip-0", [earlier, later])
52
+
53
+ assert first == second
54
+ assert first.annotation_revision == annotation_revision(
55
+ "clip-0", [later, earlier]
56
+ )
57
+ assert first.to_dict() == {
58
+ "contract_version": ANNOTATION_CONTRACT_VERSION,
59
+ "clip_id": "clip-0",
60
+ "annotation_revision": first.annotation_revision,
61
+ "annotations": [earlier, later],
62
+ }
63
+ assert parse_clip_annotations(first.to_dict()) == first
64
+
65
+
66
+ @pytest.mark.parametrize(
67
+ ("mutation", "code"),
68
+ [
69
+ (lambda doc: doc.update(extra=True), "invalid_annotation_document"),
70
+ (
71
+ lambda doc: doc.update(contract_version="x4d-clip-annotations-v0"),
72
+ "invalid_annotation_contract_version",
73
+ ),
74
+ (
75
+ lambda doc: doc.update(annotation_revision="sha256:BAD"),
76
+ "invalid_annotation_revision",
77
+ ),
78
+ (
79
+ lambda doc: doc["annotations"][0].update(category="truck"),
80
+ "annotation_revision_mismatch",
81
+ ),
82
+ (
83
+ lambda doc: doc["annotations"].reverse(),
84
+ "noncanonical_annotation_order",
85
+ ),
86
+ (
87
+ lambda doc: doc["annotations"][0].update(prev=None),
88
+ "invalid_annotation_shape",
89
+ ),
90
+ (
91
+ lambda doc: doc["annotations"][0].pop("velocity"),
92
+ "invalid_annotation_shape",
93
+ ),
94
+ ],
95
+ )
96
+ def test_parse_annotation_document_fails_closed(mutation, code):
97
+ raw = build_clip_annotations(
98
+ "clip-0",
99
+ [
100
+ _annotation("annotation-0", "sample-0"),
101
+ _annotation("annotation-1", "sample-1"),
102
+ ],
103
+ ).to_dict()
104
+ mutation(raw)
105
+
106
+ with pytest.raises(AnnotationContractError) as exc:
107
+ parse_clip_annotations(raw)
108
+
109
+ assert exc.value.code == code
110
+
111
+
112
+ def test_derive_annotation_views_uses_sample_time_not_input_order():
113
+ later = _annotation("annotation-a", "sample-1")
114
+ earlier = _annotation("annotation-z", "sample-0")
115
+
116
+ views = derive_annotation_views([later, earlier], _samples())
117
+ linked = {row["annotation_token"]: row for row in views.annotations}
118
+
119
+ assert linked["annotation-z"]["prev"] is None
120
+ assert linked["annotation-z"]["next"] == "annotation-a"
121
+ assert linked["annotation-a"]["prev"] == "annotation-z"
122
+ assert linked["annotation-a"]["next"] is None
123
+ assert views.instance_rows() == [
124
+ {
125
+ "instance_token": "instance-0",
126
+ "category": "car",
127
+ "num_annotations": 2,
128
+ "first_annotation_token": "annotation-z",
129
+ "last_annotation_token": "annotation-a",
130
+ }
131
+ ]
132
+
133
+
134
+ def test_derive_annotation_views_returns_defensive_copies():
135
+ views = derive_annotation_views(
136
+ [_annotation("annotation-0", "sample-0")],
137
+ _samples(),
138
+ )
139
+ rows = views.annotation_rows()
140
+ rows[0]["category"] = "truck"
141
+
142
+ assert views.annotations[0]["category"] == "car"
143
+
144
+
145
+ def test_annotation_documents_and_derived_views_are_recursively_immutable():
146
+ source = _annotation("annotation-0", "sample-0")
147
+ document = build_clip_annotations("clip-0", [source])
148
+ views = derive_annotation_views([source], _samples())
149
+
150
+ source["category"] = "truck"
151
+ assert document.annotations[0]["category"] == "car"
152
+ with pytest.raises(TypeError):
153
+ document.annotations[0]["bbox_3d"]["translation"]["x"] = 99.0
154
+ with pytest.raises(TypeError):
155
+ views.instances[0]["category"] = "truck"
156
+
157
+
158
+ @pytest.mark.parametrize(
159
+ ("annotations", "samples", "code"),
160
+ [
161
+ (
162
+ [
163
+ _annotation("annotation-0", "sample-0"),
164
+ _annotation("annotation-1", "sample-1", category="truck"),
165
+ ],
166
+ _samples(),
167
+ "instance_category_mismatch",
168
+ ),
169
+ (
170
+ [_annotation("annotation-0", "missing")],
171
+ _samples(),
172
+ "annotation_sample_not_found",
173
+ ),
174
+ (
175
+ [
176
+ _annotation("annotation-0", "sample-0"),
177
+ _annotation("annotation-1", "sample-1"),
178
+ ],
179
+ [
180
+ {"sample_token": "sample-0", "timestamp_us": 100},
181
+ {"sample_token": "sample-1", "timestamp_us": 100},
182
+ ],
183
+ "ambiguous_instance_order",
184
+ ),
185
+ ],
186
+ )
187
+ def test_derive_annotation_views_rejects_ambiguous_tracks(
188
+ annotations, samples, code
189
+ ):
190
+ with pytest.raises(AnnotationContractError) as exc:
191
+ derive_annotation_views(copy.deepcopy(annotations), copy.deepcopy(samples))
192
+
193
+ assert exc.value.code == code
@@ -73,15 +73,19 @@ def test_top_level_exports_only_stable_sdk_surface():
73
73
 
74
74
  assert set(x4d_devkit.__all__) == {
75
75
  "__version__",
76
+ "ANNOTATION_CONTRACT_VERSION",
76
77
  "Annotation",
78
+ "AnnotationContractError",
77
79
  "AssetRef",
78
80
  "CalibratedSensor",
79
81
  "ClipContractValidationError",
82
+ "ClipAnnotationDocument",
80
83
  "ClipLoader",
81
84
  "ClipMeta",
82
85
  "ClipWorkSessionManifest",
83
86
  "COORDINATE_CONTRACT_VERSION",
84
87
  "EgoPose",
88
+ "DerivedAnnotationViews",
85
89
  "EXPORT_BUNDLE_SCHEMA_VERSION",
86
90
  "ExportBundleClip",
87
91
  "ExportBundleContractError",
@@ -101,12 +105,16 @@ def test_top_level_exports_only_stable_sdk_surface():
101
105
  "WorkSessionCompareResult",
102
106
  "X4DClient",
103
107
  "compare_work_session",
108
+ "annotation_revision",
109
+ "build_clip_annotations",
110
+ "derive_annotation_views",
104
111
  "generate_ego_pose_token",
105
112
  "generate_token",
106
113
  "get_build_info",
107
114
  "get_training_coordinate_contract",
108
115
  "materialize_work_session_clip",
109
116
  "parse_clip_work_session",
117
+ "parse_clip_annotations",
110
118
  "parse_export_bundle_manifest",
111
119
  "parse_export_options",
112
120
  "validate_clip",