antioch-py 2.2.4__py3-none-any.whl → 3.0.1__py3-none-any.whl

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.

Potentially problematic release.


This version of antioch-py might be problematic. Click here for more details.

Files changed (94) hide show
  1. antioch/__init__.py +101 -0
  2. antioch/{module/execution.py → execution.py} +1 -1
  3. antioch/{module/input.py → input.py} +2 -4
  4. antioch/{module/module.py → module.py} +17 -34
  5. antioch/{module/node.py → node.py} +17 -16
  6. {antioch_py-2.2.4.dist-info → antioch_py-3.0.1.dist-info}/METADATA +8 -11
  7. antioch_py-3.0.1.dist-info/RECORD +61 -0
  8. {antioch_py-2.2.4.dist-info → antioch_py-3.0.1.dist-info}/WHEEL +1 -1
  9. antioch_py-3.0.1.dist-info/licenses/LICENSE +21 -0
  10. common/ark/__init__.py +6 -16
  11. common/ark/ark.py +23 -62
  12. common/ark/hardware.py +1 -1
  13. common/ark/kinematics.py +1 -1
  14. common/ark/module.py +22 -0
  15. common/ark/node.py +46 -3
  16. common/ark/scheduler.py +2 -29
  17. common/ark/sim.py +1 -1
  18. {antioch/module → common/ark}/token.py +17 -0
  19. common/assets/rigging.usd +0 -0
  20. common/constants.py +63 -5
  21. common/core/__init__.py +37 -24
  22. common/core/auth.py +87 -112
  23. common/core/container.py +261 -0
  24. common/core/registry.py +131 -152
  25. common/core/rome.py +251 -0
  26. common/core/telemetry.py +176 -0
  27. common/core/types.py +219 -0
  28. common/message/__init__.py +19 -5
  29. common/message/annotation.py +174 -23
  30. common/message/array.py +25 -1
  31. common/message/camera.py +23 -1
  32. common/message/color.py +32 -6
  33. common/message/detection.py +40 -0
  34. common/message/foxglove.py +20 -0
  35. common/message/frame.py +71 -7
  36. common/message/image.py +58 -9
  37. common/message/imu.py +24 -4
  38. common/message/joint.py +69 -10
  39. common/message/log.py +52 -7
  40. common/message/pir.py +23 -8
  41. common/message/plot.py +57 -0
  42. common/message/point.py +55 -6
  43. common/message/point_cloud.py +55 -19
  44. common/message/pose.py +59 -19
  45. common/message/quaternion.py +105 -92
  46. common/message/radar.py +195 -29
  47. common/message/twist.py +34 -0
  48. common/message/types.py +40 -5
  49. common/message/vector.py +180 -245
  50. common/sim/__init__.py +49 -0
  51. common/{session/config.py → sim/objects.py} +97 -27
  52. common/sim/state.py +11 -0
  53. common/utils/comms.py +30 -12
  54. common/utils/logger.py +26 -7
  55. antioch/message.py +0 -87
  56. antioch/module/__init__.py +0 -53
  57. antioch/session/__init__.py +0 -152
  58. antioch/session/ark.py +0 -500
  59. antioch/session/asset.py +0 -65
  60. antioch/session/error.py +0 -80
  61. antioch/session/objects/__init__.py +0 -40
  62. antioch/session/objects/animation.py +0 -162
  63. antioch/session/objects/articulation.py +0 -180
  64. antioch/session/objects/basis_curve.py +0 -180
  65. antioch/session/objects/camera.py +0 -65
  66. antioch/session/objects/collision.py +0 -46
  67. antioch/session/objects/geometry.py +0 -58
  68. antioch/session/objects/ground_plane.py +0 -48
  69. antioch/session/objects/imu.py +0 -53
  70. antioch/session/objects/joint.py +0 -49
  71. antioch/session/objects/light.py +0 -123
  72. antioch/session/objects/pir_sensor.py +0 -102
  73. antioch/session/objects/radar.py +0 -62
  74. antioch/session/objects/rigid_body.py +0 -197
  75. antioch/session/objects/xform.py +0 -119
  76. antioch/session/record.py +0 -158
  77. antioch/session/scene.py +0 -1544
  78. antioch/session/session.py +0 -211
  79. antioch/session/task.py +0 -309
  80. antioch_py-2.2.4.dist-info/RECORD +0 -85
  81. antioch_py-2.2.4.dist-info/entry_points.txt +0 -2
  82. common/core/agent.py +0 -324
  83. common/core/task.py +0 -36
  84. common/message/velocity.py +0 -11
  85. common/rome/__init__.py +0 -9
  86. common/rome/client.py +0 -435
  87. common/rome/error.py +0 -16
  88. common/session/__init__.py +0 -31
  89. common/session/environment.py +0 -31
  90. common/session/sim.py +0 -129
  91. common/utils/usd.py +0 -12
  92. /antioch/{module/clock.py → clock.py} +0 -0
  93. {antioch_py-2.2.4.dist-info → antioch_py-3.0.1.dist-info}/top_level.txt +0 -0
  94. /common/message/{base.py → message.py} +0 -0
