x4d-devkit 0.41.0rc1__tar.gz → 0.42.0__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 (123) hide show
  1. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/PKG-INFO +2 -1
  2. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/pyproject.toml +2 -1
  3. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_preannotation_client.py +102 -5
  4. x4d_devkit-0.42.0/tests/test_roadmark_preview.py +87 -0
  5. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_sdk_api.py +1 -1
  6. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/preannotation.py +104 -7
  7. x4d_devkit-0.42.0/x4d_devkit/roadmark_preview.py +219 -0
  8. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit.egg-info/PKG-INFO +2 -1
  9. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit.egg-info/SOURCES.txt +2 -0
  10. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit.egg-info/requires.txt +1 -0
  11. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/LICENSE +0 -0
  12. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/README.md +0 -0
  13. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/setup.cfg +0 -0
  14. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_annotation_contract.py +0 -0
  15. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_annotation_reference.py +0 -0
  16. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_assets_client.py +0 -0
  17. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_boundary.py +0 -0
  18. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_box.py +0 -0
  19. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_build_info.py +0 -0
  20. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_cli_auth.py +0 -0
  21. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_client_json_contract.py +0 -0
  22. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_client_projects.py +0 -0
  23. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_clip_archives.py +0 -0
  24. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_contract_validation.py +0 -0
  25. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_coordinate_frames.py +0 -0
  26. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_current_state.py +0 -0
  27. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_current_state_identity_consumers_example.py +0 -0
  28. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_dataset_contract.py +0 -0
  29. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_dataset_identity.py +0 -0
  30. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_detection_eval.py +0 -0
  31. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_evaluate.py +0 -0
  32. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_export_bundle.py +0 -0
  33. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_image_review.py +0 -0
  34. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_inference_input.py +0 -0
  35. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_inference_schemas.py +0 -0
  36. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_inference_service.py +0 -0
  37. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_label_schemas.py +0 -0
  38. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_lidar_materialization.py +0 -0
  39. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_loader.py +0 -0
  40. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_matching.py +0 -0
  41. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_metrics.py +0 -0
  42. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_models.py +0 -0
  43. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_platform_pose.py +0 -0
  44. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_reconstruction_client.py +0 -0
  45. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_release_gate.py +0 -0
  46. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_semantic_scene_authoring.py +0 -0
  47. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_semantic_scene_client.py +0 -0
  48. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_semantic_scene_contract.py +0 -0
  49. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_semantic_scene_native_clip.py +0 -0
  50. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_time_contract.py +0 -0
  51. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_token.py +0 -0
  52. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_tracking_eval.py +0 -0
  53. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_training_identity.py +0 -0
  54. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_transform.py +0 -0
  55. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_transform_chain.py +0 -0
  56. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_validation.py +0 -0
  57. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/tests/test_work_session.py +0 -0
  58. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/__init__.py +0 -0
  59. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/annotation_contract.py +0 -0
  60. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/annotation_reference.py +0 -0
  61. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/build_info.py +0 -0
  62. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/cli.py +0 -0
  63. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/__init__.py +0 -0
  64. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/_origin.py +0 -0
  65. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/annotation_reference.py +0 -0
  66. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/assets.py +0 -0
  67. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/bags.py +0 -0
  68. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/capabilities.py +0 -0
  69. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/checkpoints.py +0 -0
  70. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/client.py +0 -0
  71. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/clip_work_sessions.py +0 -0
  72. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/clips.py +0 -0
  73. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/dataset_identity.py +0 -0
  74. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/image_review.py +0 -0
  75. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/label_schemas.py +0 -0
  76. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/projects.py +0 -0
  77. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/reconstruction.py +0 -0
  78. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/semantic_scene.py +0 -0
  79. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/semantic_scene_authoring.py +0 -0
  80. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/services.py +0 -0
  81. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/client/test_sets.py +0 -0
  82. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/consumer_identity.py +0 -0
  83. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/coordinate_contract.py +0 -0
  84. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/core/__init__.py +0 -0
  85. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/core/loader.py +0 -0
  86. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/core/models.py +0 -0
  87. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/core/token.py +0 -0
  88. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/core/transform.py +0 -0
  89. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/current_state.py +0 -0
  90. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/dataset_contract.py +0 -0
  91. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/dataset_identity.py +0 -0
  92. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/__init__.py +0 -0
  93. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/api.py +0 -0
  94. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/box.py +0 -0
  95. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/detection.py +0 -0
  96. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/matching.py +0 -0
  97. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/metrics.py +0 -0
  98. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/eval/tracking.py +0 -0
  99. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/export_bundle.py +0 -0
  100. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/image_review.py +0 -0
  101. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/__init__.py +0 -0
  102. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/input.py +0 -0
  103. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/models.py +0 -0
  104. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/schemas.py +0 -0
  105. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/service.py +0 -0
  106. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/inference/tasks.py +0 -0
  107. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/json_contract.py +0 -0
  108. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/label_schema.py +0 -0
  109. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/lidar_materialization.py +0 -0
  110. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/materialize.py +0 -0
  111. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/semantic_scene_authoring.py +0 -0
  112. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/semantic_scene_contract.py +0 -0
  113. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/time_contract.py +0 -0
  114. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/training_identity.py +0 -0
  115. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/validation/__init__.py +0 -0
  116. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/validation/contract.py +0 -0
  117. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/validation/platform_pose.py +0 -0
  118. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/validation/report.py +0 -0
  119. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/validation/validator.py +0 -0
  120. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit/work_session.py +0 -0
  121. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit.egg-info/dependency_links.txt +0 -0
  122. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit.egg-info/entry_points.txt +0 -0
  123. {x4d_devkit-0.41.0rc1 → x4d_devkit-0.42.0}/x4d_devkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.41.0rc1
