modal 0.62.115__py3-none-any.whl → 0.72.13__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/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +402 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1025 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +196 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +943 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
- modal-0.72.13.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
modal/mount.py
CHANGED
@@ -9,24 +9,28 @@ import sys
|
|
9
9
|
import sysconfig
|
10
10
|
import time
|
11
11
|
import typing
|
12
|
+
from collections.abc import AsyncGenerator
|
12
13
|
from pathlib import Path, PurePosixPath
|
13
|
-
from typing import
|
14
|
+
from typing import Callable, Optional, Sequence, Union
|
14
15
|
|
15
|
-
import aiostream
|
16
16
|
from google.protobuf.message import Message
|
17
17
|
|
18
18
|
import modal.exception
|
19
|
+
import modal.file_pattern_matcher
|
19
20
|
from modal_proto import api_pb2
|
20
21
|
from modal_version import __version__
|
21
22
|
|
22
23
|
from ._resolver import Resolver
|
23
|
-
from ._utils.async_utils import synchronize_api
|
24
|
+
from ._utils.async_utils import aclosing, async_map, synchronize_api
|
24
25
|
from ._utils.blob_utils import FileUploadSpec, blob_upload_file, get_file_upload_spec_from_path
|
26
|
+
from ._utils.deprecation import deprecation_warning, renamed_parameter
|
25
27
|
from ._utils.grpc_utils import retry_transient_errors
|
28
|
+
from ._utils.name_utils import check_object_name
|
26
29
|
from ._utils.package_utils import get_module_mount_info
|
27
30
|
from .client import _Client
|
28
31
|
from .config import config, logger
|
29
|
-
from .exception import ModuleNotMountable
|
32
|
+
from .exception import InvalidError, ModuleNotMountable
|
33
|
+
from .file_pattern_matcher import FilePatternMatcher
|
30
34
|
from .object import _get_environment_name, _Object
|
31
35
|
|
32
36
|
ROOT_DIR: PurePosixPath = PurePosixPath("/root")
|
@@ -36,14 +40,19 @@ MOUNT_PUT_FILE_CLIENT_TIMEOUT = 10 * 60 # 10 min max for transferring files
|
|
36
40
|
#
|
37
41
|
# These can be updated safely, but changes will trigger a rebuild for all images
|
38
42
|
# that rely on `add_python()` in their constructor.
|
39
|
-
PYTHON_STANDALONE_VERSIONS:
|
40
|
-
"3.8": ("20230826", "3.8.17"),
|
43
|
+
PYTHON_STANDALONE_VERSIONS: dict[str, tuple[str, str]] = {
|
41
44
|
"3.9": ("20230826", "3.9.18"),
|
42
45
|
"3.10": ("20230826", "3.10.13"),
|
43
46
|
"3.11": ("20230826", "3.11.5"),
|
44
47
|
"3.12": ("20240107", "3.12.1"),
|
48
|
+
"3.13": ("20241008", "3.13.0"),
|
45
49
|
}
|
46
50
|
|
51
|
+
MOUNT_DEPRECATION_MESSAGE_PATTERN = """modal.Mount usage will soon be deprecated.
|
52
|
+
|
53
|
+
Use {replacement} instead, which is functionally and performance-wise equivalent.
|
54
|
+
"""
|
55
|
+
|
47
56
|
|
48
57
|
def client_mount_name() -> str:
|
49
58
|
"""Get the deployed name of the client package mount."""
|
@@ -58,7 +67,8 @@ def python_standalone_mount_name(version: str) -> str:
|
|
58
67
|
libc = "gnu"
|
59
68
|
if version not in PYTHON_STANDALONE_VERSIONS:
|
60
69
|
raise modal.exception.InvalidError(
|
61
|
-
f"Unsupported standalone python version: {version!r}, supported values are
|
70
|
+
f"Unsupported standalone python version: {version!r}, supported values are "
|
71
|
+
f"{list(PYTHON_STANDALONE_VERSIONS)}"
|
62
72
|
)
|
63
73
|
if libc != "gnu":
|
64
74
|
raise modal.exception.InvalidError(f"Unsupported libc identifier: {libc}")
|
@@ -72,21 +82,21 @@ class _MountEntry(metaclass=abc.ABCMeta):
|
|
72
82
|
...
|
73
83
|
|
74
84
|
@abc.abstractmethod
|
75
|
-
def get_files_to_upload(self) -> typing.Iterator[
|
85
|
+
def get_files_to_upload(self) -> typing.Iterator[tuple[Path, str]]:
|
76
86
|
...
|
77
87
|
|
78
88
|
@abc.abstractmethod
|
79
|
-
def watch_entry(self) ->
|
89
|
+
def watch_entry(self) -> tuple[Path, Path]:
|
80
90
|
...
|
81
91
|
|
82
92
|
@abc.abstractmethod
|
83
|
-
def top_level_paths(self) ->
|
93
|
+
def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
|
84
94
|
...
|
85
95
|
|
86
96
|
|
87
|
-
def _select_files(entries:
|
97
|
+
def _select_files(entries: list[_MountEntry]) -> list[tuple[Path, PurePosixPath]]:
|
88
98
|
# TODO: make this async
|
89
|
-
all_files:
|
99
|
+
all_files: set[tuple[Path, PurePosixPath]] = set()
|
90
100
|
for entry in entries:
|
91
101
|
all_files |= set(entry.get_files_to_upload())
|
92
102
|
return list(all_files)
|
@@ -101,7 +111,7 @@ class _MountFile(_MountEntry):
|
|
101
111
|
return str(self.local_file)
|
102
112
|
|
103
113
|
def get_files_to_upload(self):
|
104
|
-
local_file = self.local_file.
|
114
|
+
local_file = self.local_file.resolve()
|
105
115
|
if not local_file.exists():
|
106
116
|
raise FileNotFoundError(local_file)
|
107
117
|
|
@@ -112,7 +122,7 @@ class _MountFile(_MountEntry):
|
|
112
122
|
safe_path = self.local_file.expanduser().absolute()
|
113
123
|
return safe_path.parent, safe_path
|
114
124
|
|
115
|
-
def top_level_paths(self) ->
|
125
|
+
def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
|
116
126
|
return [(self.local_file, self.remote_path)]
|
117
127
|
|
118
128
|
|
@@ -120,13 +130,15 @@ class _MountFile(_MountEntry):
|
|
120
130
|
class _MountDir(_MountEntry):
|
121
131
|
local_dir: Path
|
122
132
|
remote_path: PurePosixPath
|
123
|
-
|
133
|
+
ignore: Callable[[Path], bool]
|
124
134
|
recursive: bool
|
125
135
|
|
126
136
|
def description(self):
|
127
137
|
return str(self.local_dir.expanduser().absolute())
|
128
138
|
|
129
139
|
def get_files_to_upload(self):
|
140
|
+
# we can't use .resolve() eagerly here since that could end up "renaming" symlinked files
|
141
|
+
# see test_mount_directory_with_symlinked_file
|
130
142
|
local_dir = self.local_dir.expanduser().absolute()
|
131
143
|
|
132
144
|
if not local_dir.exists():
|
@@ -141,25 +153,48 @@ class _MountDir(_MountEntry):
|
|
141
153
|
gen = (dir_entry.path for dir_entry in os.scandir(local_dir) if dir_entry.is_file())
|
142
154
|
|
143
155
|
for local_filename in gen:
|
144
|
-
|
145
|
-
|
156
|
+
local_path = Path(local_filename)
|
157
|
+
if not self.ignore(local_path):
|
158
|
+
local_relpath = local_path.expanduser().absolute().relative_to(local_dir)
|
146
159
|
mount_path = self.remote_path / local_relpath.as_posix()
|
147
|
-
yield
|
160
|
+
yield local_path.resolve(), mount_path
|
148
161
|
|
149
162
|
def watch_entry(self):
|
150
163
|
return self.local_dir.resolve().expanduser(), None
|
151
164
|
|
152
|
-
def top_level_paths(self) ->
|
165
|
+
def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
|
153
166
|
return [(self.local_dir, self.remote_path)]
|
154
167
|
|
155
168
|
|
156
|
-
def module_mount_condition(
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
169
|
+
def module_mount_condition(module_base: Path):
|
170
|
+
SKIP_BYTECODE = True # hard coded for now
|
171
|
+
SKIP_DOT_PREFIXED = True
|
172
|
+
|
173
|
+
def condition(f: str):
|
174
|
+
path = Path(f)
|
175
|
+
if SKIP_BYTECODE and path.suffix == ".pyc":
|
176
|
+
return False
|
177
|
+
|
178
|
+
# Check parent dir names to see if file should be included,
|
179
|
+
# but ignore dir names above root of mounted module:
|
180
|
+
# /a/.venv/site-packages/mymod/foo.py should be included by default
|
181
|
+
# /a/my_mod/.config/foo.py should *not* be included by default
|
182
|
+
while path != module_base and path != path.parent:
|
183
|
+
if SKIP_BYTECODE and path.name == "__pycache__":
|
184
|
+
return False
|
185
|
+
|
186
|
+
if SKIP_DOT_PREFIXED and path.name.startswith("."):
|
187
|
+
return False
|
188
|
+
|
189
|
+
path = path.parent
|
190
|
+
|
191
|
+
return True
|
192
|
+
|
193
|
+
return condition
|
194
|
+
|
195
|
+
|
196
|
+
def module_mount_ignore_condition(module_base: Path):
|
197
|
+
return lambda f: not module_mount_condition(module_base)(str(f))
|
163
198
|
|
164
199
|
|
165
200
|
@dataclasses.dataclass
|
@@ -170,12 +205,12 @@ class _MountedPythonModule(_MountEntry):
|
|
170
205
|
|
171
206
|
module_name: str
|
172
207
|
remote_dir: Union[PurePosixPath, str] = ROOT_DIR.as_posix() # cast needed here for type stub generation...
|
173
|
-
|
208
|
+
ignore: Optional[Callable[[Path], bool]] = None
|
174
209
|
|
175
210
|
def description(self) -> str:
|
176
211
|
return f"PythonPackage:{self.module_name}"
|
177
212
|
|
178
|
-
def _proxy_entries(self) ->
|
213
|
+
def _proxy_entries(self) -> list[_MountEntry]:
|
179
214
|
mount_infos = get_module_mount_info(self.module_name)
|
180
215
|
entries = []
|
181
216
|
for mount_info in mount_infos:
|
@@ -184,9 +219,9 @@ class _MountedPythonModule(_MountEntry):
|
|
184
219
|
remote_dir = PurePosixPath(self.remote_dir, *self.module_name.split("."))
|
185
220
|
entries.append(
|
186
221
|
_MountDir(
|
187
|
-
|
222
|
+
base_path,
|
188
223
|
remote_path=remote_dir,
|
189
|
-
|
224
|
+
ignore=self.ignore or module_mount_ignore_condition(base_path),
|
190
225
|
recursive=True,
|
191
226
|
)
|
192
227
|
)
|
@@ -201,16 +236,16 @@ class _MountedPythonModule(_MountEntry):
|
|
201
236
|
)
|
202
237
|
return entries
|
203
238
|
|
204
|
-
def get_files_to_upload(self) -> typing.Iterator[
|
239
|
+
def get_files_to_upload(self) -> typing.Iterator[tuple[Path, str]]:
|
205
240
|
for entry in self._proxy_entries():
|
206
241
|
yield from entry.get_files_to_upload()
|
207
242
|
|
208
|
-
def watch_entry(self) ->
|
243
|
+
def watch_entry(self) -> tuple[Path, Path]:
|
209
244
|
for entry in self._proxy_entries():
|
210
245
|
# TODO: fix watch for mounts of multi-path packages
|
211
246
|
return entry.watch_entry()
|
212
247
|
|
213
|
-
def top_level_paths(self) ->
|
248
|
+
def top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
|
214
249
|
paths = []
|
215
250
|
for sub in self._proxy_entries():
|
216
251
|
paths.extend(sub.top_level_paths())
|
@@ -231,7 +266,7 @@ class _Mount(_Object, type_prefix="mo"):
|
|
231
266
|
```python
|
232
267
|
import modal
|
233
268
|
import os
|
234
|
-
app = modal.App()
|
269
|
+
app = modal.App()
|
235
270
|
|
236
271
|
@app.function(mounts=[modal.Mount.from_local_dir("~/foo", remote_path="/root/foo")])
|
237
272
|
def f():
|
@@ -243,14 +278,14 @@ class _Mount(_Object, type_prefix="mo"):
|
|
243
278
|
the file's contents to skip uploading files that have been uploaded before.
|
244
279
|
"""
|
245
280
|
|
246
|
-
_entries: Optional[
|
281
|
+
_entries: Optional[list[_MountEntry]] = None
|
247
282
|
_deployment_name: Optional[str] = None
|
248
283
|
_namespace: Optional[int] = None
|
249
284
|
_environment_name: Optional[str] = None
|
250
285
|
_content_checksum_sha256_hex: Optional[str] = None
|
251
286
|
|
252
287
|
@staticmethod
|
253
|
-
def _new(entries:
|
288
|
+
def _new(entries: list[_MountEntry] = []) -> "_Mount":
|
254
289
|
rep = f"Mount({entries})"
|
255
290
|
|
256
291
|
async def mount_content_deduplication_key():
|
@@ -279,10 +314,10 @@ class _Mount(_Object, type_prefix="mo"):
|
|
279
314
|
assert isinstance(handle_metadata, api_pb2.MountHandleMetadata)
|
280
315
|
self._content_checksum_sha256_hex = handle_metadata.content_checksum_sha256_hex
|
281
316
|
|
282
|
-
def _top_level_paths(self) ->
|
317
|
+
def _top_level_paths(self) -> list[tuple[Path, PurePosixPath]]:
|
283
318
|
# Returns [(local_absolute_path, remote_path), ...] for all top level entries in the Mount
|
284
319
|
# Used to determine if a package mount is installed in a sys directory or not
|
285
|
-
res:
|
320
|
+
res: list[tuple[Path, PurePosixPath]] = []
|
286
321
|
for entry in self.entries:
|
287
322
|
res.extend(entry.top_level_paths())
|
288
323
|
return res
|
@@ -293,13 +328,29 @@ class _Mount(_Object, type_prefix="mo"):
|
|
293
328
|
# we can't rely on it to be set. Let's clean this up later.
|
294
329
|
return getattr(self, "_is_local", False)
|
295
330
|
|
331
|
+
@staticmethod
|
332
|
+
def _add_local_dir(
|
333
|
+
local_path: Path,
|
334
|
+
remote_path: PurePosixPath,
|
335
|
+
ignore: Callable[[Path], bool] = modal.file_pattern_matcher._NOTHING,
|
336
|
+
):
|
337
|
+
return _Mount._new()._extend(
|
338
|
+
_MountDir(
|
339
|
+
local_dir=local_path,
|
340
|
+
ignore=ignore,
|
341
|
+
remote_path=remote_path,
|
342
|
+
recursive=True,
|
343
|
+
),
|
344
|
+
)
|
345
|
+
|
296
346
|
def add_local_dir(
|
297
347
|
self,
|
298
348
|
local_path: Union[str, Path],
|
299
349
|
*,
|
300
350
|
# Where the directory is placed within in the mount
|
301
351
|
remote_path: Union[str, PurePosixPath, None] = None,
|
302
|
-
#
|
352
|
+
# Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
|
353
|
+
# Defaults to including all files.
|
303
354
|
condition: Optional[Callable[[str], bool]] = None,
|
304
355
|
# add files from subdirectories as well
|
305
356
|
recursive: bool = True,
|
@@ -318,10 +369,13 @@ class _Mount(_Object, type_prefix="mo"):
|
|
318
369
|
|
319
370
|
condition = include_all
|
320
371
|
|
372
|
+
def converted_condition(path: Path) -> bool:
|
373
|
+
return not condition(str(path))
|
374
|
+
|
321
375
|
return self._extend(
|
322
376
|
_MountDir(
|
323
377
|
local_dir=local_path,
|
324
|
-
|
378
|
+
ignore=converted_condition,
|
325
379
|
remote_path=remote_path,
|
326
380
|
recursive=recursive,
|
327
381
|
),
|
@@ -333,7 +387,8 @@ class _Mount(_Object, type_prefix="mo"):
|
|
333
387
|
*,
|
334
388
|
# Where the directory is placed within in the mount
|
335
389
|
remote_path: Union[str, PurePosixPath, None] = None,
|
336
|
-
#
|
390
|
+
# Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
|
391
|
+
# Defaults to including all files.
|
337
392
|
condition: Optional[Callable[[str], bool]] = None,
|
338
393
|
# add files from subdirectories as well
|
339
394
|
recursive: bool = True,
|
@@ -351,12 +406,31 @@ class _Mount(_Object, type_prefix="mo"):
|
|
351
406
|
)
|
352
407
|
```
|
353
408
|
"""
|
409
|
+
deprecation_warning(
|
410
|
+
(2024, 1, 8), MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_dir"), pending=True
|
411
|
+
)
|
412
|
+
return _Mount._from_local_dir(local_path, remote_path=remote_path, condition=condition, recursive=recursive)
|
413
|
+
|
414
|
+
@staticmethod
|
415
|
+
def _from_local_dir(
|
416
|
+
local_path: Union[str, Path],
|
417
|
+
*,
|
418
|
+
# Where the directory is placed within in the mount
|
419
|
+
remote_path: Union[str, PurePosixPath, None] = None,
|
420
|
+
# Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
|
421
|
+
# Defaults to including all files.
|
422
|
+
condition: Optional[Callable[[str], bool]] = None,
|
423
|
+
# add files from subdirectories as well
|
424
|
+
recursive: bool = True,
|
425
|
+
) -> "_Mount":
|
354
426
|
return _Mount._new().add_local_dir(
|
355
427
|
local_path, remote_path=remote_path, condition=condition, recursive=recursive
|
356
428
|
)
|
357
429
|
|
358
430
|
def add_local_file(
|
359
|
-
self,
|
431
|
+
self,
|
432
|
+
local_path: Union[str, Path],
|
433
|
+
remote_path: Union[str, PurePosixPath, None] = None,
|
360
434
|
) -> "_Mount":
|
361
435
|
"""
|
362
436
|
Add a local file to the `Mount` object.
|
@@ -387,24 +461,32 @@ class _Mount(_Object, type_prefix="mo"):
|
|
387
461
|
)
|
388
462
|
```
|
389
463
|
"""
|
464
|
+
deprecation_warning(
|
465
|
+
(2024, 1, 8), MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_file"), pending=True
|
466
|
+
)
|
467
|
+
return _Mount._from_local_file(local_path, remote_path)
|
468
|
+
|
469
|
+
@staticmethod
|
470
|
+
def _from_local_file(local_path: Union[str, Path], remote_path: Union[str, PurePosixPath, None] = None) -> "_Mount":
|
390
471
|
return _Mount._new().add_local_file(local_path, remote_path=remote_path)
|
391
472
|
|
392
473
|
@staticmethod
|
393
|
-
def _description(entries:
|
474
|
+
def _description(entries: list[_MountEntry]) -> str:
|
394
475
|
local_contents = [e.description() for e in entries]
|
395
476
|
return ", ".join(local_contents)
|
396
477
|
|
397
478
|
@staticmethod
|
398
|
-
async def _get_files(entries:
|
479
|
+
async def _get_files(entries: list[_MountEntry]) -> AsyncGenerator[FileUploadSpec, None]:
|
399
480
|
loop = asyncio.get_event_loop()
|
400
481
|
with concurrent.futures.ThreadPoolExecutor() as exe:
|
401
482
|
all_files = await loop.run_in_executor(exe, _select_files, entries)
|
402
483
|
|
403
484
|
futs = []
|
404
485
|
for local_filename, remote_filename in all_files:
|
486
|
+
logger.debug(f"Mounting {local_filename} as {remote_filename}")
|
405
487
|
futs.append(loop.run_in_executor(exe, get_file_upload_spec_from_path, local_filename, remote_filename))
|
406
488
|
|
407
|
-
logger.debug(f"Computing checksums for {len(futs)} files using {exe._max_workers}
|
489
|
+
logger.debug(f"Computing checksums for {len(futs)} files using {exe._max_workers} worker threads")
|
408
490
|
for fut in asyncio.as_completed(futs):
|
409
491
|
try:
|
410
492
|
yield await fut
|
@@ -458,7 +540,9 @@ class _Mount(_Object, type_prefix="mo"):
|
|
458
540
|
logger.debug(f"Creating blob file for {file_spec.source_description} ({file_spec.size} bytes)")
|
459
541
|
async with blob_upload_concurrency:
|
460
542
|
with file_spec.source() as fp:
|
461
|
-
blob_id = await blob_upload_file(
|
543
|
+
blob_id = await blob_upload_file(
|
544
|
+
fp, resolver.client.stub, sha256_hex=file_spec.sha256_hex, md5_hex=file_spec.md5_hex
|
545
|
+
)
|
462
546
|
logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
|
463
547
|
request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
|
464
548
|
else:
|
@@ -476,13 +560,14 @@ class _Mount(_Object, type_prefix="mo"):
|
|
476
560
|
|
477
561
|
raise modal.exception.MountUploadTimeoutError(f"Mounting of {file_spec.source_description} timed out")
|
478
562
|
|
479
|
-
# Create the asynchronous iterable for file specs.
|
480
|
-
file_specs = aiostream.stream.iterate(_Mount._get_files(self._entries))
|
481
|
-
|
482
563
|
# Upload files, or check if they already exist.
|
483
564
|
n_concurrent_uploads = 512
|
484
|
-
|
485
|
-
|
565
|
+
files: list[api_pb2.MountFile] = []
|
566
|
+
async with aclosing(
|
567
|
+
async_map(_Mount._get_files(self._entries), _put_file, concurrency=n_concurrent_uploads)
|
568
|
+
) as stream:
|
569
|
+
async for file in stream:
|
570
|
+
files.append(file)
|
486
571
|
|
487
572
|
if not files:
|
488
573
|
logger.warning(f"Mount of '{message_label}' is empty.")
|
@@ -497,12 +582,19 @@ class _Mount(_Object, type_prefix="mo"):
|
|
497
582
|
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS,
|
498
583
|
files=files,
|
499
584
|
)
|
500
|
-
|
585
|
+
elif resolver.app_id is not None:
|
501
586
|
req = api_pb2.MountGetOrCreateRequest(
|
502
587
|
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
|
503
588
|
files=files,
|
504
589
|
app_id=resolver.app_id,
|
505
590
|
)
|
591
|
+
else:
|
592
|
+
req = api_pb2.MountGetOrCreateRequest(
|
593
|
+
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL,
|
594
|
+
files=files,
|
595
|
+
environment_name=resolver.environment_name,
|
596
|
+
)
|
597
|
+
|
506
598
|
resp = await retry_transient_errors(resolver.client.stub.MountGetOrCreate, req, base_delay=1)
|
507
599
|
status_row.finish(f"Created mount {message_label}")
|
508
600
|
|
@@ -513,10 +605,15 @@ class _Mount(_Object, type_prefix="mo"):
|
|
513
605
|
def from_local_python_packages(
|
514
606
|
*module_names: str,
|
515
607
|
remote_dir: Union[str, PurePosixPath] = ROOT_DIR.as_posix(),
|
608
|
+
# Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
|
609
|
+
# Defaults to including all files.
|
516
610
|
condition: Optional[Callable[[str], bool]] = None,
|
611
|
+
ignore: Optional[Union[Sequence[str], Callable[[Path], bool]]] = None,
|
517
612
|
) -> "_Mount":
|
518
|
-
"""
|
519
|
-
|
613
|
+
"""
|
614
|
+
Returns a `modal.Mount` that makes local modules listed in `module_names` available inside the container.
|
615
|
+
This works by mounting the local path of each module's package to a directory inside the container
|
616
|
+
that's on `PYTHONPATH`.
|
520
617
|
|
521
618
|
**Usage**
|
522
619
|
|
@@ -524,7 +621,7 @@ class _Mount(_Object, type_prefix="mo"):
|
|
524
621
|
import modal
|
525
622
|
import my_local_module
|
526
623
|
|
527
|
-
app = modal.App()
|
624
|
+
app = modal.App()
|
528
625
|
|
529
626
|
@app.function(mounts=[
|
530
627
|
modal.Mount.from_local_python_packages("my_local_module", "my_other_module"),
|
@@ -533,27 +630,58 @@ class _Mount(_Object, type_prefix="mo"):
|
|
533
630
|
my_local_module.do_stuff()
|
534
631
|
```
|
535
632
|
"""
|
633
|
+
deprecation_warning(
|
634
|
+
(2024, 1, 8),
|
635
|
+
MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_python_source"),
|
636
|
+
pending=True,
|
637
|
+
)
|
638
|
+
return _Mount._from_local_python_packages(
|
639
|
+
*module_names, remote_dir=remote_dir, condition=condition, ignore=ignore
|
640
|
+
)
|
536
641
|
|
642
|
+
@staticmethod
|
643
|
+
def _from_local_python_packages(
|
644
|
+
*module_names: str,
|
645
|
+
remote_dir: Union[str, PurePosixPath] = ROOT_DIR.as_posix(),
|
646
|
+
# Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
|
647
|
+
# Defaults to including all files.
|
648
|
+
condition: Optional[Callable[[str], bool]] = None,
|
649
|
+
ignore: Optional[Union[Sequence[str], Callable[[Path], bool]]] = None,
|
650
|
+
) -> "_Mount":
|
537
651
|
# Don't re-run inside container.
|
538
652
|
|
653
|
+
if condition is not None:
|
654
|
+
if ignore is not None:
|
655
|
+
raise InvalidError("Cannot specify both `ignore` and `condition`")
|
656
|
+
|
657
|
+
def converted_condition(path: Path) -> bool:
|
658
|
+
return not condition(str(path))
|
659
|
+
|
660
|
+
ignore = converted_condition
|
661
|
+
elif isinstance(ignore, list):
|
662
|
+
ignore = FilePatternMatcher(*ignore)
|
663
|
+
|
539
664
|
mount = _Mount._new()
|
540
|
-
from .execution_context import is_local
|
665
|
+
from ._runtime.execution_context import is_local
|
541
666
|
|
542
667
|
if not is_local():
|
543
668
|
return mount # empty/non-mountable mount in case it's used from within a container
|
544
669
|
for module_name in module_names:
|
545
|
-
mount = mount._extend(_MountedPythonModule(module_name, remote_dir,
|
670
|
+
mount = mount._extend(_MountedPythonModule(module_name, remote_dir, ignore))
|
546
671
|
return mount
|
547
672
|
|
548
673
|
@staticmethod
|
674
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
549
675
|
def from_name(
|
550
|
-
|
676
|
+
name: str,
|
551
677
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
552
678
|
environment_name: Optional[str] = None,
|
553
679
|
) -> "_Mount":
|
680
|
+
"""mdmd:hidden"""
|
681
|
+
|
554
682
|
async def _load(provider: _Mount, resolver: Resolver, existing_object_id: Optional[str]):
|
555
683
|
req = api_pb2.MountGetOrCreateRequest(
|
556
|
-
deployment_name=
|
684
|
+
deployment_name=name,
|
557
685
|
namespace=namespace,
|
558
686
|
environment_name=_get_environment_name(environment_name, resolver),
|
559
687
|
)
|
@@ -563,14 +691,16 @@ class _Mount(_Object, type_prefix="mo"):
|
|
563
691
|
return _Mount._from_loader(_load, "Mount()")
|
564
692
|
|
565
693
|
@classmethod
|
694
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
566
695
|
async def lookup(
|
567
|
-
cls:
|
568
|
-
|
696
|
+
cls: type["_Mount"],
|
697
|
+
name: str,
|
569
698
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
570
699
|
client: Optional[_Client] = None,
|
571
700
|
environment_name: Optional[str] = None,
|
572
701
|
) -> "_Mount":
|
573
|
-
|
702
|
+
"""mdmd:hidden"""
|
703
|
+
obj = _Mount.from_name(name, namespace=namespace, environment_name=environment_name)
|
574
704
|
if client is None:
|
575
705
|
client = await _Client.from_env()
|
576
706
|
resolver = Resolver(client=client)
|
@@ -583,13 +713,15 @@ class _Mount(_Object, type_prefix="mo"):
|
|
583
713
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
584
714
|
environment_name: Optional[str] = None,
|
585
715
|
client: Optional[_Client] = None,
|
586
|
-
) ->
|
716
|
+
) -> None:
|
717
|
+
check_object_name(deployment_name, "Mount")
|
718
|
+
environment_name = _get_environment_name(environment_name, resolver=None)
|
587
719
|
self._deployment_name = deployment_name
|
588
720
|
self._namespace = namespace
|
589
721
|
self._environment_name = environment_name
|
590
722
|
if client is None:
|
591
723
|
client = await _Client.from_env()
|
592
|
-
resolver = Resolver(client=client)
|
724
|
+
resolver = Resolver(client=client, environment_name=environment_name)
|
593
725
|
await resolver.load(self)
|
594
726
|
|
595
727
|
def _get_metadata(self) -> api_pb2.MountHandleMetadata:
|
@@ -609,25 +741,27 @@ def _create_client_mount():
|
|
609
741
|
import modal
|
610
742
|
|
611
743
|
# Get the base_path because it also contains `modal_proto`.
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
return (
|
622
|
-
_Mount.from_local_dir(base_path, remote_path="/pkg/", condition=condition, recursive=True)
|
623
|
-
# Mount synchronicity, so version changes don't trigger image rebuilds for users.
|
624
|
-
.add_local_dir(
|
625
|
-
synchronicity.__path__[0],
|
626
|
-
remote_path="/pkg/synchronicity",
|
627
|
-
condition=module_mount_condition,
|
744
|
+
modal_parent_dir, _ = os.path.split(modal.__path__[0])
|
745
|
+
client_mount = _Mount._new()
|
746
|
+
|
747
|
+
for pkg_name in MODAL_PACKAGES:
|
748
|
+
package_base_path = Path(modal_parent_dir) / pkg_name
|
749
|
+
client_mount = client_mount.add_local_dir(
|
750
|
+
package_base_path,
|
751
|
+
remote_path=f"/pkg/{pkg_name}",
|
752
|
+
condition=module_mount_condition(package_base_path),
|
628
753
|
recursive=True,
|
629
754
|
)
|
755
|
+
|
756
|
+
# Mount synchronicity, so version changes don't trigger image rebuilds for users.
|
757
|
+
synchronicity_base_path = Path(synchronicity.__path__[0])
|
758
|
+
client_mount = client_mount.add_local_dir(
|
759
|
+
synchronicity_base_path,
|
760
|
+
remote_path="/pkg/synchronicity",
|
761
|
+
condition=module_mount_condition(synchronicity_base_path),
|
762
|
+
recursive=True,
|
630
763
|
)
|
764
|
+
return client_mount
|
631
765
|
|
632
766
|
|
633
767
|
create_client_mount = synchronize_api(_create_client_mount)
|
@@ -654,25 +788,25 @@ SYS_PREFIXES = {
|
|
654
788
|
)
|
655
789
|
}
|
656
790
|
|
791
|
+
|
657
792
|
SYS_PREFIXES |= {p.resolve() for p in SYS_PREFIXES}
|
658
793
|
|
794
|
+
MODAL_PACKAGES = ["modal", "modal_proto", "modal_version"]
|
795
|
+
|
659
796
|
|
660
797
|
def _is_modal_path(remote_path: PurePosixPath):
|
661
798
|
path_prefix = remote_path.parts[:3]
|
662
799
|
remote_python_paths = [("/", "root"), ("/", "pkg")]
|
663
800
|
for base in remote_python_paths:
|
664
|
-
is_modal_path = path_prefix in [
|
665
|
-
|
666
|
-
|
667
|
-
base + ("modal_version",),
|
668
|
-
base + ("synchronicity",),
|
669
|
-
]
|
801
|
+
is_modal_path = path_prefix in [base + (mod,) for mod in MODAL_PACKAGES] or path_prefix == base + (
|
802
|
+
"synchronicity",
|
803
|
+
)
|
670
804
|
if is_modal_path:
|
671
805
|
return True
|
672
806
|
return False
|
673
807
|
|
674
808
|
|
675
|
-
def get_auto_mounts() ->
|
809
|
+
def get_auto_mounts() -> list[_Mount]:
|
676
810
|
"""mdmd:hidden
|
677
811
|
|
678
812
|
Auto-mount local modules that have been imported in global scope.
|
@@ -698,15 +832,14 @@ def get_auto_mounts() -> typing.List[_Mount]:
|
|
698
832
|
|
699
833
|
try:
|
700
834
|
# at this point we don't know if the sys.modules module should be mounted or not
|
701
|
-
potential_mount = _Mount.
|
835
|
+
potential_mount = _Mount._from_local_python_packages(module_name)
|
702
836
|
mount_paths = potential_mount._top_level_paths()
|
703
837
|
except ModuleNotMountable:
|
704
838
|
# this typically happens if the module is a built-in, has binary components or doesn't exist
|
705
839
|
continue
|
706
840
|
|
707
841
|
for local_path, remote_path in mount_paths:
|
708
|
-
|
709
|
-
if any(str(local_path).startswith(str(p)) for p in SYS_PREFIXES) or _is_modal_path(remote_path):
|
842
|
+
if any(local_path.is_relative_to(p) for p in SYS_PREFIXES) or _is_modal_path(remote_path):
|
710
843
|
# skip any module that has paths in SYS_PREFIXES, or would overwrite the modal Package in the container
|
711
844
|
break
|
712
845
|
else:
|