x4d-devkit 0.45.0rc1__tar.gz → 0.45.1__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 (135) hide show
  1. {x4d_devkit-0.45.0rc1/x4d_devkit.egg-info → x4d_devkit-0.45.1}/PKG-INFO +1 -1
  2. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/pyproject.toml +1 -1
  3. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_sdk_api.py +1 -1
  4. x4d_devkit-0.45.1/tests/test_work_session_validation_reuse.py +109 -0
  5. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/work_session.py +85 -1
  6. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1/x4d_devkit.egg-info}/PKG-INFO +1 -1
  7. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit.egg-info/SOURCES.txt +1 -0
  8. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/LICENSE +0 -0
  9. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/README.md +0 -0
  10. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/setup.cfg +0 -0
  11. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_annotation_contract.py +0 -0
  12. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_annotation_reference.py +0 -0
  13. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_assets_client.py +0 -0
  14. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_boundary.py +0 -0
  15. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_box.py +0 -0
  16. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_build_info.py +0 -0
  17. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_clean_basemap.py +0 -0
  18. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_clean_basemap_client.py +0 -0
  19. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_clean_basemap_preannotation.py +0 -0
  20. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_cli_auth.py +0 -0
  21. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_client_json_contract.py +0 -0
  22. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_client_projects.py +0 -0
  23. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_clip_archives.py +0 -0
  24. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_contract_validation.py +0 -0
  25. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_coordinate_frames.py +0 -0
  26. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_current_state.py +0 -0
  27. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_current_state_identity_consumers_example.py +0 -0
  28. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_dataset_contract.py +0 -0
  29. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_dataset_identity.py +0 -0
  30. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_detection_eval.py +0 -0
  31. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_evaluate.py +0 -0
  32. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_export_bundle.py +0 -0
  33. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_image_review.py +0 -0
  34. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_inference_input.py +0 -0
  35. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_inference_schemas.py +0 -0
  36. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_inference_service.py +0 -0
  37. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_label_schemas.py +0 -0
  38. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_lidar_materialization.py +0 -0
  39. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_loader.py +0 -0
  40. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_matching.py +0 -0
  41. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_metrics.py +0 -0
  42. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_models.py +0 -0
  43. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_platform_pose.py +0 -0
  44. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_preannotation_client.py +0 -0
  45. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_reconstruction_client.py +0 -0
  46. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_release_gate.py +0 -0
  47. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_roadmark_preview.py +0 -0
  48. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_semantic_scene_authoring.py +0 -0
  49. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_semantic_scene_client.py +0 -0
  50. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_semantic_scene_contract.py +0 -0
  51. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_semantic_scene_native_clip.py +0 -0
  52. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_source_attributes.py +0 -0
  53. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_time_contract.py +0 -0
  54. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_token.py +0 -0
  55. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_tracking_eval.py +0 -0
  56. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_training_identity.py +0 -0
  57. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_transform.py +0 -0
  58. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_transform_chain.py +0 -0
  59. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_validation.py +0 -0
  60. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_vector_map.py +0 -0
  61. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_vector_map_geometry.py +0 -0
  62. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_vector_map_session.py +0 -0
  63. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/tests/test_work_session.py +0 -0
  64. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/__init__.py +0 -0
  65. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/annotation_contract.py +0 -0
  66. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/annotation_reference.py +0 -0
  67. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/build_info.py +0 -0
  68. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/clean_basemap.py +0 -0
  69. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/cli.py +0 -0
  70. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/__init__.py +0 -0
  71. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/_origin.py +0 -0
  72. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/annotation_reference.py +0 -0
  73. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/assets.py +0 -0
  74. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/bags.py +0 -0
  75. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/capabilities.py +0 -0
  76. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/checkpoints.py +0 -0
  77. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/clean_basemap.py +0 -0
  78. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/client.py +0 -0
  79. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/clip_work_sessions.py +0 -0
  80. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/clips.py +0 -0
  81. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/dataset_identity.py +0 -0
  82. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/image_review.py +0 -0
  83. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/label_schemas.py +0 -0
  84. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/preannotation.py +0 -0
  85. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/projects.py +0 -0
  86. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/reconstruction.py +0 -0
  87. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/semantic_scene.py +0 -0
  88. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  89. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/services.py +0 -0
  90. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/client/test_sets.py +0 -0
  91. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/consumer_identity.py +0 -0
  92. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/coordinate_contract.py +0 -0
  93. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/core/__init__.py +0 -0
  94. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/core/loader.py +0 -0
  95. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/core/models.py +0 -0
  96. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/core/token.py +0 -0
  97. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/core/transform.py +0 -0
  98. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/current_state.py +0 -0
  99. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/dataset_contract.py +0 -0
  100. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/dataset_identity.py +0 -0
  101. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/eval/__init__.py +0 -0
  102. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/eval/api.py +0 -0
  103. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/eval/box.py +0 -0
  104. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/eval/detection.py +0 -0
  105. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/eval/matching.py +0 -0
  106. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/eval/metrics.py +0 -0
  107. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/eval/tracking.py +0 -0
  108. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/export_bundle.py +0 -0
  109. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/image_review.py +0 -0
  110. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/inference/__init__.py +0 -0
  111. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/inference/input.py +0 -0
  112. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/inference/models.py +0 -0
  113. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/inference/schemas.py +0 -0
  114. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/inference/service.py +0 -0
  115. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/inference/tasks.py +0 -0
  116. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/json_contract.py +0 -0
  117. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/label_schema.py +0 -0
  118. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/lidar_materialization.py +0 -0
  119. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/materialize.py +0 -0
  120. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/roadmark_preview.py +0 -0
  121. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/semantic_scene_authoring.py +0 -0
  122. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/semantic_scene_contract.py +0 -0
  123. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/time_contract.py +0 -0
  124. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/training_identity.py +0 -0
  125. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/validation/__init__.py +0 -0
  126. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/validation/contract.py +0 -0
  127. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/validation/platform_pose.py +0 -0
  128. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/validation/report.py +0 -0
  129. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/validation/validator.py +0 -0
  130. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/vector_map.py +0 -0
  131. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit/vector_map_geometry.py +0 -0
  132. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  133. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit.egg-info/entry_points.txt +0 -0
  134. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/x4d_devkit.egg-info/requires.txt +0 -0
  135. {x4d_devkit-0.45.0rc1 → x4d_devkit-0.45.1}/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.45.0rc1
