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/auth.py CHANGED
@@ -7,25 +7,18 @@ from pathlib import Path
7
7
  import requests
8
8
  from pydantic import BaseModel
9
9
 
10
- from common.constants import get_auth_dir
11
-
12
- # Authentication routes
13
- AUTH_DOMAIN = os.environ.get("AUTH_DOMAIN", "https://staging.auth.antioch.com")
14
- AUTH_TOKEN_URL = f"{AUTH_DOMAIN}/oauth/token"
15
- DEVICE_CODE_URL = f"{AUTH_DOMAIN}/oauth/device/code"
16
-
17
- # Authentication constants
18
- AUTH_CLIENT_ID = "x0aOquV43Xe76ehqAm6Zir80O0MWpqTV"
19
- ALGORITHMS = ["RS256"]
20
- AUDIENCE = "https://sessions.antioch.com"
21
- AUTH_SCOPE = "openid profile email"
22
- AUTH_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code"
23
- AUTH_TIMEOUT_SECONDS = 120
24
-
25
- # Authentication claims
26
- AUTH_ORG_ID_CLAIM = "https://antioch.com/org_id"
27
- AUTH_ORG_NAME_CLAIM = "https://antioch.com/org_name"
28
- AUTH_ORGANIZATIONS_CLAIM = "https://antioch.com/organizations"
10
+ from common.constants import (
11
+ AUDIENCE,
12
+ AUTH_CLIENT_ID,
13
+ AUTH_GRANT_TYPE,
14
+ AUTH_ORG_ID_CLAIM,
15
+ AUTH_ORG_NAME_CLAIM,
16
+ AUTH_SCOPE,
17
+ AUTH_TIMEOUT_SECONDS,
18
+ AUTH_TOKEN_URL,
19
+ DEVICE_CODE_URL,
20
+ get_auth_dir,
21
+ )
29
22
 
30
23
 
31
24
  class AuthError(Exception):
@@ -36,38 +29,51 @@ class AuthError(Exception):
36
29
 
37
30
  class Organization(BaseModel):
38
31
  """
39
- Organization information.
32
+ Organization information extracted from JWT token.
40
33
  """
41
34
 
42
35
  org_id: str
43
36
  org_name: str
44
37
 
45
38
 
39
+ class UserInfo(BaseModel):
40
+ """
41
+ User information extracted from JWT token.
42
+ """
43
+
44
+ user_id: str
45
+ name: str | None = None
46
+ email: str | None = None
47
+
48
+
46
49
  class AuthHandler:
47
50
  """
48
- Client for handling authentication.
51
+ Client for handling authentication via OAuth2 device code flow.
49
52
 
50
- Auth is used for:
51
- - Pulling artifacts from the remote Ark registry
52
- - Pulling assets from the remote asset registry
53
+ Manages authentication tokens and organization context for interacting
54
+ with Antioch services.
53
55
  """
54
56
 
55
57
  def __init__(self):
56
58
  """
57
59
  Initialize the auth handler.
60
+
61
+ Automatically loads any existing token from disk.
58
62
  """
59
63
 
60
64
  self._token: str | None = None
61
- self._user_id: str | None = None
62
- self._current_org: Organization | None = None
63
- self._available_orgs: list[Organization] = []
65
+ self._org: Organization | None = None
66
+ self._user: UserInfo | None = None
64
67
  self._load_local_token()
65
68
 
66
69
  def login(self) -> None:
67
70
  """
68
- Authenticate the user via device code flow.
71
+ Authenticate the user via OAuth2 device code flow.
69
72
 
70
- :raises AuthError: If authentication fails.
73
+ Initiates the device code flow, prompts the user to authenticate
74
+ in their browser, and saves the token to disk on success.
75
+
76
+ :raises AuthError: If authentication fails or times out.
71
77
  """
72
78
 
73
79
  if self.is_authenticated():
@@ -97,26 +103,27 @@ class AuthHandler:
97
103
  "client_id": AUTH_CLIENT_ID,
98
104
  }
99
105
 
100
- authenticated = False
101
106
  start_time = time.time()
102
- while not authenticated:
107
+ while True:
103
108
  token_response = requests.post(AUTH_TOKEN_URL, data=token_payload)
104
109
  token_data = token_response.json()
105
110
  if token_response.status_code == 200:
106
111
  print("Authenticated!")
107
- authenticated = True
108
- elif token_data["error"] not in ("authorization_pending", "slow_down"):
112
+ self._token = token_data["access_token"]
113
+ break
114
+
115
+ if token_data["error"] not in ("authorization_pending", "slow_down"):
109
116
  print(token_data["error_description"])