3
+ Version: 0.42.0
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
@@ -23,6 +23,7 @@ Requires-Dist: numpy>=1.24
23
23
  Requires-Dist: httpx>=0.27
24
24
  Requires-Dist: rfc8785==0.1.4
25
25
  Requires-Dist: zstandard<1,>=0.23
26
+ Requires-Dist: pyproj<4,>=3.6
26
27
  Provides-Extra: inference
27
28
  Requires-Dist: pydantic>=2.0; extra == "inference"
28
29
  Requires-Dist: fastapi>=0.110; extra == "inference"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "x4d-devkit"
7
- version = "0.41.0rc1"
7
+ version = "0.42.0"
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"
@@ -29,6 +29,7 @@ dependencies = [
29
29
  "httpx>=0.27",
30
30
  "rfc8785==0.1.4",
31
31
  "zstandard>=0.23,<1",
32
+ "pyproj>=3.6,<4",
32
33
  ]
33
34
 
34
35
  [project.optional-dependencies]
@@ -50,7 +50,7 @@ def _service() -> dict:
50
50
 
51
51
  def _run(*, status: str = "queued", reused: bool = False) -> dict:
52
52
  succeeded = status == "succeeded"
53
- return {
53
+ result = {
54
54
  "run_id": "preann-run-1",
55
55
  "request_identity": _digest("a"),
56
56
  "project_id": 7,
@@ -76,7 +76,34 @@ def _run(*, status: str = "queued", reused: bool = False) -> dict:
76
76
  "created_at": "2026-08-18T08:00:00Z",
77
77
  "updated_at": "2026-08-18T08:02:00Z",
78
78
  "reused": reused,
79
+ "attempt_number": 1,
80
+ "attempt_count": 1,
81
+ "latest_attempt_id": "preann-run-1",
82
+ "is_latest_attempt": True,
83
+ "retry_of_run_id": None,
79
84
  }
85
+ result["attempts"] = [
86
+ {
87
+ key: copy.deepcopy(result[key])
88
+ for key in (
89
+ "run_id",
90
+ "attempt_number",
91
+ "retry_of_run_id",
92
+ "status",
93
+ "progress",
94
+ "error",
95
+ "candidate_id",
96
+ "temporal_workflow_id",
97
+ "actor_id",
98
+ "started_at",
99
+ "completed_at",
100
+ "cancellation_requested_at",
101
+ "created_at",
102
+ "updated_at",
103
+ )
104
+ }
105
+ ]
106
+ return result
80
107
 
81
108
 
82
109
  def _portable_candidate() -> dict:
@@ -144,7 +171,10 @@ def test_control_plane_uses_typed_service_run_cancel_and_candidate_contracts():
144
171
  requests.append(request)
145
172
  if request.url.path == "/api/v1/preannotation/services":
146
173
  return httpx.Response(200, json=[_service()])
147
- if request.url.path == "/api/v1/preannotation/runs" and request.method == "POST":
174
+ if (
175
+ request.url.path == "/api/v1/preannotation/runs"
176
+ and request.method == "POST"
177
+ ):
148
178
  return httpx.Response(202, json=_run())
149
179
  if request.url.path == "/api/v1/preannotation/runs" and request.method == "GET":
150
180
  return httpx.Response(200, json=[_run(status="succeeded")])
@@ -172,9 +202,7 @@ def test_control_plane_uses_typed_service_run_cancel_and_candidate_contracts():
172
202
  profile_id="centerpoint-track-v1",
173
203
  config={"score_threshold": 0.4},
174
204
  )
175
- runs = client.preannotation.list_runs(
176
- project_id=7, clip_id="clip-1", limit=10
177
- )
205
+ runs = client.preannotation.list_runs(project_id=7, clip_id="clip-1", limit=10)
178
206
  fetched = client.preannotation.get_run("preann-run-1")
179
207
  cancellation = client.preannotation.cancel_run("preann-run-1")
180
208
  candidate = client.preannotation.get_candidate(_digest("d"))
@@ -260,3 +288,72 @@ def test_create_run_rejects_non_i_json_config_before_request():
260
288
  profile_id="centerpoint-track-v1",
261
289
  config={"threshold": float("nan")},
262
290
  )
