modal 0.71.9__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.
@@ -1,7 +1,8 @@
1
1
  # Copyright Modal Labs 2024
2
2
  import re
3
3
  import shlex
4
- from typing import Sequence
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)
@@ -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.from_local_python_packages(self.module_name)]
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.from_local_dir(
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.from_local_file(
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, mounts=[mount], secrets=[secret], volumes={"/mnt/data": volume})
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, # Create a `modal.Mount` from a local directory.
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, Mount, Queue, Secret, Volume, forward
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
- mount = (
24
- Mount.from_local_dir(
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
  )
@@ -10,7 +10,7 @@ import time
10
10
  import webbrowser
11
11
  from typing import Any
12
12
 
13
- from modal import App, Image, Mount, Queue, Secret, Volume, forward
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
- app.image = (
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
- mount = (
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/cli/volume.py CHANGED
@@ -287,3 +287,26 @@ async def delete(
287
287
  )
288
288
 
289
289
  await _Volume.delete(volume_name, environment_name=env)
290
+
291
+
292
+ @volume_cli.command(
293
+ name="rename",
294
+ help="Rename a modal.Volume.",
295
+ rich_help_panel="Management",
296
+ )
297
+ @synchronizer.create_blocking
298
+ async def rename(
299
+ old_name: str,
300
+ new_name: str,
301
+ yes: bool = YES_OPTION,
302
+ env: Optional[str] = ENV_OPTION,
303
+ ):
304
+ if not yes:
305
+ typer.confirm(
306
+ f"Are you sure you want rename the modal.Volume '{old_name}'?"
307
+ " This may break any Apps currently using it.",
308
+ default=False,
309
+ abort=True,
310
+ )
311
+
312
+ await _Volume.rename(old_name, new_name, environment_name=env)
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.71.9"
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.71.9"
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
@@ -35,7 +35,7 @@ class _AbstractPatternMatcher:
35
35
  """
36
36
  return _CustomPatternMatcher(lambda path: not self(path))
37
37
 
38
- def with_repr(self, custom_repr) -> "_AbstractPatternMatcher":
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
- """Allows matching file paths against a list of patterns."""
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.patterns: list[Pattern] = []
75
- self.exclusions = False
76
- for p in list(pattern):
77
- p = p.strip()
78
- if not p:
79
- continue
80
- p = os.path.normpath(p)
81
- new_pattern = Pattern()
82
- if p[0] == "!":
83
- if len(p) == 1:
84
- raise ValueError('Illegal exclusion pattern: "!"')
85
- new_pattern.exclusion = True
86
- p = p[1:]
87
- self.exclusions = True
88
- # In Python, we can proceed without explicit syntax checking
89
- new_pattern.cleaned_pattern = p
90
- new_pattern.dirs = p.split(os.path.sep)
91
- self.patterns.append(new_pattern)
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
- """Check if the path matches any of the patterns.
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
- # with_repr allows us to use this matcher as a default value in a function signature
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")).with_repr(f"{__name__}.NON_PYTHON_FILES")
155
- _NOTHING = (~FilePatternMatcher()).with_repr(f"{__name__}._NOTHING") # match everything = ignore nothing
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[ReturnType_INNER, P_INNER]):
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[ReturnType, P]
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[ReturnType_INNER, P_INNER]):
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[ReturnType, P]
486
+ _experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
487
487
 
488
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
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[ReturnType, P]
492
+ spawn: __spawn_spec[P, ReturnType]
493
493
 
494
494
  def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
495
495