modAL 1.6.3.dev5__py3-none-any.whl → 1.6.3.dev7__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/_image.py +21 -22
- modal/_utils/grpc_testing.py +3 -1
- modal/builder/PREVIEW.txt +16 -19
- modal/client.pyi +2 -2
- modal/image.pyi +4 -1
- modal/mount.py +35 -13
- modal/mount.pyi +10 -8
- modal/sandbox/_common.py +17 -2
- modal/sandbox/_impl_persistent.py +470 -0
- modal/sandbox/_impl_v1.py +10 -0
- modal/sandbox/_impl_v2.py +10 -0
- modal/sandbox/_sandbox.py +21 -3
- modal/sandbox/sandbox.pyi +38 -2
- {modal-1.6.3.dev5.dist-info → modal-1.6.3.dev7.dist-info}/METADATA +1 -1
- {modal-1.6.3.dev5.dist-info → modal-1.6.3.dev7.dist-info}/RECORD +20 -19
- modal_version/__init__.py +1 -1
- {modal-1.6.3.dev5.dist-info → modal-1.6.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.6.3.dev5.dist-info → modal-1.6.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.6.3.dev5.dist-info → modal-1.6.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.6.3.dev5.dist-info → modal-1.6.3.dev7.dist-info}/top_level.txt +0 -0
modal/_image.py
CHANGED
|
@@ -57,7 +57,7 @@ from .exception import (
|
|
|
57
57
|
VersionError,
|
|
58
58
|
)
|
|
59
59
|
from .file_pattern_matcher import NON_PYTHON_FILES, FilePatternMatcher, _ignore_fn
|
|
60
|
-
from .mount import _Mount, python_standalone_mount_name
|
|
60
|
+
from .mount import PYTHON_STANDALONE_VERSIONS, _Mount, python_standalone_mount_name
|
|
61
61
|
from .network_file_system import _NetworkFileSystem
|
|
62
62
|
from .output import OutputManager
|
|
63
63
|
from .secret import _Secret
|
|
@@ -70,6 +70,7 @@ if typing.TYPE_CHECKING:
|
|
|
70
70
|
|
|
71
71
|
# This is used for both type checking and runtime validation
|
|
72
72
|
ImageBuilderVersion = Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVIEW"]
|
|
73
|
+
LATEST_IMAGE_BUILDER_VERSION: ImageBuilderVersion = "2025.06"
|
|
73
74
|
|
|
74
75
|
# Note: we also define supported Python versions via logic at the top of the package __init__.py
|
|
75
76
|
# so that we fail fast / clearly in unsupported containers. Additionally, we enumerate the supported
|
|
@@ -402,7 +403,7 @@ def _create_context_mount_function(
|
|
|
402
403
|
|
|
403
404
|
if ignore is AUTO_DOCKERIGNORE:
|
|
404
405
|
|
|
405
|
-
def auto_created_context_mount_fn() -> _Mount | None:
|
|
406
|
+
def auto_created_context_mount_fn(builder_version: ImageBuilderVersion) -> _Mount | None:
|
|
406
407
|
nonlocal context_dir
|
|
407
408
|
context_dir = Path.cwd() if context_dir is None else Path(context_dir).absolute()
|
|
408
409
|
dockerignore_file = find_dockerignore_file(context_dir, dockerfile_path)
|
|
@@ -419,7 +420,7 @@ def _create_context_mount_function(
|
|
|
419
420
|
|
|
420
421
|
else:
|
|
421
422
|
|
|
422
|
-
def auto_created_context_mount_fn() -> _Mount | None:
|
|
423
|
+
def auto_created_context_mount_fn(builder_version: ImageBuilderVersion) -> _Mount | None:
|
|
423
424
|
# use COPY commands and ignore patterns to construct implicit context mount
|
|
424
425
|
nonlocal context_dir
|
|
425
426
|
context_dir = Path.cwd() if context_dir is None else Path(context_dir).absolute()
|
|
@@ -622,7 +623,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
622
623
|
build_function: "modal._functions._Function | None" = None,
|
|
623
624
|
build_function_input: api_pb2.FunctionInput | None = None,
|
|
624
625
|
image_registry_config: _ImageRegistryConfig | None = None,
|
|
625
|
-
context_mount_function: Callable[[], _Mount | None] | None = None,
|
|
626
|
+
context_mount_function: Callable[[ImageBuilderVersion], _Mount | None] | None = None,
|
|
626
627
|
force_build: bool = False,
|
|
627
628
|
build_args: dict[str, str] = {},
|
|
628
629
|
validated_volumes: Sequence[tuple[str, _Volume]] | None = None,
|
|
@@ -661,7 +662,13 @@ class _Image(_Object, type_prefix="im"):
|
|
|
661
662
|
return deps
|
|
662
663
|
|
|
663
664
|
async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: str | None):
|
|
664
|
-
|
|
665
|
+
assert load_context.app_id # type narrowing
|
|
666
|
+
environment = await _get_environment_cached(load_context.environment_name or "", load_context.client)
|
|
667
|
+
# A bit hacky,but assume that the environment provides a valid builder version
|
|
668
|
+
image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
|
|
669
|
+
builder_version = _get_image_builder_version(image_builder_version)
|
|
670
|
+
|
|
671
|
+
context_mount = context_mount_function(builder_version) if context_mount_function else None
|
|
665
672
|
if context_mount:
|
|
666
673
|
await resolver.load(context_mount, load_context)
|
|
667
674
|
|
|
@@ -670,12 +677,6 @@ class _Image(_Object, type_prefix="im"):
|
|
|
670
677
|
# base images can't have
|
|
671
678
|
image._assert_no_mount_layers()
|
|
672
679
|
|
|
673
|
-
assert load_context.app_id # type narrowing
|
|
674
|
-
environment = await _get_environment_cached(load_context.environment_name or "", load_context.client)
|
|
675
|
-
# A bit hacky,but assume that the environment provides a valid builder version
|
|
676
|
-
image_builder_version = cast(ImageBuilderVersion, environment._settings.image_builder_version)
|
|
677
|
-
builder_version = _get_image_builder_version(image_builder_version)
|
|
678
|
-
|
|
679
680
|
if dockerfile_function is None:
|
|
680
681
|
dockerfile = DockerfileSpec(commands=[], context_files={})
|
|
681
682
|
else:
|
|
@@ -847,7 +848,7 @@ class _Image(_Object, type_prefix="im"):
|
|
|
847
848
|
return _Image._from_args(
|
|
848
849
|
base_images={"base": self},
|
|
849
850
|
dockerfile_function=build_dockerfile,
|
|
850
|
-
context_mount_function=lambda: mount,
|
|
851
|
+
context_mount_function=lambda builder_version: mount,
|
|
851
852
|
)
|
|
852
853
|
|
|
853
854
|
@_requires_image_instance
|
|
@@ -2191,12 +2192,9 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2191
2192
|
"COPY /python/. /usr/local",
|
|
2192
2193
|
"ENV TERMINFO_DIRS=/etc/terminfo:/lib/terminfo:/usr/share/terminfo:/usr/lib/terminfo",
|
|
2193
2194
|
]
|
|
2194
|
-
|
|
2195
|
-
if
|
|
2196
|
-
|
|
2197
|
-
if int(python_minor) < 13:
|
|
2198
|
-
# Previous versions did not include the `python` binary, but later ones do.
|
|
2199
|
-
# (The important factor is not the Python version itself, but the standalone dist version.)
|
|
2195
|
+
release, _ = PYTHON_STANDALONE_VERSIONS[builder_version][add_python]
|
|
2196
|
+
if release < "20241008":
|
|
2197
|
+
# Older standalone releases do not include the `python` binary.
|
|
2200
2198
|
# We insert the command in the list at the position it was previously always added
|
|
2201
2199
|
# for backwards compatibility with existing images.
|
|
2202
2200
|
add_python_commands.insert(1, "RUN ln -s /usr/local/bin/python3 /usr/local/bin/python")
|
|
@@ -2271,10 +2269,10 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2271
2269
|
```
|
|
2272
2270
|
"""
|
|
2273
2271
|
|
|
2274
|
-
def context_mount_function() -> _Mount | None:
|
|
2272
|
+
def context_mount_function(builder_version: ImageBuilderVersion) -> _Mount | None:
|
|
2275
2273
|
return (
|
|
2276
2274
|
_Mount.from_name(
|
|
2277
|
-
python_standalone_mount_name(add_python),
|
|
2275
|
+
python_standalone_mount_name(add_python, builder_version),
|
|
2278
2276
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
|
2279
2277
|
)
|
|
2280
2278
|
if add_python
|
|
@@ -2516,16 +2514,17 @@ class _Image(_Object, type_prefix="im"):
|
|
|
2516
2514
|
gpu_config=gpu_config,
|
|
2517
2515
|
secrets=secrets,
|
|
2518
2516
|
build_args=build_args,
|
|
2517
|
+
force_build=force_build,
|
|
2519
2518
|
)
|
|
2520
2519
|
|
|
2521
2520
|
# --- Now add in the modal dependencies, and, optionally a Python distribution
|
|
2522
2521
|
# This happening in two steps is probably a vestigial consequence of previous limitations,
|
|
2523
2522
|
# but it will be difficult to merge them without forcing rebuilds of images.
|
|
2524
2523
|
|
|
2525
|
-
def add_python_mount():
|
|
2524
|
+
def add_python_mount(builder_version: ImageBuilderVersion) -> _Mount | None:
|
|
2526
2525
|
return (
|
|
2527
2526
|
_Mount.from_name(
|
|
2528
|
-
python_standalone_mount_name(add_python),
|
|
2527
|
+
python_standalone_mount_name(add_python, builder_version),
|
|
2529
2528
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
|
2530
2529
|
)
|
|
2531
2530
|
if add_python
|
modal/_utils/grpc_testing.py
CHANGED
|
@@ -116,7 +116,7 @@ class InterceptionContext:
|
|
|
116
116
|
"""Adds one response payload to an expected queue of responses for a method.
|
|
117
117
|
|
|
118
118
|
These responses will be used once each instead of calling the MockServicer's
|
|
119
|
-
implementation of the method.
|
|
119
|
+
implementation of the method. An exception payload is raised instead of being sent.
|
|
120
120
|
|
|
121
121
|
The interception context will throw an exception on exit if not all of the added
|
|
122
122
|
responses have been consumed.
|
|
@@ -184,6 +184,8 @@ class InterceptionContext:
|
|
|
184
184
|
async def responder(servicer_self, stream):
|
|
185
185
|
await stream.recv_message() # get the input message so we can track that
|
|
186
186
|
for msg in next_response_messages:
|
|
187
|
+
if isinstance(msg, Exception):
|
|
188
|
+
raise msg
|
|
187
189
|
await stream.send_message(msg)
|
|
188
190
|
|
|
189
191
|
return responder
|
modal/builder/PREVIEW.txt
CHANGED
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
aiohappyeyeballs==2.
|
|
2
|
-
aiohttp==3.
|
|
3
|
-
aiosignal==1.
|
|
1
|
+
aiohappyeyeballs==2.7.1
|
|
2
|
+
aiohttp==3.14.5
|
|
3
|
+
aiosignal==1.4.0
|
|
4
4
|
async-timeout==5.0.1 ; python_version < "3.11"
|
|
5
|
-
attrs==
|
|
6
|
-
cbor2==5.
|
|
7
|
-
certifi==
|
|
8
|
-
frozenlist==1.
|
|
9
|
-
grpclib==0.4.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
hpack==4.1.0
|
|
5
|
+
attrs==26.1.0
|
|
6
|
+
cbor2==5.9.0
|
|
7
|
+
certifi==2026.10.19
|
|
8
|
+
frozenlist==1.8.0
|
|
9
|
+
grpclib==0.4.9
|
|
10
|
+
h2==4.4.1
|
|
11
|
+
hpack==4.2.0
|
|
13
12
|
hyperframe==6.1.0
|
|
14
|
-
idna==3.
|
|
15
|
-
multidict==
|
|
16
|
-
propcache==0.
|
|
17
|
-
protobuf==
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
typing_extensions==4.16.0 ; python_version >= "3.15"
|
|
21
|
-
yarl==1.20.0
|
|
13
|
+
idna==3.20
|
|
14
|
+
multidict==7.1.0
|
|
15
|
+
propcache==0.5.4
|
|
16
|
+
protobuf==7.36.2
|
|
17
|
+
typing_extensions==4.16.0
|
|
18
|
+
yarl==1.25.1
|
modal/client.pyi
CHANGED
|
@@ -80,7 +80,7 @@ class _Client:
|
|
|
80
80
|
server_url: str,
|
|
81
81
|
client_type: int,
|
|
82
82
|
credentials: typing.Optional[tuple[str, str]],
|
|
83
|
-
version: str = "1.6.3.
|
|
83
|
+
version: str = "1.6.3.dev7",
|
|
84
84
|
*,
|
|
85
85
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
86
86
|
):
|
|
@@ -290,7 +290,7 @@ class Client:
|
|
|
290
290
|
server_url: str,
|
|
291
291
|
client_type: int,
|
|
292
292
|
credentials: typing.Optional[tuple[str, str]],
|
|
293
|
-
version: str = "1.6.3.
|
|
293
|
+
version: str = "1.6.3.dev7",
|
|
294
294
|
*,
|
|
295
295
|
oauth_credentials: typing.Optional[_OAuthCredentials] = None,
|
|
296
296
|
):
|
modal/image.pyi
CHANGED
|
@@ -74,7 +74,10 @@ class Image(modal.object.Object):
|
|
|
74
74
|
build_function_input: typing.Optional[modal_proto.api_pb2.FunctionInput] = None,
|
|
75
75
|
image_registry_config: typing.Optional[modal._image._ImageRegistryConfig] = None,
|
|
76
76
|
context_mount_function: typing.Optional[
|
|
77
|
-
collections.abc.Callable[
|
|
77
|
+
collections.abc.Callable[
|
|
78
|
+
[typing.Literal["2023.12", "2024.04", "2024.10", "2025.06", "PREVIEW"]],
|
|
79
|
+
typing.Optional[modal.mount.Mount],
|
|
80
|
+
]
|
|
78
81
|
] = None,
|
|
79
82
|
force_build: bool = False,
|
|
80
83
|
build_args: dict[str, str] = {},
|
modal/mount.py
CHANGED
|
@@ -39,11 +39,11 @@ from .file_pattern_matcher import FilePatternMatcher
|
|
|
39
39
|
ROOT_DIR: PurePosixPath = PurePosixPath("/root")
|
|
40
40
|
MOUNT_PUT_FILE_CLIENT_TIMEOUT = 10 * 60 # 10 min max for transferring files
|
|
41
41
|
|
|
42
|
-
# Supported releases and versions for python-build-standalone.
|
|
42
|
+
# Supported releases and versions for python-build-standalone, keyed by image builder version.
|
|
43
43
|
#
|
|
44
|
-
#
|
|
45
|
-
# that rely on `add_python()` in their constructor.
|
|
46
|
-
|
|
44
|
+
# Changing the versions for a builder version triggers a rebuild for all images on that builder
|
|
45
|
+
# version that rely on `add_python()` in their constructor, so only "PREVIEW" should be updated.
|
|
46
|
+
_PYTHON_STANDALONE_VERSIONS_2025_06: dict[str, tuple[str, str]] = {
|
|
47
47
|
"3.10": ("20230826", "3.10.13"),
|
|
48
48
|
"3.11": ("20230826", "3.11.5"),
|
|
49
49
|
"3.12": ("20240107", "3.12.1"),
|
|
@@ -53,6 +53,22 @@ PYTHON_STANDALONE_VERSIONS: dict[str, tuple[str, str]] = {
|
|
|
53
53
|
"3.15": ("20261009", "3.15.0"),
|
|
54
54
|
"3.15t": ("20261009", "3.15.0t"),
|
|
55
55
|
}
|
|
56
|
+
PYTHON_STANDALONE_VERSIONS: dict[str, dict[str, tuple[str, str]]] = {
|
|
57
|
+
"PREVIEW": {
|
|
58
|
+
"3.10": ("20261009", "3.10.22"),
|
|
59
|
+
"3.11": ("20261021", "3.11.17"),
|
|
60
|
+
"3.12": ("20261021", "3.12.15"),
|
|
61
|
+
"3.13": ("20261021", "3.13.17"),
|
|
62
|
+
"3.14": ("20261021", "3.14.9"),
|
|
63
|
+
"3.14t": ("20261021", "3.14.9t"),
|
|
64
|
+
"3.15": ("20261021", "3.15.1"),
|
|
65
|
+
"3.15t": ("20261021", "3.15.1t"),
|
|
66
|
+
},
|
|
67
|
+
"2025.06": _PYTHON_STANDALONE_VERSIONS_2025_06,
|
|
68
|
+
"2024.10": _PYTHON_STANDALONE_VERSIONS_2025_06,
|
|
69
|
+
"2024.04": _PYTHON_STANDALONE_VERSIONS_2025_06,
|
|
70
|
+
"2023.12": _PYTHON_STANDALONE_VERSIONS_2025_06,
|
|
71
|
+
}
|
|
56
72
|
|
|
57
73
|
MOUNT_DEPRECATION_MESSAGE_PATTERN = """modal.Mount usage will soon be deprecated.
|
|
58
74
|
|
|
@@ -72,20 +88,20 @@ def client_mount_name() -> str:
|
|
|
72
88
|
return f"modal-client-mount-{version}"
|
|
73
89
|
|
|
74
90
|
|
|
75
|
-
def python_standalone_mount_name(version: str) -> str:
|
|
91
|
+
def python_standalone_mount_name(version: str, builder_version: str) -> str:
|
|
76
92
|
"""Get the deployed name of the python-build-standalone mount."""
|
|
77
93
|
if "-" in version: # default to glibc
|
|
78
94
|
version, libc = version.split("-")
|
|
79
95
|
else:
|
|
80
96
|
libc = "gnu"
|
|
81
|
-
|
|
97
|
+
standalone_versions = PYTHON_STANDALONE_VERSIONS[builder_version]
|
|
98
|
+
if version not in standalone_versions:
|
|
82
99
|
raise modal.exception.InvalidError(
|
|
83
|
-
f"Unsupported standalone python version: {version!r}, supported values are "
|
|
84
|
-
f"{list(PYTHON_STANDALONE_VERSIONS)}"
|
|
100
|
+
f"Unsupported standalone python version: {version!r}, supported values are {list(standalone_versions)}"
|
|
85
101
|
)
|
|
86
102
|
if libc != "gnu":
|
|
87
103
|
raise modal.exception.InvalidError(f"Unsupported libc identifier: {libc}")
|
|
88
|
-
release, full_version =
|
|
104
|
+
release, full_version = standalone_versions[version]
|
|
89
105
|
return f"python-build-standalone.{release}.{full_version}-{libc}"
|
|
90
106
|
|
|
91
107
|
|
|
@@ -950,19 +966,25 @@ async def _create_single_client_dependency_mount(
|
|
|
950
966
|
|
|
951
967
|
async def _create_client_dependency_mounts(
|
|
952
968
|
client=None,
|
|
953
|
-
python_versions: list[str] =
|
|
954
|
-
builder_versions: list[str] =
|
|
969
|
+
python_versions: list[str] | None = None,
|
|
970
|
+
builder_versions: list[str] | None = None, # Pass "PREVIEW" during testing
|
|
955
971
|
check_if_exists=True,
|
|
956
972
|
dry_run=False,
|
|
957
973
|
):
|
|
974
|
+
from ._image import LATEST_IMAGE_BUILDER_VERSION, SUPPORTED_PYTHON_SERIES
|
|
975
|
+
|
|
958
976
|
arch = "x86_64"
|
|
959
977
|
platform_tags = [
|
|
960
978
|
("manylinux_2_17", f"{arch}-manylinux_2_17"), # glibc >= 2.17
|
|
961
979
|
("musllinux_1_2", f"{arch}-unknown-linux-musl"), # musl >= 1.2
|
|
962
980
|
]
|
|
963
981
|
coros = []
|
|
964
|
-
for
|
|
965
|
-
|
|
982
|
+
for builder_version in builder_versions or [LATEST_IMAGE_BUILDER_VERSION]:
|
|
983
|
+
if python_versions is None:
|
|
984
|
+
builder_python_versions = SUPPORTED_PYTHON_SERIES[builder_version]
|
|
985
|
+
else:
|
|
986
|
+
builder_python_versions = python_versions
|
|
987
|
+
for python_version in builder_python_versions:
|
|
966
988
|
for platform, uv_python_platform in platform_tags:
|
|
967
989
|
coros.append(
|
|
968
990
|
_create_single_client_dependency_mount(
|
modal/mount.pyi
CHANGED
|
@@ -16,7 +16,7 @@ def client_mount_name() -> str:
|
|
|
16
16
|
"""Get the deployed name of the client package mount."""
|
|
17
17
|
...
|
|
18
18
|
|
|
19
|
-
def python_standalone_mount_name(version: str) -> str:
|
|
19
|
+
def python_standalone_mount_name(version: str, builder_version: str) -> str:
|
|
20
20
|
"""Get the deployed name of the python-build-standalone mount."""
|
|
21
21
|
...
|
|
22
22
|
|
|
@@ -444,8 +444,8 @@ async def _create_single_client_dependency_mount(
|
|
|
444
444
|
): ...
|
|
445
445
|
async def _create_client_dependency_mounts(
|
|
446
446
|
client=None,
|
|
447
|
-
python_versions: list[str] =
|
|
448
|
-
builder_versions: list[str] =
|
|
447
|
+
python_versions: typing.Optional[list[str]] = None,
|
|
448
|
+
builder_versions: typing.Optional[list[str]] = None,
|
|
449
449
|
check_if_exists=True,
|
|
450
450
|
dry_run=False,
|
|
451
451
|
): ...
|
|
@@ -455,8 +455,8 @@ class __create_client_dependency_mounts_spec(typing_extensions.Protocol):
|
|
|
455
455
|
self,
|
|
456
456
|
/,
|
|
457
457
|
client=None,
|
|
458
|
-
python_versions: list[str] =
|
|
459
|
-
builder_versions: list[str] =
|
|
458
|
+
python_versions: typing.Optional[list[str]] = None,
|
|
459
|
+
builder_versions: typing.Optional[list[str]] = None,
|
|
460
460
|
check_if_exists=True,
|
|
461
461
|
dry_run=False,
|
|
462
462
|
): ...
|
|
@@ -464,8 +464,8 @@ class __create_client_dependency_mounts_spec(typing_extensions.Protocol):
|
|
|
464
464
|
self,
|
|
465
465
|
/,
|
|
466
466
|
client=None,
|
|
467
|
-
python_versions: list[str] =
|
|
468
|
-
builder_versions: list[str] =
|
|
467
|
+
python_versions: typing.Optional[list[str]] = None,
|
|
468
|
+
builder_versions: typing.Optional[list[str]] = None,
|
|
469
469
|
check_if_exists=True,
|
|
470
470
|
dry_run=False,
|
|
471
471
|
): ...
|
|
@@ -474,4 +474,6 @@ create_client_dependency_mounts: __create_client_dependency_mounts_spec
|
|
|
474
474
|
|
|
475
475
|
ROOT_DIR: pathlib.PurePosixPath
|
|
476
476
|
|
|
477
|
-
|
|
477
|
+
_PYTHON_STANDALONE_VERSIONS_2025_06: dict[str, tuple[str, str]]
|
|
478
|
+
|
|
479
|
+
PYTHON_STANDALONE_VERSIONS: dict[str, dict[str, tuple[str, str]]]
|
modal/sandbox/_common.py
CHANGED
|
@@ -386,7 +386,7 @@ class SandboxCreateArgs:
|
|
|
386
386
|
secrets: Collection[_Secret] | None = None
|
|
387
387
|
env: dict[str, str | None] | None = None
|
|
388
388
|
name: str | None = None
|
|
389
|
-
timeout: int = 300
|
|
389
|
+
timeout: int | None = 300
|
|
390
390
|
idle_timeout: int | None = None
|
|
391
391
|
workdir: str | None = None
|
|
392
392
|
gpu: str | None = None
|
|
@@ -417,6 +417,7 @@ class SandboxCreateArgs:
|
|
|
417
417
|
custom_domain: str | None = None
|
|
418
418
|
include_oidc_identity_token: bool = False
|
|
419
419
|
runtime: SandboxRuntime | None = None
|
|
420
|
+
persistent: bool = False
|
|
420
421
|
|
|
421
422
|
|
|
422
423
|
class SandboxCreateCommon:
|
|
@@ -428,6 +429,8 @@ class SandboxCreateCommon:
|
|
|
428
429
|
|
|
429
430
|
def __init__(self, create_args: SandboxCreateArgs, *, secrets: Sequence[_Secret]) -> None:
|
|
430
431
|
a = create_args
|
|
432
|
+
if a.timeout is None:
|
|
433
|
+
raise InvalidError("`timeout=None` is only supported for experimental persistent Sandboxes.")
|
|
431
434
|
_validate_exec_args(a.args)
|
|
432
435
|
if a.name is not None:
|
|
433
436
|
check_object_name(a.name, "Sandbox")
|
|
@@ -452,6 +455,7 @@ class SandboxCreateCommon:
|
|
|
452
455
|
raise InvalidError("`inbound_cidr_allowlist` cannot be used when `block_network` is enabled")
|
|
453
456
|
|
|
454
457
|
self.create_args = a
|
|
458
|
+
self.timeout = a.timeout
|
|
455
459
|
self.secrets = secrets
|
|
456
460
|
self.image = a.image or _default_image
|
|
457
461
|
self.network_file_systems = validate_network_file_systems(a.network_file_systems)
|
|
@@ -503,7 +507,7 @@ class SandboxCreateCommon:
|
|
|
503
507
|
secret_ids=[secret.object_id for secret in self.secrets],
|
|
504
508
|
# Ephemeral disk requests are not supported on Sandboxes.
|
|
505
509
|
resources=convert_fn_config_to_resources_config(cpu=a.cpu, memory=a.memory, gpu=a.gpu, ephemeral_disk=None),
|
|
506
|
-
timeout_secs=
|
|
510
|
+
timeout_secs=self.timeout,
|
|
507
511
|
idle_timeout_secs=a.idle_timeout,
|
|
508
512
|
workdir=a.workdir,
|
|
509
513
|
cloud_provider_str=a.cloud if a.cloud else None, # pyright: ignore[reportArgumentType]
|
|
@@ -637,6 +641,10 @@ class SandboxImpl(Protocol):
|
|
|
637
641
|
"""
|
|
638
642
|
...
|
|
639
643
|
|
|
644
|
+
def clone(self) -> "SandboxImpl":
|
|
645
|
+
"""Create an implementation of the same type for another handle to the same Sandbox."""
|
|
646
|
+
...
|
|
647
|
+
|
|
640
648
|
@classmethod
|
|
641
649
|
async def lookup_by_name(
|
|
642
650
|
cls, client: _Client, req: api_pb2.SandboxGetFromNameRequest
|
|
@@ -674,6 +682,10 @@ class SandboxImpl(Protocol):
|
|
|
674
682
|
|
|
675
683
|
async def terminate(self) -> None: ...
|
|
676
684
|
|
|
685
|
+
async def pause(self) -> None: ...
|
|
686
|
+
|
|
687
|
+
async def resume(self) -> None: ...
|
|
688
|
+
|
|
677
689
|
async def close(self) -> None:
|
|
678
690
|
"""Release connections held for the Sandbox once in-flight operations complete."""
|
|
679
691
|
...
|
|
@@ -907,6 +919,9 @@ async def snapshot_filesystem_via_tcr(
|
|
|
907
919
|
return _Image._new_hydrated(res.image_id, client, None)
|
|
908
920
|
|
|
909
921
|
|
|
922
|
+
_PERSISTENT_ONLY_MESSAGE = "`{}` requires a persistent Sandbox, created with `_experimental_persistent=True`."
|
|
923
|
+
|
|
924
|
+
|
|
910
925
|
async def mount_image_via_tcr(
|
|
911
926
|
impl: SandboxImpl,
|
|
912
927
|
path: PurePosixPath | str,
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# Copyright Modal Labs 2026
|
|
2
|
+
"""Persistent Sandboxes, managed by the `PersistentSandbox*` control-plane RPCs.
|
|
3
|
+
|
|
4
|
+
A persistent Sandbox runs as a sequence of regular V2 Sandboxes, its sessions. The control plane starts each
|
|
5
|
+
session from the exit snapshot of the previous one.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import copy
|
|
10
|
+
import dataclasses
|
|
11
|
+
import time
|
|
12
|
+
import weakref
|
|
13
|
+
from collections.abc import Awaitable, Callable, Collection, Sequence
|
|
14
|
+
from pathlib import PurePosixPath
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Literal, TypeVar
|
|
16
|
+
|
|
17
|
+
from grpclib import GRPCError, Status
|
|
18
|
+
|
|
19
|
+
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
20
|
+
|
|
21
|
+
from .._image import _Image
|
|
22
|
+
from .._outbound_policy import _OutboundPolicy
|
|
23
|
+
from .._resolver import Resolver
|
|
24
|
+
from .._tunnel import Tunnel
|
|
25
|
+
from ..client import _Client
|
|
26
|
+
from ..cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
|
|
27
|
+
from ..config import logger
|
|
28
|
+
from ..container_process import _ContainerProcess
|
|
29
|
+
from ..exception import ClientClosed, InvalidError, SandboxTerminatedError
|
|
30
|
+
from ..io_streams import (
|
|
31
|
+
StreamReader,
|
|
32
|
+
StreamWriter,
|
|
33
|
+
_StreamReaderThroughSandboxCommandRouterParams,
|
|
34
|
+
_StreamWriterThroughCommandRouterSandboxParams,
|
|
35
|
+
)
|
|
36
|
+
from ..secret import _resolvable_secrets, _Secret, _secret_sources
|
|
37
|
+
from ..stream_type import StreamType
|
|
38
|
+
from ._common import SandboxCreateArgs, SandboxCreateCommon, SandboxImpl, command_router, sandbox_create_context
|
|
39
|
+
from ._impl_v2 import SandboxImplV2, auth_metadata
|
|
40
|
+
from ._snapshot import _SandboxSnapshot
|
|
41
|
+
from ._task_command_router_client import TaskCommandRouterClient
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
import modal.app
|
|
45
|
+
|
|
46
|
+
_T = TypeVar("_T")
|
|
47
|
+
|
|
48
|
+
# Errors caused by the caller rather than by the session, which a new session can't fix.
|
|
49
|
+
_NON_RECOVERABLE_ERRORS = (InvalidError, ClientClosed)
|
|
50
|
+
|
|
51
|
+
# Lifetime limits of each session. A session that hits one is paused, and resumed on next use.
|
|
52
|
+
PERSISTENT_SANDBOX_SESSION_TIMEOUT = 60 * 60
|
|
53
|
+
PERSISTENT_SANDBOX_SESSION_IDLE_TIMEOUT = 5 * 60
|
|
54
|
+
|
|
55
|
+
# How long to retry resuming while the previous session is still saving its exit snapshot.
|
|
56
|
+
_RESUME_RETRY_TIMEOUT = 120.0
|
|
57
|
+
_RESUME_RETRY_INTERVAL = 1.0
|
|
58
|
+
|
|
59
|
+
# How often `wait()` checks whether the persistent Sandbox has been terminated.
|
|
60
|
+
_WAIT_POLL_INTERVAL = 1.0
|
|
61
|
+
|
|
62
|
+
_TERMINATED_RESULT = api_pb2.GenericResult(status=api_pb2.GenericResult.GENERIC_STATUS_TERMINATED)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SandboxImplPersistent:
|
|
66
|
+
"""Backs a persistent Sandbox, whose object ID is the ID of its first session.
|
|
67
|
+
|
|
68
|
+
Operations are forwarded to the current session. When one fails and the Sandbox hasn't been terminated
|
|
69
|
+
through this handle, the control plane is asked for the running session, resuming the Sandbox from its
|
|
70
|
+
latest exit snapshot if there is none. The operation is then retried once on the new session.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
client: _Client,
|
|
76
|
+
sandbox_id: str,
|
|
77
|
+
*,
|
|
78
|
+
result: api_pb2.GenericResult | None = None,
|
|
79
|
+
persistent_sandbox_id: str = "",
|
|
80
|
+
session: SandboxImplV2 | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
self.client = client
|
|
83
|
+
self.sandbox_id = sandbox_id
|
|
84
|
+
self.result = result
|
|
85
|
+
self._persistent_sandbox_id = persistent_sandbox_id
|
|
86
|
+
self._session = session if session is not None else SandboxImplV2(client, sandbox_id)
|
|
87
|
+
self._replace_lock: asyncio.Lock | None = None
|
|
88
|
+
self._terminated = result is not None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def _delegate(self) -> SandboxImplV2:
|
|
92
|
+
return self._session
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def _client(self) -> _Client:
|
|
96
|
+
return self.client
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
async def create(
|
|
100
|
+
cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
|
|
101
|
+
) -> "tuple[SandboxImpl, api_pb2.SandboxHandleMetadata]":
|
|
102
|
+
if create_args.timeout is not None or create_args.idle_timeout is not None:
|
|
103
|
+
raise InvalidError("Persistent Sandboxes require `timeout=None` and don't support `idle_timeout`.")
|
|
104
|
+
create_args = dataclasses.replace(
|
|
105
|
+
create_args,
|
|
106
|
+
timeout=PERSISTENT_SANDBOX_SESSION_TIMEOUT,
|
|
107
|
+
idle_timeout=PERSISTENT_SANDBOX_SESSION_IDLE_TIMEOUT,
|
|
108
|
+
)
|
|
109
|
+
if create_args.name is not None:
|
|
110
|
+
raise InvalidError("Named persistent Sandboxes are not supported yet.")
|
|
111
|
+
if create_args.gpu is not None or create_args.network_file_systems or create_args.pty_info is not None:
|
|
112
|
+
raise InvalidError("GPUs, network file systems and PTYs are not supported by persistent Sandboxes yet.")
|
|
113
|
+
if create_args.enable_snapshot:
|
|
114
|
+
raise InvalidError("Memory snapshots are not supported by persistent Sandboxes.")
|
|
115
|
+
if create_args.env or create_args.tags:
|
|
116
|
+
raise InvalidError("`env` and `tags` are not supported by persistent Sandboxes yet.")
|
|
117
|
+
|
|
118
|
+
secrets = create_args.secrets or []
|
|
119
|
+
common = SandboxCreateCommon(create_args, secrets=_resolvable_secrets(secrets))
|
|
120
|
+
async with sandbox_create_context(app, client) as (client, load_context):
|
|
121
|
+
await common.load_dependencies(Resolver(), load_context, load_ephemeral_bucket_secrets=False)
|
|
122
|
+
cloud_bucket_mount_protos, cloud_bucket_credentials = cloud_bucket_mounts_to_proto(
|
|
123
|
+
common.cloud_bucket_mounts, split_ephemeral_credentials=True
|
|
124
|
+
)
|
|
125
|
+
if cloud_bucket_credentials or any(source.HasField("env") for source in _secret_sources(secrets)):
|
|
126
|
+
raise InvalidError("Ephemeral credentials are not supported by persistent Sandboxes yet.")
|
|
127
|
+
req = api_pb2.PersistentSandboxCreateRequest(
|
|
128
|
+
app_id=load_context.app_id or "",
|
|
129
|
+
definition=common.definition(cloud_bucket_mounts=cloud_bucket_mount_protos),
|
|
130
|
+
)
|
|
131
|
+
resp = await client._stub.PersistentSandboxCreate(req, metadata=await auth_metadata(client))
|
|
132
|
+
app_id = load_context.app_id or ""
|
|
133
|
+
impl = cls(client, resp.sandbox_id, persistent_sandbox_id=resp.persistent_sandbox_id)
|
|
134
|
+
return impl, api_pb2.SandboxHandleMetadata(app_id=app_id)
|
|
135
|
+
|
|
136
|
+
# Persistent Sandboxes are only created through `create`, and can't be looked up yet.
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
async def lookup_by_name(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxGetFromNameResponse:
|
|
140
|
+
raise NotImplementedError
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
async def lookup_by_id(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxWaitResponse:
|
|
144
|
+
raise NotImplementedError
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
async def list_page(cls, *args: Any, **kwargs: Any) -> api_pb2.SandboxListResponse:
|
|
148
|
+
raise NotImplementedError
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
async def restore_from_memory_snapshot(
|
|
152
|
+
cls, *args: Any, **kwargs: Any
|
|
153
|
+
) -> "tuple[SandboxImpl, api_pb2.SandboxHandleMetadata]":
|
|
154
|
+
raise NotImplementedError
|
|
155
|
+
|
|
156
|
+
def clone(self) -> "SandboxImplPersistent":
|
|
157
|
+
return copy.copy(self)
|
|
158
|
+
|
|
159
|
+
def make_stdio(self, check_open: Callable[[], None]) -> tuple[StreamReader[str], StreamReader[str], StreamWriter]:
|
|
160
|
+
"""Create stdio streams of the entrypoint of whichever session is current when they connect."""
|
|
161
|
+
weak_self = weakref.ref(self)
|
|
162
|
+
|
|
163
|
+
async def resolve_router() -> tuple[str, TaskCommandRouterClient]:
|
|
164
|
+
this = weak_self()
|
|
165
|
+
if this is None:
|
|
166
|
+
raise RuntimeError("Sandbox was garbage collected before its stdio reader connected")
|
|
167
|
+
return await command_router(this)
|
|
168
|
+
|
|
169
|
+
def reader(file_descriptor: "api_pb2.FileDescriptor.ValueType") -> StreamReader[str]:
|
|
170
|
+
return StreamReader(
|
|
171
|
+
_StreamReaderThroughSandboxCommandRouterParams(
|
|
172
|
+
file_descriptor=file_descriptor, sandbox_id=self.sandbox_id, resolve_router=resolve_router
|
|
173
|
+
),
|
|
174
|
+
by_line=True,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
stdin = StreamWriter(
|
|
178
|
+
_StreamWriterThroughCommandRouterSandboxParams(resolve_router=resolve_router, check_open=check_open)
|
|
179
|
+
)
|
|
180
|
+
return reader(api_pb2.FILE_DESCRIPTOR_STDOUT), reader(api_pb2.FILE_DESCRIPTOR_STDERR), stdin
|
|
181
|
+
|
|
182
|
+
# Session replacement
|
|
183
|
+
|
|
184
|
+
async def _run(self, op: Callable[[SandboxImplV2], Awaitable[_T]]) -> _T:
|
|
185
|
+
"""Run `op` on the session, switching to the running session and retrying once if it fails."""
|
|
186
|
+
session = self._delegate
|
|
187
|
+
try:
|
|
188
|
+
return await op(session)
|
|
189
|
+
except _NON_RECOVERABLE_ERRORS:
|
|
190
|
+
raise
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
if self._terminated:
|
|
193
|
+
# A terminated persistent Sandbox is stopped for good, so it isn't resumed on use.
|
|
194
|
+
raise
|
|
195
|
+
replacement = await self._replace_session(session)
|
|
196
|
+
if replacement is None:
|
|
197
|
+
raise
|
|
198
|
+
logger.debug(f"Retrying operation on persistent Sandbox {self._persistent_sandbox_id} after: {exc!r}")
|
|
199
|
+
return await op(replacement)
|
|
200
|
+
|
|
201
|
+
async def _get_running_session_id(self) -> str:
|
|
202
|
+
req = api_pb2.PersistentSandboxGetRequest(persistent_sandbox_id=self._persistent_sandbox_id)
|
|
203
|
+
resp = await self._client._stub.PersistentSandboxGet(req, metadata=await auth_metadata(self._client))
|
|
204
|
+
return resp.sandbox_id
|
|
205
|
+
|
|
206
|
+
async def _resume_session(self) -> str:
|
|
207
|
+
"""Resume the persistent Sandbox, returning the ID of its running session."""
|
|
208
|
+
req = api_pb2.PersistentSandboxResumeRequest(persistent_sandbox_id=self._persistent_sandbox_id)
|
|
209
|
+
deadline = time.monotonic() + _RESUME_RETRY_TIMEOUT
|
|
210
|
+
while True:
|
|
211
|
+
try:
|
|
212
|
+
resp = await self._client._stub.PersistentSandboxResume(req, metadata=await auth_metadata(self._client))
|
|
213
|
+
return resp.sandbox_id
|
|
214
|
+
except GRPCError as exc:
|
|
215
|
+
# UNAVAILABLE means the previous session is still pausing or another resume is in progress.
|
|
216
|
+
if exc.status != Status.UNAVAILABLE or time.monotonic() > deadline:
|
|
217
|
+
raise
|
|
218
|
+
await asyncio.sleep(_RESUME_RETRY_INTERVAL)
|
|
219
|
+
|
|
220
|
+
async def _replace_session(self, failed: SandboxImplV2 | None, *, resume: bool = True) -> SandboxImplV2 | None:
|
|
221
|
+
"""Switch from the `failed` session, or return None if it is still the running session."""
|
|
222
|
+
# Lazily create the lock on the event loop that first uses the Sandbox.
|
|
223
|
+
if self._replace_lock is None:
|
|
224
|
+
self._replace_lock = asyncio.Lock()
|
|
225
|
+
async with self._replace_lock:
|
|
226
|
+
if self._session is not failed:
|
|
227
|
+
# A concurrent operation has already replaced the session.
|
|
228
|
+
return self._session
|
|
229
|
+
|
|
230
|
+
session_id = await self._get_running_session_id()
|
|
231
|
+
if failed is not None and session_id == failed.sandbox_id:
|
|
232
|
+
return None
|
|
233
|
+
if not session_id:
|
|
234
|
+
if not resume:
|
|
235
|
+
return None
|
|
236
|
+
session_id = await self._resume_session()
|
|
237
|
+
logger.debug(f"Switching persistent Sandbox {self._persistent_sandbox_id} to session {session_id}")
|
|
238
|
+
replacement = SandboxImplV2(self.client, session_id)
|
|
239
|
+
self._session = replacement
|
|
240
|
+
if failed is not None:
|
|
241
|
+
await failed.close()
|
|
242
|
+
return replacement
|
|
243
|
+
|
|
244
|
+
# Operations
|
|
245
|
+
|
|
246
|
+
async def close(self) -> None:
|
|
247
|
+
await self._session.close()
|
|
248
|
+
|
|
249
|
+
def _set_terminated_result(self) -> None:
|
|
250
|
+
self.result = _TERMINATED_RESULT
|
|
251
|
+
|
|
252
|
+
async def wait(self, raise_on_termination: bool) -> None:
|
|
253
|
+
# A persistent Sandbox keeps running across sessions, until it is terminated.
|
|
254
|
+
while not self._terminated:
|
|
255
|
+
await asyncio.sleep(_WAIT_POLL_INTERVAL)
|
|
256
|
+
self._set_terminated_result()
|
|
257
|
+
if raise_on_termination:
|
|
258
|
+
raise SandboxTerminatedError()
|
|
259
|
+
|
|
260
|
+
async def poll(self) -> None:
|
|
261
|
+
if self._terminated:
|
|
262
|
+
self._set_terminated_result()
|
|
263
|
+
else:
|
|
264
|
+
self.result = None
|
|
265
|
+
|
|
266
|
+
async def terminate(self) -> None:
|
|
267
|
+
req = api_pb2.PersistentSandboxTerminateRequest(persistent_sandbox_id=self._persistent_sandbox_id)
|
|
268
|
+
await self._client._stub.PersistentSandboxTerminate(req, metadata=await auth_metadata(self._client))
|
|
269
|
+
self._terminated = True
|
|
270
|
+
self._set_terminated_result()
|
|
271
|
+
|
|
272
|
+
async def pause(self) -> None:
|
|
273
|
+
"""Stop the current session, which saves its exit snapshot for the next `resume`."""
|
|
274
|
+
self._check_not_terminated()
|
|
275
|
+
session = self._delegate
|
|
276
|
+
await session.terminate()
|
|
277
|
+
await session.wait(raise_on_termination=False)
|
|
278
|
+
|
|
279
|
+
async def resume(self) -> None:
|
|
280
|
+
"""Start a session from the latest exit snapshot, unless one is already running."""
|
|
281
|
+
self._check_not_terminated()
|
|
282
|
+
session = self._delegate
|
|
283
|
+
session_id = await self._resume_session()
|
|
284
|
+
if session_id != session.sandbox_id:
|
|
285
|
+
self._session = SandboxImplV2(self.client, session_id)
|
|
286
|
+
await session.close()
|
|
287
|
+
|
|
288
|
+
def _check_not_terminated(self) -> None:
|
|
289
|
+
if self._terminated:
|
|
290
|
+
raise InvalidError(f"Persistent Sandbox {self._persistent_sandbox_id} has been terminated.")
|
|
291
|
+
|
|
292
|
+
async def tunnels(self, timeout: int) -> dict[int, Tunnel]:
|
|
293
|
+
return await self._run(lambda d: d.tunnels(timeout))
|
|
294
|
+
|
|
295
|
+
async def get_task_id(
|
|
296
|
+
self, raise_if_task_complete: bool = False, timeout: float | None = None, retry_transient: bool = False
|
|
297
|
+
) -> str:
|
|
298
|
+
return await self._run(
|
|
299
|
+
lambda d: d.get_task_id(
|
|
300
|
+
raise_if_task_complete=raise_if_task_complete, timeout=timeout, retry_transient=retry_transient
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
async def resolve_task_id_for_logs(self) -> str:
|
|
305
|
+
return await self._delegate.resolve_task_id_for_logs()
|
|
306
|
+
|
|
307
|
+
async def get_exit_snapshot(self, timeout: float | None) -> _Image:
|
|
308
|
+
return await self._delegate.get_exit_snapshot(timeout)
|
|
309
|
+
|
|
310
|
+
async def snapshot_memory(self) -> _SandboxSnapshot:
|
|
311
|
+
raise NotImplementedError("Memory snapshots aren't supported for persistent Sandboxes.")
|
|
312
|
+
|
|
313
|
+
async def get_command_router_client(self, task_id: str) -> TaskCommandRouterClient:
|
|
314
|
+
# The task ID belongs to the current delegate, so this can't be retried on a replacement.
|
|
315
|
+
return await self._delegate.get_command_router_client(task_id)
|
|
316
|
+
|
|
317
|
+
async def get_tags(self) -> dict[str, str]:
|
|
318
|
+
raise NotImplementedError("Tags aren't supported for persistent Sandboxes yet.")
|
|
319
|
+
|
|
320
|
+
async def set_tags(self, tags: dict[str, str]) -> None:
|
|
321
|
+
raise NotImplementedError("Tags aren't supported for persistent Sandboxes yet.")
|
|
322
|
+
|
|
323
|
+
async def set_name(self, name: str) -> None:
|
|
324
|
+
raise InvalidError("The name of a persistent Sandbox can't be changed.")
|
|
325
|
+
|
|
326
|
+
async def create_connect_token(
|
|
327
|
+
self, req: api_pb2.SandboxCreateConnectTokenRequest
|
|
328
|
+
) -> api_pb2.SandboxCreateConnectTokenResponse:
|
|
329
|
+
def create(d: SandboxImplV2) -> Awaitable[api_pb2.SandboxCreateConnectTokenResponse]:
|
|
330
|
+
delegate_req = api_pb2.SandboxCreateConnectTokenRequest()
|
|
331
|
+
delegate_req.CopyFrom(req)
|
|
332
|
+
delegate_req.sandbox_id = d.sandbox_id
|
|
333
|
+
return d.create_connect_token(delegate_req)
|
|
334
|
+
|
|
335
|
+
return await self._run(create)
|
|
336
|
+
|
|
337
|
+
async def wait_until_ready(self, timeout: int) -> None:
|
|
338
|
+
await self._run(lambda d: d.wait_until_ready(timeout))
|
|
339
|
+
|
|
340
|
+
async def set_network_access(self, network_access: api_pb2.NetworkAccess) -> None:
|
|
341
|
+
raise NotImplementedError("Updating network access isn't supported for persistent Sandboxes yet.")
|
|
342
|
+
|
|
343
|
+
async def update_outbound_policy(self, outbound_policy: _OutboundPolicy) -> None:
|
|
344
|
+
raise NotImplementedError("Updating the outbound policy isn't supported for persistent Sandboxes yet.")
|
|
345
|
+
|
|
346
|
+
async def snapshot_filesystem(self, timeout: int, *, ttl: int | None, container_id: str = "") -> _Image:
|
|
347
|
+
return await self._run(lambda d: d.snapshot_filesystem(timeout, ttl=ttl, container_id=container_id))
|
|
348
|
+
|
|
349
|
+
async def mount_image(
|
|
350
|
+
self,
|
|
351
|
+
path: PurePosixPath | str,
|
|
352
|
+
image_id: str,
|
|
353
|
+
*,
|
|
354
|
+
encryption_key: bytes | None,
|
|
355
|
+
container_id: str = "",
|
|
356
|
+
) -> None:
|
|
357
|
+
raise NotImplementedError("Mounting images isn't supported for persistent Sandboxes yet.")
|
|
358
|
+
|
|
359
|
+
async def unmount_image(self, path: PurePosixPath | str, *, container_id: str = "") -> None:
|
|
360
|
+
raise NotImplementedError("Mounting images isn't supported for persistent Sandboxes yet.")
|
|
361
|
+
|
|
362
|
+
async def snapshot_directory(
|
|
363
|
+
self,
|
|
364
|
+
path: PurePosixPath | str,
|
|
365
|
+
*,
|
|
366
|
+
timeout: int,
|
|
367
|
+
ttl: int | None,
|
|
368
|
+
encryption_key: bytes | None,
|
|
369
|
+
container_id: str = "",
|
|
370
|
+
) -> _Image:
|
|
371
|
+
return await self._run(
|
|
372
|
+
lambda d: d.snapshot_directory(
|
|
373
|
+
path, timeout=timeout, ttl=ttl, encryption_key=encryption_key, container_id=container_id
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
async def reload_volumes(self, *, timeout: int, container_id: str = "") -> None:
|
|
378
|
+
await self._run(lambda d: d.reload_volumes(timeout=timeout, container_id=container_id))
|
|
379
|
+
|
|
380
|
+
async def exec(
|
|
381
|
+
self,
|
|
382
|
+
*args: str,
|
|
383
|
+
pty_info: api_pb2.PTYInfo | None,
|
|
384
|
+
stdout: StreamType,
|
|
385
|
+
stderr: StreamType,
|
|
386
|
+
timeout: int | None,
|
|
387
|
+
workdir: str | None,
|
|
388
|
+
env: dict[str, str | None] | None,
|
|
389
|
+
secrets: Collection[_Secret] | None,
|
|
390
|
+
text: bool,
|
|
391
|
+
bufsize: Literal[-1, 1],
|
|
392
|
+
container_id: str | None,
|
|
393
|
+
) -> _ContainerProcess[bytes] | _ContainerProcess[str]:
|
|
394
|
+
return await self._run(
|
|
395
|
+
lambda d: d.exec(
|
|
396
|
+
*args,
|
|
397
|
+
pty_info=pty_info,
|
|
398
|
+
stdout=stdout,
|
|
399
|
+
stderr=stderr,
|
|
400
|
+
timeout=timeout,
|
|
401
|
+
workdir=workdir,
|
|
402
|
+
env=env,
|
|
403
|
+
secrets=secrets,
|
|
404
|
+
text=text,
|
|
405
|
+
bufsize=bufsize,
|
|
406
|
+
container_id=container_id,
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Sidecar containers
|
|
411
|
+
|
|
412
|
+
def validate_sidecar_create(
|
|
413
|
+
self,
|
|
414
|
+
*,
|
|
415
|
+
cloud_bucket_mounts: Sequence[tuple[str, _CloudBucketMount]],
|
|
416
|
+
has_proxy: bool,
|
|
417
|
+
include_oidc_identity_token: bool,
|
|
418
|
+
) -> None:
|
|
419
|
+
self._delegate.validate_sidecar_create(
|
|
420
|
+
cloud_bucket_mounts=cloud_bucket_mounts,
|
|
421
|
+
has_proxy=has_proxy,
|
|
422
|
+
include_oidc_identity_token=include_oidc_identity_token,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
async def create_sidecar(
|
|
426
|
+
self,
|
|
427
|
+
*args: str,
|
|
428
|
+
name: str,
|
|
429
|
+
image_id: str,
|
|
430
|
+
secret_ids: Sequence[str],
|
|
431
|
+
secret_sources: Sequence[api_pb2.SecretSource],
|
|
432
|
+
workdir: str | None,
|
|
433
|
+
volume_mounts: Sequence[api_pb2.VolumeMount],
|
|
434
|
+
cloud_bucket_mounts: Sequence[tuple[str, _CloudBucketMount]],
|
|
435
|
+
network_access: api_pb2.NetworkAccess,
|
|
436
|
+
include_oidc_identity_token: bool,
|
|
437
|
+
proxy_id: str | None,
|
|
438
|
+
pty_info: api_pb2.PTYInfo | None,
|
|
439
|
+
memory_reserve_consume_mib: int | None,
|
|
440
|
+
) -> tuple[str, str]:
|
|
441
|
+
return await self._run(
|
|
442
|
+
lambda d: d.create_sidecar(
|
|
443
|
+
*args,
|
|
444
|
+
name=name,
|
|
445
|
+
image_id=image_id,
|
|
446
|
+
secret_ids=secret_ids,
|
|
447
|
+
secret_sources=secret_sources,
|
|
448
|
+
workdir=workdir,
|
|
449
|
+
volume_mounts=volume_mounts,
|
|
450
|
+
cloud_bucket_mounts=cloud_bucket_mounts,
|
|
451
|
+
network_access=network_access,
|
|
452
|
+
include_oidc_identity_token=include_oidc_identity_token,
|
|
453
|
+
proxy_id=proxy_id,
|
|
454
|
+
pty_info=pty_info,
|
|
455
|
+
memory_reserve_consume_mib=memory_reserve_consume_mib,
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
async def get_sidecar(self, name: str, include_terminated: bool) -> sr_pb2.TaskContainerInfo:
|
|
460
|
+
return await self._run(lambda d: d.get_sidecar(name, include_terminated))
|
|
461
|
+
|
|
462
|
+
async def list_sidecars(self, include_terminated: bool) -> list[sr_pb2.TaskContainerInfo]:
|
|
463
|
+
return await self._run(lambda d: d.list_sidecars(include_terminated))
|
|
464
|
+
|
|
465
|
+
async def wait_sidecar(self, container_id: str, timeout: float) -> api_pb2.GenericResult | None:
|
|
466
|
+
# Sidecar containers don't carry over to a replacement delegate.
|
|
467
|
+
return await self._delegate.wait_sidecar(container_id, timeout)
|
|
468
|
+
|
|
469
|
+
async def terminate_sidecar(self, container_id: str) -> None:
|
|
470
|
+
await self._delegate.terminate_sidecar(container_id)
|
modal/sandbox/_impl_v1.py
CHANGED
|
@@ -40,6 +40,7 @@ from ..io_streams import (
|
|
|
40
40
|
from ..secret import _Secret
|
|
41
41
|
from ..stream_type import StreamType
|
|
42
42
|
from ._common import (
|
|
43
|
+
_PERSISTENT_ONLY_MESSAGE,
|
|
43
44
|
_TASK_ID_POLL_INTERVAL,
|
|
44
45
|
ExitSnapshotPoll,
|
|
45
46
|
SandboxCreateArgs,
|
|
@@ -124,6 +125,9 @@ class SandboxImplV1:
|
|
|
124
125
|
def _client(self) -> _Client:
|
|
125
126
|
return self.client
|
|
126
127
|
|
|
128
|
+
def clone(self) -> "SandboxImplV1":
|
|
129
|
+
return type(self)(self.client, self.sandbox_id, result=self.result)
|
|
130
|
+
|
|
127
131
|
@classmethod
|
|
128
132
|
async def create(
|
|
129
133
|
cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
|
|
@@ -289,6 +293,12 @@ class SandboxImplV1:
|
|
|
289
293
|
req = api_pb2.SandboxTerminateRequest(sandbox_id=self.sandbox_id)
|
|
290
294
|
await self.client._stub.SandboxTerminate(req)
|
|
291
295
|
|
|
296
|
+
async def pause(self) -> None:
|
|
297
|
+
raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_pause()"))
|
|
298
|
+
|
|
299
|
+
async def resume(self) -> None:
|
|
300
|
+
raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_resume()"))
|
|
301
|
+
|
|
292
302
|
async def snapshot_filesystem(self, timeout: int, *, ttl: int | None, container_id: str = "") -> _Image:
|
|
293
303
|
if not container_id and os.environ.get("MODAL_USE_LEGACY_FILESYSTEM_SNAPSHOT") == "1":
|
|
294
304
|
if ttl != DEFAULT_SNAPSHOT_TTL:
|
modal/sandbox/_impl_v2.py
CHANGED
|
@@ -32,6 +32,7 @@ from ..io_streams import (
|
|
|
32
32
|
from ..secret import _resolvable_secrets, _Secret, _secret_sources
|
|
33
33
|
from ..stream_type import StreamType
|
|
34
34
|
from ._common import (
|
|
35
|
+
_PERSISTENT_ONLY_MESSAGE,
|
|
35
36
|
_TASK_ID_POLL_INTERVAL,
|
|
36
37
|
ExitSnapshotPoll,
|
|
37
38
|
SandboxCreateArgs,
|
|
@@ -147,6 +148,9 @@ class SandboxImplV2:
|
|
|
147
148
|
def _client(self) -> _Client:
|
|
148
149
|
return self.client
|
|
149
150
|
|
|
151
|
+
def clone(self) -> "SandboxImplV2":
|
|
152
|
+
return type(self)(self.client, self.sandbox_id, result=self.result)
|
|
153
|
+
|
|
150
154
|
@classmethod
|
|
151
155
|
async def create(
|
|
152
156
|
cls, create_args: SandboxCreateArgs, *, app: "modal.app._App | None", client: _Client | None
|
|
@@ -351,6 +355,12 @@ class SandboxImplV2:
|
|
|
351
355
|
req = api_pb2.SandboxTerminateRequest(sandbox_id=self.sandbox_id)
|
|
352
356
|
await self.client._stub.SandboxTerminateV2(req, metadata=await self._metadata())
|
|
353
357
|
|
|
358
|
+
async def pause(self) -> None:
|
|
359
|
+
raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_pause()"))
|
|
360
|
+
|
|
361
|
+
async def resume(self) -> None:
|
|
362
|
+
raise NotImplementedError(_PERSISTENT_ONLY_MESSAGE.format("_experimental_resume()"))
|
|
363
|
+
|
|
354
364
|
def _create_sidecar_via_control_plane(self) -> bool:
|
|
355
365
|
# Opt out via the `use_control_plane_sidecar_create` config setting to create
|
|
356
366
|
# sidecars over the task command router instead.
|
modal/sandbox/_sandbox.py
CHANGED
|
@@ -46,6 +46,7 @@ from ._common import (
|
|
|
46
46
|
default_pty_info,
|
|
47
47
|
)
|
|
48
48
|
from ._filesystem import _SandboxFilesystem
|
|
49
|
+
from ._impl_persistent import SandboxImplPersistent
|
|
49
50
|
from ._impl_v1 import SandboxImplV1, command_router_access_fetcher as v1_command_router_access_fetcher
|
|
50
51
|
from ._impl_v2 import SandboxImplV2, command_router_access_fetcher as v2_command_router_access_fetcher
|
|
51
52
|
from ._sidecar import _SidecarManager
|
|
@@ -69,6 +70,8 @@ def impl_class_for_sandbox_id(sandbox_id: str) -> type[SandboxImpl]:
|
|
|
69
70
|
|
|
70
71
|
def impl_class_for_create_args(create_args: SandboxCreateArgs) -> type[SandboxImpl]:
|
|
71
72
|
"""Select the implementation that creates the Sandbox described by `create_args`."""
|
|
73
|
+
if create_args.persistent:
|
|
74
|
+
return SandboxImplPersistent
|
|
72
75
|
if create_args.gpu is not None or create_args.network_file_systems or create_args.pty_info is not None:
|
|
73
76
|
# GPUs, network file systems, and PTYs are not supported on V2, so those calls use the V1
|
|
74
77
|
# backend regardless of the setting.
|
|
@@ -183,7 +186,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
183
186
|
env: dict[str, str | None] | None = None,
|
|
184
187
|
secrets: Collection[_Secret] | None = None,
|
|
185
188
|
network_file_systems: dict[str | os.PathLike, _NetworkFileSystem] = {},
|
|
186
|
-
timeout: int = 300,
|
|
189
|
+
timeout: int | None = 300,
|
|
187
190
|
idle_timeout: int | None = None,
|
|
188
191
|
workdir: str | None = None,
|
|
189
192
|
gpu: str | None = None,
|
|
@@ -209,6 +212,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
209
212
|
verbose: bool = False,
|
|
210
213
|
experimental_options: dict[str, Any] | None = None,
|
|
211
214
|
_experimental_enable_snapshot: bool = False,
|
|
215
|
+
_experimental_persistent: bool = False,
|
|
212
216
|
client: _Client | None = None,
|
|
213
217
|
environment_name: str | None = None, # *DEPRECATED*
|
|
214
218
|
pty_info: api_pb2.PTYInfo | None = None, # *DEPRECATED*
|
|
@@ -349,6 +353,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
349
353
|
custom_domain=custom_domain,
|
|
350
354
|
include_oidc_identity_token=include_oidc_identity_token,
|
|
351
355
|
runtime=runtime,
|
|
356
|
+
persistent=_experimental_persistent,
|
|
352
357
|
)
|
|
353
358
|
impl, metadata = await impl_class_for_create_args(create_args).create(create_args, app=app, client=client)
|
|
354
359
|
return _Sandbox._from_impl(impl, metadata)
|
|
@@ -495,8 +500,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
495
500
|
super()._initialize_from_other(other)
|
|
496
501
|
self._attached = other._attached
|
|
497
502
|
self._app_id = other._app_id
|
|
498
|
-
|
|
499
|
-
self._impl = type(other_impl)(other_impl.client, other_impl.sandbox_id, result=other_impl.result)
|
|
503
|
+
self._impl = other._impl.clone()
|
|
500
504
|
|
|
501
505
|
def _initialize_from_empty(self):
|
|
502
506
|
super()._initialize_from_empty()
|
|
@@ -1029,6 +1033,20 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1029
1033
|
return self.returncode
|
|
1030
1034
|
return None
|
|
1031
1035
|
|
|
1036
|
+
async def _experimental_pause(self) -> None:
|
|
1037
|
+
"""Stop the Sandbox's current container, keeping its state so it can be resumed.
|
|
1038
|
+
|
|
1039
|
+
Only supported for experimental persistent Sandboxes.
|
|
1040
|
+
"""
|
|
1041
|
+
await self._impl.pause()
|
|
1042
|
+
|
|
1043
|
+
async def _experimental_resume(self) -> None:
|
|
1044
|
+
"""Resume a paused Sandbox from its saved state.
|
|
1045
|
+
|
|
1046
|
+
Only supported for experimental persistent Sandboxes.
|
|
1047
|
+
"""
|
|
1048
|
+
await self._impl.resume()
|
|
1049
|
+
|
|
1032
1050
|
async def poll(self) -> int | None:
|
|
1033
1051
|
"""Check if the Sandbox has finished running.
|
|
1034
1052
|
|
modal/sandbox/sandbox.pyi
CHANGED
|
@@ -1333,7 +1333,7 @@ class Sandbox(modal.object.Object):
|
|
|
1333
1333
|
network_file_systems: dict[
|
|
1334
1334
|
typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
|
|
1335
1335
|
] = {},
|
|
1336
|
-
timeout: int = 300,
|
|
1336
|
+
timeout: typing.Optional[int] = 300,
|
|
1337
1337
|
idle_timeout: typing.Optional[int] = None,
|
|
1338
1338
|
workdir: typing.Optional[str] = None,
|
|
1339
1339
|
gpu: typing.Optional[str] = None,
|
|
@@ -1362,6 +1362,7 @@ class Sandbox(modal.object.Object):
|
|
|
1362
1362
|
verbose: bool = False,
|
|
1363
1363
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
1364
1364
|
_experimental_enable_snapshot: bool = False,
|
|
1365
|
+
_experimental_persistent: bool = False,
|
|
1365
1366
|
client: typing.Optional[modal.client.Client] = None,
|
|
1366
1367
|
environment_name: typing.Optional[str] = None,
|
|
1367
1368
|
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
@@ -1457,7 +1458,7 @@ class Sandbox(modal.object.Object):
|
|
|
1457
1458
|
network_file_systems: dict[
|
|
1458
1459
|
typing.Union[str, os.PathLike], modal.network_file_system.NetworkFileSystem
|
|
1459
1460
|
] = {},
|
|
1460
|
-
timeout: int = 300,
|
|
1461
|
+
timeout: typing.Optional[int] = 300,
|
|
1461
1462
|
idle_timeout: typing.Optional[int] = None,
|
|
1462
1463
|
workdir: typing.Optional[str] = None,
|
|
1463
1464
|
gpu: typing.Optional[str] = None,
|
|
@@ -1486,6 +1487,7 @@ class Sandbox(modal.object.Object):
|
|
|
1486
1487
|
verbose: bool = False,
|
|
1487
1488
|
experimental_options: typing.Optional[dict[str, typing.Any]] = None,
|
|
1488
1489
|
_experimental_enable_snapshot: bool = False,
|
|
1490
|
+
_experimental_persistent: bool = False,
|
|
1489
1491
|
client: typing.Optional[modal.client.Client] = None,
|
|
1490
1492
|
environment_name: typing.Optional[str] = None,
|
|
1491
1493
|
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
@@ -2582,6 +2584,40 @@ class Sandbox(modal.object.Object):
|
|
|
2582
2584
|
|
|
2583
2585
|
terminate: __terminate_spec
|
|
2584
2586
|
|
|
2587
|
+
class ___experimental_pause_spec(typing_extensions.Protocol):
|
|
2588
|
+
def __call__(self, /) -> None:
|
|
2589
|
+
"""Stop the Sandbox's current container, keeping its state so it can be resumed.
|
|
2590
|
+
|
|
2591
|
+
Only supported for experimental persistent Sandboxes.
|
|
2592
|
+
"""
|
|
2593
|
+
...
|
|
2594
|
+
|
|
2595
|
+
async def aio(self, /) -> None:
|
|
2596
|
+
"""Stop the Sandbox's current container, keeping its state so it can be resumed.
|
|
2597
|
+
|
|
2598
|
+
Only supported for experimental persistent Sandboxes.
|
|
2599
|
+
"""
|
|
2600
|
+
...
|
|
2601
|
+
|
|
2602
|
+
_experimental_pause: ___experimental_pause_spec
|
|
2603
|
+
|
|
2604
|
+
class ___experimental_resume_spec(typing_extensions.Protocol):
|
|
2605
|
+
def __call__(self, /) -> None:
|
|
2606
|
+
"""Resume a paused Sandbox from its saved state.
|
|
2607
|
+
|
|
2608
|
+
Only supported for experimental persistent Sandboxes.
|
|
2609
|
+
"""
|
|
2610
|
+
...
|
|
2611
|
+
|
|
2612
|
+
async def aio(self, /) -> None:
|
|
2613
|
+
"""Resume a paused Sandbox from its saved state.
|
|
2614
|
+
|
|
2615
|
+
Only supported for experimental persistent Sandboxes.
|
|
2616
|
+
"""
|
|
2617
|
+
...
|
|
2618
|
+
|
|
2619
|
+
_experimental_resume: ___experimental_resume_spec
|
|
2620
|
+
|
|
2585
2621
|
class __poll_spec(typing_extensions.Protocol):
|
|
2586
2622
|
def __call__(self, /) -> typing.Optional[int]:
|
|
2587
2623
|
"""Check if the Sandbox has finished running.
|
|
@@ -8,7 +8,7 @@ modal/_environments.py,sha256=O7YagqEUhmxEG5Q10Ch6FDSIsAKFbKXULLaeRw0uCio,27602
|
|
|
8
8
|
modal/_function_variants.py,sha256=xF34OxGaZbmxe0chAJCQb9Uyc8TP78VG8MO55deBWLs,24398
|
|
9
9
|
modal/_functions.py,sha256=-yGdaDkb2IPKDu4TWl93HpiWEU2Qv3bBGrSklEjiE3I,118392
|
|
10
10
|
modal/_grpc_client.py,sha256=ajCxTqhDk-rpWuYmUScRXwYMPVmvujo8ckvvv4l0czk,7088
|
|
11
|
-
modal/_image.py,sha256=
|
|
11
|
+
modal/_image.py,sha256=RrTSL4WBuaCpZwvimgK8jvQWzwIvBOd2crKM_QYonOM,133085
|
|
12
12
|
modal/_ipython.py,sha256=Bu9fc3hm3xt2O47qLe_d-Mu2jS81LwMsmXNJ1sawBA0,575
|
|
13
13
|
modal/_load_context.py,sha256=wrDqD3KehhFepp0vBhyz_PoLWoRVD0gZHP8dlLHCTiw,4818
|
|
14
14
|
modal/_location.py,sha256=joiX-0ZeutEUDTrrqLF1GHXCdVLF-rHzstocbMcd_-k,366
|
|
@@ -39,7 +39,7 @@ modal/billing.py,sha256=E1-P_P8bJFLp-bTaggYd4BaaViImPB5UdtXiY8VeAZQ,390
|
|
|
39
39
|
modal/billing.pyi,sha256=4m7VLYNS8IiJ2I3CJx0awvrOM6BILWRVLfdeqtAYr8M,7189
|
|
40
40
|
modal/call_graph.py,sha256=XGALIDn1bLchWo_goyX23bcPjzCZcfqKUo8INIfDapo,1775
|
|
41
41
|
modal/client.py,sha256=cstrL0wpXCZmcvRs7Sy3QRw9Vz8zkZy8NNk0tPe38sk,22852
|
|
42
|
-
modal/client.pyi,sha256=
|
|
42
|
+
modal/client.pyi,sha256=Rezm_F0bqn00RhEQ0cCYxSqFdofGx4A61ZDQIBpDpFk,23488
|
|
43
43
|
modal/cloud_bucket_mount.py,sha256=ZOg69dhIFcJcwim4JP-6s1tqwOYAfgS9806jRpspifw,7511
|
|
44
44
|
modal/cloud_bucket_mount.pyi,sha256=HoPe_NG5jYdVfmTNY-ZJ3-C8Eo_FzJovl9orJRjkGbE,10252
|
|
45
45
|
modal/cls.py,sha256=JfDAUMrkhasm26lK7kDUNarXZA_7AvZrymf7XNSGTDM,41777
|
|
@@ -58,11 +58,11 @@ modal/file_pattern_matcher.py,sha256=CpaNNj7oBlOSewX-AAUZCF8KDxH0_DmgwzfpVGbPYJ4
|
|
|
58
58
|
modal/functions.py,sha256=-5aogviGxcNXX4qZ34GSBMKnLqH-WH0eb4cGbHpCYYw,382
|
|
59
59
|
modal/functions.pyi,sha256=lnDLXYKS86J57g0gmNOWeY70KIyIho945CDSkPDN5lU,62671
|
|
60
60
|
modal/image.py,sha256=c0lUSAoU-Ip3g5DGZ0uhdCzoWSQWh1g0i-7Rxo-Sz-I,160
|
|
61
|
-
modal/image.pyi,sha256=
|
|
61
|
+
modal/image.pyi,sha256=L53g84DVKa94LvqSqqYZLpG2o9-vxcC5vp5pXKQB3f0,61210
|
|
62
62
|
modal/io_streams.py,sha256=iWwhGCcTwhNtIo2t2f-qVV3FBEK1DIi40Tm_Ai01eYA,32992
|
|
63
63
|
modal/io_streams.pyi,sha256=ciTHe1pyl6Wii7QEI5cPRmnHBLOw3sOh_AnWsQlpzTw,26345
|
|
64
|
-
modal/mount.py,sha256=
|
|
65
|
-
modal/mount.pyi,sha256=
|
|
64
|
+
modal/mount.py,sha256=Wmt6F0aUqTZHQycu_Rs-qrHlngdtNnSgLGLVv4ex4mY,38767
|
|
65
|
+
modal/mount.pyi,sha256=RjYRGZzFe0RYpnZT6aXkZRsIsFyPAa9F-3gz9uXkRfA,16639
|
|
66
66
|
modal/network_file_system.py,sha256=2uNLvtvN-WnDJlriKqQE5e4Eh10dJHFGGIEQH7xlaKc,14617
|
|
67
67
|
modal/network_file_system.pyi,sha256=Z1BFJJSd99g1IKZXydikE6fuYWu0vQ2uBsApG9WX4H8,18037
|
|
68
68
|
modal/object.py,sha256=bTeskuY8JFrESjU4_UL_nTwYlBQdOLmVaOX3X6EMxsg,164
|
|
@@ -127,7 +127,7 @@ modal/_utils/display_utils.py,sha256=5XyCCjnMAWeLEEqF0hiLSA6XrrNp-gvXVsLcngN6vAI
|
|
|
127
127
|
modal/_utils/docker_utils.py,sha256=k8fkYT6G-X_B3iamjxiSpwSE-k58xGVHtsk2_gR7i2Q,4197
|
|
128
128
|
modal/_utils/function_utils.py,sha256=dVCCnovfSlYssRuQh0Woq5oqX4Kj_tAK4vJ3USFNUFw,27537
|
|
129
129
|
modal/_utils/git_utils.py,sha256=Lat_l4O4hwOYgylEtWup3hyqWT8D7JoCIjCflXcX8BE,3148
|
|
130
|
-
modal/_utils/grpc_testing.py,sha256=
|
|
130
|
+
modal/_utils/grpc_testing.py,sha256=59FDtDsSeKrp9gmFI41dP_67yxxxydm06BhixpEnf2k,8828
|
|
131
131
|
modal/_utils/grpc_utils.py,sha256=Gcl5EyEMEHMaMlGh90F63Oi1l9K6cnmeTCbGCouwJ-A,29998
|
|
132
132
|
modal/_utils/hash_utils.py,sha256=WHuMRwmB4fnaM-qmyWXR87C2CinvLnmRoygSzsTEunA,3008
|
|
133
133
|
modal/_utils/http_utils.py,sha256=TO9nnKR0Cc-iWCmkou56wJh9MHgNZwQMosK8WK-GD5Y,3133
|
|
@@ -153,7 +153,7 @@ modal/builder/2023.12.txt,sha256=OjsbXFkCSdkzzryZP82Q73osr5wxQ6EUzmGcK7twfkA,502
|
|
|
153
153
|
modal/builder/2024.04.txt,sha256=6NnrbIE-mflwMyKyQ0tsWeY8XFE1kSW9oE8DVDoD8QU,544
|
|
154
154
|
modal/builder/2024.10.txt,sha256=qD-5cVIVM9wXesJ6JC89Ew-3m2KjEElUz3jaw_MddRo,296
|
|
155
155
|
modal/builder/2025.06.txt,sha256=90NzKtbJ32RrUtYiUv1XtJTDEaJ1SFcVpJXUE7Udle4,791
|
|
156
|
-
modal/builder/PREVIEW.txt,sha256=
|
|
156
|
+
modal/builder/PREVIEW.txt,sha256=bBLNruBc0v4ulzCraMBVfVm-6kPku3Ag1nFnTy1kr_o,326
|
|
157
157
|
modal/builder/README.md,sha256=9tK76KP0Uph7O0M5oUgsSwEZDj5y-dcUPsnpR0Sc-Ik,854
|
|
158
158
|
modal/builder/base-images.json,sha256=5wyWfI3fQ51PH_dHvmXeTJPrKQBdi5B3DihQIWEXC4Q,1202
|
|
159
159
|
modal/cli/__init__.py,sha256=6FRleWQxBDT19y7OayO4lBOzuL6Bs9r0rLINYYYbHwQ,769
|
|
@@ -198,18 +198,19 @@ modal/experimental/flash_server.py,sha256=iqxVoQij6aM58_h0la6Y_Vy3CCQj8iYmjzt79r
|
|
|
198
198
|
modal/experimental/flash_server.pyi,sha256=LuIBqeOJ1NAapKWnJMRzzrwIHlIK2GcOLOI2r-z_QN0,1025
|
|
199
199
|
modal/experimental/ipython.py,sha256=kJ6o9wkAY_MiSb7paewI7f1MJAeqdi8TX5pBdhr_mUI,2925
|
|
200
200
|
modal/sandbox/__init__.py,sha256=wHE5KCHUaMSK_nNGbn852PI423v3Q4UQEsFpoqNqMMI,203
|
|
201
|
-
modal/sandbox/_common.py,sha256=
|
|
201
|
+
modal/sandbox/_common.py,sha256=Kuibj5eS8upWLbSxtYdojPZtyCh0iLtdsAJ4RuK2Lk4,47098
|
|
202
202
|
modal/sandbox/_filesystem.py,sha256=GdkMNVahPpUpfuY04HwWWwZyMID3bcut7PHXNWOIpYg,28299
|
|
203
|
-
modal/sandbox/
|
|
204
|
-
modal/sandbox/
|
|
205
|
-
modal/sandbox/
|
|
203
|
+
modal/sandbox/_impl_persistent.py,sha256=PwlzsxfQYvsKPHDgmK1LnYSx7CIZSFKCtm_87YBSrnc,20182
|
|
204
|
+
modal/sandbox/_impl_v1.py,sha256=Xy0Mp0_X9T7WAxY20R2NVaobXiC4ciPdsRLC0k4JnCM,20918
|
|
205
|
+
modal/sandbox/_impl_v2.py,sha256=AUCRYVv-qm-eLEOqmsAacwXyRK7VTJ1faPJdwk18q5g,26271
|
|
206
|
+
modal/sandbox/_sandbox.py,sha256=wvchpp_g1zotOMpMhzmiAovaAi6_q_f5_CF2aKzijQ8,61591
|
|
206
207
|
modal/sandbox/_sidecar.py,sha256=k9SMe2V--gkj22fZRC3EN4GNPtdPFkrwJRxB7W4IFU4,21303
|
|
207
208
|
modal/sandbox/_snapshot.py,sha256=8mB6FOecXEbgTC9dyogIEGCxTdjemuFsyfVFQyQ2Cp8,3105
|
|
208
209
|
modal/sandbox/_task_command_router_client.py,sha256=ta_I2EsVruEkRE9YCaCHns4oCb_htz7aQIf0ZSp2wH0,55181
|
|
209
210
|
modal/sandbox/sandbox.py,sha256=4ekghND9qUGf5wtIQH4mVa6PHGjHgY0UN5RKryOM05k,683
|
|
210
|
-
modal/sandbox/sandbox.pyi,sha256=
|
|
211
|
+
modal/sandbox/sandbox.pyi,sha256=iWxqScPKLa6oo9vJGukv2XhuCOK0E-bj52trqetBBh8,138895
|
|
211
212
|
modal/skills/modal/SKILL.md,sha256=Bm-0BPTYppJoeQzA-N-Re8L6-nKnV6gspJXMl4cD0P0,5490
|
|
212
|
-
modal-1.6.3.
|
|
213
|
+
modal-1.6.3.dev7.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
|
213
214
|
modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
|
|
214
215
|
modal_docs/gen_cli_docs.py,sha256=9-b8vl2EsLWD9zJqpCK8IUi5h44Bjn3i3rSdedZV4bA,6415
|
|
215
216
|
modal_docs/gen_cli_docs_main.py,sha256=UPGr7O1DYTIRLwfINe2rbdh89J3cPVTU1OWL-SjxnOQ,198
|
|
@@ -233,10 +234,10 @@ modal_proto/task_command_router_pb2.py,sha256=ysC0d8p3YxwyOpP3B-qDlvE81Yo4ntQiwi
|
|
|
233
234
|
modal_proto/task_command_router_pb2.pyi,sha256=FDcd5t3ZCmQHUjefOzg07iglrgKYcBrzFvdKIWsEMHI,50628
|
|
234
235
|
modal_proto/task_command_router_pb2_grpc.py,sha256=8iGUGwEVbqOsZWPJ7OsQ9OKUGaSCf9Dr2tvb04Ww6d4,44620
|
|
235
236
|
modal_proto/task_command_router_pb2_grpc.pyi,sha256=26Fq9RQo09B9KLHsgdpRuw-I6Z4-2jrXLQkuDzB-moI,15097
|
|
236
|
-
modal_version/__init__.py,sha256=
|
|
237
|
+
modal_version/__init__.py,sha256=_NyIMw3R_WktxIyqvxZFAsrP4rSI-AsLzcRB4crWyC4,120
|
|
237
238
|
modal_version/__main__.py,sha256=nLUbubxKx7GXNzJIkTB7M7_u8080LyLM0IS0rzFkC_c,119
|
|
238
|
-
modal-1.6.3.
|
|
239
|
-
modal-1.6.3.
|
|
240
|
-
modal-1.6.3.
|
|
241
|
-
modal-1.6.3.
|
|
242
|
-
modal-1.6.3.
|
|
239
|
+
modal-1.6.3.dev7.dist-info/METADATA,sha256=YcuXMLRCPRbuK-tN6nj38HqhWH0YinACJZOjRI5NjbI,2577
|
|
240
|
+
modal-1.6.3.dev7.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
241
|
+
modal-1.6.3.dev7.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
|
|
242
|
+
modal-1.6.3.dev7.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
|
|
243
|
+
modal-1.6.3.dev7.dist-info/RECORD,,
|
modal_version/__init__.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|