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.
- antioch/__init__.py +101 -0
- antioch/{module/execution.py → execution.py} +1 -1
- antioch/{module/input.py → input.py} +2 -4
- antioch/{module/module.py → module.py} +17 -34
- antioch/{module/node.py → node.py} +17 -16
- {antioch_py-2.2.4.dist-info → antioch_py-3.0.1.dist-info}/METADATA +8 -11
- antioch_py-3.0.1.dist-info/RECORD +61 -0
- {antioch_py-2.2.4.dist-info → antioch_py-3.0.1.dist-info}/WHEEL +1 -1
- antioch_py-3.0.1.dist-info/licenses/LICENSE +21 -0
- common/ark/__init__.py +6 -16
- common/ark/ark.py +23 -62
- common/ark/hardware.py +1 -1
- common/ark/kinematics.py +1 -1
- common/ark/module.py +22 -0
- common/ark/node.py +46 -3
- common/ark/scheduler.py +2 -29
- common/ark/sim.py +1 -1
- {antioch/module → common/ark}/token.py +17 -0
- common/assets/rigging.usd +0 -0
- common/constants.py +63 -5
- common/core/__init__.py +37 -24
- common/core/auth.py +87 -112
- common/core/container.py +261 -0
- common/core/registry.py +131 -152
- common/core/rome.py +251 -0
- common/core/telemetry.py +176 -0
- common/core/types.py +219 -0
- common/message/__init__.py +19 -5
- common/message/annotation.py +174 -23
- common/message/array.py +25 -1
- common/message/camera.py +23 -1
- common/message/color.py +32 -6
- common/message/detection.py +40 -0
- common/message/foxglove.py +20 -0
- common/message/frame.py +71 -7
- common/message/image.py +58 -9
- common/message/imu.py +24 -4
- common/message/joint.py +69 -10
- common/message/log.py +52 -7
- common/message/pir.py +23 -8
- common/message/plot.py +57 -0
- common/message/point.py +55 -6
- common/message/point_cloud.py +55 -19
- common/message/pose.py +59 -19
- common/message/quaternion.py +105 -92
- common/message/radar.py +195 -29
- common/message/twist.py +34 -0
- common/message/types.py +40 -5
- common/message/vector.py +180 -245
- common/sim/__init__.py +49 -0
- common/{session/config.py → sim/objects.py} +97 -27
- common/sim/state.py +11 -0
- common/utils/comms.py +30 -12
- common/utils/logger.py +26 -7
- antioch/message.py +0 -87
- antioch/module/__init__.py +0 -53
- antioch/session/__init__.py +0 -152
- antioch/session/ark.py +0 -500
- antioch/session/asset.py +0 -65
- antioch/session/error.py +0 -80
- antioch/session/objects/__init__.py +0 -40
- antioch/session/objects/animation.py +0 -162
- antioch/session/objects/articulation.py +0 -180
- antioch/session/objects/basis_curve.py +0 -180
- antioch/session/objects/camera.py +0 -65
- antioch/session/objects/collision.py +0 -46
- antioch/session/objects/geometry.py +0 -58
- antioch/session/objects/ground_plane.py +0 -48
- antioch/session/objects/imu.py +0 -53
- antioch/session/objects/joint.py +0 -49
- antioch/session/objects/light.py +0 -123
- antioch/session/objects/pir_sensor.py +0 -102
- antioch/session/objects/radar.py +0 -62
- antioch/session/objects/rigid_body.py +0 -197
- antioch/session/objects/xform.py +0 -119
- antioch/session/record.py +0 -158
- antioch/session/scene.py +0 -1544
- antioch/session/session.py +0 -211
- antioch/session/task.py +0 -309
- antioch_py-2.2.4.dist-info/RECORD +0 -85
- antioch_py-2.2.4.dist-info/entry_points.txt +0 -2
- common/core/agent.py +0 -324
- common/core/task.py +0 -36
- common/message/velocity.py +0 -11
- common/rome/__init__.py +0 -9
- common/rome/client.py +0 -435
- common/rome/error.py +0 -16
- common/session/__init__.py +0 -31
- common/session/environment.py +0 -31
- common/session/sim.py +0 -129
- common/utils/usd.py +0 -12
- /antioch/{module/clock.py → clock.py} +0 -0
- {antioch_py-2.2.4.dist-info → antioch_py-3.0.1.dist-info}/top_level.txt +0 -0
- /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}"
|
common/core/telemetry.py
ADDED
|
@@ -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})"
|