291
+
292
+
293
+ def _retried_run():
294
+ previous = _run(status="failed")["attempts"][0]
295
+ result = _run(status="succeeded")
296
+ result.update(
297
+ run_id="preann-run-2",
298
+ attempt_number=2,
299
+ attempt_count=2,
300
+ latest_attempt_id="preann-run-2",
301
+ retry_of_run_id="preann-run-1",
302
+ )
303
+ result["attempts"] = [
304
+ previous,
305
+ {key: copy.deepcopy(result[key]) for key in previous},
306
+ ]
307
+ return result
308
+
309
+
310
+ def test_retry_history_round_trip_and_historical_run():
311
+ payload = _retried_run()
312
+ parsed = PlatformPreannotationRun.model_validate(payload)
313
+ assert parsed.attempt_count == 2
314
+ assert parsed.attempts[0].status == PlatformPreannotationRunStatus.FAILED
315
+ assert parsed.attempts[1].candidate_id == parsed.candidate_id
316
+ assert (
317
+ PlatformPreannotationRun.model_validate_json(parsed.model_dump_json()) == parsed
318
+ )
319
+ payload.update(payload["attempts"][0])
320
+ payload["is_latest_attempt"] = False
321
+ old = PlatformPreannotationRun.model_validate(payload)
322
+ assert old.run_id == "preann-run-1" and old.latest_attempt_id == "preann-run-2"
323
+ payload["attempts"] = []
324
+ assert PlatformPreannotationRun.model_validate(payload).attempt_count == 2
325
+
326
+
327
+ @pytest.mark.parametrize(
328
+ "field,value",
329
+ [
330
+ ("attempt_count", 1),
331
+ ("attempt_number", 1),
332
+ ("attempt_number", True),
333
+ ("latest_attempt_id", "preann-missing"),
334
+ ("is_latest_attempt", False),
335
+ ("is_latest_attempt", 1),
336
+ ("retry_of_run_id", None),
337
+ ("attempts", [_run(status="failed")["attempts"][0]]),
338
+ ],
339
+ )
340
+ def test_inconsistent_attempt_identity_is_rejected(field, value):
341
+ payload = _retried_run()
342
+ payload[field] = value
343
+ with pytest.raises(ValidationError):
344
+ PlatformPreannotationRun.model_validate(payload)
345
+
346
+
347
+ @pytest.mark.parametrize("mutation", ["unknown", "link", "order", "state"])
348
+ def test_malformed_attempt_history_is_rejected(mutation):
349
+ payload = _retried_run()
350
+ if mutation == "unknown":
351
+ payload["attempts"][0]["unknown_field"] = True
352
+ elif mutation == "link":
353
+ payload["attempts"][1]["retry_of_run_id"] = "another-run"
354
+ elif mutation == "order":
355
+ payload["attempts"].reverse()
356
+ else:
357
+ payload["attempts"][1]["progress"]["stage"] = "different-state"
358
+ with pytest.raises(ValidationError):
359
+ PlatformPreannotationRun.model_validate(payload)
@@ -0,0 +1,87 @@
1
+ import copy
2
+ import json
3
+
4
+ import numpy as np
5
+ import pytest
6
+
7
+ from x4d_devkit.roadmark_preview import preview_roadmark
8
+
9
+ BIG = 2024121315242816837
10
+ LINE = [[3, 0, 4, 1], [3.00001, 0.00001, 5, 1]]
11
+
12
+
13
+ def source():
14
+ return {"valid": True, "centerline": [
15
+ {"class": "centerline", "id": BIG, "points": LINE, "orientation": True,
16
+ "successor": [BIG + 1], "predecessor": [], "left_lane": [BIG + 2]},
17
+ {"class": "centerline", "id": BIG + 1, "points": LINE,
18
+ "successor": [], "predecessor": [BIG]}],
19
+ "lane": [{"class": "lane", "id": BIG + 2, "points": LINE, "dashtype": "dash"}],
20
+ "special_points": [{"class": "special_points", "id": 1, "points": [[3, 0, 8]], "type": "type"}],
21
+ "arrow": [{"class": "arrow", "id": 2, "subclass": "straight", "vertices": [],
22
+ "points": [[3, 0, 2], [3.00001, 0, 2], [3.00001, 0.00001, 2], [3, 0.00001, 2]]}],
23
+ "map_labeling_area": [{"points": [[[3, 0, 2], [3.00001, 0, 2], [3, 0.00001, 2], [3, 0, 2]]]}]}
24
+
25
+
26
+ def preview(payload, matrix=None):
27
+ if matrix is None:
28
+ matrix = [[1, 0, 0, -500000], [0, 1, 0, 0], [0, 0, 1, -1], [0, 0, 0, 1]]
29
+ return preview_roadmark(json.dumps(payload), source_crs="EPSG:4326", metric_crs="EPSG:32631",
30
+ clip_world_from_metric=matrix)
31
+
32
+
33
+ def test_exact_source_ids_graph_and_uninterpreted_values():
34
+ payload = source()
35
+ original = copy.deepcopy(payload)
36
+ view = preview(payload)
37
+ assert payload == original
38
+ assert view["features"][0]["id"] == str(BIG)
39
+ assert view["features"][1]["id"] == str(BIG + 1)
40
+ assert view["relations"] == [
41
+ {"type": "successor", "from_id": str(BIG), "to_id": str(BIG + 1)},
42
+ {"type": "left_lane", "from_id": str(BIG), "to_id": str(BIG + 2)}]
43
+ feature = view["features"][0]
44
+ assert feature["properties"]["orientation"] is True
45
+ assert feature["properties"]["source_point_extra"] == [[1], [1]]
46
+ assert view["features"][3]["properties"]["type"] == "type"
47
+ assert view["coverage"][0]["completeness"] == "unknown"
48
+
49
+
50
+ def test_horizontal_projection_axis_order_height_and_rigid_transform():
51
+ view = preview(source())
52
+ point = view["features"][0]["geometry"]["points"][0]
53
+ np.testing.assert_allclose(point, [0, 0, 3], atol=1e-8)
54
+ assert view["features"][4]["geometry"]["rings"][0][0] == view["features"][4]["geometry"]["rings"][0][-1]
55
+ assert view["source"]["vertical_datum"] == "unspecified"
56
+ # Rotation acts on height too; never reduce the transform to yaw/XY.
57
+ matrix = [[1, 0, 0, -500000], [0, 0, -1, 0], [0, 1, 0, 0], [0, 0, 0, 1]]
58
+ np.testing.assert_allclose(preview(source(), matrix)["features"][0]["geometry"]["points"][0], [0, -4, 0], atol=1e-8)
59
+
60
+
61
+ @pytest.mark.parametrize("change", [
62
+ lambda p: p["centerline"][0].update(id=float(BIG)),
63
+ lambda p: p["centerline"][0].update(successor=[0]),
64
+ lambda p: p["centerline"][1].update(predecessor=[]),
65
+ lambda p: p["centerline"][0].update(left_lane=[BIG + 1]),
66
+ lambda p: p["centerline"][0].update(points=[[float("nan"), 0, 0], [3, 1, 0]]),
67
+ lambda p: p["centerline"][0].update(points=[[3, 0, 0], [3, 0, 0]]),
68
+ lambda p: p["lane"][0].update(id=BIG),
69
+ lambda p: p.update(unknown_layer=[]),
70
+ lambda p: p.update(deceleration_zone=[{}]),
71
+ ])
72
+ def test_invalid_source_is_not_repaired(change):
73
+ payload = source()
74
+ change(payload)
75
+ with pytest.raises(ValueError):
76
+ preview(payload)
77
+
78
+
79
+ def test_no_implicit_crs_or_transform_and_duplicate_keys_rejected():
80
+ args = dict(source_crs="EPSG:4326", metric_crs="EPSG:32631", clip_world_from_metric=np.eye(4).tolist())
81
+ with pytest.raises(ValueError, match="Duplicate"):
82
+ preview_roadmark('{"valid":true,"valid":false}', **args)
83
+ with pytest.raises(ValueError, match="SE\\(3\\)"):
84
+ preview(source(), (np.eye(4) * 2).tolist())
85
+ with pytest.raises(ValueError, match="metres"):
86
+ preview_roadmark(json.dumps(source()), **{**args, "metric_crs": "EPSG:4326"})
87
+ assert preview({"valid": False})["bounds"] is None
@@ -11,7 +11,7 @@ from x4d_devkit.eval.matching import MetricData
11
11
 