3
+ Version: 0.45.1
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.45.0rc1"
7
+ version = "0.45.1"
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"
@@ -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.45.0rc1"
14
+ assert x4d_devkit.__version__ == "0.45.1"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -0,0 +1,109 @@
1
+ """Identical metadata may reuse validation; changed or mutable inputs may not."""
2
+
3
+ from collections import OrderedDict
4
+ import copy
5
+ import json
6
+
7
+ import pytest
8
+
9
+ from tests.test_work_session import _manifest
10
+ import x4d_devkit.work_session as sessions
11
+ import x4d_devkit.validation.validator as validator
12
+
13
+
14
+ @pytest.fixture(autouse=True)
15
+ def isolated_validation_cache(monkeypatch):
16
+ monkeypatch.setattr(sessions, "_validated_sessions", OrderedDict())
17
+ monkeypatch.setattr(sessions, "_validated_session_bytes", 0)
18
+
19
+
20
+ @pytest.fixture
21
+ def validations(monkeypatch):
22
+ calls = []
23
+ original = validator.validate_clip_payload
24
+
25
+ def counted(*args, **kwargs):
26
+ calls.append(None)
27
+ return original(*args, **kwargs)
28
+
29
+ monkeypatch.setattr(validator, "validate_clip_payload", counted)
30
+ return calls
31
+
32
+
33
+ def test_repeated_open_and_identity_comparison_validate_identical_payload_once(validations):
34
+ payload = _manifest()
35
+ first = sessions.parse_clip_work_session(payload)
36
+ second = sessions.parse_clip_work_session(first.to_dict())
37
+ assert sessions.compare_work_session(first, second).same
38
+ assert len(validations) == 1
39
+ assert first is not second
40
+
41
+
42
+ def test_returned_snapshots_and_caller_input_cannot_poison_validation():
43
+ payload = _manifest()
44
+ original = copy.deepcopy(payload)
45
+ first = sessions.parse_clip_work_session(payload)
46
+ first.meta["sensors"]["lidar"]["frame_id"] = "changed-by-caller"
47
+ first.samples[0]["timestamp"]["sec"] = 99
48
+ payload["calibrated_sensor"][0]["translation"]["x"] = 100.0
49
+ second = sessions.parse_clip_work_session(original)
50
+ assert second.to_dict() == original
51
+ second.annotations[0]["bbox_3d"]["size"]["width"] = 999.0
52
+ assert sessions.parse_clip_work_session(original).to_dict() == original
53
+
54
+
55
+ @pytest.mark.parametrize("changed", ["annotation", "sample_data", "typed_manifest"])
56
+ def test_changed_payload_is_revalidated_even_with_identical_revisions(changed):
57
+ payload = _manifest()
58
+ parsed = sessions.parse_clip_work_session(payload)
59
+ if changed == "annotation":
60
+ payload["annotations"][0]["bbox_3d"]["size"]["width"] = -1.0
61
+ elif changed == "sample_data":
62
+ payload["sample_data"][0]["sample_token"] = "missing-sample"
63
+ else:
64
+ parsed.meta["schema_version"] = "0.17"
65
+ payload = parsed
66
+ with pytest.raises(ValueError):
67
+ sessions.parse_clip_work_session(payload)
68
+
69
+
70
+ def test_cache_does_not_coerce_tuple_tables_into_valid_json_lists():
71
+ payload = _manifest()
72
+ sessions.parse_clip_work_session(payload)
73
+ payload["samples"] = tuple(payload["samples"])
74
+ with pytest.raises(ValueError, match="samples must be a list"):
75
+ sessions.parse_clip_work_session(payload)
76
+
77
+
78
+ def test_cache_does_not_coerce_integer_object_keys_into_strings():
79
+ payload = _manifest()
80
+ urls = {}
81
+ for index, row in enumerate(payload["sample_data"]):
82
+ urls[str(index)] = payload["asset_urls"][row["sample_data_token"]]
83
+ row["sample_data_token"] = str(index)
84
+ payload["asset_urls"] = urls
85
+ sessions.parse_clip_work_session(payload)
86
+ payload["asset_urls"] = {int(key): value for key, value in urls.items()}
87
+ with pytest.raises((ValueError, TypeError)):
88
+ sessions.parse_clip_work_session(payload)
89
+
90
+
91
+ @pytest.mark.parametrize("budget", ["entries", "bytes"])
92
+ def test_validation_reuse_has_bounded_lru_storage(monkeypatch, validations, budget):
93
+ first = _manifest()
94
+ second = copy.deepcopy(first)
95
+ second["business_metadata_revision"] = "sha256:other-business-metadata"
96
+ if budget == "entries":
97
+ monkeypatch.setattr(sessions, "_VALIDATED_SESSION_MAX_ENTRIES", 1)
98
+ else:
99
+ sizes = [len(json.dumps(value, sort_keys=True, ensure_ascii=False,
100
+ separators=(",", ":")).encode())
101
+ for value in (first, second)]
102
+ monkeypatch.setattr(sessions, "_VALIDATED_SESSION_MAX_BYTES", sum(sizes) - 1)
103
+ sessions.parse_clip_work_session(first)
104
+ sessions.parse_clip_work_session(second)
105
+ sessions.parse_clip_work_session(second)
106
+ assert len(validations) == 2
107
+ sessions.parse_clip_work_session(first)
108
+ assert len(validations) == 3
109
+ assert len(sessions._validated_sessions) == 1
@@ -7,9 +7,12 @@ without requiring a prebuilt archive.
7
7
  from __future__ import annotations