110
117
  raise AuthError("Error authenticating the user") from Exception(token_data)
111
- else:
112
- if time.time() - start_time > AUTH_TIMEOUT_SECONDS:
113
- raise AuthError("Timeout waiting for authentication")
114
- time.sleep(device_code_data["interval"])
115
118
 
116
- # Save token
117
- self._token = token_data["access_token"]
119
+ if time.time() - start_time > AUTH_TIMEOUT_SECONDS:
120
+ raise AuthError("Timeout waiting for authentication")
121
+
122
+ time.sleep(device_code_data["interval"])
123
+
118
124
  if self._token is None:
119
125
  raise AuthError("No token received")
126
+
120
127
  self._validate_token_claims(self._token)
121
128
  self.save_token()
122
129
 
@@ -124,69 +131,44 @@ class AuthHandler:
124
131
  """
125
132
  Check if the user is authenticated.
126
133
 
127
- :return: True if authenticated, False otherwise.
134
+ :return: True if authenticated with a valid token, False otherwise.
128
135
  """
129
136
 
130
- return self._current_org is not None
137
+ return self._org is not None
131
138
 
132
- def select_organization(self, org_id: str):
139
+ def get_org(self) -> Organization:
133
140
  """
134
- Select the organization to use for the session.
141
+ Get the current organization.
135
142
 
136
- :param org_id: The ID of the organization to select.
143
+ :return: The current organization.
137
144
  :raises AuthError: If the user is not authenticated.
138
145
  """
139
146
 
140
- if not self.is_authenticated():
147
+ if not self.is_authenticated() or self._org is None:
141
148
  raise AuthError("Not authenticated. Please login first")
149
+ return self._org
142
150
 
143
- for org in self._available_orgs:
144
- if org.org_id == org_id:
145
- self._current_org = org
146
- return
147
-
148
- raise AuthError(f"Organization '{org_id}' is not in your available organizations")
149
-
150
- def get_current_org(self) -> Organization | None:
151
+ def get_user_info(self) -> UserInfo | None:
151
152
  """
152
- Get the current organization.
153
+ Get the current user information.
153
154
 
154
- :return: The current organization.
155
+ :return: The current user info, or None if not available.
155
156
  :raises AuthError: If the user is not authenticated.
156
157
  """
157
158
 
158
159
  if not self.is_authenticated():
159
160
  raise AuthError("Not authenticated. Please login first")
161
+ return self._user
160
162
 
161
- return self._current_org
162
-
163
- def get_user_id(self) -> str | None:
164
- """
165
- Get the user ID.
166
-
167
- :return: The user ID.
163
+ def get_token(self) -> str:
168
164
  """
165
+ Get the current authentication token.
169
166
 
170
- return self._user_id
171
-
172
- def get_available_orgs(self) -> list[Organization]:
173
- """
174
- Get the available organizations.
175
-
176
- :return: The available organizations.
177
- """
178
-
179
- return self._available_orgs
180
-
181
- def get_token(self) -> str | None:
182
- """
183
- Get the token.
184
-
185
- :return: The token.
167
+ :return: The JWT access token.
186
168
  :raises AuthError: If the user is not authenticated.
187
169
  """
188
170
 
189
- if not self.is_authenticated():
171
+ if not self.is_authenticated() or self._token is None:
190
172
  raise AuthError("Not authenticated. Please login first")
191
173
  return self._token
192
174
 
@@ -194,22 +176,16 @@ class AuthHandler:
194
176
  """
195
177
  Save the authentication token and organization data to disk.
196
178
 
179
+ Creates the token file with restrictive permissions (0600).
180
+
197
181
  :raises AuthError: If not authenticated.
198
182
  """
199
183
 
200
184
  if not self.is_authenticated():
201
185
  raise AuthError("Not authenticated. Please login first")
202
186
 
203
- stored_data = {
204
- "token": self._token,
205
- "current_org": self._current_org.model_dump() if self._current_org else None,
206
- "available_orgs": [org.model_dump() for org in self._available_orgs],
207
- }
208
-
187
+ stored_data = {"token": self._token, "org": self._org.model_dump() if self._org else None}
209
188
  token_path = self._get_token_path()
