modAL 1.6.2.dev2__py3-none-any.whl → 1.6.2.dev4__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.
- modal/_environments.py +2 -2
- modal/_functions.py +2 -2
- modal/_utils/name_utils.py +9 -7
- modal/cli/container.py +2 -1
- modal/cli/endpoint.py +6 -1
- modal/cli/environment.py +3 -3
- modal/cli/shell.py +3 -1
- modal/client.pyi +2 -2
- modal/experimental/__init__.py +66 -0
- modal/runner.py +1 -1
- modal/sandbox/__init__.py +9 -0
- modal/{sandbox.py → sandbox/_sandbox.py} +38 -53
- modal/sandbox/sandbox.py +9 -0
- modal/{sandbox.pyi → sandbox/sandbox.pyi} +43 -1371
- modal/sandbox_fs.py +6 -4
- modal/sandbox_fs.pyi +10 -5
- modal/secret.py +13 -4
- modal/secret.pyi +4 -0
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/METADATA +1 -1
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/RECORD +31 -29
- modal_proto/api_grpc.py +16 -0
- modal_proto/api_pb2.py +1276 -1233
- modal_proto/api_pb2.pyi +113 -4
- modal_proto/api_pb2_grpc.py +33 -0
- modal_proto/api_pb2_grpc.pyi +10 -0
- modal_proto/modal_api_grpc.py +1 -0
- modal_version/__init__.py +1 -1
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/WHEEL +0 -0
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/entry_points.txt +0 -0
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/licenses/LICENSE +0 -0
- {modal-1.6.2.dev2.dist-info → modal-1.6.2.dev4.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import asyncio.locks
|
|
2
2
|
import collections.abc
|
|
3
|
-
import enum
|
|
4
3
|
import google.protobuf.message
|
|
5
|
-
import modal._image
|
|
6
4
|
import modal._logs_manager
|
|
7
|
-
import modal._object
|
|
8
5
|
import modal._outbound_policy
|
|
9
6
|
import modal._supports_logs
|
|
10
7
|
import modal._tunnel
|
|
@@ -19,6 +16,7 @@ import modal.mount
|
|
|
19
16
|
import modal.network_file_system
|
|
20
17
|
import modal.object
|
|
21
18
|
import modal.proxy
|
|
19
|
+
import modal.sandbox._sandbox
|
|
22
20
|
import modal.sandbox_fs
|
|
23
21
|
import modal.secret
|
|
24
22
|
import modal.snapshot
|
|
@@ -32,1357 +30,6 @@ import pathlib
|
|
|
32
30
|
import typing
|
|
33
31
|
import typing_extensions
|
|
34
32
|
|
|
35
|
-
SandboxRuntime = typing.Literal["gvisor", "vm"]
|
|
36
|
-
|
|
37
|
-
async def _gather_load_with_timings(
|
|
38
|
-
load_coros: collections.abc.Sequence[collections.abc.Awaitable[typing.Any]],
|
|
39
|
-
) -> list[tuple[str, float]]:
|
|
40
|
-
"""Await all loader coroutines concurrently and return [(object_id, elapsed_seconds)] per load."""
|
|
41
|
-
...
|
|
42
|
-
|
|
43
|
-
def _format_sandbox_create_timing_log(
|
|
44
|
-
sandbox_id: str, total_seconds: float, rpc_seconds: float, dep_timings: collections.abc.Sequence[tuple[str, float]]
|
|
45
|
-
) -> str:
|
|
46
|
-
"""Format the Sandbox create debug log line, listing the slowest deps first."""
|
|
47
|
-
...
|
|
48
|
-
|
|
49
|
-
def _validate_sandbox_env(env: dict[str, str]) -> None: ...
|
|
50
|
-
def _validate_sandbox_runtime(runtime: typing.Optional[typing.Literal["gvisor", "vm"]]) -> None: ...
|
|
51
|
-
def _ttl_to_wire_ttl(ttl: typing.Optional[int]) -> int:
|
|
52
|
-
"""Convert a TTL value to the wire format, validating the input."""
|
|
53
|
-
...
|
|
54
|
-
|
|
55
|
-
def _validate_experimental_encryption_key(key: typing.Optional[bytes]) -> typing.Optional[bytes]: ...
|
|
56
|
-
def _image_id_for_mount(image: modal._image._Image, method_name: str) -> str: ...
|
|
57
|
-
|
|
58
|
-
class SandboxVersion(enum.Enum):
|
|
59
|
-
V1 = 1
|
|
60
|
-
V2 = 2
|
|
61
|
-
|
|
62
|
-
def _is_v1_sandbox_id(sandbox_id: str) -> bool: ...
|
|
63
|
-
def _get_sandbox_version(sandbox_id: str) -> SandboxVersion: ...
|
|
64
|
-
def _result_returncode(result: typing.Optional[modal_proto.api_pb2.GenericResult]) -> typing.Optional[int]: ...
|
|
65
|
-
def _validate_exec_args(args: collections.abc.Sequence[str]) -> None: ...
|
|
66
|
-
def _build_outbound_network_access(
|
|
67
|
-
block_network: bool,
|
|
68
|
-
outbound_cidr_allowlist: typing.Optional[collections.abc.Sequence[str]],
|
|
69
|
-
outbound_domain_allowlist: typing.Optional[collections.abc.Sequence[str]],
|
|
70
|
-
) -> modal_proto.api_pb2.NetworkAccess:
|
|
71
|
-
"""Build the outbound `NetworkAccess` for the given params.
|
|
72
|
-
|
|
73
|
-
Shared by `Sandbox.create` and the sidecar create path. When nothing is
|
|
74
|
-
specified, defaults to open network access.
|
|
75
|
-
"""
|
|
76
|
-
...
|
|
77
|
-
|
|
78
|
-
class DefaultSandboxNameOverride(str):
|
|
79
|
-
"""A singleton class that represents the default sandbox name override.
|
|
80
|
-
|
|
81
|
-
It is used to indicate that the sandbox name should not be overridden.
|
|
82
|
-
"""
|
|
83
|
-
def __repr__(self) -> str:
|
|
84
|
-
"""Return repr(self)."""
|
|
85
|
-
...
|
|
86
|
-
|
|
87
|
-
_DEFAULT_SANDBOX_NAME_OVERRIDE: DefaultSandboxNameOverride
|
|
88
|
-
|
|
89
|
-
class Probe:
|
|
90
|
-
"""Probe configuration for the Sandbox Readiness Probe.
|
|
91
|
-
|
|
92
|
-
Examples:
|
|
93
|
-
```python notest
|
|
94
|
-
# Wait until a file exists.
|
|
95
|
-
readiness_probe = modal.Probe.with_exec(
|
|
96
|
-
"sh", "-c", "test -f /tmp/ready",
|
|
97
|
-
)
|
|
98
|
-
|
|
99
|
-
# Wait until a TCP port is accepting connections.
|
|
100
|
-
readiness_probe = modal.Probe.with_tcp(8080)
|
|
101
|
-
|
|
102
|
-
app = modal.App.lookup('sandbox-readiness-probe', create_if_missing=True)
|
|
103
|
-
sandbox = modal.Sandbox.create(
|
|
104
|
-
"python3", "-m", "http.server", "8080",
|
|
105
|
-
readiness_probe=readiness_probe,
|
|
106
|
-
app=app,
|
|
107
|
-
)
|
|
108
|
-
sandbox.wait_until_ready()
|
|
109
|
-
```
|
|
110
|
-
"""
|
|
111
|
-
|
|
112
|
-
tcp_port: typing.Optional[int]
|
|
113
|
-
exec_argv: typing.Optional[tuple[str, ...]]
|
|
114
|
-
interval_ms: int
|
|
115
|
-
|
|
116
|
-
def __post_init__(self): ...
|
|
117
|
-
@classmethod
|
|
118
|
-
def with_tcp(cls, port: int, *, interval_ms: int = 100) -> Probe: ...
|
|
119
|
-
@classmethod
|
|
120
|
-
def with_exec(cls, *argv: str, interval_ms: int = 100) -> Probe: ...
|
|
121
|
-
def _to_proto(self) -> modal_proto.api_pb2.Probe: ...
|
|
122
|
-
def __init__(
|
|
123
|
-
self,
|
|
124
|
-
tcp_port: typing.Optional[int] = None,
|
|
125
|
-
exec_argv: typing.Optional[tuple[str, ...]] = None,
|
|
126
|
-
interval_ms: int = 100,
|
|
127
|
-
) -> None:
|
|
128
|
-
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
129
|
-
...
|
|
130
|
-
|
|
131
|
-
def __repr__(self):
|
|
132
|
-
"""Return repr(self)."""
|
|
133
|
-
...
|
|
134
|
-
|
|
135
|
-
def __eq__(self, other):
|
|
136
|
-
"""Return self==value."""
|
|
137
|
-
...
|
|
138
|
-
|
|
139
|
-
def __setattr__(self, name, value):
|
|
140
|
-
"""Implement setattr(self, name, value)."""
|
|
141
|
-
...
|
|
142
|
-
|
|
143
|
-
def __delattr__(self, name):
|
|
144
|
-
"""Implement delattr(self, name)."""
|
|
145
|
-
...
|
|
146
|
-
|
|
147
|
-
def __hash__(self):
|
|
148
|
-
"""Return hash(self)."""
|
|
149
|
-
...
|
|
150
|
-
|
|
151
|
-
def _resolve_app_id_and_client(
|
|
152
|
-
app: typing.Optional[modal.app._App], client: typing.Optional[modal.client._Client]
|
|
153
|
-
) -> tuple[typing.Optional[str], typing.Optional[modal.client._Client]]:
|
|
154
|
-
"""Resolve the App id and client for Sandbox creation, validating that an App is available."""
|
|
155
|
-
...
|
|
156
|
-
|
|
157
|
-
class _Sandbox(modal._object._Object):
|
|
158
|
-
"""A `Sandbox` object lets you interact with a running sandbox. This API is similar to Python's
|
|
159
|
-
[asyncio.subprocess.Process](https://docs.python.org/3/library/asyncio-subprocess.html#asyncio.subprocess.Process).
|
|
160
|
-
|
|
161
|
-
Refer to the [guide](https://modal.com/docs/guide/sandbox) on how to spawn and use sandboxes.
|
|
162
|
-
"""
|
|
163
|
-
|
|
164
|
-
_result: typing.Optional[modal_proto.api_pb2.GenericResult]
|
|
165
|
-
_stdout: modal.io_streams._StreamReader[str]
|
|
166
|
-
_stderr: modal.io_streams._StreamReader[str]
|
|
167
|
-
_stdin: modal.io_streams._StreamWriter
|
|
168
|
-
_task_id: typing.Optional[str]
|
|
169
|
-
_tunnels: typing.Optional[dict[int, modal._tunnel.Tunnel]]
|
|
170
|
-
_enable_snapshot: bool
|
|
171
|
-
_command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]
|
|
172
|
-
_command_router_lock: typing.Optional[asyncio.locks.Lock]
|
|
173
|
-
_init_command_router_access: typing.Optional[modal_proto.api_pb2.CommandRouterAccess]
|
|
174
|
-
_attached: bool
|
|
175
|
-
_filesystem: typing.Optional[modal.sandbox_fs._SandboxFilesystem]
|
|
176
|
-
_is_v2: bool
|
|
177
|
-
_app_id: typing.Optional[str]
|
|
178
|
-
|
|
179
|
-
@staticmethod
|
|
180
|
-
def _default_pty_info() -> modal_proto.api_pb2.PTYInfo: ...
|
|
181
|
-
@staticmethod
|
|
182
|
-
def _new(
|
|
183
|
-
args: collections.abc.Sequence[str],
|
|
184
|
-
image: modal._image._Image,
|
|
185
|
-
secrets: collections.abc.Collection[modal.secret._Secret],
|
|
186
|
-
name: typing.Optional[str] = None,
|
|
187
|
-
timeout: int = 300,
|
|
188
|
-
idle_timeout: typing.Optional[int] = None,
|
|
189
|
-
workdir: typing.Optional[str] = None,
|
|
190
|
-
gpu: typing.Optional[str] = None,
|
|
191
|
-
cloud: typing.Optional[str] = None,
|
|
192
|
-
region: typing.Union[str, collections.abc.Sequence[str], None] = None,
|
|
193
|
-
cpu: typing.Union[float, tuple[float, float], None] = None,
|
|
194
|
-
memory: typing.Union[int, tuple[int, int], None] = None,
|
|
195
|
-
mounts: collections.abc.Sequence[modal.mount._Mount] = (),
|
|
196
|
-
network_file_systems: dict[typing.Union[str, os.PathLike], modal.network_file_system._NetworkFileSystem] = {},
|
|
197
|
-
block_network: bool = False,
|
|
198
|
-
outbound_cidr_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
199
|
-
outbound_domain_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
200
|
-
outbound_policy: typing.Optional[modal._outbound_policy._OutboundPolicy] = None,
|
|
201
|
-
inbound_cidr_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
202
|
-
volumes: dict[
|
|
203
|
-
typing.Union[str, os.PathLike],
|
|
204
|
-
typing.Union[modal.volume._Volume, modal.cloud_bucket_mount._CloudBucketMount],
|
|
205
|
-
] = {},
|
|
206
|
-
pty: bool = False,
|
|
207
|
-
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
208
|
-
encrypted_ports: collections.abc.Sequence[int] = [],
|
|
209
|
-
h2_ports: collections.abc.Sequence[int] = [],
|
|
210
|
-
unencrypted_ports: collections.abc.Sequence[int] = [],
|
|
211
|
-
proxy: typing.Optional[modal.proxy._Proxy] = None,
|
|
212
|
-
readiness_probe: typing.Optional[Probe] = None,
|
|
213
|
-
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
214
|
-
tags: typing.Optional[dict[str, str]] = None,
|
|
215
|
-
enable_snapshot: bool = False,
|
|
216
|
-
verbose: bool = False,
|
|
217
|
-
custom_domain: typing.Optional[str] = None,
|
|
218
|
-
include_oidc_identity_token: bool = False,
|
|
219
|
-
runtime: typing.Optional[typing.Literal["gvisor", "vm"]] = None,
|
|
220
|
-
) -> _Sandbox:
|
|
221
|
-
"""mdmd:hidden"""
|
|
222
|
-
...
|
|
223
|
-
|
|
224
|
-
@staticmethod
|
|
225
|
-
async def create(
|
|
226
|
-
*args: str,
|
|
227
|
-
app: typing.Optional[modal.app._App] = None,
|
|
228
|
-
name: typing.Optional[str] = None,
|
|
229
|
-
tags: typing.Optional[dict[str, str]] = None,
|
|
230
|
-
image: typing.Optional[modal._image._Image] = None,
|
|
231
|
-
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
232
|
-
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
233
|
-
network_file_systems: dict[typing.Union[str, os.PathLike], modal.network_file_system._NetworkFileSystem] = {},
|
|
234
|
-
timeout: int = 300,
|
|
235
|
-
idle_timeout: typing.Optional[int] = None,
|
|
236
|
-
workdir: typing.Optional[str] = None,
|
|
237
|
-
gpu: typing.Optional[str] = None,
|
|
238
|
-
cloud: typing.Optional[str] = None,
|
|
239
|
-
region: typing.Union[str, collections.abc.Sequence[str], None] = None,
|
|
240
|
-
cpu: typing.Union[float, tuple[float, float], None] = None,
|
|
241
|
-
memory: typing.Union[int, tuple[int, int], None] = None,
|
|
242
|
-
runtime: typing.Optional[typing.Literal["gvisor", "vm"]] = None,
|
|
243
|
-
block_network: bool = False,
|
|
244
|
-
outbound_cidr_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
245
|
-
outbound_domain_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
246
|
-
_experimental_outbound_policy: typing.Optional[modal._outbound_policy._OutboundPolicy] = None,
|
|
247
|
-
inbound_cidr_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
248
|
-
volumes: dict[
|
|
249
|
-
typing.Union[str, os.PathLike],
|
|
250
|
-
typing.Union[modal.volume._Volume, modal.cloud_bucket_mount._CloudBucketMount],
|
|
251
|
-
] = {},
|
|
252
|
-
pty: bool = False,
|
|
253
|
-
encrypted_ports: collections.abc.Sequence[int] = [],
|
|
254
|
-
h2_ports: collections.abc.Sequence[int] = [],
|
|
255
|
-
unencrypted_ports: collections.abc.Sequence[int] = [],
|
|
256
|
-
custom_domain: typing.Optional[str] = None,
|
|
257
|
-
proxy: typing.Optional[modal.proxy._Proxy] = None,
|
|
258
|
-
include_oidc_identity_token: bool = False,
|
|
259
|
-
readiness_probe: typing.Optional[Probe] = None,
|
|
260
|
-
verbose: bool = False,
|
|
261
|
-
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
262
|
-
_experimental_enable_snapshot: bool = False,
|
|
263
|
-
client: typing.Optional[modal.client._Client] = None,
|
|
264
|
-
environment_name: typing.Optional[str] = None,
|
|
265
|
-
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
266
|
-
cidr_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
267
|
-
) -> _Sandbox:
|
|
268
|
-
"""Create a new Sandbox to run untrusted, arbitrary code.
|
|
269
|
-
|
|
270
|
-
The Sandbox's corresponding container will be created asynchronously.
|
|
271
|
-
|
|
272
|
-
Args:
|
|
273
|
-
*args: Set the CMD of the Sandbox, overriding any CMD of the container image.
|
|
274
|
-
app: Associate the sandbox with an app. Required unless creating from a container.
|
|
275
|
-
name: Optionally give the sandbox a name. Unique within an app.
|
|
276
|
-
tags: Tags to assign to the Sandbox.
|
|
277
|
-
image: The image to run as the container for the sandbox.
|
|
278
|
-
env: Environment variables to set in the Sandbox.
|
|
279
|
-
secrets: Secrets to inject into the Sandbox as environment variables.
|
|
280
|
-
network_file_systems: Network file systems to mount into the sandbox.
|
|
281
|
-
timeout: Maximum lifetime of the sandbox in seconds.
|
|
282
|
-
idle_timeout: The amount of time in seconds that a sandbox can be idle before being terminated.
|
|
283
|
-
workdir: Working directory of the sandbox.
|
|
284
|
-
gpu: GPU reservation for the sandbox.
|
|
285
|
-
cloud: Cloud provider for the sandbox.
|
|
286
|
-
region: Region or regions to run the sandbox on.
|
|
287
|
-
cpu:
|
|
288
|
-
Specify, in fractional CPU cores, how many CPU cores to request. Or, pass (request, limit) to
|
|
289
|
-
additionally specify a hard limit in fractional CPU cores. CPU throttling will prevent a container
|
|
290
|
-
from exceeding its specified limit.
|
|
291
|
-
memory:
|
|
292
|
-
Specify, in MiB, a memory request which is the minimum memory required. Or, pass (request, limit) to
|
|
293
|
-
additionally specify a hard limit in MiB.
|
|
294
|
-
runtime:
|
|
295
|
-
Runtime under which the Sandbox executes, or None to let Modal pick.
|
|
296
|
-
block_network: Whether to block network access.
|
|
297
|
-
outbound_cidr_allowlist: List of CIDRs the sandbox is allowed to access. If None, all CIDRs are allowed.
|
|
298
|
-
outbound_domain_allowlist: List of domain names the sandbox is allowed to access. Supports
|
|
299
|
-
wildcard prefixes (``*.``); a bare ``"*"`` allows all domains. The outbound policy
|
|
300
|
-
can be replaced later via `Sandbox._experimental_set_outbound_network_policy`.
|
|
301
|
-
inbound_cidr_allowlist:
|
|
302
|
-
List of CIDRs allowed to connect inbound to the sandbox (tunnels and connection tokens). If None,
|
|
303
|
-
all CIDRs are allowed.
|
|
304
|
-
_experimental_outbound_policy: Configuration for replacing headers in outbound HTTPS requests from
|
|
305
|
-
the Sandbox. Secrets referenced by the policy are resolved outside the Sandbox and are never
|
|
306
|
-
visible to the workload. See `modal.experimental.OutboundPolicy`. This API is experimental and
|
|
307
|
-
may change in the future.
|
|
308
|
-
volumes: Mount points for Modal Volumes and CloudBucketMounts.
|
|
309
|
-
pty:
|
|
310
|
-
Enable a PTY for the Sandbox entrypoint command. When enabled, all output (stdout and stderr from the
|
|
311
|
-
process) is multiplexed into stdout, and the stderr stream is effectively empty.
|
|
312
|
-
encrypted_ports: List of ports to tunnel into the sandbox. Encrypted ports are tunneled with TLS.
|
|
313
|
-
h2_ports: List of encrypted ports to tunnel into the sandbox, using HTTP/2.
|
|
314
|
-
unencrypted_ports: List of ports to tunnel into the sandbox without encryption.
|
|
315
|
-
custom_domain:
|
|
316
|
-
Allow connections to the Sandbox via a subdomain of this parent rather than a default Modal domain.
|
|
317
|
-
proxy: Reference to a Modal Proxy to use in front of this Sandbox.
|
|
318
|
-
include_oidc_identity_token:
|
|
319
|
-
If True, the sandbox will receive a MODAL_IDENTITY_TOKEN env var for OIDC-based auth.
|
|
320
|
-
readiness_probe: Probe used to determine when the sandbox has become ready.
|
|
321
|
-
verbose: Enable verbose logging for sandbox operations.
|
|
322
|
-
experimental_options: Experimental options to pass to the sandbox.
|
|
323
|
-
_experimental_enable_snapshot: Enable memory snapshots.
|
|
324
|
-
client: Modal Client to use for the sandbox.
|
|
325
|
-
environment_name: *DEPRECATED* Optionally override the default environment
|
|
326
|
-
pty_info: *DEPRECATED* Use `pty` instead. `pty` will override `pty_info`.
|
|
327
|
-
cidr_allowlist: *DEPRECATED* Use outbound_cidr_allowlist instead.
|
|
328
|
-
|
|
329
|
-
Returns:
|
|
330
|
-
A `Sandbox` object representing the created sandbox which can be used to interact with the sandbox.
|
|
331
|
-
|
|
332
|
-
Raises:
|
|
333
|
-
AlreadyExistsError: If a sandbox with the same name already exists.
|
|
334
|
-
|
|
335
|
-
Examples:
|
|
336
|
-
```python
|
|
337
|
-
app = modal.App.lookup('sandbox-hello-world', create_if_missing=True)
|
|
338
|
-
sandbox = modal.Sandbox.create("echo", "hello world", app=app)
|
|
339
|
-
print(sandbox.stdout.read())
|
|
340
|
-
sandbox.wait()
|
|
341
|
-
```
|
|
342
|
-
"""
|
|
343
|
-
...
|
|
344
|
-
|
|
345
|
-
@staticmethod
|
|
346
|
-
async def _create(
|
|
347
|
-
*args: str,
|
|
348
|
-
app: typing.Optional[modal.app._App] = None,
|
|
349
|
-
name: typing.Optional[str] = None,
|
|
350
|
-
tags: typing.Optional[dict[str, str]] = None,
|
|
351
|
-
image: typing.Optional[modal._image._Image] = None,
|
|
352
|
-
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
353
|
-
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
354
|
-
mounts: collections.abc.Sequence[modal.mount._Mount] = (),
|
|
355
|
-
network_file_systems: dict[typing.Union[str, os.PathLike], modal.network_file_system._NetworkFileSystem] = {},
|
|
356
|
-
timeout: int = 300,
|
|
357
|
-
idle_timeout: typing.Optional[int] = None,
|
|
358
|
-
workdir: typing.Optional[str] = None,
|
|
359
|
-
gpu: typing.Optional[str] = None,
|
|
360
|
-
cloud: typing.Optional[str] = None,
|
|
361
|
-
region: typing.Union[str, collections.abc.Sequence[str], None] = None,
|
|
362
|
-
cpu: typing.Union[float, tuple[float, float], None] = None,
|
|
363
|
-
memory: typing.Union[int, tuple[int, int], None] = None,
|
|
364
|
-
runtime: typing.Optional[typing.Literal["gvisor", "vm"]] = None,
|
|
365
|
-
block_network: bool = False,
|
|
366
|
-
outbound_cidr_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
367
|
-
outbound_domain_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
368
|
-
_experimental_outbound_policy: typing.Optional[modal._outbound_policy._OutboundPolicy] = None,
|
|
369
|
-
inbound_cidr_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
370
|
-
volumes: dict[
|
|
371
|
-
typing.Union[str, os.PathLike],
|
|
372
|
-
typing.Union[modal.volume._Volume, modal.cloud_bucket_mount._CloudBucketMount],
|
|
373
|
-
] = {},
|
|
374
|
-
pty: bool = False,
|
|
375
|
-
encrypted_ports: collections.abc.Sequence[int] = [],
|
|
376
|
-
h2_ports: collections.abc.Sequence[int] = [],
|
|
377
|
-
unencrypted_ports: collections.abc.Sequence[int] = [],
|
|
378
|
-
proxy: typing.Optional[modal.proxy._Proxy] = None,
|
|
379
|
-
include_oidc_identity_token: bool = False,
|
|
380
|
-
readiness_probe: typing.Optional[Probe] = None,
|
|
381
|
-
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
382
|
-
_experimental_enable_snapshot: bool = False,
|
|
383
|
-
client: typing.Optional[modal.client._Client] = None,
|
|
384
|
-
verbose: bool = False,
|
|
385
|
-
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
386
|
-
custom_domain: typing.Optional[str] = None,
|
|
387
|
-
):
|
|
388
|
-
"""Private method used internally.
|
|
389
|
-
|
|
390
|
-
This method exposes some internal arguments (currently `mounts`) which are not in the public API.
|
|
391
|
-
`mounts` is currently only used by modal shell (cli) to provide a function's mounts to the
|
|
392
|
-
sandbox that runs the shell session.
|
|
393
|
-
"""
|
|
394
|
-
...
|
|
395
|
-
|
|
396
|
-
@staticmethod
|
|
397
|
-
async def _experimental_create(
|
|
398
|
-
*args: str,
|
|
399
|
-
app: typing.Optional[modal.app._App] = None,
|
|
400
|
-
name: typing.Optional[str] = None,
|
|
401
|
-
tags: typing.Optional[dict[str, str]] = None,
|
|
402
|
-
image: typing.Optional[modal._image._Image] = None,
|
|
403
|
-
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
404
|
-
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
405
|
-
mounts: collections.abc.Sequence[modal.mount._Mount] = (),
|
|
406
|
-
timeout: int = 300,
|
|
407
|
-
idle_timeout: typing.Optional[int] = None,
|
|
408
|
-
workdir: typing.Optional[str] = None,
|
|
409
|
-
cpu: typing.Union[float, tuple[float, float], None] = None,
|
|
410
|
-
memory: typing.Union[int, tuple[int, int], None] = None,
|
|
411
|
-
runtime: typing.Optional[typing.Literal["gvisor", "vm"]] = None,
|
|
412
|
-
cloud: typing.Optional[str] = None,
|
|
413
|
-
region: typing.Union[str, collections.abc.Sequence[str], None] = None,
|
|
414
|
-
block_network: bool = False,
|
|
415
|
-
outbound_cidr_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
416
|
-
outbound_domain_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
417
|
-
_experimental_outbound_policy: typing.Optional[modal._outbound_policy._OutboundPolicy] = None,
|
|
418
|
-
inbound_cidr_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
419
|
-
i6pn: bool = False,
|
|
420
|
-
volumes: dict[
|
|
421
|
-
typing.Union[str, os.PathLike],
|
|
422
|
-
typing.Union[modal.volume._Volume, modal.cloud_bucket_mount._CloudBucketMount],
|
|
423
|
-
] = {},
|
|
424
|
-
pty: bool = False,
|
|
425
|
-
encrypted_ports: collections.abc.Sequence[int] = [],
|
|
426
|
-
h2_ports: collections.abc.Sequence[int] = [],
|
|
427
|
-
unencrypted_ports: collections.abc.Sequence[int] = [],
|
|
428
|
-
proxy: typing.Optional[modal.proxy._Proxy] = None,
|
|
429
|
-
readiness_probe: typing.Optional[Probe] = None,
|
|
430
|
-
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
431
|
-
include_oidc_identity_token: bool = False,
|
|
432
|
-
verbose: bool = False,
|
|
433
|
-
custom_domain: typing.Optional[str] = None,
|
|
434
|
-
client: typing.Optional[modal.client._Client] = None,
|
|
435
|
-
_experimental_enable_snapshot: bool = False,
|
|
436
|
-
) -> _Sandbox:
|
|
437
|
-
"""Create a sandbox using the V2 backend.
|
|
438
|
-
|
|
439
|
-
Supported features include exec, encrypted tunnels, wait/poll/terminate,
|
|
440
|
-
CPU and memory configuration, region placement, private IPv6 networking
|
|
441
|
-
(i6pn), volumes, cloud bucket mounts (with static credentials via
|
|
442
|
-
`secret=...` or `oidc_auth_role_arn`), OIDC identity tokens, proxies,
|
|
443
|
-
filesystem snapshots, and custom domains (`custom_domain=...` allows
|
|
444
|
-
connections to the sandbox via a subdomain of that parent domain rather
|
|
445
|
-
than a default Modal domain; requires prior setup by Modal).
|
|
446
|
-
|
|
447
|
-
`cpu` and `memory` accept either a scalar request or a `(request, limit)`
|
|
448
|
-
tuple that additionally sets a hard limit (fractional CPU cores; memory
|
|
449
|
-
in MiB), matching `Sandbox.create()`.
|
|
450
|
-
|
|
451
|
-
Pass `_experimental_enable_snapshot=True` to create a sandbox that can be
|
|
452
|
-
snapshotted with `._experimental_snapshot()`. Features like network
|
|
453
|
-
file systems and GPUs are not supported.
|
|
454
|
-
|
|
455
|
-
Set `i6pn=True` to enable private IPv6 networking so sandboxes in the same
|
|
456
|
-
workspace can address each other directly at their `i6pn.modal.local`
|
|
457
|
-
address. i6pn only connects sandboxes co-located on the same routable
|
|
458
|
-
network, so pin every sandbox in the group to the same specific region
|
|
459
|
-
(e.g. `region="us-east-1"`).
|
|
460
|
-
|
|
461
|
-
V2 sandboxes created with this method are not currently returned by
|
|
462
|
-
`Sandbox.list()`. A named sandbox can be looked up with
|
|
463
|
-
`Sandbox._experimental_from_name(app_name, name)`; otherwise store
|
|
464
|
-
`sandbox.object_id` and use `Sandbox.from_id(sandbox.object_id)` to
|
|
465
|
-
reattach.
|
|
466
|
-
"""
|
|
467
|
-
...
|
|
468
|
-
|
|
469
|
-
async def _v2_metadata(self, client: typing.Optional[modal.client._Client] = None) -> list[tuple[str, str]]: ...
|
|
470
|
-
def _get_metadata(self) -> modal_proto.api_pb2.SandboxHandleMetadata: ...
|
|
471
|
-
def _hydrate_metadata(self, handle_metadata: typing.Optional[google.protobuf.message.Message]) -> None: ...
|
|
472
|
-
def _hydrate_metadata_v2(self) -> None:
|
|
473
|
-
"""Wire up V2 stdio readers that read directly from the worker. Cheap
|
|
474
|
-
to call eagerly: the router connection is opened lazily on first read.
|
|
475
|
-
"""
|
|
476
|
-
...
|
|
477
|
-
|
|
478
|
-
def _initialize_from_other(self, other): ...
|
|
479
|
-
def _initialize_from_empty(self): ...
|
|
480
|
-
async def detach(self):
|
|
481
|
-
"""Disconnects your client from the sandbox and cleans up resources associated with the connection.
|
|
482
|
-
|
|
483
|
-
Be sure to only call `detach` when you are done interacting with the sandbox. After calling `detach`,
|
|
484
|
-
any operation using the Sandbox object is not guaranteed to work anymore. If you want to continue interacting
|
|
485
|
-
with a running sandbox, use `Sandbox.from_id` to get a new Sandbox object.
|
|
486
|
-
|
|
487
|
-
This method does not interrupt or wait for running concurrent operations on the sandbox. Resources are
|
|
488
|
-
promptly closed once those operations complete.
|
|
489
|
-
"""
|
|
490
|
-
...
|
|
491
|
-
|
|
492
|
-
@property
|
|
493
|
-
def _client(self) -> modal.client._Client: ...
|
|
494
|
-
@_client.setter
|
|
495
|
-
def _client(self, value): ...
|
|
496
|
-
def _ensure_attached(self): ...
|
|
497
|
-
def _ensure_v1(self, method_name: str): ...
|
|
498
|
-
@staticmethod
|
|
499
|
-
async def from_name(
|
|
500
|
-
app_name: str,
|
|
501
|
-
name: str,
|
|
502
|
-
*,
|
|
503
|
-
environment_name: typing.Optional[str] = None,
|
|
504
|
-
client: typing.Optional[modal.client._Client] = None,
|
|
505
|
-
) -> _Sandbox:
|
|
506
|
-
"""Get a running Sandbox by name from a deployed App.
|
|
507
|
-
|
|
508
|
-
A Sandbox's name is the `name` argument passed to `Sandbox.create`.
|
|
509
|
-
|
|
510
|
-
Args:
|
|
511
|
-
app_name: Name of the deployed app to look up the sandbox under.
|
|
512
|
-
name: Sandbox name to resolve.
|
|
513
|
-
environment_name: Optional environment name for the lookup; defaults to the configured environment.
|
|
514
|
-
client: Modal client to use for the RPC; defaults to `Client.from_env()` when omitted.
|
|
515
|
-
|
|
516
|
-
Returns:
|
|
517
|
-
A `Sandbox` handle for the running sandbox.
|
|
518
|
-
|
|
519
|
-
Raises:
|
|
520
|
-
NotFoundError: If no running sandbox exists with the given name.
|
|
521
|
-
"""
|
|
522
|
-
...
|
|
523
|
-
|
|
524
|
-
@staticmethod
|
|
525
|
-
async def _experimental_from_name(
|
|
526
|
-
app_name: str,
|
|
527
|
-
name: str,
|
|
528
|
-
*,
|
|
529
|
-
environment_name: typing.Optional[str] = None,
|
|
530
|
-
client: typing.Optional[modal.client._Client] = None,
|
|
531
|
-
) -> _Sandbox:
|
|
532
|
-
"""Get a running V2 Sandbox by name from a deployed App.
|
|
533
|
-
|
|
534
|
-
This looks up V2 sandboxes, ie sandboxes created via `modal.Sandbox._experimental_create`.
|
|
535
|
-
|
|
536
|
-
Args:
|
|
537
|
-
app_name: Name of the deployed app to look up the sandbox under.
|
|
538
|
-
name: Sandbox name to resolve.
|
|
539
|
-
environment_name: Optional environment name for the lookup; defaults to the configured environment.
|
|
540
|
-
client: Modal client to use for the RPC; defaults to `Client.from_env()` when omitted.
|
|
541
|
-
|
|
542
|
-
Returns:
|
|
543
|
-
A `Sandbox` handle for the running sandbox.
|
|
544
|
-
|
|
545
|
-
Raises:
|
|
546
|
-
NotFoundError: If no running sandbox exists with the given name.
|
|
547
|
-
"""
|
|
548
|
-
...
|
|
549
|
-
|
|
550
|
-
@staticmethod
|
|
551
|
-
async def from_id(sandbox_id: str, client: typing.Optional[modal.client._Client] = None) -> _Sandbox:
|
|
552
|
-
"""Construct a Sandbox from an id and look up the Sandbox result.
|
|
553
|
-
|
|
554
|
-
The ID of a Sandbox object can be accessed using `.object_id`.
|
|
555
|
-
|
|
556
|
-
Args:
|
|
557
|
-
sandbox_id: Sandbox object ID to attach to.
|
|
558
|
-
client: Modal client to use for the lookup; defaults to the environment client when omitted.
|
|
559
|
-
|
|
560
|
-
Returns:
|
|
561
|
-
A `Sandbox` handle with any available result metadata populated from the server.
|
|
562
|
-
"""
|
|
563
|
-
...
|
|
564
|
-
|
|
565
|
-
async def get_tags(self) -> dict[str, str]:
|
|
566
|
-
"""Fetches any tags (key-value pairs) currently attached to this Sandbox from the server.
|
|
567
|
-
|
|
568
|
-
Returns:
|
|
569
|
-
Tags as a map from tag name to tag value.
|
|
570
|
-
"""
|
|
571
|
-
...
|
|
572
|
-
|
|
573
|
-
async def set_tags(self, tags: dict[str, str], *, client: typing.Optional[modal.client._Client] = None) -> None:
|
|
574
|
-
"""Set tags (key-value pairs) on the Sandbox. Tags can be used to filter results in `Sandbox.list`.
|
|
575
|
-
|
|
576
|
-
Setting tags replaces the Sandbox's entire tag set; passing an empty dict clears all tags.
|
|
577
|
-
|
|
578
|
-
Args:
|
|
579
|
-
tags: Tag names and values to set on this sandbox.
|
|
580
|
-
client: Deprecated. Prefer setting the client when creating or re-attaching to the sandbox.
|
|
581
|
-
"""
|
|
582
|
-
...
|
|
583
|
-
|
|
584
|
-
async def _experimental_set_name(self, name: str) -> None:
|
|
585
|
-
"""Assign a name to a running V2 Sandbox that was created without one.
|
|
586
|
-
|
|
587
|
-
This is only supported for V2 sandboxes, ie sandboxes created via
|
|
588
|
-
`modal.Sandbox._experimental_create`. A name may only be set once and
|
|
589
|
-
only on a Sandbox that has never had one; afterwards the Sandbox can
|
|
590
|
-
be looked up with `Sandbox._experimental_from_name(app_name, name)`.
|
|
591
|
-
Repeating the Sandbox's current name is idempotent and succeeds, so
|
|
592
|
-
the call is safe to retry.
|
|
593
|
-
|
|
594
|
-
Args:
|
|
595
|
-
name: Name to assign to the Sandbox. Must be unique within the App.
|
|
596
|
-
|
|
597
|
-
Raises:
|
|
598
|
-
AlreadyExistsError: If another running Sandbox in the App already holds the name.
|
|
599
|
-
ConflictError: If the Sandbox already has a different name or is no longer running.
|
|
600
|
-
"""
|
|
601
|
-
...
|
|
602
|
-
|
|
603
|
-
async def _experimental_set_outbound_network_policy(
|
|
604
|
-
self,
|
|
605
|
-
*,
|
|
606
|
-
outbound_cidr_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
607
|
-
outbound_domain_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
608
|
-
) -> None:
|
|
609
|
-
"""Replace the outbound network policy of a running Sandbox.
|
|
610
|
-
|
|
611
|
-
Established connections that the new policy no longer permits are
|
|
612
|
-
terminated.
|
|
613
|
-
|
|
614
|
-
Args:
|
|
615
|
-
outbound_cidr_allowlist: List of CIDRs the Sandbox is allowed to access. If None, all CIDRs are allowed.
|
|
616
|
-
outbound_domain_allowlist: List of domain names the Sandbox is allowed to access. Supports
|
|
617
|
-
wildcard prefixes (``*.``); a bare ``"*"`` allows all domains.
|
|
618
|
-
"""
|
|
619
|
-
...
|
|
620
|
-
|
|
621
|
-
async def _experimental_update_outbound_policy(
|
|
622
|
-
self, outbound_policy: modal._outbound_policy._OutboundPolicy
|
|
623
|
-
) -> None:
|
|
624
|
-
"""Replace the outbound policy of a running Sandbox.
|
|
625
|
-
|
|
626
|
-
This API is experimental and may change in the future.
|
|
627
|
-
|
|
628
|
-
The new policy replaces all existing policy configuration on the
|
|
629
|
-
Sandbox; build a policy including any existing rules you want to keep.
|
|
630
|
-
|
|
631
|
-
Only Sandboxes created with an `_experimental_outbound_policy` can be
|
|
632
|
-
updated this way; for Sandboxes created without one this fails, since
|
|
633
|
-
header replacement is only set up at creation time.
|
|
634
|
-
|
|
635
|
-
Args:
|
|
636
|
-
outbound_policy: The new policy to apply.
|
|
637
|
-
"""
|
|
638
|
-
...
|
|
639
|
-
|
|
640
|
-
async def _experimental_get_exit_snapshot(self, timeout: typing.Optional[float] = None) -> modal._image._Image:
|
|
641
|
-
"""Get the exit filesystem snapshot image.
|
|
642
|
-
|
|
643
|
-
An exit snapshot captures the Sandbox filesystem when the Sandbox exits,
|
|
644
|
-
whether its entrypoint finishes gracefully, abruptly, or it is stopped with
|
|
645
|
-
`terminate()`. Exit snapshots are opt-in: the Sandbox must have been
|
|
646
|
-
created with `experimental_options={"enable_exit_snapshot": True}`.
|
|
647
|
-
|
|
648
|
-
Args:
|
|
649
|
-
timeout: Total time to wait in seconds, spread across repeated long
|
|
650
|
-
polls of at most `_EXIT_SNAPSHOT_LONG_POLL_TIMEOUT` seconds each.
|
|
651
|
-
`None` (the default) waits until the snapshot reaches a terminal
|
|
652
|
-
state; `0` performs an immediate check without waiting.
|
|
653
|
-
|
|
654
|
-
Returns:
|
|
655
|
-
The exit snapshot Image.
|
|
656
|
-
|
|
657
|
-
Raises:
|
|
658
|
-
InvalidError: If `timeout` is negative, or if exit snapshot is not
|
|
659
|
-
enabled for the sandbox.
|
|
660
|
-
TimeoutError: If `timeout` elapses before the snapshot reaches a
|
|
661
|
-
terminal state. This includes `timeout=0` when the snapshot is
|
|
662
|
-
still pending.
|
|
663
|
-
SnapshotCreationError: Snapshot operation is done and failed.
|
|
664
|
-
Polling again will not produce an Image; filesystem state is gone.
|
|
665
|
-
NotFoundError: If the sandbox does not exist.
|
|
666
|
-
PermissionDeniedError: If the caller cannot access the sandbox.
|
|
667
|
-
"""
|
|
668
|
-
...
|
|
669
|
-
|
|
670
|
-
async def snapshot_filesystem(
|
|
671
|
-
self, timeout: int = 55, *, ttl: typing.Optional[int] = 2592000
|
|
672
|
-
) -> modal._image._Image:
|
|
673
|
-
"""Snapshot the filesystem of the Sandbox.
|
|
674
|
-
|
|
675
|
-
Args:
|
|
676
|
-
timeout:
|
|
677
|
-
Maximum time in seconds to wait for the snapshot operation. If the snapshot does not return within
|
|
678
|
-
that window, the call is cancelled and `modal.exception.TimeoutError` is raised.
|
|
679
|
-
ttl:
|
|
680
|
-
The resulting Image is retained for `ttl` seconds (default: 30 days). Pass `ttl=None` to retain
|
|
681
|
-
the image indefinitely.
|
|
682
|
-
|
|
683
|
-
Returns:
|
|
684
|
-
An [`Image`](https://modal.com/docs/sdk/py/latest/Image) object which can be used to spawn a new
|
|
685
|
-
Sandbox with the same filesystem.
|
|
686
|
-
"""
|
|
687
|
-
...
|
|
688
|
-
|
|
689
|
-
async def _snapshot_filesystem(
|
|
690
|
-
self, timeout: int, *, ttl: typing.Optional[int], container_id: str = ""
|
|
691
|
-
) -> modal._image._Image: ...
|
|
692
|
-
async def _legacy_snapshot_filesystem(self, timeout: int = 55) -> modal._image._Image: ...
|
|
693
|
-
async def mount_image(
|
|
694
|
-
self,
|
|
695
|
-
path: typing.Union[pathlib.PurePosixPath, str],
|
|
696
|
-
image: modal._image._Image,
|
|
697
|
-
*,
|
|
698
|
-
_experimental_encryption_key: typing.Optional[bytes] = None,
|
|
699
|
-
):
|
|
700
|
-
"""Mount an Image at a specified path in a running Sandbox.
|
|
701
|
-
|
|
702
|
-
`path` should be a directory that is **not** the root path (`/`). If the path doesn't exist
|
|
703
|
-
it will be created. If it exists and contains data, the previous directory will be replaced
|
|
704
|
-
by the mount.
|
|
705
|
-
|
|
706
|
-
The `image` argument supports any Image that has an object ID, including:
|
|
707
|
-
- Images built using `image.build()`
|
|
708
|
-
- Images referenced by ID, e.g. `Image.from_id(...)`
|
|
709
|
-
- Filesystem/directory snapshots, e.g. created by `.snapshot_directory()` or `.snapshot_filesystem()`
|
|
710
|
-
- Empty images created with `Image.from_scratch()`
|
|
711
|
-
|
|
712
|
-
Args:
|
|
713
|
-
path: Absolute mount point directory inside the sandbox (not `/`).
|
|
714
|
-
image: Image to mount at `path` (must be built, referenced by ID, or snapshot-based as described above).
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
Examples:
|
|
718
|
-
```py notest
|
|
719
|
-
user_project_snapshot: Image = sandbox_session_1.snapshot_directory("/user_project")
|
|
720
|
-
|
|
721
|
-
# You can later mount this snapshot to another Sandbox:
|
|
722
|
-
sandbox_session_2 = modal.Sandbox.create(...)
|
|
723
|
-
sandbox_session_2.mount_image("/user_project", user_project_snapshot)
|
|
724
|
-
sandbox_session_2.filesystem.list_files("/user_project")
|
|
725
|
-
```
|
|
726
|
-
"""
|
|
727
|
-
...
|
|
728
|
-
|
|
729
|
-
async def unmount_image(self, path: typing.Union[pathlib.PurePosixPath, str]):
|
|
730
|
-
"""Unmount a previously mounted Image from a running Sandbox.
|
|
731
|
-
|
|
732
|
-
`path` must be the exact mount point that was passed to `.mount_image()`.
|
|
733
|
-
After unmounting, the underlying Sandbox filesystem at that path becomes
|
|
734
|
-
visible again.
|
|
735
|
-
|
|
736
|
-
Args:
|
|
737
|
-
path: Absolute mount point directory to unmount.
|
|
738
|
-
"""
|
|
739
|
-
...
|
|
740
|
-
|
|
741
|
-
async def snapshot_directory(
|
|
742
|
-
self,
|
|
743
|
-
path: typing.Union[pathlib.PurePosixPath, str],
|
|
744
|
-
*,
|
|
745
|
-
timeout: int = 55,
|
|
746
|
-
ttl: typing.Optional[int] = 2592000,
|
|
747
|
-
_experimental_encryption_key: typing.Optional[bytes] = None,
|
|
748
|
-
) -> modal._image._Image:
|
|
749
|
-
"""Snapshot a directory in a running Sandbox, creating a new Image with its content.
|
|
750
|
-
|
|
751
|
-
`timeout` If the snapshot does not return within that window, the call is cancelled
|
|
752
|
-
and `modal.exception.TimeoutError` is raised.
|
|
753
|
-
|
|
754
|
-
`ttl` The resulting Image is retained for `ttl` seconds (default: 30 days)
|
|
755
|
-
Pass `ttl=None` to retain the image indefinitely.
|
|
756
|
-
|
|
757
|
-
Args:
|
|
758
|
-
path: Absolute path of the directory inside the sandbox to snapshot.
|
|
759
|
-
|
|
760
|
-
Returns:
|
|
761
|
-
An `Image` containing the directory contents.
|
|
762
|
-
|
|
763
|
-
Examples:
|
|
764
|
-
```py notest
|
|
765
|
-
user_project_snapshot: Image = sandbox_session_1.snapshot_directory("/user_project")
|
|
766
|
-
|
|
767
|
-
# You can later mount this snapshot to another Sandbox:
|
|
768
|
-
sandbox_session_2 = modal.Sandbox.create(...)
|
|
769
|
-
sandbox_session_2.mount_image("/user_project", user_project_snapshot)
|
|
770
|
-
sandbox_session_2.filesystem.list_files("/user_project")
|
|
771
|
-
```
|
|
772
|
-
"""
|
|
773
|
-
...
|
|
774
|
-
|
|
775
|
-
async def wait(self, raise_on_termination: bool = True):
|
|
776
|
-
"""Wait for the Sandbox to finish running.
|
|
777
|
-
|
|
778
|
-
Args:
|
|
779
|
-
raise_on_termination: If True, raise when the sandbox is terminated externally.
|
|
780
|
-
"""
|
|
781
|
-
...
|
|
782
|
-
|
|
783
|
-
async def wait_until_ready(self, *, timeout: int = 300) -> None:
|
|
784
|
-
"""Wait for the Sandbox readiness probe to report that the Sandbox is ready.
|
|
785
|
-
|
|
786
|
-
The Sandbox must be configured with a `readiness_probe` in order to use this method.
|
|
787
|
-
|
|
788
|
-
Args:
|
|
789
|
-
timeout: Maximum time in seconds to wait for readiness.
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
Examples:
|
|
793
|
-
```py notest
|
|
794
|
-
app = modal.App.lookup('sandbox-wait-until-ready', create_if_missing=True)
|
|
795
|
-
sandbox = modal.Sandbox.create(
|
|
796
|
-
"python3", "-m", "http.server", "8080",
|
|
797
|
-
readiness_probe=modal.Probe.with_tcp(8080),
|
|
798
|
-
app=app,
|
|
799
|
-
)
|
|
800
|
-
sandbox.wait_until_ready()
|
|
801
|
-
```
|
|
802
|
-
"""
|
|
803
|
-
...
|
|
804
|
-
|
|
805
|
-
async def tunnels(self, timeout: int = 50) -> dict[int, modal._tunnel.Tunnel]:
|
|
806
|
-
"""Get Tunnel metadata for the sandbox.
|
|
807
|
-
|
|
808
|
-
NOTE: Previous to client [v0.64.153](https://modal.com/docs/sdk/py/changelog#064153-2024-09-30), this
|
|
809
|
-
returned a list of `TunnelData` objects.
|
|
810
|
-
|
|
811
|
-
Args:
|
|
812
|
-
timeout: Maximum time in seconds to wait for tunnel metadata when not already cached.
|
|
813
|
-
|
|
814
|
-
Returns:
|
|
815
|
-
A dictionary mapping container port to `Tunnel` metadata.
|
|
816
|
-
|
|
817
|
-
Raises:
|
|
818
|
-
SandboxTimeoutError: If the tunnels are not available after the timeout.
|
|
819
|
-
"""
|
|
820
|
-
...
|
|
821
|
-
|
|
822
|
-
async def create_connect_token(
|
|
823
|
-
self, user_metadata: typing.Union[str, dict[str, typing.Any], None] = None, port: int = 8080
|
|
824
|
-
) -> modal.types.SandboxConnectCredentials:
|
|
825
|
-
"""Create a token for making HTTP connections to the Sandbox.
|
|
826
|
-
|
|
827
|
-
Accepts an optional user_metadata string or dict to associate with the token. This metadata
|
|
828
|
-
will be added to the headers by the proxy when forwarding requests to the Sandbox.
|
|
829
|
-
Also accepts a port that requests will be routed to.
|
|
830
|
-
|
|
831
|
-
Args:
|
|
832
|
-
user_metadata: Optional JSON-serializable metadata or string stored with the connect token.
|
|
833
|
-
port: Optional container port that requests are routed to when using this token.
|
|
834
|
-
|
|
835
|
-
Returns:
|
|
836
|
-
URL and token credentials for connecting to the sandbox over HTTP.
|
|
837
|
-
"""
|
|
838
|
-
...
|
|
839
|
-
|
|
840
|
-
async def reload_volumes(self, *, timeout: int = 55) -> None:
|
|
841
|
-
"""Reload all Volumes mounted in the Sandbox.
|
|
842
|
-
|
|
843
|
-
Added in v1.1.0.
|
|
844
|
-
|
|
845
|
-
Blocks until the reload completes, or raises `modal.exception.TimeoutError` on timeout (the reload
|
|
846
|
-
may still complete in the background).
|
|
847
|
-
|
|
848
|
-
Args:
|
|
849
|
-
timeout: Defaults to 55 seconds.
|
|
850
|
-
"""
|
|
851
|
-
...
|
|
852
|
-
|
|
853
|
-
async def _reload_volumes(self, *, timeout: int, container_id: str = "") -> None: ...
|
|
854
|
-
@typing.overload
|
|
855
|
-
async def terminate(self, *, wait: typing.Literal[True]) -> int: ...
|
|
856
|
-
@typing.overload
|
|
857
|
-
async def terminate(self, *, wait: typing.Literal[False] = False) -> None: ...
|
|
858
|
-
async def poll(self) -> typing.Optional[int]:
|
|
859
|
-
"""Check if the Sandbox has finished running.
|
|
860
|
-
|
|
861
|
-
Returns:
|
|
862
|
-
`None` if the Sandbox is still running, otherwise the exit code.
|
|
863
|
-
"""
|
|
864
|
-
...
|
|
865
|
-
|
|
866
|
-
async def _get_task_id(
|
|
867
|
-
self, raise_if_task_complete=False, timeout: typing.Optional[float] = None, retry_transient: bool = False
|
|
868
|
-
) -> str: ...
|
|
869
|
-
async def _resolve_task_id_for_logs(self) -> str:
|
|
870
|
-
"""Try to resolve the task ID in one shot for historical log fetches.
|
|
871
|
-
|
|
872
|
-
For a historical log api, we should not wait for the task to be created.
|
|
873
|
-
If the Sandbox created logs worth fetching, the task ID should be available.
|
|
874
|
-
"""
|
|
875
|
-
...
|
|
876
|
-
|
|
877
|
-
async def _get_command_router_client(
|
|
878
|
-
self, task_id: str
|
|
879
|
-
) -> modal._utils.task_command_router_client.TaskCommandRouterClient: ...
|
|
880
|
-
@property
|
|
881
|
-
def _experimental_sidecars(self) -> _SidecarManager:
|
|
882
|
-
"""Manage sidecar containers running in this Sandbox."""
|
|
883
|
-
...
|
|
884
|
-
|
|
885
|
-
@typing.overload
|
|
886
|
-
async def exec(
|
|
887
|
-
self,
|
|
888
|
-
*args: str,
|
|
889
|
-
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
890
|
-
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
891
|
-
timeout: typing.Optional[int] = None,
|
|
892
|
-
workdir: typing.Optional[str] = None,
|
|
893
|
-
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
894
|
-
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
895
|
-
text: typing.Literal[True] = True,
|
|
896
|
-
bufsize: typing.Literal[-1, 1] = -1,
|
|
897
|
-
pty: bool = False,
|
|
898
|
-
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
899
|
-
_pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
900
|
-
) -> modal.container_process._ContainerProcess[str]: ...
|
|
901
|
-
@typing.overload
|
|
902
|
-
async def exec(
|
|
903
|
-
self,
|
|
904
|
-
*args: str,
|
|
905
|
-
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
906
|
-
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
907
|
-
timeout: typing.Optional[int] = None,
|
|
908
|
-
workdir: typing.Optional[str] = None,
|
|
909
|
-
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
910
|
-
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
911
|
-
text: typing.Literal[False] = False,
|
|
912
|
-
bufsize: typing.Literal[-1, 1] = -1,
|
|
913
|
-
pty: bool = False,
|
|
914
|
-
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
915
|
-
_pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
916
|
-
) -> modal.container_process._ContainerProcess[bytes]: ...
|
|
917
|
-
async def _exec(
|
|
918
|
-
self,
|
|
919
|
-
*args: str,
|
|
920
|
-
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
921
|
-
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
922
|
-
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
923
|
-
timeout: typing.Optional[int] = None,
|
|
924
|
-
workdir: typing.Optional[str] = None,
|
|
925
|
-
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
926
|
-
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
927
|
-
text: bool = True,
|
|
928
|
-
bufsize: typing.Literal[-1, 1] = -1,
|
|
929
|
-
container_id: typing.Optional[str] = None,
|
|
930
|
-
) -> typing.Union[modal.container_process._ContainerProcess[bytes], modal.container_process._ContainerProcess[str]]:
|
|
931
|
-
"""Private method used internally.
|
|
932
|
-
|
|
933
|
-
This method exposes some internal arguments (currently `pty_info`) which are not in the public API.
|
|
934
|
-
"""
|
|
935
|
-
...
|
|
936
|
-
|
|
937
|
-
async def _exec_through_command_router(
|
|
938
|
-
self,
|
|
939
|
-
*args: str,
|
|
940
|
-
task_id: str,
|
|
941
|
-
command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient,
|
|
942
|
-
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
943
|
-
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
944
|
-
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
945
|
-
timeout: typing.Optional[int] = None,
|
|
946
|
-
workdir: typing.Optional[str] = None,
|
|
947
|
-
secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
|
|
948
|
-
env: typing.Optional[dict[str, str]] = None,
|
|
949
|
-
text: bool = True,
|
|
950
|
-
bufsize: typing.Literal[-1, 1] = -1,
|
|
951
|
-
runtime_debug: bool = False,
|
|
952
|
-
container_id: typing.Optional[str] = None,
|
|
953
|
-
) -> typing.Union[modal.container_process._ContainerProcess[bytes], modal.container_process._ContainerProcess[str]]:
|
|
954
|
-
"""Execute a command through a task command router running on the Modal worker."""
|
|
955
|
-
...
|
|
956
|
-
|
|
957
|
-
async def _experimental_snapshot(self) -> modal.snapshot._SandboxSnapshot: ...
|
|
958
|
-
@staticmethod
|
|
959
|
-
async def _experimental_from_snapshot(
|
|
960
|
-
snapshot: modal.snapshot._SandboxSnapshot,
|
|
961
|
-
client: typing.Optional[modal.client._Client] = None,
|
|
962
|
-
*,
|
|
963
|
-
name: typing.Optional[str] = _DEFAULT_SANDBOX_NAME_OVERRIDE,
|
|
964
|
-
):
|
|
965
|
-
"""Restore a Sandbox from a memory snapshot.
|
|
966
|
-
|
|
967
|
-
The restore targets the same backend the snapshot was taken from. A V1
|
|
968
|
-
snapshot restores as a V1 sandbox, a V2 snapshot as a V2 sandbox.
|
|
969
|
-
"""
|
|
970
|
-
...
|
|
971
|
-
|
|
972
|
-
@staticmethod
|
|
973
|
-
async def _experimental_from_snapshot_v2(
|
|
974
|
-
snapshot: modal.snapshot._SandboxSnapshot,
|
|
975
|
-
client: modal.client._Client,
|
|
976
|
-
*,
|
|
977
|
-
override_type: int,
|
|
978
|
-
name_override: typing.Optional[str],
|
|
979
|
-
) -> _Sandbox: ...
|
|
980
|
-
@property
|
|
981
|
-
def filesystem(self) -> modal.sandbox_fs._SandboxFilesystem:
|
|
982
|
-
"""Namespace for Sandbox filesystem APIs."""
|
|
983
|
-
...
|
|
984
|
-
|
|
985
|
-
@property
|
|
986
|
-
def stdout(self) -> modal.io_streams._StreamReader[str]:
|
|
987
|
-
"""[`StreamReader`](https://modal.com/docs/sdk/py/latest/io_streams#streamreader)
|
|
988
|
-
for the sandbox's stdout stream.
|
|
989
|
-
|
|
990
|
-
Returns:
|
|
991
|
-
Stream reader for sandbox stdout.
|
|
992
|
-
"""
|
|
993
|
-
...
|
|
994
|
-
|
|
995
|
-
@property
|
|
996
|
-
def stderr(self) -> modal.io_streams._StreamReader[str]:
|
|
997
|
-
"""[`StreamReader`](https://modal.com/docs/sdk/py/latest/io_streams#streamreader)
|
|
998
|
-
for the Sandbox's stderr stream.
|
|
999
|
-
|
|
1000
|
-
Returns:
|
|
1001
|
-
Stream reader for sandbox stderr.
|
|
1002
|
-
"""
|
|
1003
|
-
...
|
|
1004
|
-
|
|
1005
|
-
@property
|
|
1006
|
-
def stdin(self) -> modal.io_streams._StreamWriter:
|
|
1007
|
-
"""[`StreamWriter`](https://modal.com/docs/sdk/py/latest/io_streams#streamwriter)
|
|
1008
|
-
for the Sandbox's stdin stream.
|
|
1009
|
-
|
|
1010
|
-
Returns:
|
|
1011
|
-
Stream writer for sandbox stdin.
|
|
1012
|
-
"""
|
|
1013
|
-
...
|
|
1014
|
-
|
|
1015
|
-
@property
|
|
1016
|
-
def returncode(self) -> typing.Optional[int]:
|
|
1017
|
-
"""Return code of the Sandbox process if it has finished running, else `None`.
|
|
1018
|
-
|
|
1019
|
-
Returns:
|
|
1020
|
-
Exit code when the sandbox process has completed, otherwise None.
|
|
1021
|
-
"""
|
|
1022
|
-
...
|
|
1023
|
-
|
|
1024
|
-
@staticmethod
|
|
1025
|
-
def list(
|
|
1026
|
-
*,
|
|
1027
|
-
app_id: typing.Optional[str] = None,
|
|
1028
|
-
tags: typing.Optional[dict[str, str]] = None,
|
|
1029
|
-
client: typing.Optional[modal.client._Client] = None,
|
|
1030
|
-
) -> collections.abc.AsyncGenerator[_Sandbox, None]:
|
|
1031
|
-
"""List all Sandboxes for the current Environment or App ID (if specified). If tags are specified, only
|
|
1032
|
-
Sandboxes that have at least those tags are returned.
|
|
1033
|
-
|
|
1034
|
-
Args:
|
|
1035
|
-
app_id: If set, restrict results to sandboxes under this app ID.
|
|
1036
|
-
tags: If set, only sandboxes containing at least these tags are returned.
|
|
1037
|
-
client: Modal client to use for listing; defaults to `Client.from_env()` when omitted.
|
|
1038
|
-
|
|
1039
|
-
Returns:
|
|
1040
|
-
An async generator yielding `Sandbox` objects.
|
|
1041
|
-
"""
|
|
1042
|
-
...
|
|
1043
|
-
|
|
1044
|
-
@staticmethod
|
|
1045
|
-
def _experimental_list(
|
|
1046
|
-
*,
|
|
1047
|
-
app_id: typing.Optional[str] = None,
|
|
1048
|
-
tags: typing.Optional[dict[str, str]] = None,
|
|
1049
|
-
client: typing.Optional[modal.client._Client] = None,
|
|
1050
|
-
) -> collections.abc.AsyncGenerator[_Sandbox, None]:
|
|
1051
|
-
"""List all sandboxes (both v1 and v2).
|
|
1052
|
-
|
|
1053
|
-
Pass `app_id` to scope to an App; without it, Sandboxes across the current
|
|
1054
|
-
Environment are listed (deprecated — prefer scoping by `app_id`).
|
|
1055
|
-
|
|
1056
|
-
Args:
|
|
1057
|
-
app_id: If set, restrict results to Sandboxes under this App.
|
|
1058
|
-
tags: If set, only sandboxes containing at least these tags are returned.
|
|
1059
|
-
client: Optional client to use for the session.
|
|
1060
|
-
|
|
1061
|
-
Yields:
|
|
1062
|
-
`Sandbox` objects that are currently running.
|
|
1063
|
-
"""
|
|
1064
|
-
...
|
|
1065
|
-
|
|
1066
|
-
async def _get_log_query_data(self) -> modal._supports_logs._LogQueryData: ...
|
|
1067
|
-
@property
|
|
1068
|
-
def logs(self) -> modal._logs_manager._SandboxLogsManager:
|
|
1069
|
-
"""Access logs for a `Sandbox` entrypoint.
|
|
1070
|
-
|
|
1071
|
-
Useful for inspecting logs after a Sandbox terminates.
|
|
1072
|
-
Use [`fetch()`](#logsfetch)
|
|
1073
|
-
to read logs from a UTC time range, [`tail()`](#logstail)
|
|
1074
|
-
to read the most recent logs.
|
|
1075
|
-
|
|
1076
|
-
Note that the logs from executed commands in the sandbox (via `exec()`) are not included in the
|
|
1077
|
-
entrypoint logs.
|
|
1078
|
-
|
|
1079
|
-
See also:
|
|
1080
|
-
- [`modal app logs`](https://modal.com/docs/cli/latest/app#modal-app-logs):
|
|
1081
|
-
CLI access to logs for an App.
|
|
1082
|
-
"""
|
|
1083
|
-
...
|
|
1084
|
-
|
|
1085
|
-
class _SidecarContainer:
|
|
1086
|
-
"""Handle to an additional container running in a Sandbox."""
|
|
1087
|
-
|
|
1088
|
-
_result: typing.Optional[modal_proto.api_pb2.GenericResult]
|
|
1089
|
-
_filesystem: typing.Optional[modal.sandbox_fs._SandboxFilesystem]
|
|
1090
|
-
|
|
1091
|
-
def __init__(
|
|
1092
|
-
self,
|
|
1093
|
-
sandbox: _Sandbox,
|
|
1094
|
-
container_id: str,
|
|
1095
|
-
container_name: str,
|
|
1096
|
-
result: typing.Optional[modal_proto.api_pb2.GenericResult] = None,
|
|
1097
|
-
) -> None:
|
|
1098
|
-
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
1099
|
-
...
|
|
1100
|
-
|
|
1101
|
-
@property
|
|
1102
|
-
def object_id(self) -> str: ...
|
|
1103
|
-
@property
|
|
1104
|
-
def name(self) -> str: ...
|
|
1105
|
-
@staticmethod
|
|
1106
|
-
def _from_container_info(
|
|
1107
|
-
sandbox: _Sandbox, container_info: modal_proto.task_command_router_pb2.TaskContainerInfo
|
|
1108
|
-
) -> _SidecarContainer: ...
|
|
1109
|
-
async def _get_command_router(self) -> tuple[str, modal._utils.task_command_router_client.TaskCommandRouterClient]:
|
|
1110
|
-
"""Get task ID and command router client."""
|
|
1111
|
-
...
|
|
1112
|
-
|
|
1113
|
-
async def snapshot_filesystem(
|
|
1114
|
-
self, timeout: int = 55, *, ttl: typing.Optional[int] = 2592000
|
|
1115
|
-
) -> modal._image._Image:
|
|
1116
|
-
"""Snapshot this Sidecar container's filesystem.
|
|
1117
|
-
|
|
1118
|
-
Args:
|
|
1119
|
-
timeout:
|
|
1120
|
-
Maximum time in seconds to wait for the snapshot operation.
|
|
1121
|
-
ttl:
|
|
1122
|
-
The resulting Image is retained for `ttl` seconds (default: 30 days). Pass `ttl=None` to retain
|
|
1123
|
-
the image indefinitely.
|
|
1124
|
-
|
|
1125
|
-
Returns:
|
|
1126
|
-
An [`Image`](https://modal.com/docs/sdk/py/latest/Image) containing a snapshot of this Sidecar's
|
|
1127
|
-
filesystem.
|
|
1128
|
-
"""
|
|
1129
|
-
...
|
|
1130
|
-
|
|
1131
|
-
@typing.overload
|
|
1132
|
-
async def exec(
|
|
1133
|
-
self,
|
|
1134
|
-
*args: str,
|
|
1135
|
-
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1136
|
-
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1137
|
-
timeout: typing.Optional[int] = None,
|
|
1138
|
-
workdir: typing.Optional[str] = None,
|
|
1139
|
-
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
1140
|
-
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
1141
|
-
text: typing.Literal[True] = True,
|
|
1142
|
-
bufsize: typing.Literal[-1, 1] = -1,
|
|
1143
|
-
pty: bool = False,
|
|
1144
|
-
) -> modal.container_process._ContainerProcess[str]: ...
|
|
1145
|
-
@typing.overload
|
|
1146
|
-
async def exec(
|
|
1147
|
-
self,
|
|
1148
|
-
*args: str,
|
|
1149
|
-
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1150
|
-
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1151
|
-
timeout: typing.Optional[int] = None,
|
|
1152
|
-
workdir: typing.Optional[str] = None,
|
|
1153
|
-
env: typing.Optional[dict[str, typing.Optional[str]]] = None,
|
|
1154
|
-
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
1155
|
-
text: typing.Literal[False],
|
|
1156
|
-
bufsize: typing.Literal[-1, 1] = -1,
|
|
1157
|
-
pty: bool = False,
|
|
1158
|
-
) -> modal.container_process._ContainerProcess[bytes]: ...
|
|
1159
|
-
@property
|
|
1160
|
-
def filesystem(self) -> modal.sandbox_fs._SandboxFilesystem:
|
|
1161
|
-
"""Namespace for Sandbox filesystem APIs."""
|
|
1162
|
-
...
|
|
1163
|
-
|
|
1164
|
-
async def mount_image(
|
|
1165
|
-
self,
|
|
1166
|
-
path: typing.Union[pathlib.PurePosixPath, str],
|
|
1167
|
-
image: modal._image._Image,
|
|
1168
|
-
*,
|
|
1169
|
-
_experimental_encryption_key: typing.Optional[bytes] = None,
|
|
1170
|
-
) -> None:
|
|
1171
|
-
"""Mount an Image at a specified path in this Sidecar container.
|
|
1172
|
-
|
|
1173
|
-
`path` should be a directory that is **not** the root path (`/`). If the path doesn't exist,
|
|
1174
|
-
it will be created. If it exists and contains data, the previous directory will be replaced
|
|
1175
|
-
by the mount.
|
|
1176
|
-
|
|
1177
|
-
The `image` argument supports any Image that has an object ID, including:
|
|
1178
|
-
- Images built using `image.build()`
|
|
1179
|
-
- Images referenced by ID, e.g. `Image.from_id(...)`
|
|
1180
|
-
- Filesystem/directory snapshots, e.g. created by `.snapshot_directory()` or `.snapshot_filesystem()`
|
|
1181
|
-
- Empty images created with `Image.from_scratch()`
|
|
1182
|
-
|
|
1183
|
-
Args:
|
|
1184
|
-
path: Absolute mount point directory inside the Sidecar container (not `/`).
|
|
1185
|
-
image: Image to mount at `path` (must be built, referenced by ID, or snapshot-based as described above).
|
|
1186
|
-
|
|
1187
|
-
Examples:
|
|
1188
|
-
```py notest
|
|
1189
|
-
sidecar_1.mount_image("/workspace", modal.Image.from_scratch())
|
|
1190
|
-
workspace_snapshot = sidecar_1.snapshot_directory("/workspace")
|
|
1191
|
-
|
|
1192
|
-
# You can later mount this snapshot in another Sidecar:
|
|
1193
|
-
sidecar_2.mount_image("/workspace", workspace_snapshot)
|
|
1194
|
-
sidecar_2.filesystem.list_files("/workspace")
|
|
1195
|
-
```
|
|
1196
|
-
"""
|
|
1197
|
-
...
|
|
1198
|
-
|
|
1199
|
-
async def unmount_image(self, path: typing.Union[pathlib.PurePosixPath, str]) -> None:
|
|
1200
|
-
"""Unmount a previously mounted Image from this Sidecar container.
|
|
1201
|
-
|
|
1202
|
-
`path` must be the exact mount point that was passed to `.mount_image()`.
|
|
1203
|
-
After unmounting, the underlying Sidecar filesystem at that path becomes
|
|
1204
|
-
visible again.
|
|
1205
|
-
|
|
1206
|
-
Args:
|
|
1207
|
-
path: Absolute mount point directory to unmount.
|
|
1208
|
-
"""
|
|
1209
|
-
...
|
|
1210
|
-
|
|
1211
|
-
async def snapshot_directory(
|
|
1212
|
-
self,
|
|
1213
|
-
path: typing.Union[pathlib.PurePosixPath, str],
|
|
1214
|
-
*,
|
|
1215
|
-
timeout: int = 55,
|
|
1216
|
-
ttl: typing.Optional[int] = 2592000,
|
|
1217
|
-
_experimental_encryption_key: typing.Optional[bytes] = None,
|
|
1218
|
-
) -> modal._image._Image:
|
|
1219
|
-
"""Snapshot a directory in this Sidecar container, creating a new Image with its content.
|
|
1220
|
-
|
|
1221
|
-
`timeout` If the snapshot does not return within that window, the call is cancelled
|
|
1222
|
-
and `modal.exception.TimeoutError` is raised.
|
|
1223
|
-
|
|
1224
|
-
`ttl` The resulting Image is retained for `ttl` seconds (default: 30 days).
|
|
1225
|
-
Pass `ttl=None` to retain the Image indefinitely.
|
|
1226
|
-
|
|
1227
|
-
The returned Image can be used anywhere an Image is accepted, including
|
|
1228
|
-
as a mount or as the base filesystem for another container.
|
|
1229
|
-
|
|
1230
|
-
Args:
|
|
1231
|
-
path: Absolute path of the directory inside the Sidecar container to snapshot.
|
|
1232
|
-
|
|
1233
|
-
Returns:
|
|
1234
|
-
An `Image` containing the directory contents.
|
|
1235
|
-
|
|
1236
|
-
Examples:
|
|
1237
|
-
```py notest
|
|
1238
|
-
workspace_snapshot = sidecar_1.snapshot_directory("/workspace")
|
|
1239
|
-
|
|
1240
|
-
# You can later mount this snapshot in another Sidecar:
|
|
1241
|
-
sidecar_2.mount_image("/workspace", workspace_snapshot)
|
|
1242
|
-
sidecar_2.filesystem.list_files("/workspace")
|
|
1243
|
-
```
|
|
1244
|
-
"""
|
|
1245
|
-
...
|
|
1246
|
-
|
|
1247
|
-
async def wait(self, raise_on_termination: bool = True) -> None: ...
|
|
1248
|
-
async def poll(self) -> typing.Optional[int]: ...
|
|
1249
|
-
@typing.overload
|
|
1250
|
-
async def terminate(self, *, wait: typing.Literal[True]) -> int: ...
|
|
1251
|
-
@typing.overload
|
|
1252
|
-
async def terminate(self, *, wait: typing.Literal[False] = False) -> None: ...
|
|
1253
|
-
async def reload_volumes(self, *, timeout: int = 55) -> None:
|
|
1254
|
-
"""Reload all Volumes mounted in this sidecar container.
|
|
1255
|
-
|
|
1256
|
-
EXPERIMENTAL: the API is subject to change.
|
|
1257
|
-
|
|
1258
|
-
Blocks until the reload completes, or raises `modal.exception.TimeoutError` on timeout (the reload
|
|
1259
|
-
may still complete in the background).
|
|
1260
|
-
|
|
1261
|
-
Args:
|
|
1262
|
-
timeout: Defaults to 55 seconds.
|
|
1263
|
-
"""
|
|
1264
|
-
...
|
|
1265
|
-
|
|
1266
|
-
def _use_control_plane_sidecar_create(is_v2: bool) -> bool:
|
|
1267
|
-
"""Whether a sidecar create request goes to the Modal server rather than over the Sandbox connection.
|
|
1268
|
-
|
|
1269
|
-
V2 Sandboxes do unless opted out via the `use_control_plane_sidecar_create` config
|
|
1270
|
-
setting (`MODAL_USE_CONTROL_PLANE_SIDECAR_CREATE=0`). V1 Sandboxes always create
|
|
1271
|
-
sidecars over the Sandbox connection.
|
|
1272
|
-
"""
|
|
1273
|
-
...
|
|
1274
|
-
|
|
1275
|
-
class _SidecarManager:
|
|
1276
|
-
"""Creates and manages sidecar containers in a Sandbox."""
|
|
1277
|
-
def __init__(self, sandbox: _Sandbox) -> None:
|
|
1278
|
-
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
1279
|
-
...
|
|
1280
|
-
|
|
1281
|
-
async def _get_command_router(self) -> tuple[str, modal._utils.task_command_router_client.TaskCommandRouterClient]:
|
|
1282
|
-
"""Get task ID and command router client."""
|
|
1283
|
-
...
|
|
1284
|
-
|
|
1285
|
-
async def create(
|
|
1286
|
-
self,
|
|
1287
|
-
*args: str,
|
|
1288
|
-
name: str,
|
|
1289
|
-
image: modal._image._Image,
|
|
1290
|
-
env: typing.Optional[dict[str, str]] = None,
|
|
1291
|
-
secrets: typing.Optional[collections.abc.Collection[modal.secret._Secret]] = None,
|
|
1292
|
-
workdir: typing.Optional[str] = None,
|
|
1293
|
-
volumes: typing.Optional[
|
|
1294
|
-
dict[
|
|
1295
|
-
typing.Union[str, os.PathLike],
|
|
1296
|
-
typing.Union[modal.volume._Volume, modal.cloud_bucket_mount._CloudBucketMount],
|
|
1297
|
-
]
|
|
1298
|
-
] = None,
|
|
1299
|
-
outbound_cidr_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
1300
|
-
outbound_domain_allowlist: typing.Optional[collections.abc.Sequence[str]] = None,
|
|
1301
|
-
include_oidc_identity_token: bool = False,
|
|
1302
|
-
proxy: typing.Optional[modal.proxy._Proxy] = None,
|
|
1303
|
-
pty: bool = False,
|
|
1304
|
-
experimental_memory_reserve_consume_mib: typing.Optional[int] = None,
|
|
1305
|
-
) -> _SidecarContainer:
|
|
1306
|
-
"""Create a sidecar container running alongside the Sandbox's main container.
|
|
1307
|
-
|
|
1308
|
-
Sidecar containers share the Sandbox's lifecycle but run their own Image and command. They
|
|
1309
|
-
can be used to run auxiliary processes, such as a database or a service the main container
|
|
1310
|
-
depends on.
|
|
1311
|
-
|
|
1312
|
-
The sidecar's outbound network policy is independent of the main container's and defaults to
|
|
1313
|
-
open network access. To restrict it, pass `outbound_cidr_allowlist` and/or
|
|
1314
|
-
`outbound_domain_allowlist`. To block all external egress while keeping connectivity to the
|
|
1315
|
-
main container, pass an empty allowlist (`outbound_cidr_allowlist=[]`); a fully network-blocked
|
|
1316
|
-
sidecar is not supported because it would have no IP and could not reach the main container.
|
|
1317
|
-
|
|
1318
|
-
Args:
|
|
1319
|
-
*args: Command and arguments to run inside the sidecar container.
|
|
1320
|
-
name: Unique name for the sidecar container. The name ``"main"`` is reserved.
|
|
1321
|
-
image: Image to run the sidecar container with. Must be a pre-built or referenced Image.
|
|
1322
|
-
env: Environment variables to set in the sidecar container.
|
|
1323
|
-
secrets: Secrets to inject as environment variables in the sidecar container.
|
|
1324
|
-
workdir: Working directory for the command; must be absolute if set.
|
|
1325
|
-
volumes: Mapping of mount paths to `Volume` or `CloudBucketMount` objects to mount in the
|
|
1326
|
-
sidecar container. Cloud bucket mounts are not supported for GPU Sandboxes.
|
|
1327
|
-
outbound_cidr_allowlist: If set, restrict the sidecar's outbound traffic to these CIDR
|
|
1328
|
-
blocks. An empty list blocks all external egress while preserving connectivity to the
|
|
1329
|
-
main container.
|
|
1330
|
-
outbound_domain_allowlist: If set, restrict the sidecar's outbound TLS connections (port
|
|
1331
|
-
443) to these SNI domains. Supports wildcards like ``*.example.com``.
|
|
1332
|
-
include_oidc_identity_token: If True, the sidecar receives a MODAL_IDENTITY_TOKEN env var for
|
|
1333
|
-
OIDC-based auth (e.g. to AWS, GCP). The token identifies the sidecar container itself,
|
|
1334
|
-
not the main container. Not supported for GPU Sandboxes.
|
|
1335
|
-
proxy: Reference to a Modal Proxy to use in front of this sidecar. Not supported for GPU
|
|
1336
|
-
Sandboxes.
|
|
1337
|
-
pty: Whether to enable PTY for the sidecar container.
|
|
1338
|
-
experimental_memory_reserve_consume_mib: Memory, in MiB, this sidecar consumes from the Sandbox's
|
|
1339
|
-
sidecar memory reserve (the experimental `vm_sidecar_memory_reserve_mib` option).
|
|
1340
|
-
Unset consumes whatever is left of the reserve; creation fails if the request exceeds
|
|
1341
|
-
what is left. Ignored by Sandboxes without a reserve.
|
|
1342
|
-
|
|
1343
|
-
Returns:
|
|
1344
|
-
A `SidecarContainer` handle for the running container.
|
|
1345
|
-
"""
|
|
1346
|
-
...
|
|
1347
|
-
|
|
1348
|
-
async def get(self, *, name: str, include_terminated: bool = False) -> _SidecarContainer: ...
|
|
1349
|
-
async def list(self, include_terminated: bool = False) -> list[_SidecarContainer]: ...
|
|
1350
|
-
|
|
1351
|
-
class ___container_exec_spec(typing_extensions.Protocol):
|
|
1352
|
-
def __call__(
|
|
1353
|
-
self,
|
|
1354
|
-
/,
|
|
1355
|
-
pty: bool,
|
|
1356
|
-
container_id: str = "",
|
|
1357
|
-
command: tuple[str, ...] = (),
|
|
1358
|
-
sandbox_id_v2: typing.Optional[str] = None,
|
|
1359
|
-
):
|
|
1360
|
-
"""Execute a command in a container.
|
|
1361
|
-
|
|
1362
|
-
For tasks belonging to V2 sandboxes, `sandbox_id_v2` may be set to the
|
|
1363
|
-
sandbox ID; otherwise the V2 backend is used whenever the task ID itself
|
|
1364
|
-
marks a V2 sandbox task.
|
|
1365
|
-
"""
|
|
1366
|
-
...
|
|
1367
|
-
|
|
1368
|
-
async def aio(
|
|
1369
|
-
self,
|
|
1370
|
-
/,
|
|
1371
|
-
pty: bool,
|
|
1372
|
-
container_id: str = "",
|
|
1373
|
-
command: tuple[str, ...] = (),
|
|
1374
|
-
sandbox_id_v2: typing.Optional[str] = None,
|
|
1375
|
-
):
|
|
1376
|
-
"""Execute a command in a container.
|
|
1377
|
-
|
|
1378
|
-
For tasks belonging to V2 sandboxes, `sandbox_id_v2` may be set to the
|
|
1379
|
-
sandbox ID; otherwise the V2 backend is used whenever the task ID itself
|
|
1380
|
-
marks a V2 sandbox task.
|
|
1381
|
-
"""
|
|
1382
|
-
...
|
|
1383
|
-
|
|
1384
|
-
_container_exec: ___container_exec_spec
|
|
1385
|
-
|
|
1386
33
|
class SidecarContainer:
|
|
1387
34
|
"""Handle to an additional container running in a Sandbox."""
|
|
1388
35
|
|
|
@@ -1968,7 +615,7 @@ class Sandbox(modal.object.Object):
|
|
|
1968
615
|
h2_ports: collections.abc.Sequence[int] = [],
|
|
1969
616
|
unencrypted_ports: collections.abc.Sequence[int] = [],
|
|
1970
617
|
proxy: typing.Optional[modal.proxy.Proxy] = None,
|
|
1971
|
-
readiness_probe: typing.Optional[Probe] = None,
|
|
618
|
+
readiness_probe: typing.Optional[modal.sandbox._sandbox.Probe] = None,
|
|
1972
619
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
1973
620
|
tags: typing.Optional[dict[str, str]] = None,
|
|
1974
621
|
enable_snapshot: bool = False,
|
|
@@ -2019,7 +666,7 @@ class Sandbox(modal.object.Object):
|
|
|
2019
666
|
custom_domain: typing.Optional[str] = None,
|
|
2020
667
|
proxy: typing.Optional[modal.proxy.Proxy] = None,
|
|
2021
668
|
include_oidc_identity_token: bool = False,
|
|
2022
|
-
readiness_probe: typing.Optional[Probe] = None,
|
|
669
|
+
readiness_probe: typing.Optional[modal.sandbox._sandbox.Probe] = None,
|
|
2023
670
|
verbose: bool = False,
|
|
2024
671
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
2025
672
|
_experimental_enable_snapshot: bool = False,
|
|
@@ -2143,7 +790,7 @@ class Sandbox(modal.object.Object):
|
|
|
2143
790
|
custom_domain: typing.Optional[str] = None,
|
|
2144
791
|
proxy: typing.Optional[modal.proxy.Proxy] = None,
|
|
2145
792
|
include_oidc_identity_token: bool = False,
|
|
2146
|
-
readiness_probe: typing.Optional[Probe] = None,
|
|
793
|
+
readiness_probe: typing.Optional[modal.sandbox._sandbox.Probe] = None,
|
|
2147
794
|
verbose: bool = False,
|
|
2148
795
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
2149
796
|
_experimental_enable_snapshot: bool = False,
|
|
@@ -2270,7 +917,7 @@ class Sandbox(modal.object.Object):
|
|
|
2270
917
|
unencrypted_ports: collections.abc.Sequence[int] = [],
|
|
2271
918
|
proxy: typing.Optional[modal.proxy.Proxy] = None,
|
|
2272
919
|
include_oidc_identity_token: bool = False,
|
|
2273
|
-
readiness_probe: typing.Optional[Probe] = None,
|
|
920
|
+
readiness_probe: typing.Optional[modal.sandbox._sandbox.Probe] = None,
|
|
2274
921
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
2275
922
|
_experimental_enable_snapshot: bool = False,
|
|
2276
923
|
client: typing.Optional[modal.client.Client] = None,
|
|
@@ -2324,7 +971,7 @@ class Sandbox(modal.object.Object):
|
|
|
2324
971
|
unencrypted_ports: collections.abc.Sequence[int] = [],
|
|
2325
972
|
proxy: typing.Optional[modal.proxy.Proxy] = None,
|
|
2326
973
|
include_oidc_identity_token: bool = False,
|
|
2327
|
-
readiness_probe: typing.Optional[Probe] = None,
|
|
974
|
+
readiness_probe: typing.Optional[modal.sandbox._sandbox.Probe] = None,
|
|
2328
975
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
2329
976
|
_experimental_enable_snapshot: bool = False,
|
|
2330
977
|
client: typing.Optional[modal.client.Client] = None,
|
|
@@ -2377,7 +1024,7 @@ class Sandbox(modal.object.Object):
|
|
|
2377
1024
|
h2_ports: collections.abc.Sequence[int] = [],
|
|
2378
1025
|
unencrypted_ports: collections.abc.Sequence[int] = [],
|
|
2379
1026
|
proxy: typing.Optional[modal.proxy.Proxy] = None,
|
|
2380
|
-
readiness_probe: typing.Optional[Probe] = None,
|
|
1027
|
+
readiness_probe: typing.Optional[modal.sandbox._sandbox.Probe] = None,
|
|
2381
1028
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
2382
1029
|
include_oidc_identity_token: bool = False,
|
|
2383
1030
|
verbose: bool = False,
|
|
@@ -2451,7 +1098,7 @@ class Sandbox(modal.object.Object):
|
|
|
2451
1098
|
h2_ports: collections.abc.Sequence[int] = [],
|
|
2452
1099
|
unencrypted_ports: collections.abc.Sequence[int] = [],
|
|
2453
1100
|
proxy: typing.Optional[modal.proxy.Proxy] = None,
|
|
2454
|
-
readiness_probe: typing.Optional[Probe] = None,
|
|
1101
|
+
readiness_probe: typing.Optional[modal.sandbox._sandbox.Probe] = None,
|
|
2455
1102
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
2456
1103
|
include_oidc_identity_token: bool = False,
|
|
2457
1104
|
verbose: bool = False,
|
|
@@ -3526,8 +2173,7 @@ class Sandbox(modal.object.Object):
|
|
|
3526
2173
|
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
3527
2174
|
timeout: typing.Optional[int] = None,
|
|
3528
2175
|
workdir: typing.Optional[str] = None,
|
|
3529
|
-
|
|
3530
|
-
env: typing.Optional[dict[str, str]] = None,
|
|
2176
|
+
secret_sources: typing.Optional[collections.abc.Collection[modal_proto.api_pb2.SecretSource]] = None,
|
|
3531
2177
|
text: bool = True,
|
|
3532
2178
|
bufsize: typing.Literal[-1, 1] = -1,
|
|
3533
2179
|
runtime_debug: bool = False,
|
|
@@ -3549,8 +2195,7 @@ class Sandbox(modal.object.Object):
|
|
|
3549
2195
|
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
3550
2196
|
timeout: typing.Optional[int] = None,
|
|
3551
2197
|
workdir: typing.Optional[str] = None,
|
|
3552
|
-
|
|
3553
|
-
env: typing.Optional[dict[str, str]] = None,
|
|
2198
|
+
secret_sources: typing.Optional[collections.abc.Collection[modal_proto.api_pb2.SecretSource]] = None,
|
|
3554
2199
|
text: bool = True,
|
|
3555
2200
|
bufsize: typing.Literal[-1, 1] = -1,
|
|
3556
2201
|
runtime_debug: bool = False,
|
|
@@ -3576,7 +2221,7 @@ class Sandbox(modal.object.Object):
|
|
|
3576
2221
|
snapshot: modal.snapshot.SandboxSnapshot,
|
|
3577
2222
|
client: typing.Optional[modal.client.Client] = None,
|
|
3578
2223
|
*,
|
|
3579
|
-
name: typing.Optional[str] = _DEFAULT_SANDBOX_NAME_OVERRIDE,
|
|
2224
|
+
name: typing.Optional[str] = modal.sandbox._sandbox._DEFAULT_SANDBOX_NAME_OVERRIDE,
|
|
3580
2225
|
):
|
|
3581
2226
|
"""Restore a Sandbox from a memory snapshot.
|
|
3582
2227
|
|
|
@@ -3591,7 +2236,7 @@ class Sandbox(modal.object.Object):
|
|
|
3591
2236
|
snapshot: modal.snapshot.SandboxSnapshot,
|
|
3592
2237
|
client: typing.Optional[modal.client.Client] = None,
|
|
3593
2238
|
*,
|
|
3594
|
-
name: typing.Optional[str] = _DEFAULT_SANDBOX_NAME_OVERRIDE,
|
|
2239
|
+
name: typing.Optional[str] = modal.sandbox._sandbox._DEFAULT_SANDBOX_NAME_OVERRIDE,
|
|
3595
2240
|
):
|
|
3596
2241
|
"""Restore a Sandbox from a memory snapshot.
|
|
3597
2242
|
|
|
@@ -3786,10 +2431,37 @@ class Sandbox(modal.object.Object):
|
|
|
3786
2431
|
"""
|
|
3787
2432
|
...
|
|
3788
2433
|
|
|
3789
|
-
|
|
2434
|
+
class ___container_exec_spec(typing_extensions.Protocol):
|
|
2435
|
+
def __call__(
|
|
2436
|
+
self,
|
|
2437
|
+
/,
|
|
2438
|
+
pty: bool,
|
|
2439
|
+
container_id: str = "",
|
|
2440
|
+
command: tuple[str, ...] = (),
|
|
2441
|
+
sandbox_id_v2: typing.Optional[str] = None,
|
|
2442
|
+
):
|
|
2443
|
+
"""Execute a command in a container.
|
|
2444
|
+
|
|
2445
|
+
For tasks belonging to V2 sandboxes, `sandbox_id_v2` may be set to the
|
|
2446
|
+
sandbox ID; otherwise the V2 backend is used whenever the task ID itself
|
|
2447
|
+
marks a V2 sandbox task.
|
|
2448
|
+
"""
|
|
2449
|
+
...
|
|
3790
2450
|
|
|
3791
|
-
|
|
2451
|
+
async def aio(
|
|
2452
|
+
self,
|
|
2453
|
+
/,
|
|
2454
|
+
pty: bool,
|
|
2455
|
+
container_id: str = "",
|
|
2456
|
+
command: tuple[str, ...] = (),
|
|
2457
|
+
sandbox_id_v2: typing.Optional[str] = None,
|
|
2458
|
+
):
|
|
2459
|
+
"""Execute a command in a container.
|
|
3792
2460
|
|
|
3793
|
-
|
|
2461
|
+
For tasks belonging to V2 sandboxes, `sandbox_id_v2` may be set to the
|
|
2462
|
+
sandbox ID; otherwise the V2 backend is used whenever the task ID itself
|
|
2463
|
+
marks a V2 sandbox task.
|
|
2464
|
+
"""
|
|
2465
|
+
...
|
|
3794
2466
|
|
|
3795
|
-
|
|
2467
|
+
_container_exec: ___container_exec_spec
|