12
12
 
13
13
  def test_package_version_is_current_release():
14
- assert x4d_devkit.__version__ == "0.41.0rc1"
14
+ assert x4d_devkit.__version__ == "0.42.0"
15
15
 
16
16
 
17
17
  def test_loader_exposes_and_enforces_only_current_schema(tmp_path):
@@ -86,7 +86,53 @@ class PlatformPreannotationService(_ControlPlaneModel):
86
86
  return value
87
87
 
88
88
 
89
- class PlatformPreannotationRun(_ControlPlaneModel):
89
+ class PlatformPreannotationRunAttempt(_ControlPlaneModel):
90
+ """One immutable attempt identity and its current execution state."""
91
+
92
+ run_id: str
93
+ attempt_number: int = Field(ge=1, strict=True)
94
+ retry_of_run_id: str | None
95
+ status: PlatformPreannotationRunStatus
96
+ progress: PreannotationRunProgress
97
+ error: PreannotationRunError | None
98
+ candidate_id: str | None
99
+ temporal_workflow_id: str
100
+ actor_id: int | None = Field(gt=0, strict=True)
101
+ started_at: datetime | None
102
+ completed_at: datetime | None
103
+ cancellation_requested_at: datetime | None
104
+ created_at: datetime
105
+ updated_at: datetime
106
+
107
+ @field_validator("run_id", "retry_of_run_id", mode="before")
108
+ @classmethod
109
+ def validate_attempt_id(cls, value: Any, info: Any) -> str | None:
110
+ if value is None and info.field_name == "retry_of_run_id":
111
+ return None
112
+ return _protocol_token(value, path="preannotation attempt identifier")
113
+
114
+ @field_validator("candidate_id", mode="before")
115
+ @classmethod
116
+ def validate_attempt_candidate(cls, value: Any) -> str | None:
117
+ return None if value is None else _sha256(value, path="attempt.candidate_id")
118
+
119
+ @field_validator("temporal_workflow_id", mode="before")
120
+ @classmethod
121
+ def validate_attempt_workflow(cls, value: Any) -> str:
122
+ return _canonical_string(value, path="attempt.temporal_workflow_id")
123
+
124
+ @model_validator(mode="after")
125
+ def validate_attempt_result(self) -> "PlatformPreannotationRunAttempt":
126
+ if (self.status == PlatformPreannotationRunStatus.SUCCEEDED) != (
127
+ self.candidate_id is not None
128
+ ):
129
+ raise ValueError("only succeeded attempts must contain candidate_id")
130
+ if (self.attempt_number == 1) != (self.retry_of_run_id is None):
131
+ raise ValueError("only the first attempt has no retry predecessor")
132
+ return self
133
+
134
+
135
+ class PlatformPreannotationRun(PlatformPreannotationRunAttempt):
90
136
  """One durable X-4D preannotation run."""