8
8
 
9
9
  import copy
10
+ from collections import OrderedDict
10
11
  from dataclasses import dataclass
12
+ import json
11
13
  from pathlib import PurePosixPath
12
14
  import re
15
+ import threading
13
16
  from typing import Any, Mapping
14
17
  from urllib.parse import urljoin
15
18
 
@@ -290,6 +293,82 @@ class WorkSessionCompareResult:
290
293
  return {"status": self.status, "changed": list(self.changed)}
291
294
 
292
295
 
296
+ _VALIDATED_SESSION_MAX_ENTRIES = 16
297
+ _VALIDATED_SESSION_MAX_BYTES = 32 * 1024 * 1024
298
+ _validated_sessions: OrderedDict[bytes, ClipWorkSessionManifest] = OrderedDict()
299
+ _validated_session_bytes = 0
300
+ _validated_session_guard = threading.Lock()
301
+
302
+
303
+ def _validation_payload(data: dict[str, Any]) -> bytes | None:
304
+ # JSON serialization must never coerce an invalid key or container into a
305
+ # previously validated payload. Other Python values take the normal parser.
306
+ pending: list[Any] = [data]
307
+ seen: set[int] = set()
308
+ while pending:
309
+ value = pending.pop()
310
+ if type(value) in (dict, list):
311
+ if id(value) in seen:
312
+ continue
313
+ seen.add(id(value))
314
+ if type(value) is dict:
315
+ if any(type(key) is not str for key in value):
316
+ return None
317
+ pending.extend(value.values())
318
+ else:
319
+ pending.extend(value)
320
+ elif type(value) not in (str, int, float, bool, type(None)):
321
+ return None
322
+ try:
323
+ payload = json.dumps(
324
+ data, sort_keys=True, ensure_ascii=False, allow_nan=False,
325
+ separators=(",", ":"),
326
+ ).encode("utf-8")
327
+ except (ValueError, TypeError, UnicodeError, RecursionError):
328
+ return None
329
+ return payload if len(payload) <= _VALIDATED_SESSION_MAX_BYTES else None
330
+
331
+
332
+ def _session_snapshot(session: ClipWorkSessionManifest) -> ClipWorkSessionManifest:
333
+ # Schema internals contain immutable mapping proxies; reconstruct their
334
+ # small public snapshot and isolate every mutable table from the cache.
335
+ schema = parse_clip_label_schema_snapshot(session.label_schema.to_dict())
336
+ return copy.deepcopy(session, {id(session.label_schema): schema})
337
+
338
+
339
+ def _cached_session(data: dict[str, Any]) -> ClipWorkSessionManifest | None:
340
+ payload = _validation_payload(data)
341
+ if payload is None:
342
+ return None
343
+ with _validated_session_guard:
344
+ session = _validated_sessions.get(payload)
345
+ if session is not None:
346
+ _validated_sessions.move_to_end(payload)
347
+ return _session_snapshot(session) if session is not None else None
348
+
349
+
350
+ def _remember_session(session: ClipWorkSessionManifest) -> None:
351
+ global _validated_session_bytes
352
+ # Bind the validated result, never a caller-owned dictionary that could
353
+ # have changed during parsing. Bytes equality covers the complete payload.
354
+ payload = _validation_payload(session.to_dict())
355
+ if payload is None:
356
+ return
357
+ snapshot = _session_snapshot(session)
358
+ with _validated_session_guard:
359
+ if payload in _validated_sessions:
360
+ _validated_sessions.move_to_end(payload)
361
+ return
362
+ _validated_sessions[payload] = snapshot
363
+ _validated_session_bytes += len(payload)
364
+ while (
365
+ len(_validated_sessions) > _VALIDATED_SESSION_MAX_ENTRIES
366
+ or _validated_session_bytes > _VALIDATED_SESSION_MAX_BYTES
367
+ ):
368
+ evicted, _ = _validated_sessions.popitem(last=False)
369
+ _validated_session_bytes -= len(evicted)
370
+
371
+
293
372
  def parse_clip_work_session(
294
373
  value: ClipWorkSessionManifest | Mapping[str, Any],
295
374
  ) -> ClipWorkSessionManifest:
