modal 1.0.3.dev10__py3-none-any.whl → 1.2.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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__init__.py +0 -2
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +15 -3
- modal/_container_entrypoint.py +51 -69
- modal/_functions.py +508 -240
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +81 -21
- modal/_output.py +58 -45
- modal/_partial_function.py +48 -73
- modal/_pty.py +7 -3
- modal/_resolver.py +26 -46
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +358 -220
- modal/_runtime/container_io_manager.pyi +296 -101
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +64 -7
- modal/_runtime/gpu_memory_snapshot.py +262 -57
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +90 -6
- modal/_traceback.py +42 -1
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +84 -29
- modal/_utils/auth_token_manager.py +111 -0
- modal/_utils/blob_utils.py +181 -58
- modal/_utils/deprecation.py +19 -0
- modal/_utils/function_utils.py +91 -47
- modal/_utils/grpc_utils.py +89 -66
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +256 -88
- modal/app.pyi +909 -92
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +18 -0
- modal/builder/PREVIEW.txt +18 -0
- modal/builder/base-images.json +58 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +11 -12
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +91 -23
- modal/cli/secret.py +48 -22
- modal/cli/token.py +7 -8
- modal/cli/utils.py +4 -7
- modal/cli/volume.py +31 -25
- modal/client.py +15 -85
- modal/client.pyi +183 -62
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +197 -5
- modal/cls.py +200 -126
- modal/cls.pyi +446 -68
- modal/config.py +29 -11
- modal/container_process.py +319 -19
- modal/container_process.pyi +190 -20
- modal/dict.py +290 -71
- modal/dict.pyi +835 -83
- modal/environments.py +15 -27
- modal/environments.pyi +46 -24
- modal/exception.py +14 -2
- modal/experimental/__init__.py +194 -40
- modal/experimental/flash.py +618 -0
- modal/experimental/flash.pyi +380 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.py +29 -36
- modal/file_io.pyi +251 -53
- modal/file_pattern_matcher.py +56 -16
- modal/functions.pyi +673 -92
- modal/gpu.py +1 -1
- modal/image.py +528 -176
- modal/image.pyi +1572 -145
- modal/io_streams.py +458 -128
- modal/io_streams.pyi +433 -52
- modal/mount.py +216 -151
- modal/mount.pyi +225 -78
- modal/network_file_system.py +45 -62
- modal/network_file_system.pyi +277 -56
- modal/object.pyi +93 -17
- modal/parallel_map.py +942 -129
- modal/parallel_map.pyi +294 -15
- modal/partial_function.py +0 -2
- modal/partial_function.pyi +234 -19
- modal/proxy.py +17 -8
- modal/proxy.pyi +36 -3
- modal/queue.py +270 -65
- modal/queue.pyi +817 -57
- modal/runner.py +115 -101
- modal/runner.pyi +205 -49
- modal/sandbox.py +512 -136
- modal/sandbox.pyi +845 -111
- modal/schedule.py +1 -1
- modal/secret.py +300 -70
- modal/secret.pyi +589 -34
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/snapshot.pyi +25 -4
- modal/token_flow.py +4 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +416 -158
- modal/volume.pyi +1117 -121
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +17 -4
- modal_proto/api.proto +534 -79
- modal_proto/api_grpc.py +337 -1
- modal_proto/api_pb2.py +1522 -968
- modal_proto/api_pb2.pyi +1619 -134
- modal_proto/api_pb2_grpc.py +699 -4
- modal_proto/api_pb2_grpc.pyi +226 -14
- modal_proto/modal_api_grpc.py +175 -154
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal/requirements/PREVIEW.txt +0 -16
- modal/requirements/base-images.json +0 -26
- modal-1.0.3.dev10.dist-info/RECORD +0 -179
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- /modal/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/mount.py
CHANGED
|
@@ -8,23 +8,24 @@ import re
|
|
|
8
8
|
import time
|
|
9
9
|
import typing
|
|
10
10
|
import warnings
|
|
11
|
-
from collections.abc import AsyncGenerator
|
|
11
|
+
from collections.abc import AsyncGenerator, Generator
|
|
12
12
|
from pathlib import Path, PurePosixPath
|
|
13
13
|
from typing import Callable, Optional, Sequence, Union
|
|
14
14
|
|
|
15
15
|
from google.protobuf.message import Message
|
|
16
|
+
from grpclib import GRPCError
|
|
16
17
|
|
|
17
18
|
import modal.exception
|
|
18
19
|
import modal.file_pattern_matcher
|
|
19
20
|
from modal_proto import api_pb2
|
|
20
21
|
from modal_version import __version__
|
|
21
22
|
|
|
22
|
-
from .
|
|
23
|
+
from ._load_context import LoadContext
|
|
24
|
+
from ._object import _Object
|
|
23
25
|
from ._resolver import Resolver
|
|
24
|
-
from ._utils.async_utils import aclosing, async_map, synchronize_api
|
|
26
|
+
from ._utils.async_utils import TaskContext, aclosing, async_map, synchronize_api
|
|
25
27
|
from ._utils.blob_utils import FileUploadSpec, blob_upload_file, get_file_upload_spec_from_path
|
|
26
|
-
from ._utils.
|
|
27
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
28
|
+
from ._utils.grpc_utils import Retry
|
|
28
29
|
from ._utils.name_utils import check_object_name
|
|
29
30
|
from ._utils.package_utils import get_module_mount_info
|
|
30
31
|
from .client import _Client
|
|
@@ -115,7 +116,8 @@ class _MountFile(_MountEntry):
|
|
|
115
116
|
def get_files_to_upload(self):
|
|
116
117
|
local_file = self.local_file.resolve()
|
|
117
118
|
if not local_file.exists():
|
|
118
|
-
|
|
119
|
+
msg = f"local file {local_file} does not exist"
|
|
120
|
+
raise FileNotFoundError(msg)
|
|
119
121
|
|
|
120
122
|
rel_filename = self.remote_path
|
|
121
123
|
yield local_file, rel_filename
|
|
@@ -132,25 +134,48 @@ class _MountFile(_MountEntry):
|
|
|
132
134
|
class _MountDir(_MountEntry):
|
|
133
135
|
local_dir: Path
|
|
134
136
|
remote_path: PurePosixPath
|
|
135
|
-
ignore: Callable[[Path], bool]
|
|
137
|
+
ignore: Union[Callable[[Path], bool], modal.file_pattern_matcher._AbstractPatternMatcher]
|
|
136
138
|
recursive: bool
|
|
137
139
|
|
|
138
140
|
def description(self):
|
|
139
141
|
return str(self.local_dir.expanduser().absolute())
|
|
140
142
|
|
|
143
|
+
def _walk_and_prune(self, top_dir: Path) -> Generator[str, None, None]:
|
|
144
|
+
"""Walk directories and prune ignored directories early."""
|
|
145
|
+
for root, dirs, files in os.walk(top_dir, topdown=True):
|
|
146
|
+
# with topdown=True, os.walk allows modifying the dirs list in-place, and will only
|
|
147
|
+
# recurse into dirs that are not ignored.
|
|
148
|
+
dirs[:] = [d for d in dirs if not self.ignore(Path(os.path.join(root, d)).relative_to(top_dir))]
|
|
149
|
+
for file in files:
|
|
150
|
+
yield os.path.join(root, file)
|
|
151
|
+
|
|
152
|
+
def _walk_all(self, top_dir: Path) -> Generator[str, None, None]:
|
|
153
|
+
"""Walk all directories without early pruning - safe for complex/inverted ignore patterns."""
|
|
154
|
+
for root, _, files in os.walk(top_dir):
|
|
155
|
+
for file in files:
|
|
156
|
+
yield os.path.join(root, file)
|
|
157
|
+
|
|
141
158
|
def get_files_to_upload(self):
|
|
142
159
|
# we can't use .resolve() eagerly here since that could end up "renaming" symlinked files
|
|
143
160
|
# see test_mount_directory_with_symlinked_file
|
|
144
161
|
local_dir = self.local_dir.expanduser().absolute()
|
|
145
162
|
|
|
146
163
|
if not local_dir.exists():
|
|
147
|
-
|
|
164
|
+
msg = f"local dir {local_dir} does not exist"
|
|
165
|
+
raise FileNotFoundError(msg)
|
|
148
166
|
|
|
149
167
|
if not local_dir.is_dir():
|
|
150
|
-
|
|
168
|
+
msg = f"local dir {local_dir} is not a directory"
|
|
169
|
+
raise NotADirectoryError(msg)
|
|
151
170
|
|
|
152
171
|
if self.recursive:
|
|
153
|
-
|
|
172
|
+
if (
|
|
173
|
+
isinstance(self.ignore, modal.file_pattern_matcher._AbstractPatternMatcher)
|
|
174
|
+
and self.ignore.can_prune_directories()
|
|
175
|
+
):
|
|
176
|
+
gen = self._walk_and_prune(local_dir)
|
|
177
|
+
else:
|
|
178
|
+
gen = self._walk_all(local_dir)
|
|
154
179
|
else:
|
|
155
180
|
gen = (dir_entry.path for dir_entry in os.scandir(local_dir) if dir_entry.is_file())
|
|
156
181
|
|
|
@@ -286,7 +311,8 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
286
311
|
_entries: Optional[list[_MountEntry]] = None
|
|
287
312
|
_deployment_name: Optional[str] = None
|
|
288
313
|
_namespace: Optional[int] = None
|
|
289
|
-
|
|
314
|
+
|
|
315
|
+
_allow_overwrite: bool = False
|
|
290
316
|
_content_checksum_sha256_hex: Optional[str] = None
|
|
291
317
|
|
|
292
318
|
@staticmethod
|
|
@@ -300,7 +326,12 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
300
326
|
return None
|
|
301
327
|
return (_Mount._type_prefix, "local", frozenset(included_files))
|
|
302
328
|
|
|
303
|
-
obj = _Mount._from_loader(
|
|
329
|
+
obj = _Mount._from_loader(
|
|
330
|
+
_Mount._load_mount,
|
|
331
|
+
rep,
|
|
332
|
+
deduplication_key=mount_content_deduplication_key,
|
|
333
|
+
load_context_overrides=LoadContext.empty(),
|
|
334
|
+
)
|
|
304
335
|
obj._entries = entries
|
|
305
336
|
obj._is_local = True
|
|
306
337
|
return obj
|
|
@@ -386,39 +417,6 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
386
417
|
),
|
|
387
418
|
)
|
|
388
419
|
|
|
389
|
-
@staticmethod
|
|
390
|
-
def from_local_dir(
|
|
391
|
-
local_path: Union[str, Path],
|
|
392
|
-
*,
|
|
393
|
-
# Where the directory is placed within in the mount
|
|
394
|
-
remote_path: Union[str, PurePosixPath, None] = None,
|
|
395
|
-
# Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
|
|
396
|
-
# Defaults to including all files.
|
|
397
|
-
condition: Optional[Callable[[str], bool]] = None,
|
|
398
|
-
# add files from subdirectories as well
|
|
399
|
-
recursive: bool = True,
|
|
400
|
-
) -> "_Mount":
|
|
401
|
-
"""
|
|
402
|
-
**Deprecated:** Use image.add_local_dir() instead
|
|
403
|
-
|
|
404
|
-
Create a `Mount` from a local directory.
|
|
405
|
-
|
|
406
|
-
**Usage**
|
|
407
|
-
|
|
408
|
-
```python notest
|
|
409
|
-
assets = modal.Mount.from_local_dir(
|
|
410
|
-
"~/assets",
|
|
411
|
-
condition=lambda pth: not ".venv" in pth,
|
|
412
|
-
remote_path="/assets",
|
|
413
|
-
)
|
|
414
|
-
```
|
|
415
|
-
"""
|
|
416
|
-
deprecation_warning(
|
|
417
|
-
(2025, 1, 8),
|
|
418
|
-
MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_dir"),
|
|
419
|
-
)
|
|
420
|
-
return _Mount._from_local_dir(local_path, remote_path=remote_path, condition=condition, recursive=recursive)
|
|
421
|
-
|
|
422
420
|
@staticmethod
|
|
423
421
|
def _from_local_dir(
|
|
424
422
|
local_path: Union[str, Path],
|
|
@@ -454,29 +452,6 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
454
452
|
),
|
|
455
453
|
)
|
|
456
454
|
|
|
457
|
-
@staticmethod
|
|
458
|
-
def from_local_file(local_path: Union[str, Path], remote_path: Union[str, PurePosixPath, None] = None) -> "_Mount":
|
|
459
|
-
"""
|
|
460
|
-
**Deprecated**: Use image.add_local_file() instead
|
|
461
|
-
|
|
462
|
-
Create a `Mount` mounting a single local file.
|
|
463
|
-
|
|
464
|
-
**Usage**
|
|
465
|
-
|
|
466
|
-
```python notest
|
|
467
|
-
# Mount the DBT profile in user's home directory into container.
|
|
468
|
-
dbt_profiles = modal.Mount.from_local_file(
|
|
469
|
-
local_path="~/profiles.yml",
|
|
470
|
-
remote_path="/root/dbt_profile/profiles.yml",
|
|
471
|
-
)
|
|
472
|
-
```
|
|
473
|
-
"""
|
|
474
|
-
deprecation_warning(
|
|
475
|
-
(2025, 1, 8),
|
|
476
|
-
MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_file"),
|
|
477
|
-
)
|
|
478
|
-
return _Mount._from_local_file(local_path, remote_path)
|
|
479
|
-
|
|
480
455
|
@staticmethod
|
|
481
456
|
def _from_local_file(local_path: Union[str, Path], remote_path: Union[str, PurePosixPath, None] = None) -> "_Mount":
|
|
482
457
|
return _Mount._new().add_local_file(local_path, remote_path=remote_path)
|
|
@@ -508,6 +483,7 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
508
483
|
async def _load_mount(
|
|
509
484
|
self: "_Mount",
|
|
510
485
|
resolver: Resolver,
|
|
486
|
+
load_context: LoadContext,
|
|
511
487
|
existing_object_id: Optional[str],
|
|
512
488
|
):
|
|
513
489
|
t0 = time.monotonic()
|
|
@@ -549,7 +525,7 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
549
525
|
|
|
550
526
|
request = api_pb2.MountPutFileRequest(sha256_hex=file_spec.sha256_hex)
|
|
551
527
|
accounted_hashes.add(file_spec.sha256_hex)
|
|
552
|
-
response = await
|
|
528
|
+
response = await load_context.client.stub.MountPutFile(request, retry=Retry(base_delay=1))
|
|
553
529
|
|
|
554
530
|
if response.exists:
|
|
555
531
|
n_finished += 1
|
|
@@ -563,7 +539,7 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
563
539
|
async with blob_upload_concurrency:
|
|
564
540
|
with file_spec.source() as fp:
|
|
565
541
|
blob_id = await blob_upload_file(
|
|
566
|
-
fp,
|
|
542
|
+
fp, load_context.client.stub, sha256_hex=file_spec.sha256_hex, md5_hex=file_spec.md5_hex
|
|
567
543
|
)
|
|
568
544
|
logger.debug(f"Uploading blob file {file_spec.source_description} as {remote_filename}")
|
|
569
545
|
request2 = api_pb2.MountPutFileRequest(data_blob_id=blob_id, sha256_hex=file_spec.sha256_hex)
|
|
@@ -575,7 +551,7 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
575
551
|
|
|
576
552
|
start_time = time.monotonic()
|
|
577
553
|
while time.monotonic() - start_time < MOUNT_PUT_FILE_CLIENT_TIMEOUT:
|
|
578
|
-
response = await
|
|
554
|
+
response = await load_context.client.stub.MountPutFile(request2, retry=Retry(base_delay=1))
|
|
579
555
|
if response.exists:
|
|
580
556
|
n_finished += 1
|
|
581
557
|
return mount_file
|
|
@@ -583,7 +559,7 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
583
559
|
raise modal.exception.MountUploadTimeoutError(f"Mounting of {file_spec.source_description} timed out")
|
|
584
560
|
|
|
585
561
|
# Upload files, or check if they already exist.
|
|
586
|
-
n_concurrent_uploads =
|
|
562
|
+
n_concurrent_uploads = 64
|
|
587
563
|
files: list[api_pb2.MountFile] = []
|
|
588
564
|
async with aclosing(
|
|
589
565
|
async_map(_Mount._get_files(self._entries), _put_file, concurrency=n_concurrent_uploads)
|
|
@@ -597,70 +573,36 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
597
573
|
# Build the mount.
|
|
598
574
|
status_row.message(f"Creating mount {message_label}: Finalizing index of {len(files)} files")
|
|
599
575
|
if self._deployment_name:
|
|
576
|
+
creation_type = (
|
|
577
|
+
api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING
|
|
578
|
+
if self._allow_overwrite
|
|
579
|
+
else api_pb2.OBJECT_CREATION_TYPE_CREATE_FAIL_IF_EXISTS
|
|
580
|
+
)
|
|
600
581
|
req = api_pb2.MountGetOrCreateRequest(
|
|
601
582
|
deployment_name=self._deployment_name,
|
|
602
583
|
namespace=self._namespace,
|
|
603
|
-
environment_name=
|
|
604
|
-
object_creation_type=
|
|
584
|
+
environment_name=load_context.environment_name,
|
|
585
|
+
object_creation_type=creation_type,
|
|
605
586
|
files=files,
|
|
606
587
|
)
|
|
607
|
-
elif
|
|
588
|
+
elif load_context.app_id is not None:
|
|
608
589
|
req = api_pb2.MountGetOrCreateRequest(
|
|
609
590
|
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
|
|
610
591
|
files=files,
|
|
611
|
-
app_id=
|
|
592
|
+
app_id=load_context.app_id,
|
|
612
593
|
)
|
|
613
594
|
else:
|
|
614
595
|
req = api_pb2.MountGetOrCreateRequest(
|
|
615
596
|
object_creation_type=api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL,
|
|
616
597
|
files=files,
|
|
617
|
-
environment_name=
|
|
598
|
+
environment_name=load_context.environment_name,
|
|
618
599
|
)
|
|
619
600
|
|
|
620
|
-
resp = await
|
|
601
|
+
resp = await load_context.client.stub.MountGetOrCreate(req, retry=Retry(base_delay=1))
|
|
621
602
|
status_row.finish(f"Created mount {message_label}")
|
|
622
603
|
|
|
623
604
|
logger.debug(f"Uploaded {total_uploads} new files and {total_bytes} bytes in {time.monotonic() - t0}s")
|
|
624
|
-
self._hydrate(resp.mount_id,
|
|
625
|
-
|
|
626
|
-
@staticmethod
|
|
627
|
-
def from_local_python_packages(
|
|
628
|
-
*module_names: str,
|
|
629
|
-
remote_dir: Union[str, PurePosixPath] = ROOT_DIR.as_posix(),
|
|
630
|
-
# Predicate filter function for file selection, which should accept a filepath and return `True` for inclusion.
|
|
631
|
-
# Defaults to including all files.
|
|
632
|
-
condition: Optional[Callable[[str], bool]] = None,
|
|
633
|
-
ignore: Optional[Union[Sequence[str], Callable[[Path], bool]]] = None,
|
|
634
|
-
) -> "_Mount":
|
|
635
|
-
"""
|
|
636
|
-
**Deprecated**: Use image.add_local_python_source instead
|
|
637
|
-
|
|
638
|
-
Returns a `modal.Mount` that makes local modules listed in `module_names` available inside the container.
|
|
639
|
-
This works by mounting the local path of each module's package to a directory inside the container
|
|
640
|
-
that's on `PYTHONPATH`.
|
|
641
|
-
|
|
642
|
-
**Usage**
|
|
643
|
-
|
|
644
|
-
```python notest
|
|
645
|
-
import modal
|
|
646
|
-
import my_local_module
|
|
647
|
-
|
|
648
|
-
app = modal.App()
|
|
649
|
-
|
|
650
|
-
@app.function(mounts=[
|
|
651
|
-
modal.Mount.from_local_python_packages("my_local_module", "my_other_module"),
|
|
652
|
-
])
|
|
653
|
-
def f():
|
|
654
|
-
my_local_module.do_stuff()
|
|
655
|
-
```
|
|
656
|
-
"""
|
|
657
|
-
deprecation_warning(
|
|
658
|
-
(2025, 1, 8),
|
|
659
|
-
MOUNT_DEPRECATION_MESSAGE_PATTERN.format(replacement="image.add_local_python_source"),
|
|
660
|
-
)
|
|
661
|
-
return _Mount._from_local_python_packages(
|
|
662
|
-
*module_names, remote_dir=remote_dir, condition=condition, ignore=ignore
|
|
663
|
-
)
|
|
605
|
+
self._hydrate(resp.mount_id, load_context.client, resp.handle_metadata)
|
|
664
606
|
|
|
665
607
|
@staticmethod
|
|
666
608
|
def _from_local_python_packages(
|
|
@@ -693,58 +635,42 @@ class _Mount(_Object, type_prefix="mo"):
|
|
|
693
635
|
*,
|
|
694
636
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
|
695
637
|
environment_name: Optional[str] = None,
|
|
638
|
+
client: Optional[_Client] = None,
|
|
696
639
|
) -> "_Mount":
|
|
697
640
|
"""mdmd:hidden"""
|
|
698
641
|
|
|
699
|
-
async def _load(provider: _Mount, resolver: Resolver, existing_object_id: Optional[str]):
|
|
642
|
+
async def _load(provider: _Mount, resolver: Resolver, load_context, existing_object_id: Optional[str]):
|
|
700
643
|
req = api_pb2.MountGetOrCreateRequest(
|
|
701
644
|
deployment_name=name,
|
|
702
645
|
namespace=namespace,
|
|
703
|
-
environment_name=
|
|
646
|
+
environment_name=load_context.environment_name,
|
|
704
647
|
)
|
|
705
|
-
response = await
|
|
706
|
-
provider._hydrate(response.mount_id,
|
|
707
|
-
|
|
708
|
-
return _Mount._from_loader(
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
name: str,
|
|
714
|
-
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
|
715
|
-
client: Optional[_Client] = None,
|
|
716
|
-
environment_name: Optional[str] = None,
|
|
717
|
-
) -> "_Mount":
|
|
718
|
-
"""mdmd:hidden"""
|
|
719
|
-
deprecation_warning(
|
|
720
|
-
(2025, 1, 27),
|
|
721
|
-
"`modal.Mount.lookup` is deprecated and will be removed in a future release."
|
|
722
|
-
" It can be replaced with `modal.Mount.from_name`."
|
|
723
|
-
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
648
|
+
response = await load_context.client.stub.MountGetOrCreate(req)
|
|
649
|
+
provider._hydrate(response.mount_id, load_context.client, response.handle_metadata)
|
|
650
|
+
|
|
651
|
+
return _Mount._from_loader(
|
|
652
|
+
_load,
|
|
653
|
+
"Mount()",
|
|
654
|
+
hydrate_lazily=True,
|
|
655
|
+
load_context_overrides=LoadContext(environment_name=environment_name, client=client),
|
|
724
656
|
)
|
|
725
|
-
obj = _Mount.from_name(name, namespace=namespace, environment_name=environment_name)
|
|
726
|
-
if client is None:
|
|
727
|
-
client = await _Client.from_env()
|
|
728
|
-
resolver = Resolver(client=client)
|
|
729
|
-
await resolver.load(obj)
|
|
730
|
-
return obj
|
|
731
657
|
|
|
732
658
|
async def _deploy(
|
|
733
659
|
self: "_Mount",
|
|
734
660
|
deployment_name: Optional[str] = None,
|
|
735
661
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
|
662
|
+
*,
|
|
736
663
|
environment_name: Optional[str] = None,
|
|
664
|
+
allow_overwrite: bool = False,
|
|
737
665
|
client: Optional[_Client] = None,
|
|
738
666
|
) -> None:
|
|
739
667
|
check_object_name(deployment_name, "Mount")
|
|
740
|
-
environment_name = _get_environment_name(environment_name, resolver=None)
|
|
741
668
|
self._deployment_name = deployment_name
|
|
742
669
|
self._namespace = namespace
|
|
743
|
-
self.
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
resolver
|
|
747
|
-
await resolver.load(self)
|
|
670
|
+
self._allow_overwrite = allow_overwrite
|
|
671
|
+
resolver = Resolver()
|
|
672
|
+
root_metadata = LoadContext(client=client, environment_name=environment_name)
|
|
673
|
+
await resolver.load(self, root_metadata)
|
|
748
674
|
|
|
749
675
|
def _get_metadata(self) -> api_pb2.MountHandleMetadata:
|
|
750
676
|
if self._content_checksum_sha256_hex is None:
|
|
@@ -810,3 +736,142 @@ def _is_modal_path(remote_path: PurePosixPath):
|
|
|
810
736
|
if is_modal_path:
|
|
811
737
|
return True
|
|
812
738
|
return False
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
REMOTE_PACKAGES_PATH = "/__modal/deps"
|
|
742
|
+
REMOTE_SITECUSTOMIZE_PATH = "/pkg/sitecustomize.py"
|
|
743
|
+
|
|
744
|
+
SITECUSTOMIZE_CONTENT = f"""
|
|
745
|
+
# This file is automatically generated by Modal.
|
|
746
|
+
# It ensures that Modal's python dependencies are available in the Python PATH,
|
|
747
|
+
# while prioritizing user-installed packages.
|
|
748
|
+
import sys; sys.path.append('{REMOTE_PACKAGES_PATH}')
|
|
749
|
+
""".strip()
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
async def _create_single_client_dependency_mount(
|
|
753
|
+
client: _Client,
|
|
754
|
+
builder_version: str,
|
|
755
|
+
python_version: str,
|
|
756
|
+
arch: str,
|
|
757
|
+
platform: str,
|
|
758
|
+
uv_python_platform: str,
|
|
759
|
+
check_if_exists: bool = True,
|
|
760
|
+
allow_overwrite: bool = False,
|
|
761
|
+
dry_run: bool = False,
|
|
762
|
+
):
|
|
763
|
+
import tempfile
|
|
764
|
+
|
|
765
|
+
profile_environment = config.get("environment")
|
|
766
|
+
abi_tag = "cp" + python_version.replace(".", "")
|
|
767
|
+
mount_name = f"{builder_version}-{abi_tag}-{platform}-{arch}"
|
|
768
|
+
|
|
769
|
+
if check_if_exists:
|
|
770
|
+
try:
|
|
771
|
+
await Mount.from_name(mount_name, namespace=api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL).hydrate.aio(client)
|
|
772
|
+
print(f"➖ Found existing mount {mount_name} in global namespace.")
|
|
773
|
+
return
|
|
774
|
+
except modal.exception.NotFoundError:
|
|
775
|
+
pass
|
|
776
|
+
|
|
777
|
+
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpd:
|
|
778
|
+
print(f"📦 Building {mount_name}.")
|
|
779
|
+
requirements = os.path.join(os.path.dirname(__file__), f"builder/{builder_version}.txt")
|
|
780
|
+
cmd = " ".join(
|
|
781
|
+
[
|
|
782
|
+
"uv",
|
|
783
|
+
"pip",
|
|
784
|
+
"install",
|
|
785
|
+
"--strict",
|
|
786
|
+
"--no-deps",
|
|
787
|
+
"--no-cache",
|
|
788
|
+
"-r",
|
|
789
|
+
requirements,
|
|
790
|
+
"--compile-bytecode",
|
|
791
|
+
"--target",
|
|
792
|
+
tmpd,
|
|
793
|
+
"--python-platform",
|
|
794
|
+
uv_python_platform,
|
|
795
|
+
"--python-version",
|
|
796
|
+
python_version,
|
|
797
|
+
]
|
|
798
|
+
)
|
|
799
|
+
proc = await asyncio.create_subprocess_shell(
|
|
800
|
+
cmd,
|
|
801
|
+
stdout=asyncio.subprocess.PIPE,
|
|
802
|
+
stderr=asyncio.subprocess.PIPE,
|
|
803
|
+
)
|
|
804
|
+
await proc.wait()
|
|
805
|
+
if proc.returncode:
|
|
806
|
+
stdout, stderr = await proc.communicate()
|
|
807
|
+
print(stdout.decode("utf-8"))
|
|
808
|
+
print(stderr.decode("utf-8"))
|
|
809
|
+
raise RuntimeError(f"Subprocess failed with {proc.returncode}")
|
|
810
|
+
|
|
811
|
+
print(f"🌐 Downloaded and unpacked {mount_name} packages to {tmpd}.")
|
|
812
|
+
|
|
813
|
+
python_mount = Mount._from_local_dir(tmpd, remote_path=REMOTE_PACKAGES_PATH)
|
|
814
|
+
|
|
815
|
+
with tempfile.NamedTemporaryFile() as sitecustomize:
|
|
816
|
+
sitecustomize.write(
|
|
817
|
+
SITECUSTOMIZE_CONTENT.encode("utf-8"),
|
|
818
|
+
)
|
|
819
|
+
sitecustomize.flush()
|
|
820
|
+
|
|
821
|
+
python_mount = python_mount.add_local_file(
|
|
822
|
+
sitecustomize.name,
|
|
823
|
+
remote_path=REMOTE_SITECUSTOMIZE_PATH,
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
if not dry_run:
|
|
827
|
+
try:
|
|
828
|
+
await python_mount._deploy.aio(
|
|
829
|
+
mount_name,
|
|
830
|
+
api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
|
831
|
+
environment_name=profile_environment,
|
|
832
|
+
allow_overwrite=allow_overwrite,
|
|
833
|
+
client=client,
|
|
834
|
+
)
|
|
835
|
+
print(f"✅ Deployed mount {mount_name} to global namespace.")
|
|
836
|
+
except GRPCError as e:
|
|
837
|
+
print(f"⚠️ Mount creation failed with {e.status}: {e.message}")
|
|
838
|
+
else:
|
|
839
|
+
print(f"Dry run - skipping deployment of mount {mount_name}")
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
async def _create_client_dependency_mounts(
|
|
843
|
+
client=None,
|
|
844
|
+
python_versions: list[str] = list(PYTHON_STANDALONE_VERSIONS),
|
|
845
|
+
builder_versions: list[str] = ["2025.06"], # Reenable "PREVIEW" during testing
|
|
846
|
+
check_if_exists=True,
|
|
847
|
+
dry_run=False,
|
|
848
|
+
):
|
|
849
|
+
arch = "x86_64"
|
|
850
|
+
platform_tags = [
|
|
851
|
+
("manylinux_2_17", f"{arch}-manylinux_2_17"), # glibc >= 2.17
|
|
852
|
+
("musllinux_1_2", f"{arch}-unknown-linux-musl"), # musl >= 1.2
|
|
853
|
+
]
|
|
854
|
+
coros = []
|
|
855
|
+
for python_version in python_versions:
|
|
856
|
+
for builder_version in builder_versions:
|
|
857
|
+
for platform, uv_python_platform in platform_tags:
|
|
858
|
+
coros.append(
|
|
859
|
+
_create_single_client_dependency_mount(
|
|
860
|
+
client,
|
|
861
|
+
builder_version,
|
|
862
|
+
python_version,
|
|
863
|
+
arch,
|
|
864
|
+
platform,
|
|
865
|
+
uv_python_platform,
|
|
866
|
+
# This check_if_exists / allow_overwrite parameterization is very awkward
|
|
867
|
+
# Also it doesn't provide a hook for overwriting a non-preview version, which
|
|
868
|
+
# in theory we may need to do at some point (hopefully not, but...)
|
|
869
|
+
check_if_exists=check_if_exists and builder_version != "PREVIEW",
|
|
870
|
+
allow_overwrite=builder_version == "PREVIEW",
|
|
871
|
+
dry_run=dry_run,
|
|
872
|
+
)
|
|
873
|
+
)
|
|
874
|
+
await TaskContext.gather(*coros)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
create_client_dependency_mounts = synchronize_api(_create_client_dependency_mounts)
|