210
-
211
- # Create file with restrictive permissions (owner read/write only)
212
- # Use os.open to atomically create file with 0o600 permissions
213
189
  fd = os.open(token_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
214
190
  with os.fdopen(fd, "w") as f:
215
191
  json.dump(stored_data, f, indent=2)
@@ -225,9 +201,9 @@ class AuthHandler:
225
201
 
226
202
  def _load_local_token(self) -> None:
227
203
  """
228
- Load the authentication token and organization data from disk.
204
+ Load the authentication token from disk.
229
205
 
230
- Silently returns if no token exists or if loading fails. Clears invalid tokens.
206
+ Silently returns if no token exists. Clears invalid or expired tokens.
231
207
  """
232
208
 
233
209
  token_path = self._get_token_path()
@@ -244,17 +220,16 @@ class AuthHandler:
244
220
 
245
221
  # Validate and extract all claims from token
246
222
  self._validate_token_claims(self._token)
223
+ if stored_data.get("org"):
224
+ self._org = Organization(**stored_data["org"])
247
225
  except Exception as e:
248
226
  print(f"Error loading local token: {e}")
249
-
250
- # Clear invalid or expired tokens
251
227
  self._token = None
252
228
  self.clear_token()
253
- return
254
229
 
255
- def _validate_token_claims(self, token: str):
230
+ def _validate_token_claims(self, token: str) -> None:
256
231
  """
257
- Validate the token and extract all claims including user ID and organization information.
232
+ Validate the token and extract organization and user information.
258
233
 
259
234
  :param token: The JWT token to validate.
260
235
  :raises AuthError: If the token is invalid, expired, or missing required claims.
@@ -264,7 +239,7 @@ class AuthHandler:
264
239
  if len(parts) != 3:
265
240
  raise AuthError("Invalid token format")
266
241
 
267
- # Decode the payload (middle part)
242
+ # Decode the payload
268
243
  payload_encoded = parts[1]
269
244
  padding = len(payload_encoded) % 4
270
245
  if padding:
@@ -277,29 +252,27 @@ class AuthHandler:
277
252
  if exp and time.time() > exp:
278
253
  raise AuthError("Token has expired")
279
254
 
280
- # Extract user ID
281
- self._user_id = payload.get("sub")
282
- if self._user_id is None:
283
- raise AuthError("User ID not found in token claims")
284
-
285
- # Extract current organization
286
- self._current_org = Organization(
287
- org_id=payload.get(AUTH_ORG_ID_CLAIM),
288
- org_name=payload.get(AUTH_ORG_NAME_CLAIM),
289
- )
290
-
291
- # Extract available organizations
292
- # Note: Auth0 returns organizations with "id" and "name" keys, not "org_id" and "org_name"
293
- self._available_orgs = [
294
- Organization(org_id=org.get("id") or org.get("org_id"), org_name=org.get("name") or org.get("org_name"))
295
- for org in payload.get(AUTH_ORGANIZATIONS_CLAIM, [])
296
- ]
255
+ # Extract organization
256
+ org_id = payload.get(AUTH_ORG_ID_CLAIM)
257
+ org_name = payload.get(AUTH_ORG_NAME_CLAIM)
258
+ if not org_id or not org_name:
259
+ raise AuthError("Organization information not found in token claims")
260
+ self._org = Organization(org_id=org_id, org_name=org_name)
261
+
262
+ # Extract user info (optional claims)
263
+ user_id = payload.get("sub")
264
+ if user_id:
265
+ self._user = UserInfo(
266
+ user_id=user_id,
267
+ name=payload.get("name") or payload.get("nickname"),
268
+ email=payload.get("email"),
269
+ )
297
270
 
298
271
  def _get_token_path(self) -> Path:
299
272
  """
300
273
  Get the token file path.
301
274
 
302
- :return: Path to the token file.
275
+ :return: Path to the token.json file.
303
276
  """
304
277
 
305
278
  return get_auth_dir() / "token.json"
@@ -0,0 +1,261 @@
1
+ import contextlib
2
+ import time
3
+ from enum import Enum
4
+
5
+ import docker
6
+ from docker.errors import APIError
7
+ from docker.models.containers import Container
8
+
9
+ from common.ark import Ark as ArkDefinition, Environment
10
+ from common.ark.module import ModuleImage, ModuleReady, ModuleStart
11
+ from common.constants import ANTIOCH_API_URL
12
+ from common.core.auth import AuthHandler
13
+ from common.core.rome import RomeClient
14
+ from common.utils.comms import CommsSession
15
+ from common.utils.time import now_us
16
+
17
+ # Container naming prefix for all Antioch module containers
18
+ CONTAINER_PREFIX = "antioch-module-"
19
+
20
+ # Synchronization paths for module coordination
21
+ ARK_MODULE_READY_PATH = "_ark/module_ready"
22
+ ARK_MODULE_START_PATH = "_ark/module_start"
23
+
24
+
25
+ class ContainerSource(str, Enum):
26
+ """
27
+ Source location for container images.
28
+ """
29
+
30
+ LOCAL = "Local"
31
+ REMOTE = "Remote"
32
+
33
+
34
+ class ContainerManagerError(Exception):
35
+ """
36
+ Raised when container management operations fail.
37
+ """
38
+
39
+ pass
40
+
41
+
42
+ class ContainerManager:
43
+ """
44
+ Manages Docker containers for Ark modules.
45
+
46
+ Handles launching, coordination, and cleanup of module containers.
47
+ Uses host networking for Zenoh communication.
48
+ """
49
+
50
+ def __init__(self) -> None:
51
+ """
52
+ Create a new container manager.
53
+
54
+ Initializes Docker client and Zenoh communication session.
55
+ """
56
+
57
+ self._comms = CommsSession()
58
+ self._client = docker.from_env()
59
+ self._containers: dict[str, Container] = {}
60
+
61
+ def launch_ark(
62
+ self,
63
+ ark: ArkDefinition,
64
+ source: ContainerSource = ContainerSource.LOCAL,
65
+ environment: Environment = Environment.SIM,
66
+ debug: bool = False,
67
+ timeout: float = 30.0,
68
+ ) -> int:
69
+ """
70
+ Launch all module containers for an Ark.
71
+
72
+ Stops any existing module containers first to ensure idempotent behavior.
73
+
74
+ :param ark: Ark definition to launch.
75
+ :param source: Container image source (local or remote).
76
+ :param environment: Environment to run in (sim or real).
77
+ :param debug: Enable debug mode.
78
+ :param timeout: Timeout in seconds for modules to become ready.
79
+ :return: Global start time in microseconds.
80
+ :raises ContainerManagerError: If environment is incompatible or launch fails.
81
+ """
82
+
83
+ # Validate environment compatibility
84
+ if ark.capability == Environment.SIM and environment == Environment.REAL:
85
+ raise ContainerManagerError(f"Ark '{ark.name}' has sim capability but requested for real")
86
+ if ark.capability == Environment.REAL and environment == Environment.SIM:
87
+ raise ContainerManagerError(f"Ark '{ark.name}' has real capability but requested for sim")
88
+
89
+ # Stop all existing module containers (idempotent)
90
+ self._stop_all()
91
+
92
+ # Get GAR credentials if pulling from remote
93
+ gar_auth = self._get_gar_auth() if source == ContainerSource.REMOTE else None
94
+
95
+ # Build container configs
96
+ configs: list[tuple[str, str, str]] = []
97
+ for module in ark.modules:
98
+ image = self._get_image(module.image, environment)
99
+ if image is None:
100
+ raise ContainerManagerError(f"No image for module '{module.name}' in {environment}")
101
+ if gar_auth is not None:
102
+ image = f"{gar_auth['registry_host']}/{gar_auth['repository']}/{image}"
103
+ container_name = f"{CONTAINER_PREFIX}{ark.name.replace('_', '-')}-{module.name.replace('_', '-')}"
104
+ configs.append((module.name, container_name, image))
105
+
106
+ # Pull images if remote
107
+ if gar_auth is not None:
108
+ self._pull_images([c[2] for c in configs], gar_auth)
109
+
110
+ # Set up ready subscriber before launching
111
+ ready_sub = self._comms.declare_async_subscriber(ARK_MODULE_READY_PATH)
112
+
113
+ # Launch containers
114
+ ark_json = ark.model_dump_json()
115
+ for module_name, container_name, image in configs:
116
+ self._launch(module_name, container_name, image, ark_json, environment, debug)
117
+
118
+ # Wait for all modules to be ready
119
+ pending = {m.name for m in ark.modules}
120
+ start = time.time()
121
+ while pending:
122
+ if time.time() - start > timeout:
123
+ raise ContainerManagerError(f"Timeout waiting for modules: {', '.join(sorted(pending))}")
124
+ msg = ready_sub.recv_timeout(ModuleReady, timeout=0.1)
125
+ if msg is not None:
126
+ print(f"Module ready: {msg.module_name}")
127
+ pending.discard(msg.module_name)
128
+
129
+ # Broadcast global start time (2s in future for sync)
130
+ global_start_us = ((now_us() // 1_000_000) + 2) * 1_000_000
131
+ self._comms.declare_publisher(ARK_MODULE_START_PATH).publish(ModuleStart(global_start_time_us=global_start_us))
132
+ return global_start_us
133
+
134
+ def stop(self, timeout: float = 10.0) -> None:
135
+ """
136
+ Stop all module containers.
137
+
138
+ :param timeout: Timeout in seconds for container stop operation.
139
+ """
140
+
141
+ self._stop_all(timeout)
142
+
143
+ def close(self, timeout: float = 10.0) -> None:
144
+ """
145
+ Close the container manager and clean up resources.
146
+
147
+ Stops all module containers and closes the Zenoh session.
148
+
149
+ :param timeout: Timeout in seconds for container stop operation.
150
+ """
151
+
152
+ self._stop_all(timeout)
153
+ self._comms.close()
154
+
155
+ def _launch(
156
+ self,
157
+ module_name: str,
158
+ container_name: str,
159
+ image: str,
160
+ ark_json: str,
161
+ environment: Environment,
162
+ debug: bool,
163
+ ) -> None:
164
+ """
165
+ Launch a single module container.
166
+
167
+ :param module_name: Name of the module.
168
+ :param container_name: Docker container name.
169
+ :param image: Docker image to use.
170
+ :param ark_json: Serialized Ark definition.
171
+ :param environment: Environment (sim or real).
172
+ :param debug: Enable debug mode.
173
+ :raises ContainerManagerError: If container launch fails.
174
+ """
175
+
176
+ try:
177
+ container = self._client.containers.run(
178
+ image=image,
179
+ name=container_name,
180
+ environment={
181
+ "_MODULE_NAME": module_name,
182
+ "_ARK": ark_json,
183
+ "_ENVIRONMENT": str(environment.value),
184
+ "_DEBUG": str(debug).lower(),
185
+ },
186
+ network_mode="host",
187
+ ipc_mode="host",
188
+ detach=True,
189
+ remove=False,
190
+ )
191
+ self._containers[container_name] = container
192
+ print(f"Launched container: {container_name}")
193
+ except APIError as e:
194
+ raise ContainerManagerError(f"Failed to launch '{container_name}': {e}") from e
195
+
196
+ def _stop_all(self, timeout: float = 10.0) -> None:
197
+ """
198
+ Stop all Antioch module containers.
199
+
200
+ Finds all containers with the antioch-module- prefix and stops them.
201
+
202
+ :param timeout: Timeout in seconds for stop operation.
203
+ """
204
+
205
+ with contextlib.suppress(APIError):
206
+ for container in self._client.containers.list(all=True):
207
+ if container.name and container.name.startswith(CONTAINER_PREFIX):
208
+ print(f"Stopping container: {container.name}")
209
+ self._stop_container(container, timeout)
210
+
211
+ self._containers.clear()
212
+
213
+ def _stop_container(self, container: Container, timeout: float) -> None:
214
+ """
215
+ Stop and remove a single container.
216
+
217
+ :param container: Docker container to stop.
218
+ :param timeout: Timeout in seconds for stop operation.
219
+ """
220
+
221
+ with contextlib.suppress(APIError):
222
+ container.stop(timeout=int(timeout))
223
+ with contextlib.suppress(APIError):
224
+ container.remove(force=True)
225
+
226
+ def _get_image(self, image: str | ModuleImage, environment: Environment) -> str | None:
227
+ """
228
+ Get the image name for the given environment.
229
+
230
+ :param image: Image specification (string or ModuleImage).
231
+ :param environment: Environment to get image for.
232
+ :return: Image name or None if not available.
233
+ """
234
+
235
+ if isinstance(image, str):
236
+ return image
237
+ return image.sim if environment == Environment.SIM else image.real
238
+
239
+ def _get_gar_auth(self) -> dict:
240
+ """
241
+ Get Google Artifact Registry authentication credentials.
242
+
243
+ :return: Dictionary containing registry host, repository, and access token.
244
+ """
245
+
246
+ auth = AuthHandler()
247
+ rome = RomeClient(ANTIOCH_API_URL, auth.get_token())
248
+ return rome.get_gar_token()
249
+
250
+ def _pull_images(self, images: list[str], gar_auth: dict) -> None:
251
+ """
252
+ Pull container images from the registry.
253
+
254
+ :param images: List of image names to pull.
255
+ :param gar_auth: GAR authentication credentials.
256
+ """
257
+
258
+ auth_config = {"username": "oauth2accesstoken", "password": gar_auth["access_token"]}
259
+ for image in set(images):
260
+ print(f"Pulling image: {image}")
261
+ self._client.images.pull(image, auth_config=auth_config)