@@ -301,6 +380,9 @@ def parse_clip_work_session(
301
380
  data = dict(value)
302
381
  else:
303
382
  raise TypeError("work-session payload must be an object")
383
+ cached = _cached_session(data)
384
+ if cached is not None:
385
+ return cached
304
386
  if data.get("contract_version") != WORK_SESSION_CONTRACT_VERSION:
305
387
  raise ValueError(
306
388
  "work-session contract_version must be "
@@ -562,7 +644,7 @@ def parse_clip_work_session(
562
644
  )
563
645
  if not report.ok:
564
646
  raise ValueError(f"work-session payload violates the current clip contract:\n{report}")
565
- return ClipWorkSessionManifest(
647
+ session = ClipWorkSessionManifest(
566
648
  project_id=project_id,
567
649
  clip_id=clip_id,
568
650
  ready=ready,
@@ -587,6 +669,8 @@ def parse_clip_work_session(
587
669
  asset_urls=asset_urls,
588
670
  semantic_scene_asset_urls=semantic_scene_asset_urls,
589
671
  )
672
+ _remember_session(session)
673
+ return session
590
674
 
591
675
 
592
676
  def compare_work_session(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.45.0rc1
3
+ Version: 0.45.1
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
@@ -55,6 +55,7 @@ tests/test_vector_map.py
55
55
  tests/test_vector_map_geometry.py
56
56
  tests/test_vector_map_session.py
57
57
  tests/test_work_session.py
58
+ tests/test_work_session_validation_reuse.py
58
59
  x4d_devkit/__init__.py
59
60
  x4d_devkit/annotation_contract.py
60
61
  x4d_devkit/annotation_reference.py
File without changes
File without changes
File without changes