antioch-py 2.0.6__py3-none-any.whl → 3.0.12__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.0.6.dist-info → antioch_py-3.0.12.dist-info}/METADATA +8 -11
- antioch_py-3.0.12.dist-info/RECORD +61 -0
- {antioch_py-2.0.6.dist-info → antioch_py-3.0.12.dist-info}/WHEEL +1 -1
- antioch_py-3.0.12.dist-info/licenses/LICENSE +21 -0
- common/ark/__init__.py +6 -16
- common/ark/ark.py +23 -60
- common/ark/hardware.py +13 -37
- 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 +83 -4
- common/core/__init__.py +37 -24
- common/core/auth.py +87 -114
- 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 -3
- 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 +22 -5
- 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/sim/objects.py +460 -0
- 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 -150
- antioch/session/ark.py +0 -504
- antioch/session/asset.py +0 -65
- antioch/session/error.py +0 -80
- antioch/session/record.py +0 -158
- antioch/session/scene.py +0 -1521
- antioch/session/session.py +0 -220
- antioch/session/task.py +0 -323
- antioch/session/views/__init__.py +0 -40
- antioch/session/views/animation.py +0 -189
- antioch/session/views/articulation.py +0 -245
- antioch/session/views/basis_curve.py +0 -186
- antioch/session/views/camera.py +0 -92
- antioch/session/views/collision.py +0 -75
- antioch/session/views/geometry.py +0 -74
- antioch/session/views/ground_plane.py +0 -63
- antioch/session/views/imu.py +0 -73
- antioch/session/views/joint.py +0 -64
- antioch/session/views/light.py +0 -175
- antioch/session/views/pir_sensor.py +0 -140
- antioch/session/views/radar.py +0 -73
- antioch/session/views/rigid_body.py +0 -282
- antioch/session/views/xform.py +0 -119
- antioch_py-2.0.6.dist-info/RECORD +0 -99
- antioch_py-2.0.6.dist-info/entry_points.txt +0 -2
- common/core/agent.py +0 -296
- common/core/task.py +0 -36
- common/rome/__init__.py +0 -9
- common/rome/client.py +0 -430
- common/rome/error.py +0 -16
- common/session/__init__.py +0 -54
- common/session/environment.py +0 -31
- common/session/sim.py +0 -240
- common/session/views/__init__.py +0 -263
- common/session/views/animation.py +0 -73
- common/session/views/articulation.py +0 -184
- common/session/views/basis_curve.py +0 -102
- common/session/views/camera.py +0 -147
- common/session/views/collision.py +0 -59
- common/session/views/geometry.py +0 -102
- common/session/views/ground_plane.py +0 -41
- common/session/views/imu.py +0 -66
- common/session/views/joint.py +0 -81
- common/session/views/light.py +0 -96
- common/session/views/pir_sensor.py +0 -115
- common/session/views/radar.py +0 -82
- common/session/views/rigid_body.py +0 -236
- common/session/views/viewport.py +0 -21
- common/session/views/xform.py +0 -39
- common/utils/usd.py +0 -12
- /antioch/{module/clock.py → clock.py} +0 -0
- {antioch_py-2.0.6.dist-info → antioch_py-3.0.12.dist-info}/top_level.txt +0 -0
- /common/message/{base.py → message.py} +0 -0
common/core/registry.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
2
4
|
from collections import defaultdict
|
|
3
5
|
from datetime import datetime
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
|
|
6
|
-
from common.ark import Ark as ArkDefinition
|
|
8
|
+
from common.ark import Ark as ArkDefinition
|
|
7
9
|
from common.constants import ANTIOCH_API_URL, get_ark_dir, get_asset_dir
|
|
8
|
-
from common.core.auth import
|
|
9
|
-
from common.rome import RomeClient
|
|
10
|
+
from common.core.auth import AuthHandler
|
|
11
|
+
from common.core.rome import RomeClient
|
|
12
|
+
from common.core.types import ArkReference, ArkVersionReference, AssetReference, AssetVersionReference
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
def list_local_arks() -> list[ArkReference]:
|
|
@@ -17,17 +20,43 @@ def list_local_arks() -> list[ArkReference]:
|
|
|
17
20
|
"""
|
|
18
21
|
|
|
19
22
|
arks_dir = get_ark_dir()
|
|
20
|
-
files_by_name = defaultdict(list)
|
|
21
|
-
for file_path in arks_dir.iterdir():
|
|
22
|
-
if file_path.is_file() and (file_path.name.endswith(":ark.json") or file_path.name.endswith(":asset.usdz")):
|
|
23
|
-
name = file_path.name.split(":")[0]
|
|
24
|
-
files_by_name[name].append(file_path)
|
|
25
23
|
|
|
24
|
+
# Group files by ark name and version
|
|
25
|
+
# File format: {name}:{version}:ark.json or {name}:{version}:asset.usdz
|
|
26
|
+
files_by_name_version: dict[str, dict[str, dict[str, Path]]] = defaultdict(lambda: defaultdict(dict))
|
|
27
|
+
for file_path in arks_dir.iterdir():
|
|
28
|
+
if not file_path.is_file():
|
|
29
|
+
continue
|
|
30
|
+
if file_path.name.endswith(":ark.json"):
|
|
31
|
+
name, version, _ = file_path.name.rsplit(":", 2)
|
|
32
|
+
files_by_name_version[name][version]["ark"] = file_path
|
|
33
|
+
elif file_path.name.endswith(":asset.usdz"):
|
|
34
|
+
name, version, _ = file_path.name.rsplit(":", 2)
|
|
35
|
+
files_by_name_version[name][version]["asset"] = file_path
|
|
36
|
+
|
|
37
|
+
# Build references for each ark
|
|
26
38
|
results = []
|
|
27
|
-
for name,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
for name, versions in files_by_name_version.items():
|
|
40
|
+
version_refs = []
|
|
41
|
+
for version, files in versions.items():
|
|
42
|
+
ark_file = files.get("ark")
|
|
43
|
+
if ark_file is None:
|
|
44
|
+
continue
|
|
45
|
+
asset_file = files.get("asset")
|
|
46
|
+
ark_stat = ark_file.stat()
|
|
47
|
+
version_refs.append(
|
|
48
|
+
ArkVersionReference(
|
|
49
|
+
version=version,
|
|
50
|
+
full_path=str(ark_file),
|
|
51
|
+
asset_path=str(asset_file) if asset_file else None,
|
|
52
|
+
size_bytes=ark_stat.st_size,
|
|
53
|
+
created_at=datetime.fromtimestamp(ark_stat.st_ctime).isoformat(),
|
|
54
|
+
updated_at=datetime.fromtimestamp(ark_stat.st_mtime).isoformat(),
|
|
55
|
+
asset_size_bytes=asset_file.stat().st_size if asset_file else None,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
if version_refs:
|
|
59
|
+
results.append(build_ark_reference_from_versions(name, version_refs))
|
|
31
60
|
|
|
32
61
|
return results
|
|
33
62
|
|
|
@@ -60,8 +89,8 @@ def get_ark_version_reference(name: str, version: str) -> ArkVersionReference:
|
|
|
60
89
|
for version_ref in ark_ref.versions:
|
|
61
90
|
if version_ref.version == version:
|
|
62
91
|
return version_ref
|
|
63
|
-
raise FileNotFoundError(f"Version {version} of Ark {name} not found
|
|
64
|
-
raise FileNotFoundError(f"No versions of Ark {name} found
|
|
92
|
+
raise FileNotFoundError(f"Version {version} of Ark {name} not found locally. Please pull the Ark first.")
|
|
93
|
+
raise FileNotFoundError(f"No versions of Ark {name} found locally. Please pull the Ark first.")
|
|
65
94
|
|
|
66
95
|
|
|
67
96
|
def get_asset_path(name: str, version: str, extension: str = "usdz", assert_exists: bool = True) -> Path:
|
|
@@ -94,19 +123,34 @@ def list_local_assets() -> list[AssetReference]:
|
|
|
94
123
|
if not assets_dir.exists():
|
|
95
124
|
return []
|
|
96
125
|
|
|
97
|
-
|
|
126
|
+
# Group files by asset name and version
|
|
127
|
+
# File format: {name}:{version}:file.{extension}
|
|
128
|
+
files_by_name_version: dict[str, dict[str, Path]] = defaultdict(dict)
|
|
98
129
|
for file_path in assets_dir.iterdir():
|
|
99
|
-
if file_path.is_file():
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
130
|
+
if not file_path.is_file():
|
|
131
|
+
continue
|
|
132
|
+
parts = file_path.stem.split(":")
|
|
133
|
+
if len(parts) == 3 and parts[-1] == "file":
|
|
134
|
+
name, version = parts[0], parts[1]
|
|
135
|
+
files_by_name_version[name][version] = file_path
|
|
104
136
|
|
|
137
|
+
# Build references for each asset
|
|
105
138
|
results = []
|
|
106
|
-
for name,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
139
|
+
for name, versions in files_by_name_version.items():
|
|
140
|
+
version_refs = []
|
|
141
|
+
for version, asset_file in versions.items():
|
|
142
|
+
asset_stat = asset_file.stat()
|
|
143
|
+
version_refs.append(
|
|
144
|
+
AssetVersionReference(
|
|
145
|
+
version=version,
|
|
146
|
+
full_path=str(asset_file),
|
|
147
|
+
size_bytes=asset_stat.st_size,
|
|
148
|
+
created_at=datetime.fromtimestamp(asset_stat.st_ctime).isoformat(),
|
|
149
|
+
updated_at=datetime.fromtimestamp(asset_stat.st_mtime).isoformat(),
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
if version_refs:
|
|
153
|
+
results.append(build_asset_reference_from_versions(name, version_refs))
|
|
110
154
|
|
|
111
155
|
return results
|
|
112
156
|
|
|
@@ -121,13 +165,8 @@ def list_remote_arks() -> list[ArkReference]:
|
|
|
121
165
|
:raises AuthError: If not authenticated.
|
|
122
166
|
"""
|
|
123
167
|
|
|
124
|
-
# Get auth token
|
|
125
168
|
auth = AuthHandler()
|
|
126
169
|
token = auth.get_token()
|
|
127
|
-
if token is None:
|
|
128
|
-
raise AuthError("User not authenticated. Please login first")
|
|
129
|
-
|
|
130
|
-
# Create Rome client and list arks
|
|
131
170
|
rome_client = RomeClient(api_url=ANTIOCH_API_URL, token=token)
|
|
132
171
|
return rome_client.list_arks()
|
|
133
172
|
|
|
@@ -136,6 +175,8 @@ def pull_remote_ark(name: str, version: str, overwrite: bool = False) -> ArkDefi
|
|
|
136
175
|
"""
|
|
137
176
|
Pull an Ark from remote registry to local storage.
|
|
138
177
|
|
|
178
|
+
Downloads the Ark config (ark.json) and asset (asset.usdz) if present.
|
|
179
|
+
|
|
139
180
|
Requires authentication.
|
|
140
181
|
|
|
141
182
|
:param name: Name of the Ark.
|
|
@@ -148,30 +189,26 @@ def pull_remote_ark(name: str, version: str, overwrite: bool = False) -> ArkDefi
|
|
|
148
189
|
# Check if Ark already exists locally
|
|
149
190
|
arks_dir = get_ark_dir()
|
|
150
191
|
ark_json_path = arks_dir / f"{name}:{version}:ark.json"
|
|
192
|
+
ark_asset_path = arks_dir / f"{name}:{version}:asset.usdz"
|
|
151
193
|
if ark_json_path.exists() and not overwrite:
|
|
152
194
|
return load_local_ark(name, version)
|
|
153
195
|
|
|
154
|
-
# Get auth token
|
|
155
196
|
auth = AuthHandler()
|
|
156
197
|
token = auth.get_token()
|
|
157
|
-
if not token:
|
|
158
|
-
raise AuthError("User not authenticated. Please login first")
|
|
159
|
-
|
|
160
|
-
# Create Rome client and fetch Ark definition and save to local storage
|
|
161
198
|
rome_client = RomeClient(api_url=ANTIOCH_API_URL, token=token)
|
|
162
|
-
ark = rome_client.get_ark(name=name, version=version)
|
|
163
|
-
|
|
164
|
-
# Save Ark JSON
|
|
165
|
-
with open(ark_json_path, "wb") as f:
|
|
166
|
-
f.write(json.dumps(ark).encode("utf-8"))
|
|
167
199
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
200
|
+
print(f"Pulling {name} v{version}")
|
|
201
|
+
downloaded_asset = rome_client.pull_ark(
|
|
202
|
+
name=name,
|
|
203
|
+
version=version,
|
|
204
|
+
config_output_path=str(ark_json_path),
|
|
205
|
+
asset_output_path=str(ark_asset_path),
|
|
206
|
+
)
|
|
207
|
+
print(" ✓ Config downloaded")
|
|
208
|
+
if downloaded_asset:
|
|
209
|
+
print(" ✓ Asset downloaded")
|
|
210
|
+
print(f"✓ Ark {name} v{version} pulled successfully")
|
|
211
|
+
return load_local_ark(name, version)
|
|
175
212
|
|
|
176
213
|
|
|
177
214
|
def list_remote_assets() -> list[AssetReference]:
|
|
@@ -184,17 +221,12 @@ def list_remote_assets() -> list[AssetReference]:
|
|
|
184
221
|
:raises AuthError: If not authenticated.
|
|
185
222
|
"""
|
|
186
223
|
|
|
187
|
-
# Get auth token
|
|
188
224
|
token = AuthHandler().get_token()
|
|
189
|
-
if token is None:
|
|
190
|
-
raise AuthError("User not authenticated. Please login first")
|
|
191
|
-
|
|
192
|
-
# Create Rome client and list assets
|
|
193
225
|
rome_client = RomeClient(api_url=ANTIOCH_API_URL, token=token)
|
|
194
226
|
return rome_client.list_assets()
|
|
195
227
|
|
|
196
228
|
|
|
197
|
-
def pull_remote_asset(name: str, version: str, overwrite: bool = False
|
|
229
|
+
def pull_remote_asset(name: str, version: str, overwrite: bool = False) -> Path:
|
|
198
230
|
"""
|
|
199
231
|
Pull an asset from remote registry to local storage.
|
|
200
232
|
|
|
@@ -203,129 +235,76 @@ def pull_remote_asset(name: str, version: str, overwrite: bool = False, show_pro
|
|
|
203
235
|
:param name: Name of the asset.
|
|
204
236
|
:param version: Version of the asset.
|
|
205
237
|
:param overwrite: Overwrite local asset if it already exists.
|
|
206
|
-
:param show_progress: Show download progress bar.
|
|
207
238
|
:return: Path to the downloaded asset file.
|
|
208
239
|
:raises AuthError: If not authenticated.
|
|
209
240
|
"""
|
|
210
241
|
|
|
211
|
-
# Get auth token
|
|
212
|
-
token = AuthHandler().get_token()
|
|
213
|
-
if token is None:
|
|
214
|
-
raise AuthError("User not authenticated. Please login first")
|
|
215
|
-
|
|
216
|
-
# Create Rome client and get asset metadata to determine extension
|
|
217
|
-
rome_client = RomeClient(api_url=ANTIOCH_API_URL, token=token)
|
|
218
|
-
metadata = rome_client.get_asset_metadata(name=name, version=version)
|
|
219
|
-
extension = metadata.get("extension", "usdz")
|
|
220
|
-
|
|
221
242
|
# Check if asset already exists locally
|
|
222
|
-
|
|
243
|
+
# NOTE: Only checks USDZ assets for now
|
|
244
|
+
asset_file_path = get_asset_path(name=name, version=version, extension="usdz", assert_exists=False)
|
|
223
245
|
if asset_file_path.exists() and not overwrite:
|
|
224
|
-
print(f"Asset {name}:{version} already exists locally, skipping download")
|
|
225
246
|
return asset_file_path
|
|
226
247
|
|
|
227
|
-
|
|
228
|
-
rome_client
|
|
229
|
-
|
|
248
|
+
token = AuthHandler().get_token()
|
|
249
|
+
rome_client = RomeClient(api_url=ANTIOCH_API_URL, token=token)
|
|
250
|
+
temp_path: str | None = None
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
# Download to a temp file in the destination directory so publishing can be atomic
|
|
254
|
+
# This avoids EXDEV when the asset directory is a separate mount (common in Kubernetes)
|
|
255
|
+
asset_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
256
|
+
safe_prefix = f".{name.replace(':', '_')}.{version.replace(':', '_')}."
|
|
257
|
+
fd, temp_path = tempfile.mkstemp(prefix=safe_prefix, dir=str(asset_file_path.parent))
|
|
258
|
+
os.close(fd)
|
|
259
|
+
|
|
260
|
+
# Pull asset - metadata comes back from response body
|
|
261
|
+
metadata = rome_client.pull_asset(name=name, version=version, output_path=temp_path)
|
|
262
|
+
extension = metadata.get("extension", "usdz")
|
|
263
|
+
|
|
264
|
+
# Get final path with correct extension
|
|
265
|
+
asset_file_path = get_asset_path(name=name, version=version, extension=extension, assert_exists=False)
|
|
266
|
+
asset_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
|
|
268
|
+
# Publish atomically on the same filesystem
|
|
269
|
+
Path(temp_path).replace(asset_file_path)
|
|
270
|
+
return asset_file_path
|
|
271
|
+
finally:
|
|
272
|
+
# Clean up if download fails or publish raises
|
|
273
|
+
if temp_path is not None:
|
|
274
|
+
Path(temp_path).unlink(missing_ok=True)
|
|
230
275
|
|
|
231
276
|
|
|
232
|
-
def
|
|
277
|
+
def build_ark_reference_from_versions(name: str, version_refs: list[ArkVersionReference]) -> ArkReference | None:
|
|
233
278
|
"""
|
|
234
|
-
|
|
279
|
+
Build an ArkReference from version references.
|
|
235
280
|
|
|
236
|
-
:param name: The name
|
|
237
|
-
:param
|
|
238
|
-
:return: ArkReference
|
|
281
|
+
:param name: The Ark name.
|
|
282
|
+
:param version_refs: List of ArkVersionReference instances.
|
|
283
|
+
:return: ArkReference or None if no versions exist.
|
|
239
284
|
"""
|
|
240
285
|
|
|
241
|
-
file_stats = [f.stat() for f in files]
|
|
242
|
-
created_at = min(datetime.fromtimestamp(stat.st_ctime) for stat in file_stats).isoformat()
|
|
243
|
-
updated_at = max(datetime.fromtimestamp(stat.st_mtime) for stat in file_stats).isoformat()
|
|
244
|
-
|
|
245
|
-
# Group files by version - parse from {name}:{version}:ark.json or {name}:{version}:asset.usdz
|
|
246
|
-
files_by_version: dict[str, list[Path]] = defaultdict(list)
|
|
247
|
-
for file_path in files:
|
|
248
|
-
files_by_version[file_path.name.split(":")[1]].append(file_path)
|
|
249
|
-
|
|
250
|
-
# Create an ArkVersionReference for each version
|
|
251
|
-
version_refs = []
|
|
252
|
-
for version, version_files in files_by_version.items():
|
|
253
|
-
ark_file = None
|
|
254
|
-
asset_file = None
|
|
255
|
-
for file_path in version_files:
|
|
256
|
-
if file_path.name.endswith(":ark.json"):
|
|
257
|
-
ark_file = file_path
|
|
258
|
-
elif file_path.name.endswith(":asset.usdz"):
|
|
259
|
-
asset_file = file_path
|
|
260
|
-
if ark_file is None:
|
|
261
|
-
continue
|
|
262
|
-
|
|
263
|
-
ark_stat = ark_file.stat()
|
|
264
|
-
version_refs.append(
|
|
265
|
-
ArkVersionReference(
|
|
266
|
-
version=version,
|
|
267
|
-
full_path=str(ark_file),
|
|
268
|
-
asset_path=str(asset_file) if asset_file else None,
|
|
269
|
-
size_bytes=ark_stat.st_size,
|
|
270
|
-
created_at=datetime.fromtimestamp(ark_stat.st_ctime).isoformat(),
|
|
271
|
-
updated_at=datetime.fromtimestamp(ark_stat.st_mtime).isoformat(),
|
|
272
|
-
asset_size_bytes=asset_file.stat().st_size if asset_file else None,
|
|
273
|
-
)
|
|
274
|
-
)
|
|
275
|
-
|
|
276
286
|
if not version_refs:
|
|
277
287
|
return None
|
|
278
288
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
updated_at=updated_at,
|
|
284
|
-
)
|
|
289
|
+
# Aggregate timestamps from versions, fallback to empty string if all are missing
|
|
290
|
+
created_at = min((v.created_at for v in version_refs if v.created_at), default="")
|
|
291
|
+
updated_at = max((v.updated_at for v in version_refs if v.updated_at), default="")
|
|
292
|
+
return ArkReference(name=name, versions=version_refs, created_at=created_at, updated_at=updated_at)
|
|
285
293
|
|
|
286
294
|
|
|
287
|
-
def
|
|
295
|
+
def build_asset_reference_from_versions(name: str, version_refs: list[AssetVersionReference]) -> AssetReference | None:
|
|
288
296
|
"""
|
|
289
|
-
|
|
297
|
+
Build an AssetReference from version references.
|
|
290
298
|
|
|
291
|
-
:param name: The name
|
|
292
|
-
:param
|
|
293
|
-
:return: AssetReference
|
|
299
|
+
:param name: The Asset name.
|
|
300
|
+
:param version_refs: List of AssetVersionReference instances.
|
|
301
|
+
:return: AssetReference or None if no versions exist.
|
|
294
302
|
"""
|
|
295
303
|
|
|
296
|
-
file_stats = [f.stat() for f in files]
|
|
297
|
-
created_at = min(datetime.fromtimestamp(stat.st_ctime) for stat in file_stats).isoformat()
|
|
298
|
-
updated_at = max(datetime.fromtimestamp(stat.st_mtime) for stat in file_stats).isoformat()
|
|
299
|
-
|
|
300
|
-
# Group files by version
|
|
301
|
-
files_by_version: dict[str, list[Path]] = defaultdict(list)
|
|
302
|
-
for file_path in files:
|
|
303
|
-
# Parse filename format: {name}:{version}:file.usdz
|
|
304
|
-
version = file_path.name.split(":")[1]
|
|
305
|
-
files_by_version[version].append(file_path)
|
|
306
|
-
|
|
307
|
-
# Create an AssetVersionReference for each version
|
|
308
|
-
version_refs = []
|
|
309
|
-
for version, version_files in files_by_version.items():
|
|
310
|
-
# Should only be one file per version, but take first if multiple
|
|
311
|
-
asset_file = version_files[0]
|
|
312
|
-
asset_stat = asset_file.stat()
|
|
313
|
-
version_refs.append(
|
|
314
|
-
AssetVersionReference(
|
|
315
|
-
version=version,
|
|
316
|
-
full_path=str(asset_file),
|
|
317
|
-
size_bytes=asset_stat.st_size,
|
|
318
|
-
created_at=datetime.fromtimestamp(asset_stat.st_ctime).isoformat(),
|
|
319
|
-
updated_at=datetime.fromtimestamp(asset_stat.st_mtime).isoformat(),
|
|
320
|
-
)
|
|
321
|
-
)
|
|
322
|
-
|
|
323
304
|
if not version_refs:
|
|
324
305
|
return None
|
|
325
306
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
updated_at=updated_at,
|
|
331
|
-
)
|
|
307
|
+
# Aggregate timestamps from versions, fallback to empty string if all are missing
|
|
308
|
+
created_at = min((v.created_at for v in version_refs if v.created_at), default="")
|
|
309
|
+
updated_at = max((v.updated_at for v in version_refs if v.updated_at), default="")
|
|
310
|
+
return AssetReference(name=name, versions=version_refs, created_at=created_at, updated_at=updated_at)
|
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}"
|