common/core/rome.py ADDED
@@ -0,0 +1,251 @@
1
+ from typing import overload
2
+
3
+ import requests
4
+ from requests import Response
5
+
6
+ from common.core.types import ArkReference, AssetReference, TaskRun
7
+
8
+
9
+ class RomeError(Exception):
10
+ """
11
+ Base error for Rome API operations.
12
+ """
13
+
14
+
15
+ class RomeAuthError(RomeError):
16
+ """
17
+ Authentication error when interacting with Rome API.
18
+ """
19
+
20
+
21
+ class RomeNetworkError(RomeError):
22
+ """
23
+ Network error when interacting with Rome API.
24
+ """
25
+
26
+
27
+ class RomeClient:
28
+ """
29
+ Client for interacting with Rome (Antioch's cloud API).
30
+
31
+ Handles task runs, artifact uploads/downloads, and registry operations for Arks and Assets.
32
+ """
33
+
34
+ def __init__(self, api_url: str, token: str):
35
+ """
36
+ Initialize the Rome client.
37
+
38
+ :param api_url: Base URL for Rome API.
39
+ :param token: Authentication token.
40
+ """
41
+
42
+ self._api_url = api_url
43
+ self._token = token
44
+
45
+ def create_task_run(
46
+ self,
47
+ task_run: TaskRun,
48
+ upload_mcap: bool = False,
49
+ upload_bundle: bool = False,
50
+ ) -> tuple[str | None, str | None]:
51
+ """
52
+ Create a task run and optionally get signed URLs for artifact uploads.
53
+
54
+ :param task_run: TaskRun model with run data.
55
+ :param upload_mcap: Whether client will upload an MCAP file.
56
+ :param upload_bundle: Whether client will upload a bundle file.
57
+ :return: Tuple of (mcap_upload_url, bundle_upload_url).
58
+ """
59
+
60
+ response = self._send_request(
61
+ "POST",
62
+ "/tasks/runs",
63
+ json={
64
+ "task_run": task_run.model_dump(mode="json"),
65
+ "upload_mcap": upload_mcap,
66
+ "upload_bundle": upload_bundle,
67
+ },
68
+ )
69
+
70
+ return response.get("mcap_upload_url"), response.get("bundle_upload_url")
71
+
72
+ def list_arks(self) -> list[ArkReference]:
73
+ """
74
+ List all Arks from Rome registry.
75
+
76
+ :return: List of ArkReference objects from remote registry.
77
+ """
78
+
79
+ response = self._send_request("GET", "/ark/list")
80
+ return [ArkReference(**ark) for ark in response.get("data", [])]
81
+
82
+ def pull_ark(self, name: str, version: str, config_output_path: str, asset_output_path: str | None = None) -> bool:
83
+ """
84
+ Pull Ark config and optionally asset from Rome via signed URLs.
85
+
86
+ :param name: Name of the Ark.
87
+ :param version: Version of the Ark.
88
+ :param config_output_path: Path where ark.json should be saved.
89
+ :param asset_output_path: Path where asset.usdz should be saved (if present).
90
+ :return: True if an asset was downloaded, False otherwise.
91
+ """
92
+
93
+ if not self._token:
94
+ raise RomeAuthError("User not authenticated")
95
+
96
+ try:
97
+ # Get ark data with download URLs
98
+ response = self._send_request("GET", "/ark/get", json={"name": name, "version": version})
99
+
100
+ # Download config
101
+ config_response = requests.get(response["config_download_url"], timeout=60)
102
+ config_response.raise_for_status()
103
+ with open(config_output_path, "wb") as f:
104
+ f.write(config_response.content)
105
+
106
+ # Download asset if present and output path provided
107
+ has_asset = response.get("asset_download_url") is not None
108
+ if has_asset and asset_output_path:
109
+ asset_response = requests.get(response["asset_download_url"], timeout=None)
110
+ asset_response.raise_for_status()
111
+ with open(asset_output_path, "wb") as f:
112
+ f.write(asset_response.content)
113
+ return True
114
+
115
+ return False
116
+ except requests.exceptions.RequestException as e:
117
+ raise RomeNetworkError(f"Network error: {e}") from e
118
+
119
+ def list_assets(self) -> list[AssetReference]:
120
+ """
121
+ List all assets from Rome registry.
122
+
123
+ :return: List of AssetReference objects from remote registry.
124
+ """
125
+
126
+ response = self._send_request("GET", "/asset/list")
127
+ return [AssetReference(**asset) for asset in response.get("data", [])]
128
+
129
+ def pull_asset(self, name: str, version: str, output_path: str) -> dict[str, str]:
130
+ """
131
+ Pull asset file from Rome registry via signed URL.
132
+
133
+ :param name: Name of the asset.
134
+ :param version: Version of the asset.
135
+ :param output_path: Path where the file should be saved.
136
+ :return: Metadata dictionary containing extension, file_size, and modified_time.
137
+ """
138
+
139
+ if not self._token:
140
+ raise RomeAuthError("User not authenticated")
141
+
142
+ try:
143
+ response = self._send_request("GET", "/asset/pull", params={"name": name, "version": version})
144
+ print(f"Downloading {name}:{version}...")
145
+ download_response = requests.get(response["download_url"], timeout=None)
146
+ download_response.raise_for_status()
147
+ with open(output_path, "wb") as f:
148
+ f.write(download_response.content)
149
+ return {
150
+ "extension": response.get("extension", ""),
151
+ "file_size": response.get("file_size", ""),
152
+ "modified_time": response.get("modified_time", ""),
153
+ }
154
+ except requests.exceptions.RequestException as e:
155
+ raise RomeNetworkError(f"Network error: {e}") from e
156
+
157
+ def get_gar_token(self) -> dict:
158
+ """
159
+ Get a GAR (Google Artifact Registry) access token for Docker operations.
160
+
161
+ :return: Dictionary with registry_host, repository, access_token, and expires_at.
162
+ """
163
+
164
+ response = self._send_request("GET", "/token/gar")
165
+ return response["data"]
166
+
167
+ @overload
168
+ def _send_request(
169
+ self,
170
+ method: str,
171
+ endpoint: str,
172
+ json: dict | None = None,
173
+ params: dict | None = None,
174
+ return_content: bool = False,
175
+ ) -> dict: ...
176
+
177
+ @overload
178
+ def _send_request(
179
+ self,
180
+ method: str,
181
+ endpoint: str,
182
+ json: dict | None = None,
183
+ params: dict | None = None,
184
+ return_content: bool = True,
185
+ ) -> bytes: ...
186
+
187
+ def _send_request(
188
+ self,
189
+ method: str,
190
+ endpoint: str,
191
+ json: dict | None = None,
192
+ params: dict | None = None,
193
+ return_content: bool = False,
194
+ ) -> dict | bytes:
195
+ """
196
+ Send a request to Rome API with standardized error handling.
197
+
198
+ :param method: HTTP method (GET, POST, etc.).
199
+ :param endpoint: API endpoint path.
200
+ :param json: Optional JSON payload.
201
+ :param params: Optional query parameters.
202
+ :param return_content: If True, return raw bytes content instead of JSON.
203
+ :return: Response JSON data or raw content bytes.
204
+ """
205
+
206
+ if not self._token:
207
+ raise RomeAuthError("User not authenticated")
208
+
209
+ try:
210
+ url = f"{self._api_url}{endpoint}"
211
+ headers = {"Authorization": f"Bearer {self._token}", "Content-Type": "application/json"}
212
+ response = requests.request(method, url, json=json, params=params, headers=headers, timeout=30)
213
+ self._check_response_errors(response)
214
+ if return_content:
215
+ return response.content
216
+
217
+ try:
218
+ return response.json()
219
+ except requests.exceptions.JSONDecodeError as e:
220
+ raise RomeError(f"Invalid JSON response: {e}") from e
221
+ except requests.exceptions.RequestException as e:
222
+ raise RomeNetworkError(f"Network error: {e}") from e
223
+
224
+ def _check_response_errors(self, response: Response) -> None:
225
+ """
226
+ Check response for HTTP errors and raise appropriate exceptions.
227
+
228
+ :param response: HTTP response object.
229
+ """
230
+
231
+ if response.status_code >= 400:
232
+ error_message = self._extract_error_message(response)
233
+ if response.status_code < 500:
234
+ raise RomeError(error_message)
235
+ raise RomeNetworkError(f"Server error: {error_message}")
236
+
237
+ def _extract_error_message(self, response: Response) -> str:
238
+ """
239
+ Extract error message from response JSON or return generic message.
240
+
241
+ :param response: HTTP response object.
242
+ :return: Error message string from response or generic HTTP status message.
243
+ """
244
+
245
+ try:
246
+ data = response.json()
247
+ if isinstance(data, dict) and "message" in data:
248
+ return data["message"]
249
+ except Exception:
250
+ pass
251
+ return f"HTTP {response.status_code}"
@@ -0,0 +1,176 @@
1
+ import time
2
+ from pathlib import Path
3
+
4
+ import foxglove
5
+ import zenoh
6
+ from foxglove import Capability, MCAPWriter # type: ignore[attr-defined]
7
+
8
+ from common.constants import FOXGLOVE_WEBSOCKET_PORT
9
+ from common.message import (
10
+ DetectionDistances,
11
+ FrameTransforms,
12
+ Image,
13
+ ImageAnnotations,
14
+ Log,
15
+ Message,
16
+ PlotData,
17
+ PointCloud,
18
+ Pose,
19
+ RadarScan,
20
+ RangeMap,
21
+ Vector2,
22
+ Vector3,
23
+ )
24
+ from common.utils.comms import CommsSession
25
+
26
+ TYPE_MAP: dict[str, type] = {
27
+ "antioch/vector2": Vector2,
28
+ "antioch/vector3": Vector3,
29
+ "antioch/pose": Pose,
30
+ "antioch/image": Image,
31
+ "antioch/image_annotations": ImageAnnotations,
32
+ "antioch/point_cloud": PointCloud,
33
+ "antioch/frame_transforms": FrameTransforms,
34
+ "antioch/log": Log,
35
+ "antioch/radar_scan": RadarScan,
36
+ "antioch/range_map": RangeMap,
37
+ "antioch/plot_data": PlotData,
38
+ "antioch/detection_distances": DetectionDistances,
39
+ }
40
+
41
+
42
+ class TelemetryManager:
43
+ """
44
+ Manages Foxglove WebSocket server and MCAP recording.
45
+
46
+ The WebSocket server persists across tasks. MCAP recording is per-task.
47
+ Uses a callback subscriber to process logs from Zenoh and forward to Foxglove.
48
+ """
49
+
50
+ def __init__(self, run_websocket: bool = True) -> None:
51
+ """
52
+ Create a new telemetry manager.
53
+
54
+ Initializes the Foxglove WebSocket server (if enabled) and starts listening for logs.
55
+
56
+ :param run_websocket: Whether to start the WebSocket server. Set to False for
57
+ headless non-streaming mode to avoid port conflicts.
58
+ """
59
+
60
+ self._comms = CommsSession()
61
+ self._mcap_writer: MCAPWriter | None = None
62
+ self._start_timestamp_ns: int = time.time_ns()
63
+ self._time_offset_ns: int = 0
64
+ self._last_let_us: int = 0
65
+ self._run_websocket = run_websocket
66
+
67
+ if run_websocket:
68
+ try:
69
+ self._server = foxglove.start_server(
70
+ name="antioch-telemetry",
71
+ host="0.0.0.0",
72
+ port=FOXGLOVE_WEBSOCKET_PORT,
73
+ capabilities=[Capability.Time],
74
+ )
75
+ except RuntimeError as e:
76
+ if "Address already in use" in str(e):
77
+ raise RuntimeError(
78
+ f"Foxglove server port {FOXGLOVE_WEBSOCKET_PORT} is already in use. "
79
+ f"Another simulation may be running. Stop it first with sim.stop(), "
80
+ f"or kill the process: lsof -ti :{FOXGLOVE_WEBSOCKET_PORT} | xargs -r kill"
81
+ ) from None
82
+ raise
83
+ else:
84
+ self._server = None
85
+
86
+ self._subscriber = self._comms.declare_callback_subscriber("_logs", self._on_log)
87
+
88
+ def stop(self) -> None:
89
+ """
90
+ Stop the telemetry manager and clean up resources.
91
+
92
+ Stops the log subscriber, MCAP recording, and WebSocket server.
93
+ """
94
+
95
+ self._subscriber.undeclare()
96
+ self.stop_recording()
97
+ if self._server is not None:
98
+ self._server.stop()
99
+ self._comms.close()
100
+
101
+ def start_recording(self, mcap_path: str) -> None:
102
+ """
103
+ Start recording telemetry to an MCAP file.
104
+
105
+ :param mcap_path: Path to save the MCAP file.
106
+ """
107
+
108
+ self.stop_recording()
109
+ path = Path(mcap_path)
110
+ path.parent.mkdir(parents=True, exist_ok=True)
111
+ self._mcap_writer = foxglove.open_mcap(str(path), allow_overwrite=True)
112
+
113
+ def stop_recording(self) -> None:
114
+ """
115
+ Stop MCAP recording and finalize the file.
116
+ """
117
+
118
+ if self._mcap_writer:
119
+ self._mcap_writer.close()
120
+ self._mcap_writer = None
121
+
122
+ def reset_time(self) -> None:
123
+ """
124
+ Accumulate the current time offset for monotonic timestamps.
125
+
126
+ Call this when the simulation is cleared or stopped. This ensures that
127
+ timestamps continue monotonically increasing across multiple episodes
128
+ in the same MCAP file.
129
+ """
130
+
131
+ self._time_offset_ns += self._last_let_us * 1000
132
+ self._last_let_us = 0
133
+
134
+ def _on_log(self, sample: zenoh.Sample) -> None:
135
+ """
136
+ Callback for incoming log samples.
137
+ """
138
+
139
+ log = Log.unpack(bytes(sample.payload))
140
+ if log.channel is None:
141
+ return
142
+
143
+ self._last_let_us = log.let_us
144
+
145
+ # Calculate monotonically increasing timestamp
146
+ log_time_ns = self._start_timestamp_ns + self._time_offset_ns + (log.let_us * 1000)
147
+
148
+ # Broadcast time if websocket server is running
149
+ if self._server is not None:
150
+ self._server.broadcast_time(log_time_ns)
151
+
152
+ if log.telemetry is not None:
153
+ msg_type = Message.extract_type(log.telemetry)
154
+ self._log_payload(log.channel, log.telemetry, msg_type, log_time_ns)
155
+ return
156
+
157
+ foxglove_log = log.to_foxglove()
158
+ if foxglove_log is not None:
159
+ foxglove.log(log.channel, foxglove_log, log_time=log_time_ns)
160
+
161
+ def _log_payload(self, channel: str, data: bytes, msg_type: str | None, log_time_ns: int) -> None:
162
+ """
163
+ Extract and log a telemetry payload.
164
+ """
165
+
166
+ msg_class = TYPE_MAP.get(msg_type) if msg_type else None
167
+ if msg_class is None:
168
+ # Unknown type - log as JSON directly
169
+ json_data = Message.extract_data_as_json(data)
170
+ foxglove.log(channel, json_data, log_time=log_time_ns)
171
+ return
172
+
173
+ msg = msg_class.unpack(data)
174
+ foxglove_msg = msg.to_foxglove()
175
+ if foxglove_msg is not None:
176
+ foxglove.log(channel, foxglove_msg, log_time=log_time_ns)
common/core/types.py ADDED
@@ -0,0 +1,219 @@
1
+ from datetime import datetime
2
+ from enum import Enum
3
+
4
+ from common.message import Message
5
+
6
+ # =============================================================================
7
+ # Task Types
8
+ # =============================================================================
9
+
10
+
11
+ class TaskOutcome(str, Enum):
12
+ """
13
+ Task outcome status.
14
+ """
15
+
16
+ SUCCESS = "success"
17
+ FAILURE = "failure"
18
+ ERROR = "error"
19
+ TIMEOUT = "timeout"
20
+ SKIPPED = "skipped"
21
+
22
+
23
+ class TaskTriggerSource(str, Enum):
24
+ """
25
+ How the task was triggered.
26
+ """
27
+
28
+ MANUAL = "manual"
29
+ CI = "ci"
30
+ SCHEDULED = "scheduled"
31
+ API = "api"
32
+
33
+
34
+ class TaskRunner(Message):
35
+ """
36
+ Runner/execution environment metadata for a task run.
37
+ """
38
+
39
+ runner_id: str | None = None
40
+ runner_name: str | None = None
41
+ environment: dict | None = None
42
+
43
+
44
+ class TaskRun(Message):
45
+ """
46
+ A single execution of a task with timing, outcome, and results.
47
+
48
+ Task runs are grouped by task_name for display.
49
+ """
50
+
51
+ # Identity - run_id is generated server-side, org_id/user_id set from context
52
+ run_id: str | None = None
53
+ org_id: str | None = None
54
+ user_id: str | None = None
55
+
56
+ # Timing
57
+ started_at: datetime
58
+ completed_at: datetime
59
+ duration_ms: int | None = None
60
+
61
+ # Task identity
62
+ task_name: str
63
+ description: str | None = None
64
+
65
+ # Outcome and results
66
+ outcome: TaskOutcome
67
+ result: dict | None = None
68
+
69
+ # User info (display names for search/filtering)
70
+ user_name: str | None = None
71
+ user_email: str | None = None
72
+
73
+ # Filtering
74
+ tags: list[str] | None = None
75
+ parameters: dict | None = None
76
+
77
+ # Execution context
78
+ runner: TaskRunner | None = None
79
+ trigger_source: TaskTriggerSource | None = None
80
+
81
+
82
+ # =============================================================================
83
+ # Registry Types
84
+ # =============================================================================
85
+
86
+
87
+ class ArkRegistryMetadata(Message):
88
+ """
89
+ Metadata stored in GCS for a published Ark version.
90
+
91
+ This is the canonical schema for Ark registry metadata. All fields are
92
+ populated by Rome during push and validated against this schema.
93
+ """
94
+
95
+ module_count: int
96
+ image_count: int
97
+ capability: str
98
+ has_assets: bool
99
+ description: str
100
+ timestamp: str
101
+ digest: str
102
+
103
+ @classmethod
104
+ def from_dict(cls, data: dict[str, str]) -> "ArkRegistryMetadata":
105
+ """
106
+ Parse metadata from GCS blob metadata (all values are strings).
107
+
108
+ :param data: Raw metadata dict from GCS.
109
+ :return: Parsed ArkRegistryMetadata.
110
+ """
111
+
112
+ return cls(
113
+ module_count=int(data.get("module_count", "0")),
114
+ image_count=int(data.get("image_count", "0")),
115
+ capability=data.get("capability", ""),
116
+ has_assets=data.get("has_assets", "false").lower() == "true",
117
+ description=data.get("description", ""),
118
+ timestamp=data.get("timestamp", ""),
119
+ digest=data.get("digest", ""),
120
+ )
121
+
122
+ def to_gcs_metadata(self) -> dict[str, str]:
123
+ """
124
+ Convert to GCS metadata format (all values as strings).
125
+
126
+ :return: Dict suitable for GCS blob metadata.
127
+ """
128
+
129
+ return {
130
+ "module_count": str(self.module_count),
131
+ "image_count": str(self.image_count),
132
+ "capability": self.capability,
133
+ "has_assets": str(self.has_assets).lower(),
134
+ "description": self.description,
135
+ "timestamp": self.timestamp,
136
+ "digest": self.digest,
137
+ }
138
+
139
+
140
+ class ArkVersionReference(Message):
141
+ """
142
+ Reference to an Ark version in the remote Ark registry.
143
+ """
144
+
145
+ version: str
146
+ created_at: str
147
+ updated_at: str
148
+ full_path: str
149
+ size_bytes: int
150
+ asset_path: str | None = None
151
+ asset_size_bytes: int | None = None
152
+ metadata: ArkRegistryMetadata | None = None
153
+
154
+
155
+ class ArkReference(Message):
156
+ """
157
+ Reference to an Ark in the remote Ark registry.
158
+ """
159
+
160
+ name: str
161
+ versions: list[ArkVersionReference]
162
+ created_at: str
163
+ updated_at: str
164
+
165
+ def __str__(self) -> str:
166
+ """
167
+ Return a string representation of the ArkReference.
168
+ """
169
+
170
+ versions_str = ", ".join(v.version for v in self.versions)
171
+ return f"{self.name} [{versions_str}]"
172
+
173
+ def __repr__(self) -> str:
174
+ """
175
+ Return a detailed representation of the ArkReference.
176
+ """
177
+
178
+ versions_list = [v.version for v in self.versions]
179
+ return f"ArkReference(name='{self.name}', versions={versions_list})"
180
+
181
+
182
+ class AssetVersionReference(Message):
183
+ """
184
+ Reference to a specific asset version.
185
+ """
186
+
187
+ version: str
188
+ full_path: str
189
+ size_bytes: int
190
+ created_at: str
191
+ updated_at: str
192
+ metadata: dict[str, str] = {}
193
+
194
+
195
+ class AssetReference(Message):
196
+ """
197
+ Reference to an asset with all its versions.
198
+ """
199
+
200
+ name: str
201
+ versions: list[AssetVersionReference]
202
+ created_at: str
203
+ updated_at: str
204
+
205
+ def __str__(self) -> str:
206
+ """
207
+ Return a string representation of the AssetReference.
208
+ """
209
+
210
+ versions_str = ", ".join(v.version for v in self.versions)
211
+ return f"{self.name} [{versions_str}]"
212
+
213
+ def __repr__(self) -> str:
214
+ """
215
+ Return a detailed representation of the AssetReference.
216
+ """
217
+
218
+ versions_list = [v.version for v in self.versions]
219
+ return f"AssetReference(name='{self.name}', versions={versions_list})"