91
137
 
92
138
  run_id: str
@@ -99,6 +145,10 @@ class PlatformPreannotationRun(_ControlPlaneModel):
99
145
  profile_id: str
100
146
  pipeline_identity: str
101
147
  checkpoint_identity: str
148
+ attempt_count: int = Field(ge=1, strict=True)
149
+ latest_attempt_id: str
150
+ is_latest_attempt: bool = Field(strict=True)
151
+ attempts: list[PlatformPreannotationRunAttempt]
102
152
  status: PlatformPreannotationRunStatus
103
153
  progress: PreannotationRunProgress
104
154
  error: PreannotationRunError | None
@@ -117,7 +167,9 @@ class PlatformPreannotationRun(_ControlPlaneModel):
117
167
  def validate_project_id(cls, value: Any) -> int:
118
168
  return _positive_int(value, path="preannotation run.project_id")
119
169
 
120
- @field_validator("run_id", "service_id", "profile_id", mode="before")
170
+ @field_validator(
171
+ "run_id", "service_id", "profile_id", "latest_attempt_id", mode="before"
172
+ )
121
173
  @classmethod
122
174
  def validate_protocol_id(cls, value: Any) -> str:
123
175
  return _protocol_token(value, path="preannotation run identifier")
@@ -169,9 +221,48 @@ class PlatformPreannotationRun(_ControlPlaneModel):
169
221
  def succeeded_run_has_candidate(self) -> "PlatformPreannotationRun":
170
222
  if self.status == PlatformPreannotationRunStatus.SUCCEEDED:
171
223
  if self.candidate_id is None:
172
- raise ValueError("succeeded preannotation run must contain candidate_id")
224
+ raise ValueError(
225
+ "succeeded preannotation run must contain candidate_id"
226
+ )
173
227
  elif self.candidate_id is not None:
174
- raise ValueError("only a succeeded preannotation run may contain candidate_id")
228
+ raise ValueError(
229
+ "only a succeeded preannotation run may contain candidate_id"
230
+ )
231
+ return self
232
+
233
+ @model_validator(mode="after")
234
+ def validate_attempt_history(self) -> "PlatformPreannotationRun":
235
+ if self.attempt_number > self.attempt_count:
236
+ raise ValueError("attempt_number must not exceed attempt_count")
237
+ if self.is_latest_attempt != (self.attempt_number == self.attempt_count):
238
+ raise ValueError("latest attempt flag disagrees with attempt count")
239
+ if self.is_latest_attempt != (self.run_id == self.latest_attempt_id):
240
+ raise ValueError("latest attempt flag disagrees with run identity")
241
+ # The list endpoint explicitly omits expansion; detail includes history.
242
+ if not self.attempts:
243
+ return self
244
+ if len(self.attempts) != self.attempt_count:
245
+ raise ValueError("attempt_count must match complete attempt history")
246
+ ids = [attempt.run_id for attempt in self.attempts]
247
+ if len(set(ids)) != len(ids) or self.run_id not in ids:
248
+ raise ValueError("attempt history must uniquely contain this run")
249
+ for number, attempt in enumerate(self.attempts, 1):
250
+ predecessor = None if number == 1 else ids[number - 2]
251
+ if (
252
+ attempt.attempt_number != number
253
+ or attempt.retry_of_run_id != predecessor
254
+ ):
255
+ raise ValueError(
256
+ "attempt history must be ordered with exact retry links"
257
+ )
258
+ if self.latest_attempt_id != ids[-1] or self.is_latest_attempt != (
259
+ self.run_id == self.latest_attempt_id
260
+ ):
261
+ raise ValueError("latest attempt identity disagrees with history")
262
+ current = self.attempts[ids.index(self.run_id)]
263
+ for field in PlatformPreannotationRunAttempt.model_fields:
264
+ if getattr(current, field) != getattr(self, field):
265
+ raise ValueError(f"current attempt disagrees with history: {field}")
175
266
  return self
