modal 0.71.12__py3-none-any.whl → 0.72.5__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/_utils/docker_utils.py +35 -1
- modal/_utils/function_utils.py +3 -3
- modal/app.py +1 -2
- modal/cli/launch.py +1 -1
- modal/cli/programs/run_jupyter.py +5 -10
- modal/cli/programs/vscode.py +5 -10
- modal/client.pyi +2 -2
- modal/file_pattern_matcher.py +74 -41
- modal/functions.pyi +6 -6
- modal/image.py +166 -40
- modal/image.pyi +24 -4
- modal/mount.py +48 -2
- modal/mount.pyi +38 -0
- {modal-0.71.12.dist-info → modal-0.72.5.dist-info}/METADATA +1 -1
- {modal-0.71.12.dist-info → modal-0.72.5.dist-info}/RECORD +22 -22
- modal_global_objects/mounts/python_standalone.py +1 -1
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +1 -1
- {modal-0.71.12.dist-info → modal-0.72.5.dist-info}/LICENSE +0 -0
- {modal-0.71.12.dist-info → modal-0.72.5.dist-info}/WHEEL +0 -0
- {modal-0.71.12.dist-info → modal-0.72.5.dist-info}/entry_points.txt +0 -0
- {modal-0.71.12.dist-info → modal-0.72.5.dist-info}/top_level.txt +0 -0
modal/_utils/docker_utils.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# Copyright Modal Labs 2024
|
2
2
|
import re
|
3
3
|
import shlex
|
4
|
-
from
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Optional, Sequence
|
5
6
|
|
6
7
|
from ..exception import InvalidError
|
7
8
|
|
@@ -62,3 +63,36 @@ def extract_copy_command_patterns(dockerfile_lines: Sequence[str]) -> list[str]:
|
|
62
63
|
current_command = ""
|
63
64
|
|
64
65
|
return list(copy_source_patterns)
|
66
|
+
|
67
|
+
|
68
|
+
def find_dockerignore_file(context_directory: Path, dockerfile_path: Optional[Path] = None) -> Optional[Path]:
|
69
|
+
"""
|
70
|
+
Find dockerignore file relative to current context directory
|
71
|
+
and if dockerfile path is provided, check if specific <dockerfile_name>.dockerignore
|
72
|
+
file exists in the same directory as <dockerfile_name>
|
73
|
+
Finds the most specific dockerignore file that exists.
|
74
|
+
"""
|
75
|
+
|
76
|
+
def valid_dockerignore_file(fp):
|
77
|
+
# fp has to exist
|
78
|
+
if not fp.exists():
|
79
|
+
return False
|
80
|
+
# fp has to be subpath to current working directory
|
81
|
+
if not fp.is_relative_to(context_directory):
|
82
|
+
return False
|
83
|
+
|
84
|
+
return True
|
85
|
+
|
86
|
+
generic_name = ".dockerignore"
|
87
|
+
possible_locations = []
|
88
|
+
if dockerfile_path:
|
89
|
+
specific_name = f"{dockerfile_path.name}.dockerignore"
|
90
|
+
# 1. check if specific <dockerfile_name>.dockerignore file exists in the same directory as <dockerfile_name>
|
91
|
+
possible_locations.append(dockerfile_path.parent / specific_name)
|
92
|
+
# 2. check if generic .dockerignore file exists in the same directory as <dockerfile_name>
|
93
|
+
possible_locations.append(dockerfile_path.parent / generic_name)
|
94
|
+
|
95
|
+
# 3. check if generic .dockerignore file exists in current working directory
|
96
|
+
possible_locations.append(context_directory / generic_name)
|
97
|
+
|
98
|
+
return next((e for e in possible_locations if valid_dockerignore_file(e)), None)
|
modal/_utils/function_utils.py
CHANGED
@@ -326,11 +326,11 @@ class FunctionInfo:
|
|
326
326
|
# make sure the function's own entrypoint is included:
|
327
327
|
if self._type == FunctionInfoType.PACKAGE:
|
328
328
|
if config.get("automount"):
|
329
|
-
return [_Mount.
|
329
|
+
return [_Mount._from_local_python_packages(self.module_name)]
|
330
330
|
elif not self.is_serialized():
|
331
331
|
# mount only relevant file and __init__.py:s
|
332
332
|
return [
|
333
|
-
_Mount.
|
333
|
+
_Mount._from_local_dir(
|
334
334
|
self._base_dir,
|
335
335
|
remote_path=self._remote_dir,
|
336
336
|
recursive=True,
|
@@ -341,7 +341,7 @@ class FunctionInfo:
|
|
341
341
|
remote_path = ROOT_DIR / Path(self._file).name
|
342
342
|
if not _is_modal_path(remote_path):
|
343
343
|
return [
|
344
|
-
_Mount.
|
344
|
+
_Mount._from_local_file(
|
345
345
|
self._file,
|
346
346
|
remote_path=remote_path,
|
347
347
|
)
|
modal/app.py
CHANGED
@@ -200,10 +200,9 @@ class _App:
|
|
200
200
|
|
201
201
|
```python notest
|
202
202
|
image = modal.Image.debian_slim().pip_install(...)
|
203
|
-
mount = modal.Mount.from_local_dir("./config")
|
204
203
|
secret = modal.Secret.from_name("my-secret")
|
205
204
|
volume = modal.Volume.from_name("my-data")
|
206
|
-
app = modal.App(image=image,
|
205
|
+
app = modal.App(image=image, secrets=[secret], volumes={"/mnt/data": volume})
|
207
206
|
```
|
208
207
|
"""
|
209
208
|
if name is not None and not isinstance(name, str):
|
modal/cli/launch.py
CHANGED
@@ -55,7 +55,7 @@ def jupyter(
|
|
55
55
|
timeout: int = 3600,
|
56
56
|
image: str = "ubuntu:22.04",
|
57
57
|
add_python: Optional[str] = "3.11",
|
58
|
-
mount: Optional[str] = None, #
|
58
|
+
mount: Optional[str] = None, # Adds a local directory to the jupyter container
|
59
59
|
volume: Optional[str] = None, # Attach a persisted `modal.Volume` by name (creating if missing).
|
60
60
|
detach: bool = False, # Run the app in "detached" mode to persist after local client disconnects
|
61
61
|
):
|
@@ -10,25 +10,20 @@ import time
|
|
10
10
|
import webbrowser
|
11
11
|
from typing import Any
|
12
12
|
|
13
|
-
from modal import App, Image,
|
13
|
+
from modal import App, Image, Queue, Secret, Volume, forward
|
14
14
|
|
15
15
|
# Passed by `modal launch` locally via CLI, plumbed to remote runner through secrets.
|
16
16
|
args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
|
17
17
|
|
18
|
-
|
19
18
|
app = App()
|
20
|
-
app.image = Image.from_registry(args.get("image"), add_python=args.get("add_python")).pip_install("jupyterlab")
|
21
19
|
|
20
|
+
image = Image.from_registry(args.get("image"), add_python=args.get("add_python")).pip_install("jupyterlab")
|
22
21
|
|
23
|
-
|
24
|
-
|
22
|
+
if args.get("mount"):
|
23
|
+
image = image.add_local_dir(
|
25
24
|
args.get("mount"),
|
26
25
|
remote_path="/root/lab/mount",
|
27
26
|
)
|
28
|
-
if args.get("mount")
|
29
|
-
else None
|
30
|
-
)
|
31
|
-
mounts = [mount] if mount else []
|
32
27
|
|
33
28
|
volume = (
|
34
29
|
Volume.from_name(
|
@@ -55,12 +50,12 @@ def wait_for_port(url: str, q: Queue):
|
|
55
50
|
|
56
51
|
|
57
52
|
@app.function(
|
53
|
+
image=image,
|
58
54
|
cpu=args.get("cpu"),
|
59
55
|
memory=args.get("memory"),
|
60
56
|
gpu=args.get("gpu"),
|
61
57
|
timeout=args.get("timeout"),
|
62
58
|
secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
|
63
|
-
mounts=mounts,
|
64
59
|
volumes=volumes,
|
65
60
|
concurrency_limit=1 if volume else None,
|
66
61
|
)
|
modal/cli/programs/vscode.py
CHANGED
@@ -10,7 +10,7 @@ import time
|
|
10
10
|
import webbrowser
|
11
11
|
from typing import Any
|
12
12
|
|
13
|
-
from modal import App, Image,
|
13
|
+
from modal import App, Image, Queue, Secret, Volume, forward
|
14
14
|
|
15
15
|
# Passed by `modal launch` locally via CLI, plumbed to remote runner through secrets.
|
16
16
|
args: dict[str, Any] = json.loads(os.environ.get("MODAL_LAUNCH_ARGS", "{}"))
|
@@ -23,7 +23,7 @@ FIXUD_INSTALLER = "https://github.com/boxboat/fixuid/releases/download/v0.6.0/fi
|
|
23
23
|
|
24
24
|
|
25
25
|
app = App()
|
26
|
-
|
26
|
+
image = (
|
27
27
|
Image.from_registry(args.get("image"), add_python="3.11")
|
28
28
|
.apt_install("curl", "dumb-init", "git", "git-lfs")
|
29
29
|
.run_commands(
|
@@ -44,16 +44,11 @@ app.image = (
|
|
44
44
|
.env({"ENTRYPOINTD": ""})
|
45
45
|
)
|
46
46
|
|
47
|
-
|
48
|
-
|
49
|
-
Mount.from_local_dir(
|
47
|
+
if args.get("mount"):
|
48
|
+
image = image.add_local_dir(
|
50
49
|
args.get("mount"),
|
51
50
|
remote_path="/home/coder/mount",
|
52
51
|
)
|
53
|
-
if args.get("mount")
|
54
|
-
else None
|
55
|
-
)
|
56
|
-
mounts = [mount] if mount else []
|
57
52
|
|
58
53
|
volume = (
|
59
54
|
Volume.from_name(
|
@@ -80,12 +75,12 @@ def wait_for_port(data: tuple[str, str], q: Queue):
|
|
80
75
|
|
81
76
|
|
82
77
|
@app.function(
|
78
|
+
image=image,
|
83
79
|
cpu=args.get("cpu"),
|
84
80
|
memory=args.get("memory"),
|
85
81
|
gpu=args.get("gpu"),
|
86
82
|
timeout=args.get("timeout"),
|
87
83
|
secrets=[Secret.from_dict({"MODAL_LAUNCH_ARGS": json.dumps(args)})],
|
88
|
-
mounts=mounts,
|
89
84
|
volumes=volumes,
|
90
85
|
concurrency_limit=1 if volume else None,
|
91
86
|
)
|
modal/client.pyi
CHANGED
@@ -26,7 +26,7 @@ class _Client:
|
|
26
26
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
27
27
|
|
28
28
|
def __init__(
|
29
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.
|
29
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.5"
|
30
30
|
): ...
|
31
31
|
def is_closed(self) -> bool: ...
|
32
32
|
@property
|
@@ -81,7 +81,7 @@ class Client:
|
|
81
81
|
_stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
|
82
82
|
|
83
83
|
def __init__(
|
84
|
-
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.
|
84
|
+
self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.72.5"
|
85
85
|
): ...
|
86
86
|
def is_closed(self) -> bool: ...
|
87
87
|
@property
|
modal/file_pattern_matcher.py
CHANGED
@@ -35,7 +35,7 @@ class _AbstractPatternMatcher:
|
|
35
35
|
"""
|
36
36
|
return _CustomPatternMatcher(lambda path: not self(path))
|
37
37
|
|
38
|
-
def
|
38
|
+
def _with_repr(self, custom_repr) -> "_AbstractPatternMatcher":
|
39
39
|
# use to give an instance of a matcher a custom name - useful for visualizing default values in signatures
|
40
40
|
self._custom_repr = custom_repr
|
41
41
|
return self
|
@@ -60,7 +60,45 @@ class _CustomPatternMatcher(_AbstractPatternMatcher):
|
|
60
60
|
|
61
61
|
|
62
62
|
class FilePatternMatcher(_AbstractPatternMatcher):
|
63
|
-
"""
|
63
|
+
"""
|
64
|
+
Allows matching file Path objects against a list of patterns.
|
65
|
+
|
66
|
+
**Usage:**
|
67
|
+
```python
|
68
|
+
from pathlib import Path
|
69
|
+
from modal import FilePatternMatcher
|
70
|
+
|
71
|
+
matcher = FilePatternMatcher("*.py")
|
72
|
+
|
73
|
+
assert matcher(Path("foo.py"))
|
74
|
+
|
75
|
+
# You can also negate the matcher.
|
76
|
+
negated_matcher = ~matcher
|
77
|
+
|
78
|
+
assert not negated_matcher(Path("foo.py"))
|
79
|
+
```
|
80
|
+
"""
|
81
|
+
|
82
|
+
patterns: list[Pattern]
|
83
|
+
_delayed_init: Callable[[], None] = None
|
84
|
+
|
85
|
+
def _set_patterns(self, patterns: Sequence[str]) -> None:
|
86
|
+
self.patterns = []
|
87
|
+
for pattern in list(patterns):
|
88
|
+
pattern = pattern.strip()
|
89
|
+
if not pattern:
|
90
|
+
continue
|
91
|
+
pattern = os.path.normpath(pattern)
|
92
|
+
new_pattern = Pattern()
|
93
|
+
if pattern[0] == "!":
|
94
|
+
if len(pattern) == 1:
|
95
|
+
raise ValueError('Illegal exclusion pattern: "!"')
|
96
|
+
new_pattern.exclusion = True
|
97
|
+
pattern = pattern[1:]
|
98
|
+
# In Python, we can proceed without explicit syntax checking
|
99
|
+
new_pattern.cleaned_pattern = pattern
|
100
|
+
new_pattern.dirs = pattern.split(os.path.sep)
|
101
|
+
self.patterns.append(new_pattern)
|
64
102
|
|
65
103
|
def __init__(self, *pattern: str) -> None:
|
66
104
|
"""Initialize a new FilePatternMatcher instance.
|
@@ -71,24 +109,34 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
71
109
|
Raises:
|
72
110
|
ValueError: If an illegal exclusion pattern is provided.
|
73
111
|
"""
|
74
|
-
self.
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
112
|
+
self._set_patterns(pattern)
|
113
|
+
|
114
|
+
@classmethod
|
115
|
+
def from_file(cls, file_path: Path) -> "FilePatternMatcher":
|
116
|
+
"""Initialize a new FilePatternMatcher instance from a file.
|
117
|
+
|
118
|
+
The patterns in the file will be read lazily when the matcher is first used.
|
119
|
+
|
120
|
+
Args:
|
121
|
+
file_path (Path): The path to the file containing patterns.
|
122
|
+
|
123
|
+
**Usage:**
|
124
|
+
```python
|
125
|
+
from pathlib import Path
|
126
|
+
from modal import FilePatternMatcher
|
127
|
+
|
128
|
+
matcher = FilePatternMatcher.from_file(Path("/path/to/ignorefile"))
|
129
|
+
```
|
130
|
+
|
131
|
+
"""
|
132
|
+
uninitialized = cls.__new__(cls)
|
133
|
+
|
134
|
+
def _delayed_init():
|
135
|
+
uninitialized._set_patterns(file_path.read_text("utf8").splitlines())
|
136
|
+
uninitialized._delayed_init = None
|
137
|
+
|
138
|
+
uninitialized._delayed_init = _delayed_init
|
139
|
+
return uninitialized
|
92
140
|
|
93
141
|
def _matches(self, file_path: str) -> bool:
|
94
142
|
"""Check if the file path or any of its parent directories match the patterns.
|
@@ -97,6 +145,7 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
97
145
|
library. The reason is that `Matches()` in the original library is
|
98
146
|
deprecated due to buggy behavior.
|
99
147
|
"""
|
148
|
+
|
100
149
|
matched = False
|
101
150
|
file_path = os.path.normpath(file_path)
|
102
151
|
if file_path == ".":
|
@@ -128,31 +177,15 @@ class FilePatternMatcher(_AbstractPatternMatcher):
|
|
128
177
|
return matched
|
129
178
|
|
130
179
|
def __call__(self, file_path: Path) -> bool:
|
131
|
-
|
132
|
-
|
133
|
-
Args:
|
134
|
-
file_path (Path): The path to check.
|
135
|
-
|
136
|
-
Returns:
|
137
|
-
True if the path matches any of the patterns.
|
138
|
-
|
139
|
-
Usage:
|
140
|
-
```python
|
141
|
-
from pathlib import Path
|
142
|
-
from modal import FilePatternMatcher
|
143
|
-
|
144
|
-
matcher = FilePatternMatcher("*.py")
|
145
|
-
|
146
|
-
assert matcher(Path("foo.py"))
|
147
|
-
```
|
148
|
-
"""
|
180
|
+
if self._delayed_init:
|
181
|
+
self._delayed_init()
|
149
182
|
return self._matches(str(file_path))
|
150
183
|
|
151
184
|
|
152
|
-
#
|
185
|
+
# _with_repr allows us to use this matcher as a default value in a function signature
|
153
186
|
# and get a nice repr in the docs and auto-generated type stubs:
|
154
|
-
NON_PYTHON_FILES = (~FilePatternMatcher("**/*.py")).
|
155
|
-
_NOTHING = (~FilePatternMatcher()).
|
187
|
+
NON_PYTHON_FILES = (~FilePatternMatcher("**/*.py"))._with_repr(f"{__name__}.NON_PYTHON_FILES")
|
188
|
+
_NOTHING = (~FilePatternMatcher())._with_repr(f"{__name__}._NOTHING") # match everything = ignore nothing
|
156
189
|
|
157
190
|
|
158
191
|
def _ignore_fn(ignore: Union[Sequence[str], Callable[[Path], bool]]) -> Callable[[Path], bool]:
|
modal/functions.pyi
CHANGED
@@ -462,11 +462,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
462
462
|
|
463
463
|
_call_generator_nowait: ___call_generator_nowait_spec
|
464
464
|
|
465
|
-
class __remote_spec(typing_extensions.Protocol[
|
465
|
+
class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
|
466
466
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
467
467
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
468
468
|
|
469
|
-
remote: __remote_spec[
|
469
|
+
remote: __remote_spec[P, ReturnType]
|
470
470
|
|
471
471
|
class __remote_gen_spec(typing_extensions.Protocol):
|
472
472
|
def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
|
@@ -479,17 +479,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
479
479
|
def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
|
480
480
|
def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
|
481
481
|
|
482
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
482
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
|
483
483
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
484
484
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
485
485
|
|
486
|
-
_experimental_spawn: ___experimental_spawn_spec[
|
486
|
+
_experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
|
487
487
|
|
488
|
-
class __spawn_spec(typing_extensions.Protocol[
|
488
|
+
class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
|
489
489
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
490
490
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
491
491
|
|
492
|
-
spawn: __spawn_spec[
|
492
|
+
spawn: __spawn_spec[P, ReturnType]
|
493
493
|
|
494
494
|
def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
|
495
495
|
|
modal/image.py
CHANGED
@@ -33,6 +33,7 @@ from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
|
|
33
33
|
from ._utils.deprecation import deprecation_error, deprecation_warning
|
34
34
|
from ._utils.docker_utils import (
|
35
35
|
extract_copy_command_patterns,
|
36
|
+
find_dockerignore_file,
|
36
37
|
)
|
37
38
|
from ._utils.function_utils import FunctionInfo
|
38
39
|
from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
|
@@ -71,6 +72,17 @@ LOCAL_REQUIREMENTS_DIR = Path(__file__).parent / "requirements"
|
|
71
72
|
CONTAINER_REQUIREMENTS_PATH = "/modal_requirements.txt"
|
72
73
|
|
73
74
|
|
75
|
+
class _AutoDockerIgnoreSentinel:
|
76
|
+
def __repr__(self) -> str:
|
77
|
+
return f"{__name__}.AUTO_DOCKERIGNORE"
|
78
|
+
|
79
|
+
def __call__(self, _: Path) -> bool:
|
80
|
+
raise NotImplementedError("This is only a placeholder. Do not call")
|
81
|
+
|
82
|
+
|
83
|
+
AUTO_DOCKERIGNORE = _AutoDockerIgnoreSentinel()
|
84
|
+
|
85
|
+
|
74
86
|
def _validate_python_version(
|
75
87
|
python_version: Optional[str], builder_version: ImageBuilderVersion, allow_micro_granularity: bool = True
|
76
88
|
) -> str:
|
@@ -266,6 +278,47 @@ def _create_context_mount(
|
|
266
278
|
return _Mount._add_local_dir(Path("./"), PurePosixPath("/"), ignore=ignore_with_include)
|
267
279
|
|
268
280
|
|
281
|
+
def _create_context_mount_function(
|
282
|
+
ignore: Union[Sequence[str], Callable[[Path], bool]],
|
283
|
+
dockerfile_cmds: list[str] = [],
|
284
|
+
dockerfile_path: Optional[Path] = None,
|
285
|
+
context_mount: Optional[_Mount] = None,
|
286
|
+
):
|
287
|
+
if dockerfile_path and dockerfile_cmds:
|
288
|
+
raise InvalidError("Cannot provide both dockerfile and docker commands")
|
289
|
+
|
290
|
+
if context_mount:
|
291
|
+
if ignore is not AUTO_DOCKERIGNORE:
|
292
|
+
raise InvalidError("Cannot set both `context_mount` and `ignore`")
|
293
|
+
|
294
|
+
def identity_context_mount_fn() -> Optional[_Mount]:
|
295
|
+
return context_mount
|
296
|
+
|
297
|
+
return identity_context_mount_fn
|
298
|
+
elif ignore is AUTO_DOCKERIGNORE:
|
299
|
+
|
300
|
+
def auto_created_context_mount_fn() -> Optional[_Mount]:
|
301
|
+
context_dir = Path.cwd()
|
302
|
+
dockerignore_file = find_dockerignore_file(context_dir, dockerfile_path)
|
303
|
+
ignore_fn = (
|
304
|
+
FilePatternMatcher(*dockerignore_file.read_text("utf8").splitlines())
|
305
|
+
if dockerignore_file
|
306
|
+
else _ignore_fn(())
|
307
|
+
)
|
308
|
+
|
309
|
+
cmds = dockerfile_path.read_text("utf8").splitlines() if dockerfile_path else dockerfile_cmds
|
310
|
+
return _create_context_mount(cmds, ignore_fn=ignore_fn, context_dir=context_dir)
|
311
|
+
|
312
|
+
return auto_created_context_mount_fn
|
313
|
+
|
314
|
+
def auto_created_context_mount_fn() -> Optional[_Mount]:
|
315
|
+
# use COPY commands and ignore patterns to construct implicit context mount
|
316
|
+
cmds = dockerfile_path.read_text("utf8").splitlines() if dockerfile_path else dockerfile_cmds
|
317
|
+
return _create_context_mount(cmds, ignore_fn=_ignore_fn(ignore), context_dir=Path.cwd())
|
318
|
+
|
319
|
+
return auto_created_context_mount_fn
|
320
|
+
|
321
|
+
|
269
322
|
class _ImageRegistryConfig:
|
270
323
|
"""mdmd:hidden"""
|
271
324
|
|
@@ -654,7 +707,7 @@ class _Image(_Object, type_prefix="im"):
|
|
654
707
|
if remote_path.endswith("/"):
|
655
708
|
remote_path = remote_path + Path(local_path).name
|
656
709
|
|
657
|
-
mount = _Mount.
|
710
|
+
mount = _Mount._from_local_file(local_path, remote_path)
|
658
711
|
return self._add_mount_layer_or_copy(mount, copy=copy)
|
659
712
|
|
660
713
|
def add_local_dir(
|
@@ -683,6 +736,7 @@ class _Image(_Object, type_prefix="im"):
|
|
683
736
|
**Usage:**
|
684
737
|
|
685
738
|
```python
|
739
|
+
from pathlib import Path
|
686
740
|
from modal import FilePatternMatcher
|
687
741
|
|
688
742
|
image = modal.Image.debian_slim().add_local_dir(
|
@@ -697,18 +751,25 @@ class _Image(_Object, type_prefix="im"):
|
|
697
751
|
ignore=lambda p: p.is_relative_to(".venv"),
|
698
752
|
)
|
699
753
|
|
700
|
-
image = modal.Image.debian_slim().
|
754
|
+
image = modal.Image.debian_slim().add_local_dir(
|
701
755
|
"~/assets",
|
702
756
|
remote_path="/assets",
|
703
757
|
ignore=FilePatternMatcher("**/*.txt"),
|
704
758
|
)
|
705
759
|
|
706
760
|
# When including files is simpler than excluding them, you can use the `~` operator to invert the matcher.
|
707
|
-
image = modal.Image.debian_slim().
|
761
|
+
image = modal.Image.debian_slim().add_local_dir(
|
708
762
|
"~/assets",
|
709
763
|
remote_path="/assets",
|
710
764
|
ignore=~FilePatternMatcher("**/*.py"),
|
711
765
|
)
|
766
|
+
|
767
|
+
# You can also read ignore patterns from a file.
|
768
|
+
image = modal.Image.debian_slim().add_local_dir(
|
769
|
+
"~/assets",
|
770
|
+
remote_path="/assets",
|
771
|
+
ignore=FilePatternMatcher.from_file(Path("/path/to/ignorefile")),
|
772
|
+
)
|
712
773
|
```
|
713
774
|
"""
|
714
775
|
if not PurePosixPath(remote_path).is_absolute():
|
@@ -734,7 +795,7 @@ class _Image(_Object, type_prefix="im"):
|
|
734
795
|
return _Image._from_args(
|
735
796
|
base_images={"base": self},
|
736
797
|
dockerfile_function=build_dockerfile,
|
737
|
-
context_mount_function=lambda: _Mount.
|
798
|
+
context_mount_function=lambda: _Mount._from_local_file(local_path, remote_path=f"/{basename}"),
|
738
799
|
)
|
739
800
|
|
740
801
|
def add_local_python_source(
|
@@ -772,7 +833,7 @@ class _Image(_Object, type_prefix="im"):
|
|
772
833
|
)
|
773
834
|
```
|
774
835
|
"""
|
775
|
-
mount = _Mount.
|
836
|
+
mount = _Mount._from_local_python_packages(*modules, ignore=ignore)
|
776
837
|
return self._add_mount_layer_or_copy(mount, copy=copy)
|
777
838
|
|
778
839
|
def copy_local_dir(
|
@@ -792,6 +853,7 @@ class _Image(_Object, type_prefix="im"):
|
|
792
853
|
**Usage:**
|
793
854
|
|
794
855
|
```python
|
856
|
+
from pathlib import Path
|
795
857
|
from modal import FilePatternMatcher
|
796
858
|
|
797
859
|
image = modal.Image.debian_slim().copy_local_dir(
|
@@ -818,6 +880,13 @@ class _Image(_Object, type_prefix="im"):
|
|
818
880
|
remote_path="/assets",
|
819
881
|
ignore=~FilePatternMatcher("**/*.py"),
|
820
882
|
)
|
883
|
+
|
884
|
+
# You can also read ignore patterns from a file.
|
885
|
+
image = modal.Image.debian_slim().copy_local_dir(
|
886
|
+
"~/assets",
|
887
|
+
remote_path="/assets",
|
888
|
+
ignore=FilePatternMatcher.from_file(Path("/path/to/ignorefile")),
|
889
|
+
)
|
821
890
|
```
|
822
891
|
"""
|
823
892
|
|
@@ -1205,28 +1274,53 @@ class _Image(_Object, type_prefix="im"):
|
|
1205
1274
|
# modal.Mount with local files to supply as build context for COPY commands
|
1206
1275
|
context_mount: Optional[_Mount] = None,
|
1207
1276
|
force_build: bool = False, # Ignore cached builds, similar to 'docker build --no-cache'
|
1208
|
-
ignore: Union[Sequence[str], Callable[[Path], bool]] =
|
1277
|
+
ignore: Union[Sequence[str], Callable[[Path], bool]] = AUTO_DOCKERIGNORE,
|
1209
1278
|
) -> "_Image":
|
1210
|
-
"""
|
1211
|
-
|
1212
|
-
if not cmds:
|
1213
|
-
return self
|
1279
|
+
"""
|
1280
|
+
Extend an image with arbitrary Dockerfile-like commands.
|
1214
1281
|
|
1215
|
-
|
1216
|
-
if ignore:
|
1217
|
-
raise InvalidError("Cannot set both `context_mount` and `ignore`")
|
1282
|
+
**Usage:**
|
1218
1283
|
|
1219
|
-
|
1220
|
-
|
1284
|
+
```python
|
1285
|
+
from pathlib import Path
|
1286
|
+
from modal import FilePatternMatcher
|
1221
1287
|
|
1222
|
-
|
1223
|
-
|
1288
|
+
# By default a .dockerignore file is used if present in the current working directory
|
1289
|
+
image = modal.Image.debian_slim().dockerfile_commands(
|
1290
|
+
["COPY data /data"],
|
1291
|
+
)
|
1224
1292
|
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1293
|
+
image = modal.Image.debian_slim().dockerfile_commands(
|
1294
|
+
["COPY data /data"],
|
1295
|
+
ignore=["*.venv"],
|
1296
|
+
)
|
1228
1297
|
|
1229
|
-
|
1298
|
+
image = modal.Image.debian_slim().dockerfile_commands(
|
1299
|
+
["COPY data /data"],
|
1300
|
+
ignore=lambda p: p.is_relative_to(".venv"),
|
1301
|
+
)
|
1302
|
+
|
1303
|
+
image = modal.Image.debian_slim().dockerfile_commands(
|
1304
|
+
["COPY data /data"],
|
1305
|
+
ignore=FilePatternMatcher("**/*.txt"),
|
1306
|
+
)
|
1307
|
+
|
1308
|
+
# When including files is simpler than excluding them, you can use the `~` operator to invert the matcher.
|
1309
|
+
image = modal.Image.debian_slim().dockerfile_commands(
|
1310
|
+
["COPY data /data"],
|
1311
|
+
ignore=~FilePatternMatcher("**/*.py"),
|
1312
|
+
)
|
1313
|
+
|
1314
|
+
# You can also read ignore patterns from a file.
|
1315
|
+
image = modal.Image.debian_slim().dockerfile_commands(
|
1316
|
+
["COPY data /data"],
|
1317
|
+
ignore=FilePatternMatcher.from_file(Path("/path/to/dockerignore")),
|
1318
|
+
)
|
1319
|
+
```
|
1320
|
+
"""
|
1321
|
+
cmds = _flatten_str_args("dockerfile_commands", "dockerfile_commands", dockerfile_commands)
|
1322
|
+
if not cmds:
|
1323
|
+
return self
|
1230
1324
|
|
1231
1325
|
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
1232
1326
|
return DockerfileSpec(commands=["FROM base", *cmds], context_files=context_files)
|
@@ -1236,7 +1330,9 @@ class _Image(_Object, type_prefix="im"):
|
|
1236
1330
|
dockerfile_function=build_dockerfile,
|
1237
1331
|
secrets=secrets,
|
1238
1332
|
gpu_config=parse_gpu_config(gpu),
|
1239
|
-
context_mount_function=
|
1333
|
+
context_mount_function=_create_context_mount_function(
|
1334
|
+
ignore=ignore, dockerfile_cmds=cmds, context_mount=context_mount
|
1335
|
+
),
|
1240
1336
|
force_build=self.force_build or force_build,
|
1241
1337
|
)
|
1242
1338
|
|
@@ -1408,9 +1504,14 @@ class _Image(_Object, type_prefix="im"):
|
|
1408
1504
|
_validate_python_version(add_python, builder_version, allow_micro_granularity=False)
|
1409
1505
|
add_python_commands = [
|
1410
1506
|
"COPY /python/. /usr/local",
|
1411
|
-
"RUN ln -s /usr/local/bin/python3 /usr/local/bin/python",
|
1412
1507
|
"ENV TERMINFO_DIRS=/etc/terminfo:/lib/terminfo:/usr/share/terminfo:/usr/lib/terminfo",
|
1413
1508
|
]
|
1509
|
+
if add_python < "3.13":
|
1510
|
+
# Previous versions did not include the `python` binary, but later ones do.
|
1511
|
+
# (The important factor is not the Python version itself, but the standalone dist version.)
|
1512
|
+
# We insert the command in the list at the position it was previously always added
|
1513
|
+
# for backwards compatibility with existing images.
|
1514
|
+
add_python_commands.insert(1, "RUN ln -s /usr/local/bin/python3 /usr/local/bin/python")
|
1414
1515
|
|
1415
1516
|
# Note: this change is because we install dependencies with uv in 2024.10+
|
1416
1517
|
requirements_prefix = "python -m " if builder_version < "2024.10" else ""
|
@@ -1601,35 +1702,58 @@ class _Image(_Object, type_prefix="im"):
|
|
1601
1702
|
secrets: Sequence[_Secret] = [],
|
1602
1703
|
gpu: GPU_T = None,
|
1603
1704
|
add_python: Optional[str] = None,
|
1604
|
-
ignore: Union[Sequence[str], Callable[[Path], bool]] =
|
1705
|
+
ignore: Union[Sequence[str], Callable[[Path], bool]] = AUTO_DOCKERIGNORE,
|
1605
1706
|
) -> "_Image":
|
1606
1707
|
"""Build a Modal image from a local Dockerfile.
|
1607
1708
|
|
1608
1709
|
If your Dockerfile does not have Python installed, you can use the `add_python` parameter
|
1609
1710
|
to specify a version of Python to add to the image.
|
1610
1711
|
|
1611
|
-
**
|
1712
|
+
**Usage:**
|
1612
1713
|
|
1613
1714
|
```python
|
1614
|
-
|
1615
|
-
|
1616
|
-
"""
|
1715
|
+
from pathlib import Path
|
1716
|
+
from modal import FilePatternMatcher
|
1617
1717
|
|
1618
|
-
if
|
1619
|
-
|
1620
|
-
|
1718
|
+
# By default a .dockerignore file is used if present in the current working directory
|
1719
|
+
image = modal.Image.from_dockerfile(
|
1720
|
+
"./Dockerfile",
|
1721
|
+
add_python="3.12",
|
1722
|
+
)
|
1621
1723
|
|
1622
|
-
|
1623
|
-
|
1724
|
+
image = modal.Image.from_dockerfile(
|
1725
|
+
"./Dockerfile",
|
1726
|
+
add_python="3.12",
|
1727
|
+
ignore=["*.venv"],
|
1728
|
+
)
|
1624
1729
|
|
1625
|
-
|
1626
|
-
|
1730
|
+
image = modal.Image.from_dockerfile(
|
1731
|
+
"./Dockerfile",
|
1732
|
+
add_python="3.12",
|
1733
|
+
ignore=lambda p: p.is_relative_to(".venv"),
|
1734
|
+
)
|
1627
1735
|
|
1628
|
-
|
1629
|
-
|
1630
|
-
|
1736
|
+
image = modal.Image.from_dockerfile(
|
1737
|
+
"./Dockerfile",
|
1738
|
+
add_python="3.12",
|
1739
|
+
ignore=FilePatternMatcher("**/*.txt"),
|
1740
|
+
)
|
1631
1741
|
|
1632
|
-
|
1742
|
+
# When including files is simpler than excluding them, you can use the `~` operator to invert the matcher.
|
1743
|
+
image = modal.Image.from_dockerfile(
|
1744
|
+
"./Dockerfile",
|
1745
|
+
add_python="3.12",
|
1746
|
+
ignore=~FilePatternMatcher("**/*.py"),
|
1747
|
+
)
|
1748
|
+
|
1749
|
+
# You can also read ignore patterns from a file.
|
1750
|
+
image = modal.Image.from_dockerfile(
|
1751
|
+
"./Dockerfile",
|
1752
|
+
add_python="3.12",
|
1753
|
+
ignore=FilePatternMatcher.from_file(Path("/path/to/dockerignore")),
|
1754
|
+
)
|
1755
|
+
```
|
1756
|
+
"""
|
1633
1757
|
|
1634
1758
|
# --- Build the base dockerfile
|
1635
1759
|
|
@@ -1641,7 +1765,9 @@ class _Image(_Object, type_prefix="im"):
|
|
1641
1765
|
gpu_config = parse_gpu_config(gpu)
|
1642
1766
|
base_image = _Image._from_args(
|
1643
1767
|
dockerfile_function=build_dockerfile_base,
|
1644
|
-
context_mount_function=
|
1768
|
+
context_mount_function=_create_context_mount_function(
|
1769
|
+
ignore=ignore, dockerfile_path=Path(path), context_mount=context_mount
|
1770
|
+
),
|
1645
1771
|
gpu_config=gpu_config,
|
1646
1772
|
secrets=secrets,
|
1647
1773
|
)
|
modal/image.pyi
CHANGED
@@ -16,6 +16,12 @@ import typing_extensions
|
|
16
16
|
|
17
17
|
ImageBuilderVersion = typing.Literal["2023.12", "2024.04", "2024.10"]
|
18
18
|
|
19
|
+
class _AutoDockerIgnoreSentinel:
|
20
|
+
def __repr__(self) -> str: ...
|
21
|
+
def __call__(self, _: pathlib.Path) -> bool: ...
|
22
|
+
|
23
|
+
AUTO_DOCKERIGNORE: _AutoDockerIgnoreSentinel
|
24
|
+
|
19
25
|
def _validate_python_version(
|
20
26
|
python_version: typing.Optional[str],
|
21
27
|
builder_version: typing.Literal["2023.12", "2024.04", "2024.10"],
|
@@ -49,6 +55,12 @@ def _create_context_mount(
|
|
49
55
|
ignore_fn: typing.Callable[[pathlib.Path], bool],
|
50
56
|
context_dir: pathlib.Path,
|
51
57
|
) -> typing.Optional[modal.mount._Mount]: ...
|
58
|
+
def _create_context_mount_function(
|
59
|
+
ignore: typing.Union[collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]],
|
60
|
+
dockerfile_cmds: list[str] = [],
|
61
|
+
dockerfile_path: typing.Optional[pathlib.Path] = None,
|
62
|
+
context_mount: typing.Optional[modal.mount._Mount] = None,
|
63
|
+
): ...
|
52
64
|
|
53
65
|
class _ImageRegistryConfig:
|
54
66
|
def __init__(self, registry_auth_type: int = 0, secret: typing.Optional[modal.secret._Secret] = None): ...
|
@@ -202,7 +214,9 @@ class _Image(modal.object._Object):
|
|
202
214
|
gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
|
203
215
|
context_mount: typing.Optional[modal.mount._Mount] = None,
|
204
216
|
force_build: bool = False,
|
205
|
-
ignore: typing.Union[
|
217
|
+
ignore: typing.Union[
|
218
|
+
collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
|
219
|
+
] = modal.image.AUTO_DOCKERIGNORE,
|
206
220
|
) -> _Image: ...
|
207
221
|
def entrypoint(self, entrypoint_commands: list[str]) -> _Image: ...
|
208
222
|
def shell(self, shell_commands: list[str]) -> _Image: ...
|
@@ -288,7 +302,9 @@ class _Image(modal.object._Object):
|
|
288
302
|
secrets: collections.abc.Sequence[modal.secret._Secret] = [],
|
289
303
|
gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
|
290
304
|
add_python: typing.Optional[str] = None,
|
291
|
-
ignore: typing.Union[
|
305
|
+
ignore: typing.Union[
|
306
|
+
collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
|
307
|
+
] = modal.image.AUTO_DOCKERIGNORE,
|
292
308
|
) -> _Image: ...
|
293
309
|
@staticmethod
|
294
310
|
def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> _Image: ...
|
@@ -470,7 +486,9 @@ class Image(modal.object.Object):
|
|
470
486
|
gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
|
471
487
|
context_mount: typing.Optional[modal.mount.Mount] = None,
|
472
488
|
force_build: bool = False,
|
473
|
-
ignore: typing.Union[
|
489
|
+
ignore: typing.Union[
|
490
|
+
collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
|
491
|
+
] = modal.image.AUTO_DOCKERIGNORE,
|
474
492
|
) -> Image: ...
|
475
493
|
def entrypoint(self, entrypoint_commands: list[str]) -> Image: ...
|
476
494
|
def shell(self, shell_commands: list[str]) -> Image: ...
|
@@ -556,7 +574,9 @@ class Image(modal.object.Object):
|
|
556
574
|
secrets: collections.abc.Sequence[modal.secret.Secret] = [],
|
557
575
|
gpu: typing.Union[None, bool, str, modal.gpu._GPUConfig] = None,
|
558
576
|
add_python: typing.Optional[str] = None,
|
559
|
-
ignore: typing.Union[
|
577
|
+
ignore: typing.Union[
|
578
|
+
collections.abc.Sequence[str], typing.Callable[[pathlib.Path], bool]
|
579
|
+
] = modal.image.AUTO_DOCKERIGNORE,
|
560
580
|
) -> Image: ...
|
561
581
|
@staticmethod
|
562
582
|
def debian_slim(python_version: typing.Optional[str] = None, force_build: bool = False) -> Image: ...
|
modal/mount.py
CHANGED
@@ -23,7 +23,7 @@ from modal_version import __version__
|
|
23
23
|
from ._resolver import Resolver
|
24
24
|
from ._utils.async_utils import aclosing, async_map, synchronize_api
|
25
25
|
from ._utils.blob_utils import FileUploadSpec, blob_upload_file, get_file_upload_spec_from_path
|
26
|
-
from ._utils.deprecation import renamed_parameter
|
26
|
+
from ._utils.deprecation import deprecation_warning, renamed_parameter
|
27
27
|
from ._utils.grpc_utils import retry_transient_errors
|
28
28
|
from ._utils.name_utils import check_object_name
|
29
29
|
from ._utils.package_utils import get_module_mount_info
|
@@ -48,6 +48,11 @@ PYTHON_STANDALONE_VERSIONS: dict[str, tuple[str, str]] = {
|
|
48
48
|
"3.13": ("20241008", "3.13.0"),
|
49
49
|
}
|
50
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
|
+
|
51
56
|
|
52
57
|
def client_mount_name() -> str:
|
53
58
|
"""Get the deployed name of the client package mount."""
|
@@ -401,6 +406,23 @@ class _Mount(_Object, type_prefix="mo"):
|
|
401
406
|
)
|
402
407
|
```
|
403
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":
|
404
426
|
return _Mount._new().add_local_dir(
|
405
427
|
local_path, remote_path=remote_path, condition=condition, recursive=recursive
|
406
428
|
)
|
@@ -439,6 +461,13 @@ class _Mount(_Object, type_prefix="mo"):
|
|
439
461
|
)
|
440
462
|
```
|
441
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":
|
442
471
|
return _Mount._new().add_local_file(local_path, remote_path=remote_path)
|
443
472
|
|
444
473
|
@staticmethod
|
@@ -601,7 +630,24 @@ class _Mount(_Object, type_prefix="mo"):
|
|
601
630
|
my_local_module.do_stuff()
|
602
631
|
```
|
603
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
|
+
)
|
604
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":
|
605
651
|
# Don't re-run inside container.
|
606
652
|
|
607
653
|
if condition is not None:
|
@@ -786,7 +832,7 @@ def get_auto_mounts() -> list[_Mount]:
|
|
786
832
|
|
787
833
|
try:
|
788
834
|
# at this point we don't know if the sys.modules module should be mounted or not
|
789
|
-
potential_mount = _Mount.
|
835
|
+
potential_mount = _Mount._from_local_python_packages(module_name)
|
790
836
|
mount_paths = potential_mount._top_level_paths()
|
791
837
|
except ModuleNotMountable:
|
792
838
|
# this typically happens if the module is a built-in, has binary components or doesn't exist
|
modal/mount.pyi
CHANGED
@@ -113,6 +113,14 @@ class _Mount(modal.object._Object):
|
|
113
113
|
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
114
114
|
recursive: bool = True,
|
115
115
|
) -> _Mount: ...
|
116
|
+
@staticmethod
|
117
|
+
def _from_local_dir(
|
118
|
+
local_path: typing.Union[str, pathlib.Path],
|
119
|
+
*,
|
120
|
+
remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
|
121
|
+
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
122
|
+
recursive: bool = True,
|
123
|
+
) -> _Mount: ...
|
116
124
|
def add_local_file(
|
117
125
|
self,
|
118
126
|
local_path: typing.Union[str, pathlib.Path],
|
@@ -123,6 +131,10 @@ class _Mount(modal.object._Object):
|
|
123
131
|
local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
|
124
132
|
) -> _Mount: ...
|
125
133
|
@staticmethod
|
134
|
+
def _from_local_file(
|
135
|
+
local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
|
136
|
+
) -> _Mount: ...
|
137
|
+
@staticmethod
|
126
138
|
def _description(entries: list[_MountEntry]) -> str: ...
|
127
139
|
@staticmethod
|
128
140
|
def _get_files(
|
@@ -139,6 +151,13 @@ class _Mount(modal.object._Object):
|
|
139
151
|
ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
|
140
152
|
) -> _Mount: ...
|
141
153
|
@staticmethod
|
154
|
+
def _from_local_python_packages(
|
155
|
+
*module_names: str,
|
156
|
+
remote_dir: typing.Union[str, pathlib.PurePosixPath] = "/root",
|
157
|
+
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
158
|
+
ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
|
159
|
+
) -> _Mount: ...
|
160
|
+
@staticmethod
|
142
161
|
def from_name(name: str, namespace=1, environment_name: typing.Optional[str] = None) -> _Mount: ...
|
143
162
|
@classmethod
|
144
163
|
async def lookup(
|
@@ -195,6 +214,14 @@ class Mount(modal.object.Object):
|
|
195
214
|
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
196
215
|
recursive: bool = True,
|
197
216
|
) -> Mount: ...
|
217
|
+
@staticmethod
|
218
|
+
def _from_local_dir(
|
219
|
+
local_path: typing.Union[str, pathlib.Path],
|
220
|
+
*,
|
221
|
+
remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None,
|
222
|
+
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
223
|
+
recursive: bool = True,
|
224
|
+
) -> Mount: ...
|
198
225
|
def add_local_file(
|
199
226
|
self,
|
200
227
|
local_path: typing.Union[str, pathlib.Path],
|
@@ -205,6 +232,10 @@ class Mount(modal.object.Object):
|
|
205
232
|
local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
|
206
233
|
) -> Mount: ...
|
207
234
|
@staticmethod
|
235
|
+
def _from_local_file(
|
236
|
+
local_path: typing.Union[str, pathlib.Path], remote_path: typing.Union[str, pathlib.PurePosixPath, None] = None
|
237
|
+
) -> Mount: ...
|
238
|
+
@staticmethod
|
208
239
|
def _description(entries: list[_MountEntry]) -> str: ...
|
209
240
|
|
210
241
|
class ___get_files_spec(typing_extensions.Protocol):
|
@@ -231,6 +262,13 @@ class Mount(modal.object.Object):
|
|
231
262
|
ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
|
232
263
|
) -> Mount: ...
|
233
264
|
@staticmethod
|
265
|
+
def _from_local_python_packages(
|
266
|
+
*module_names: str,
|
267
|
+
remote_dir: typing.Union[str, pathlib.PurePosixPath] = "/root",
|
268
|
+
condition: typing.Optional[typing.Callable[[str], bool]] = None,
|
269
|
+
ignore: typing.Union[typing.Sequence[str], typing.Callable[[pathlib.Path], bool], None] = None,
|
270
|
+
) -> Mount: ...
|
271
|
+
@staticmethod
|
234
272
|
def from_name(name: str, namespace=1, environment_name: typing.Optional[str] = None) -> Mount: ...
|
235
273
|
@classmethod
|
236
274
|
def lookup(
|
@@ -15,11 +15,11 @@ modal/_traceback.py,sha256=IZQzB3fVlUfMHOSyKUgw0H6qv4yHnpyq-XVCNZKfUdA,5023
|
|
15
15
|
modal/_tunnel.py,sha256=o-jJhS4vQ6-XswDhHcJWGMZZmD03SC0e9i8fEu1JTjo,6310
|
16
16
|
modal/_tunnel.pyi,sha256=JmmDYAy9F1FpgJ_hWx0xkom2nTOFQjn4mTPYlU3PFo4,1245
|
17
17
|
modal/_watcher.py,sha256=K6LYnlmSGQB4tWWI9JADv-tvSvQ1j522FwT71B51CX8,3584
|
18
|
-
modal/app.py,sha256=
|
18
|
+
modal/app.py,sha256=1h8v96KuIg_SAdWDK9gNWVOYYFciBIerhldRPjvDgxs,45511
|
19
19
|
modal/app.pyi,sha256=Gx7gxjfQ70sxhbwfpx1VjvzEON-ZEMTJ_Vy8qt0oQvo,25302
|
20
20
|
modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
|
21
21
|
modal/client.py,sha256=JAnd4-GCN093BwkvOFAK5a6iy5ycxofjpUncMxlrIMw,15253
|
22
|
-
modal/client.pyi,sha256=
|
22
|
+
modal/client.pyi,sha256=aGMg6G-KZvllpIhVe9lKCjGRODL2eG7ur3StgGlwOGI,7278
|
23
23
|
modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
|
24
24
|
modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
|
25
25
|
modal/cls.py,sha256=3hjb0JcoPjxKZNeK22f5rR43bZRBjoRI7_EMZXY7YrE,31172
|
@@ -35,16 +35,16 @@ modal/exception.py,sha256=4JyO-SACaLNDe2QC48EjsK8GMkZ8AgEurZ8j1YdRu8E,5263
|
|
35
35
|
modal/experimental.py,sha256=npfKbyMpI41uZZs9HW_QiB3E4ykWfDXZbACXXbw6qeA,2385
|
36
36
|
modal/file_io.py,sha256=lcMs_E9Xfm0YX1t9U2wNIBPnqHRxmImqjLW1GHqVmyg,20945
|
37
37
|
modal/file_io.pyi,sha256=NrIoB0YjIqZ8MDMe826xAnybT0ww_kxQM3iPLo82REU,8898
|
38
|
-
modal/file_pattern_matcher.py,sha256=
|
38
|
+
modal/file_pattern_matcher.py,sha256=2ozXoalgCByA-aVk81Jv9W4_DWqLgBZAix_NADwPOrs,6527
|
39
39
|
modal/functions.py,sha256=3uJPbrEAWhpFfLfUnoRjGmvEUC-_wVh-8yNJBx8eVeM,68249
|
40
|
-
modal/functions.pyi,sha256=
|
40
|
+
modal/functions.pyi,sha256=LiSDgH-X7jcZ56pAoLMwo3x9Dzdp_3Sd7W5MVAJPoCg,25407
|
41
41
|
modal/gpu.py,sha256=MTxj6ql8EpgfBg8YmZ5a1cLznyuZFssX1qXbEX4LKVM,7503
|
42
|
-
modal/image.py,sha256=
|
43
|
-
modal/image.pyi,sha256=
|
42
|
+
modal/image.py,sha256=_ALAfk8YEiWnv_pHSnixBPoR88RGSDstsAUvlJa1nKc,89802
|
43
|
+
modal/image.pyi,sha256=NfZyLkl4rmxpc5fokaO4mmEeGFOwGn0AndV1vKwBdbs,26027
|
44
44
|
modal/io_streams.py,sha256=QkQiizKRzd5bnbKQsap31LJgBYlAnj4-XkV_50xPYX0,15079
|
45
45
|
modal/io_streams.pyi,sha256=bCCVSxkMcosYd8O3PQDDwJw7TQ8JEcnYonLJ5t27TQs,4804
|
46
|
-
modal/mount.py,sha256=
|
47
|
-
modal/mount.pyi,sha256=
|
46
|
+
modal/mount.py,sha256=nGimxizRzCpyZu9cpTdKQRgU9uqMnl3xBrTRR_rD6fE,31480
|
47
|
+
modal/mount.pyi,sha256=mcKMYwt8dX3oO8OzzKqCdh7K2EhMhWIHEPoJwzi1rl8,12194
|
48
48
|
modal/network_file_system.py,sha256=INj1TfN_Fsmabmlte7anvey1epodjbMmjBW_TIJSST4,14406
|
49
49
|
modal/network_file_system.pyi,sha256=61M-sdWrtaRjmuNVsvusI6kf1Qw-jUOVXvEAeOkM8Aw,7751
|
50
50
|
modal/object.py,sha256=HZs3N59C6JxlMuPQWJYvrWV1FEEkH9txUovVDorVUbs,9763
|
@@ -88,8 +88,8 @@ modal/_utils/async_utils.py,sha256=9ubwMkwiDB4gzOYG2jL9j7Fs-5dxHjcifZe3r7JRg-k,2
|
|
88
88
|
modal/_utils/blob_utils.py,sha256=N66LtZI8PpCkZ7maA7GLW5CAmYUoNJdG-GjaAUR4_NQ,14509
|
89
89
|
modal/_utils/bytes_io_segment_payload.py,sha256=uunxVJS4PE1LojF_UpURMzVK9GuvmYWRqQo_bxEj5TU,3385
|
90
90
|
modal/_utils/deprecation.py,sha256=dycySRBxyZf3ITzEqPNM6MxXTk9-0VVLA8oCPQ5j_Os,3426
|
91
|
-
modal/_utils/docker_utils.py,sha256=
|
92
|
-
modal/_utils/function_utils.py,sha256=
|
91
|
+
modal/_utils/docker_utils.py,sha256=h1uETghR40mp_y3fSWuZAfbIASH1HMzuphJHghAL6DU,3722
|
92
|
+
modal/_utils/function_utils.py,sha256=VFz3RdQc0yOKRmw5u5gkZCRzkJNqm9WoQAKg9CeUnJo,25521
|
93
93
|
modal/_utils/grpc_testing.py,sha256=H1zHqthv19eGPJz2HKXDyWXWGSqO4BRsxah3L5Xaa8A,8619
|
94
94
|
modal/_utils/grpc_utils.py,sha256=PPB5ay-vXencXNIWPVw5modr3EH7gfq2QPcO5YJ1lMU,7737
|
95
95
|
modal/_utils/hash_utils.py,sha256=zg3J6OGxTFGSFri1qQ12giDz90lWk8bzaxCTUCRtiX4,3034
|
@@ -115,7 +115,7 @@ modal/cli/dict.py,sha256=HaEcjfll7i3Uj3Fg56aj4407if5UljsYfr6fIq-D2W8,4589
|
|
115
115
|
modal/cli/entry_point.py,sha256=aaNxFAqZcmtSjwzkYIA_Ba9CkL4cL4_i2gy5VjoXxkM,4228
|
116
116
|
modal/cli/environment.py,sha256=Ayddkiq9jdj3XYDJ8ZmUqFpPPH8xajYlbexRkzGtUcg,4334
|
117
117
|
modal/cli/import_refs.py,sha256=wnqE5AMeyAN3IZmQvJCp54KRnJh8Nq_5fMqB6u6GEL8,9147
|
118
|
-
modal/cli/launch.py,sha256=
|
118
|
+
modal/cli/launch.py,sha256=t1XsExzTWip4n9Cqa-Jhzj65P6bVHO0je2d76JQRi_w,2990
|
119
119
|
modal/cli/network_file_system.py,sha256=o6VLTgN4xn5XUiNPBfxYec-5uWCgYrDmfFFLM1ZW_eE,8180
|
120
120
|
modal/cli/profile.py,sha256=rLXfjJObfPNjaZvNfHGIKqs7y9bGYyGe-K7V0w-Ni0M,3110
|
121
121
|
modal/cli/queues.py,sha256=MIh2OsliNE2QeL1erubfsRsNuG4fxqcqWA2vgIfQ4Mg,4494
|
@@ -125,8 +125,8 @@ modal/cli/token.py,sha256=mxSgOWakXG6N71hQb1ko61XAR9ZGkTMZD-Txn7gmTac,1924
|
|
125
125
|
modal/cli/utils.py,sha256=hZmjyzcPjDnQSkLvycZD2LhGdcsfdZshs_rOU78EpvI,3717
|
126
126
|
modal/cli/volume.py,sha256=Jrm-1R9u92JbbUM62bkB9RzAM_jO8wi7T2-i6Cb2XG0,10568
|
127
127
|
modal/cli/programs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
|
128
|
-
modal/cli/programs/run_jupyter.py,sha256=
|
129
|
-
modal/cli/programs/vscode.py,sha256=
|
128
|
+
modal/cli/programs/run_jupyter.py,sha256=KNaxG4LixbBJdLOzwKZB5IMA6tO1CSjHMKY6SpaIXd0,2685
|
129
|
+
modal/cli/programs/vscode.py,sha256=0k65NdAvRL3WGtngEYt-3w_kn9x2y3PqzA-dqdaVHtw,3383
|
130
130
|
modal/extensions/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
|
131
131
|
modal/extensions/ipython.py,sha256=Xvzy-A7cvwMSDa9p4c4CEMLOX2_Xsg9DkM1J9uyu7jc,983
|
132
132
|
modal/requirements/2023.12.312.txt,sha256=zWWUVgVQ92GXBKNYYr2-5vn9rlnXcmkqlwlX5u1eTYw,400
|
@@ -146,7 +146,7 @@ modal_global_objects/images/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0
|
|
146
146
|
modal_global_objects/images/base_images.py,sha256=tFc7tzQRJHtq23kURd6DTrnnO4Yp5ujr34WdJOM5ubI,775
|
147
147
|
modal_global_objects/mounts/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
|
148
148
|
modal_global_objects/mounts/modal_client_package.py,sha256=W0E_yShsRojPzWm6LtIQqNVolapdnrZkm2hVEQuZK_4,767
|
149
|
-
modal_global_objects/mounts/python_standalone.py,sha256=
|
149
|
+
modal_global_objects/mounts/python_standalone.py,sha256=pEML5GaV2_0ahci_1vpfc_FnySpsfi2fhYmFF5I7IiQ,1837
|
150
150
|
modal_proto/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
|
151
151
|
modal_proto/api.proto,sha256=1hO6_dn7DwgFra9TQSAXBt1NV4ETiiURPHe09bodinc,80368
|
152
152
|
modal_proto/api_grpc.py,sha256=VakjV_Ge3fgZDRJN6EeG2yY_LMkZvn6yVXr5SnFKIDA,103542
|
@@ -163,12 +163,12 @@ modal_proto/options_pb2.pyi,sha256=l7DBrbLO7q3Ir-XDkWsajm0d0TQqqrfuX54i4BMpdQg,1
|
|
163
163
|
modal_proto/options_pb2_grpc.py,sha256=1oboBPFxaTEXt9Aw7EAj8gXHDCNMhZD2VXqocC9l_gk,159
|
164
164
|
modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0yJSI,247
|
165
165
|
modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
166
|
-
modal_version/__init__.py,sha256=
|
166
|
+
modal_version/__init__.py,sha256=kGya2ZlItX2zB7oHORs-wvP4PG8lg_mtbi1QIK3G6SQ,470
|
167
167
|
modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
|
168
|
-
modal_version/_version_generated.py,sha256=
|
169
|
-
modal-0.
|
170
|
-
modal-0.
|
171
|
-
modal-0.
|
172
|
-
modal-0.
|
173
|
-
modal-0.
|
174
|
-
modal-0.
|
168
|
+
modal_version/_version_generated.py,sha256=YZkNwzq3yMtpJ2OS9vVQTjRRZGXPKR1UDV1uxVuO4WI,148
|
169
|
+
modal-0.72.5.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
170
|
+
modal-0.72.5.dist-info/METADATA,sha256=hYo1a_nRmFW_lrtVpvKcvxu6-OO1mZu1pj5YosbGk1U,2328
|
171
|
+
modal-0.72.5.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
172
|
+
modal-0.72.5.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
|
173
|
+
modal-0.72.5.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
|
174
|
+
modal-0.72.5.dist-info/RECORD,,
|
@@ -34,7 +34,7 @@ def publish_python_standalone_mount(client, version: str) -> None:
|
|
34
34
|
urllib.request.urlretrieve(url, f"{d}/cpython.tar.gz")
|
35
35
|
shutil.unpack_archive(f"{d}/cpython.tar.gz", d)
|
36
36
|
print(f"🌐 Downloaded and unpacked archive to {d}.")
|
37
|
-
python_mount = Mount.
|
37
|
+
python_mount = Mount._from_local_dir(f"{d}/python")
|
38
38
|
python_mount._deploy(
|
39
39
|
mount_name,
|
40
40
|
api_pb2.DEPLOYMENT_NAMESPACE_GLOBAL,
|
modal_version/__init__.py
CHANGED
@@ -7,7 +7,7 @@ from ._version_generated import build_number
|
|
7
7
|
major_number = 0
|
8
8
|
|
9
9
|
# Bump this manually on breaking changes, then reset the number in _version_generated.py
|
10
|
-
minor_number =
|
10
|
+
minor_number = 72
|
11
11
|
|
12
12
|
# Right now, automatically increment the patch number in CI
|
13
13
|
__version__ = f"{major_number}.{minor_number}.{max(build_number, 0)}"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|