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.

Files changed (109) 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.0.6.dist-info → antioch_py-3.0.12.dist-info}/METADATA +8 -11
  7. antioch_py-3.0.12.dist-info/RECORD +61 -0
  8. {antioch_py-2.0.6.dist-info → antioch_py-3.0.12.dist-info}/WHEEL +1 -1
  9. antioch_py-3.0.12.dist-info/licenses/LICENSE +21 -0
  10. common/ark/__init__.py +6 -16
  11. common/ark/ark.py +23 -60
  12. common/ark/hardware.py +13 -37
  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 +83 -4
  21. common/core/__init__.py +37 -24
  22. common/core/auth.py +87 -114
  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 -3
  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 +22 -5
  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/sim/objects.py +460 -0
  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 -150
  58. antioch/session/ark.py +0 -504
  59. antioch/session/asset.py +0 -65
  60. antioch/session/error.py +0 -80
  61. antioch/session/record.py +0 -158
  62. antioch/session/scene.py +0 -1521
  63. antioch/session/session.py +0 -220
  64. antioch/session/task.py +0 -323
  65. antioch/session/views/__init__.py +0 -40
  66. antioch/session/views/animation.py +0 -189
  67. antioch/session/views/articulation.py +0 -245
  68. antioch/session/views/basis_curve.py +0 -186
  69. antioch/session/views/camera.py +0 -92
  70. antioch/session/views/collision.py +0 -75
  71. antioch/session/views/geometry.py +0 -74
  72. antioch/session/views/ground_plane.py +0 -63
  73. antioch/session/views/imu.py +0 -73
  74. antioch/session/views/joint.py +0 -64
  75. antioch/session/views/light.py +0 -175
  76. antioch/session/views/pir_sensor.py +0 -140
  77. antioch/session/views/radar.py +0 -73
  78. antioch/session/views/rigid_body.py +0 -282
  79. antioch/session/views/xform.py +0 -119
  80. antioch_py-2.0.6.dist-info/RECORD +0 -99
  81. antioch_py-2.0.6.dist-info/entry_points.txt +0 -2
  82. common/core/agent.py +0 -296
  83. common/core/task.py +0 -36
  84. common/rome/__init__.py +0 -9
  85. common/rome/client.py +0 -430
  86. common/rome/error.py +0 -16
  87. common/session/__init__.py +0 -54
  88. common/session/environment.py +0 -31
  89. common/session/sim.py +0 -240
  90. common/session/views/__init__.py +0 -263
  91. common/session/views/animation.py +0 -73
  92. common/session/views/articulation.py +0 -184
  93. common/session/views/basis_curve.py +0 -102
  94. common/session/views/camera.py +0 -147
  95. common/session/views/collision.py +0 -59
  96. common/session/views/geometry.py +0 -102
  97. common/session/views/ground_plane.py +0 -41
  98. common/session/views/imu.py +0 -66
  99. common/session/views/joint.py +0 -81
  100. common/session/views/light.py +0 -96
  101. common/session/views/pir_sensor.py +0 -115
  102. common/session/views/radar.py +0 -82
  103. common/session/views/rigid_body.py +0 -236
  104. common/session/views/viewport.py +0 -21
  105. common/session/views/xform.py +0 -39
  106. common/utils/usd.py +0 -12
  107. /antioch/{module/clock.py → clock.py} +0 -0
  108. {antioch_py-2.0.6.dist-info → antioch_py-3.0.12.dist-info}/top_level.txt +0 -0
  109. /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, ArkReference, ArkVersionReference, AssetReference, AssetVersionReference
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 AuthError, AuthHandler
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, files in files_by_name.items():
28
- ref = _build_ark_reference(name, files)
29
- if ref is not None:
30
- results.append(ref)
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 in local storage")
64
- raise FileNotFoundError(f"No versions of Ark {name} found in local storage")
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
- files_by_name: dict[str, list[Path]] = defaultdict(list)
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
- # Parse filename format: {name}:{version}:file.{extension}
101
- parts = file_path.stem.split(":")
102
- if len(parts) == 3 and parts[-1] == "file":
103
- files_by_name[parts[0]].append(file_path)
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, files in files_by_name.items():
107
- ref = _build_asset_reference(name, files)
108
- if ref is not None:
109
- results.append(ref)
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
- # Download asset usdz only if ark has asset_hash (hardware modules exist)
169
- if ark.get("metadata", {}).get("asset_hash") is not None:
170
- asset_content = rome_client.download_ark_assets(name=name, version=version)
171
- with open(arks_dir / f"{name}:{version}:asset.usdz", "wb") as f:
172
- f.write(asset_content)
173
-
174
- return ArkDefinition(**ark)
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, show_progress: bool = True) -> Path:
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
- asset_file_path = get_asset_path(name=name, version=version, extension=extension, assert_exists=False)
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
- # Download the asset file
228
- rome_client.download_asset(name=name, version=version, output_path=str(asset_file_path), show_progress=show_progress)
229
- return asset_file_path
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 _build_ark_reference(name: str, files: list[Path]) -> ArkReference | None:
277
+ def build_ark_reference_from_versions(name: str, version_refs: list[ArkVersionReference]) -> ArkReference | None:
233
278
  """
234
- Create an ArkReference from a list of files for a given ark.
279
+ Build an ArkReference from version references.
235
280
 
236
- :param name: The name of the ark.
237
- :param files: List of file paths (ark JSON and asset USDZ files).
238
- :return: ArkReference object or None if no valid versions found.
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
- return ArkReference(
280
- name=name,
281
- versions=version_refs,
282
- created_at=created_at,
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 _build_asset_reference(name: str, files: list[Path]) -> AssetReference | None:
295
+ def build_asset_reference_from_versions(name: str, version_refs: list[AssetVersionReference]) -> AssetReference | None:
288
296
  """
289
- Create an AssetReference from a list of files for a given asset.
297
+ Build an AssetReference from version references.
290
298
 
291
- :param name: The name of the asset.
292
- :param files: List of file paths (.usdz files).
293
- :return: AssetReference object or None if no valid versions found.
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
- return AssetReference(
327
- name=name,
328
- versions=version_refs,
329
- created_at=created_at,
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}"