176
267
 
177
268
 
@@ -384,13 +475,17 @@ class PreannotationAPI:
384
475
  payload = self._get("/preannotation/runs", params=params)
385
476
  if type(payload) is not list:
386
477
  raise TypeError("preannotation runs response must be a list")
387
- results = tuple(PlatformPreannotationRun.model_validate(item) for item in payload)
478
+ results = tuple(
479
+ PlatformPreannotationRun.model_validate(item) for item in payload
480
+ )
388
481
  if any(
389
482
  item.project_id != project_id
390
483
  or (clip_id is not None and item.clip_id != clip_id)
391
484
  for item in results
392
485
  ):
393
- raise ValueError("preannotation runs response is outside the requested scope")
486
+ raise ValueError(
487
+ "preannotation runs response is outside the requested scope"
488
+ )
394
489
  return results
395
490
 
396
491
  def get_run(self, run_id: str) -> PlatformPreannotationRun:
@@ -401,7 +496,9 @@ class PreannotationAPI:
401
496
  self._get(f"/preannotation/runs/{run_id}")
402
497
  )
403
498
  if result.run_id != run_id:
404
- raise ValueError("preannotation run response identity does not match request")
499
+ raise ValueError(
500
+ "preannotation run response identity does not match request"
501
+ )
405
502
  return result
406
503
 
407
504
  def cancel_run(self, run_id: str) -> PlatformPreannotationCancellation:
@@ -0,0 +1,219 @@
1
+ """Explicit RoadMark source inspection; never a native Dataset compatibility reader.
2
+
3
+ The caller supplies the geographic CRS and the rigid metric-to-Clip transform.
4
+ Source layer names and unresolved properties remain source facts. This preview
5
+ does not create native categories, lane segments, or annotation state.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import math
13
+ from typing import Any
14
+
15
+ import numpy as np
16
+ from pyproj import CRS, Transformer
17
+ from pyproj.exceptions import ProjError
18
+
19
+ from x4d_devkit.core.transform import Transform
20
+ from x4d_devkit.json_contract import ensure_i_json
21
+
22
+ CONTRACT = "x4d-roadmark-preview-v1"
23
+ SOURCE_LAYERS = {
24
+ "arrow": "polygon", "centerline": "polyline", "crosswalk": "polygon",
25
+ "curb": "polyline", "lane": "polyline", "special_points": "point",
26
+ "stop_line": "polyline",
27
+ }
28
+ _EMPTY_ONLY = {"construction_area", "deceleration_line", "deceleration_zone"}
29
+ _RELATIONS = ("predecessor", "successor", "left_lane", "right_lane")
30
+
31
+
32
+ def _source_json(raw: str) -> dict:
33
+ # RoadMark IDs are integers beyond I-JSON's safe range. Decode exactly once,
34
+ # before converting identity fields to strings. Native I-JSON is unchanged.
35
+ def members(pairs):
36
+ result = {}
37
+ for key, value in pairs:
38
+ if key in result:
39
+ raise ValueError(f"Duplicate RoadMark field: {key}")
40
+ result[key] = value
41
+ return result
42
+
43
+ def constant(value):
44
+ raise ValueError(f"Non-finite RoadMark number: {value}")
45
+
46
+ if type(raw) is not str or len(raw.encode("utf-8")) > 8_000_000:
47
+ raise ValueError("RoadMark input must be UTF-8 text of at most 8 MB")
48
+ payload = json.loads(raw, object_pairs_hook=members, parse_constant=constant)
49
+ if type(payload) is not dict:
50
+ raise ValueError("RoadMark total map must be an object")
51
+ unknown = set(payload) - set(SOURCE_LAYERS) - _EMPTY_ONLY - {"valid", "map_labeling_area"}
52
+ if unknown:
53
+ raise ValueError(f"Unsupported RoadMark layers: {sorted(unknown)}")
54
+ if type(payload.get("valid")) is not bool:
55
+ raise ValueError("RoadMark valid must be a boolean")
56
+ return payload
57
+
58
+
59
+ def _id(value: Any) -> str:
60
+ if type(value) is not int:
61
+ raise ValueError("RoadMark IDs must be JSON integers, not rounded floats")
62
+ return str(value)
63
+
64
+
65
+ def _property(value: Any) -> Any:
66
+ if type(value) is int and abs(value) > 2**53 - 1:
67
+ return str(value)
68
+ if type(value) is list:
69
+ return [_property(item) for item in value]
70
+ if type(value) is dict:
71
+ return {key: _property(item) for key, item in value.items()}
72
+ return value
73
+
74
+
75
+ def preview_roadmark(
76
+ raw: str, *, source_crs: str, metric_crs: str,
77
+ clip_world_from_metric: list[list[float]],
78
+ ) -> dict[str, Any]:
79
+ """Parse one total map into a transient, metre/XYZ, Clip-world read view.
80
+
81
+ Geographic inputs use longitude, latitude, source height. Only horizontal
82
+ CRS projection runs: source Z is preserved before the explicit rigid
83
+ transform. No vertical datum or time correction is guessed. Polygon rings
84
+ must already be closed; source line orientation is never reinterpreted.
85
+ """
86
+ source = _source_json(raw)
87
+ try:
88
+ geographic, metric = CRS(source_crs), CRS(metric_crs)
89
+ except ProjError as exc:
90
+ raise ValueError(f"Invalid source/metric CRS: {exc}") from exc
91
+ if not geographic.is_geographic or len(geographic.axis_info) != 2:
92
+ raise ValueError("source_crs must be an explicit two-dimensional geographic CRS")
93
+ if not metric.is_projected or len(metric.axis_info) != 2 or any(
94
+ axis.unit_conversion_factor != 1 for axis in metric.axis_info
95
+ ):
96
+ raise ValueError("metric_crs must be an explicit projected CRS in metres")
97
+ matrix = np.asarray(clip_world_from_metric)
98
+ if (matrix.shape != (4, 4) or matrix.dtype.kind not in "fiu"
99
+ or not np.isfinite(matrix).all()
100
+ or not np.allclose(matrix[3], [0, 0, 0, 1], atol=1e-9, rtol=0)
101
+ or not np.allclose(matrix[:3, :3].T @ matrix[:3, :3], np.eye(3), atol=1e-8, rtol=0)
102
+ or not np.isclose(np.linalg.det(matrix[:3, :3]), 1, atol=1e-8, rtol=0)):
103
+ raise ValueError("clip_world_from_metric must be a finite SE(3) matrix")
104
+ rigid = Transform(matrix[:3, :3], matrix[:3, 3])
105
+ try:
106
+ projector = Transformer.from_crs(geographic, metric, always_xy=True, allow_ballpark=False)
107
+ except ProjError as exc:
108
+ raise ValueError(f"No supported horizontal projection: {exc}") from exc
109
+ all_points: list[list[float]] = []
110
+
111
+ def points(rows: Any, kind: str) -> list[list[float]]:
112
+ if type(rows) is not list or not rows:
113
+ raise ValueError("Geometry must contain points")
114
+ for row in rows:
115
+ if (type(row) is not list or len(row) not in (3, 4)
116
+ or any(type(n) not in (int, float) or not math.isfinite(n) for n in row)
117
+ or abs(row[0]) > 180 or abs(row[1]) > 90):
118
+ raise ValueError("RoadMark points must be finite longitude/latitude/height[/source extra]")
119
+ minimum = {"point": 1, "polyline": 2, "polygon": 3}[kind]
120
+ if len({tuple(row[:3]) for row in rows}) < minimum or (kind == "point" and len(rows) != 1):
121
+ raise ValueError(f"Invalid {kind} vertex count")
122
+ if kind == "polygon" and rows[0][:3] != rows[-1][:3]:
123
+ raise ValueError("RoadMark polygon rings must be explicitly closed")
124
+ xyz = np.array([row[:3] for row in rows], dtype=float)
125
+ try:
126
+ xyz[:, 0], xyz[:, 1] = projector.transform(xyz[:, 0], xyz[:, 1], errcheck=True)
127
+ except ProjError as exc:
128
+ raise ValueError(f"Source coordinates cannot be projected: {exc}") from exc
129
+ converted = rigid.apply(xyz)
130
+ if not np.isfinite(converted).all():
131
+ raise ValueError("Non-finite projected geometry")
132
+ result = converted.tolist()
133
+ all_points.extend(result)
134
+ return result
135
+
136
+ features, relations, coverage = [], [], []
137
+ records: dict[str, dict] = {}
138
+ for layer, rows in source.items():
139
+ if layer == "valid":
140
+ continue
141
+ if type(rows) is not list:
142
+ raise ValueError(f"RoadMark {layer} must be an array")
143
+ if layer in _EMPTY_ONLY:
144
+ if rows:
145
+ raise ValueError(f"Non-empty {layer} has no verified source geometry mapping")
146
+ continue
147
+ if layer == "map_labeling_area":
148
+ for area in rows:
149
+ if (type(area) is not dict or set(area) != {"points"}
150
+ or type(area["points"]) is not list or len(area["points"]) != 1):
151
+ raise ValueError("Invalid RoadMark map_labeling_area")
152
+ coverage.append({"geometry": {"type": "polygon", "rings": [
153
+ points(ring, "polygon") for ring in area["points"]]}, "completeness": "unknown"})
154
+ continue
155
+ for row in rows:
156
+ if type(row) is not dict or row.get("class") != layer:
157
+ raise ValueError(f"RoadMark class must match layer {layer}")
158
+ identity = _id(row.get("id"))
159
+ if identity in records:
160
+ raise ValueError(f"Duplicate RoadMark ID: {identity}")
161
+ records[identity] = row
162
+ kind = SOURCE_LAYERS[layer]
163
+ if layer == "arrow":
164
+ # This source uses four ordered corners for the marked region,
165
+ # not a closed silhouette of the painted arrow itself.
166
+ converted = points(row.get("points"), "polyline")
167
+ if len(converted) != 4 or len({tuple(p) for p in converted}) != 4:
168
+ raise ValueError("RoadMark arrow regions require four distinct corners")
169
+ converted = [*converted, converted[0]]
170
+ else:
171
+ converted = points(row.get("points"), kind)
172
+ geometry = {"type": kind}
173
+ geometry.update({"point": converted[0]} if kind == "point" else
174
+ {"rings": [converted]} if kind == "polygon" else {"points": converted})
175
+ properties = {key: _property(value) for key, value in row.items()
176
+ if key not in {"id", "class", "points", *_RELATIONS}}
177
+ if any(len(point) == 4 for point in row["points"]):
178
+ properties["source_point_extra"] = [point[3:] for point in row["points"]]
179
+ features.append({"id": identity, "layer": layer, "geometry": geometry, "properties": properties})
180
+ for identity, row in records.items():
181
+ for name in _RELATIONS:
182
+ refs = row.get(name, [])
183
+ if type(refs) is not list:
184
+ raise ValueError(f"RoadMark {name} must be an array")
185
+ targets = [_id(value) for value in refs]
186
+ if len(set(targets)) != len(targets):
187
+ raise ValueError(f"Duplicate {name} relation on {identity}")
188
+ for target in targets:
189
+ other = records.get(target)
190
+ if other is None:
191
+ raise ValueError(f"Dangling RoadMark reference: {identity} -> {target}")
192
+ if name in ("left_lane", "right_lane"):
193
+ if row["class"] != "centerline" or other["class"] != "lane":
194
+ raise ValueError("RoadMark boundary references must connect centerline to lane")
195
+ else:
196
+ reverse = "predecessor" if name == "successor" else "successor"
197
+ if (row["class"] != other["class"] or type(other.get(reverse, [])) is not list
198
+ or int(identity) not in other.get(reverse, [])):
199
+ raise ValueError("RoadMark predecessor/successor must be reciprocal within a source layer")
200
+ if name != "predecessor":
201
+ relations.append({"type": name, "from_id": identity, "to_id": target})
202
+ bounds = [np.min(all_points, axis=0).tolist(), np.max(all_points, axis=0).tolist()] if all_points else None
203
+ result = {
204
+ "contract_version": CONTRACT, "frame_id": "clip_world", "features": features,
205
+ "relations": relations, "coverage": coverage, "bounds": bounds,
206
+ "source": {"format": "roadmark-lb-all", "sha256": hashlib.sha256(raw.encode("utf-8")).hexdigest(),
207
+ "valid": source["valid"], "source_crs": geographic.to_string(), "metric_crs": metric.to_string(),
208
+ "clip_world_from_metric": matrix.tolist(), "vertical_datum": "unspecified"},
209
+ "limitations": ["Source layers and relation names are not native Label Schema categories.",
210
+ "Source height is retained; the vertical datum is unspecified.",
211
+ "Source orientation, extra point values and special-point types remain uninterpreted.",
212
+ "Coverage completeness is unknown; source valid does not establish completeness.",
213
+ "No per-frame visibility or pose-time correction is inferred."],
214
+ }
215
+ ensure_i_json(result)
216
+ return result
217
+
218
+
219
+ __all__ = ["CONTRACT", "preview_roadmark"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: x4d-devkit
3
- Version: 0.41.0rc1
3
+ Version: 0.42.0
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
@@ -23,6 +23,7 @@ Requires-Dist: numpy>=1.24
23
23
  Requires-Dist: httpx>=0.27
24
24
  Requires-Dist: rfc8785==0.1.4
25
25
  Requires-Dist: zstandard<1,>=0.23
26
+ Requires-Dist: pyproj<4,>=3.6
26
27
  Provides-Extra: inference
27
28
  Requires-Dist: pydantic>=2.0; extra == "inference"
28
29
  Requires-Dist: fastapi>=0.110; extra == "inference"
@@ -34,6 +34,7 @@ tests/test_platform_pose.py
34
34
  tests/test_preannotation_client.py
35
35
  tests/test_reconstruction_client.py
36
36
  tests/test_release_gate.py
37
+ tests/test_roadmark_preview.py
37
38
  tests/test_sdk_api.py
38
39
  tests/test_semantic_scene_authoring.py
39
40
  tests/test_semantic_scene_client.py
@@ -63,6 +64,7 @@ x4d_devkit/json_contract.py
63
64
  x4d_devkit/label_schema.py
64
65
  x4d_devkit/lidar_materialization.py
65
66
  x4d_devkit/materialize.py
67
+ x4d_devkit/roadmark_preview.py
66
68
  x4d_devkit/semantic_scene_authoring.py
67
69
  x4d_devkit/semantic_scene_contract.py
68
70
  x4d_devkit/time_contract.py
@@ -2,6 +2,7 @@ numpy>=1.24
2
2
  httpx>=0.27
3
3
  rfc8785==0.1.4
4
4
  zstandard<1,>=0.23
5
+ pyproj<4,>=3.6
5
6
 
6
7
  [dev]
7
8
  pytest>=7.0
File without changes
File without changes
File without changes