modal 0.68.24__py3-none-any.whl → 0.68.42__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/_traceback.py +6 -2
- modal/_utils/deprecation.py +89 -0
- modal/app.py +15 -34
- modal/app.pyi +6 -7
- modal/cli/app.py +1 -1
- modal/cli/dict.py +6 -2
- modal/cli/network_file_system.py +1 -1
- modal/cli/run.py +1 -0
- modal/cli/volume.py +1 -1
- modal/client.pyi +2 -2
- modal/cls.py +15 -9
- modal/cls.pyi +5 -5
- modal/config.py +2 -1
- modal/container_process.py +2 -1
- modal/dict.py +12 -17
- modal/dict.pyi +8 -12
- modal/environments.py +10 -7
- modal/environments.pyi +6 -6
- modal/exception.py +0 -54
- modal/file_io.py +54 -7
- modal/file_io.pyi +18 -8
- modal/file_pattern_matcher.py +48 -15
- modal/functions.py +11 -13
- modal/functions.pyi +18 -12
- modal/image.py +21 -20
- modal/image.pyi +16 -28
- modal/mount.py +7 -4
- modal/mount.pyi +4 -4
- modal/network_file_system.py +13 -19
- modal/network_file_system.pyi +8 -12
- modal/partial_function.py +2 -30
- modal/queue.py +12 -17
- modal/queue.pyi +8 -12
- modal/runner.py +2 -7
- modal/sandbox.py +25 -13
- modal/sandbox.pyi +21 -0
- modal/secret.py +7 -4
- modal/secret.pyi +5 -5
- modal/serving.py +1 -1
- modal/volume.py +12 -17
- modal/volume.pyi +8 -12
- {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/METADATA +2 -2
- {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/RECORD +51 -50
- modal_proto/api.proto +1 -1
- modal_proto/api_pb2.py +750 -750
- modal_proto/api_pb2.pyi +4 -4
- modal_version/_version_generated.py +1 -1
- {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/LICENSE +0 -0
- {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/WHEEL +0 -0
- {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/entry_points.txt +0 -0
- {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/top_level.txt +0 -0
modal/environments.py
CHANGED
@@ -10,6 +10,7 @@ from modal_proto import api_pb2
|
|
10
10
|
|
11
11
|
from ._resolver import Resolver
|
12
12
|
from ._utils.async_utils import synchronize_api, synchronizer
|
13
|
+
from ._utils.deprecation import renamed_parameter
|
13
14
|
from ._utils.grpc_utils import retry_transient_errors
|
14
15
|
from ._utils.name_utils import check_object_name
|
15
16
|
from .client import _Client
|
@@ -52,21 +53,22 @@ class _Environment(_Object, type_prefix="en"):
|
|
52
53
|
)
|
53
54
|
|
54
55
|
@staticmethod
|
56
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
55
57
|
async def from_name(
|
56
|
-
|
58
|
+
name: str,
|
57
59
|
create_if_missing: bool = False,
|
58
60
|
):
|
59
|
-
if
|
60
|
-
# Allow null
|
61
|
+
if name:
|
62
|
+
# Allow null names for the case where we want to look up the "default" environment,
|
61
63
|
# which is defined by the server. It feels messy to have "from_name" without a name, though?
|
62
64
|
# We're adding this mostly for internal use right now. We could consider an environment-only
|
63
65
|
# alternate constructor, like `Environment.get_default`, rather than exposing "unnamed"
|
64
66
|
# environments as part of public API when we make this class more useful.
|
65
|
-
check_object_name(
|
67
|
+
check_object_name(name, "Environment")
|
66
68
|
|
67
69
|
async def _load(self: _Environment, resolver: Resolver, existing_object_id: Optional[str]):
|
68
70
|
request = api_pb2.EnvironmentGetOrCreateRequest(
|
69
|
-
deployment_name=
|
71
|
+
deployment_name=name,
|
70
72
|
object_creation_type=(
|
71
73
|
api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING
|
72
74
|
if create_if_missing
|
@@ -81,12 +83,13 @@ class _Environment(_Object, type_prefix="en"):
|
|
81
83
|
return _Environment._from_loader(_load, "Environment()", is_another_app=True, hydrate_lazily=True)
|
82
84
|
|
83
85
|
@staticmethod
|
86
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
84
87
|
async def lookup(
|
85
|
-
|
88
|
+
name: str,
|
86
89
|
client: Optional[_Client] = None,
|
87
90
|
create_if_missing: bool = False,
|
88
91
|
):
|
89
|
-
obj = await _Environment.from_name(
|
92
|
+
obj = await _Environment.from_name(name, create_if_missing=create_if_missing)
|
90
93
|
if client is None:
|
91
94
|
client = await _Client.from_env()
|
92
95
|
resolver = Resolver(client=client)
|
modal/environments.pyi
CHANGED
@@ -22,10 +22,10 @@ class _Environment(modal.object._Object):
|
|
22
22
|
def __init__(self): ...
|
23
23
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
24
24
|
@staticmethod
|
25
|
-
async def from_name(
|
25
|
+
async def from_name(name: str, create_if_missing: bool = False): ...
|
26
26
|
@staticmethod
|
27
27
|
async def lookup(
|
28
|
-
|
28
|
+
name: str, client: typing.Optional[modal.client._Client] = None, create_if_missing: bool = False
|
29
29
|
): ...
|
30
30
|
|
31
31
|
class Environment(modal.object.Object):
|
@@ -35,17 +35,17 @@ class Environment(modal.object.Object):
|
|
35
35
|
def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
|
36
36
|
|
37
37
|
class __from_name_spec(typing_extensions.Protocol):
|
38
|
-
def __call__(self,
|
39
|
-
async def aio(self,
|
38
|
+
def __call__(self, name: str, create_if_missing: bool = False): ...
|
39
|
+
async def aio(self, name: str, create_if_missing: bool = False): ...
|
40
40
|
|
41
41
|
from_name: __from_name_spec
|
42
42
|
|
43
43
|
class __lookup_spec(typing_extensions.Protocol):
|
44
44
|
def __call__(
|
45
|
-
self,
|
45
|
+
self, name: str, client: typing.Optional[modal.client.Client] = None, create_if_missing: bool = False
|
46
46
|
): ...
|
47
47
|
async def aio(
|
48
|
-
self,
|
48
|
+
self, name: str, client: typing.Optional[modal.client.Client] = None, create_if_missing: bool = False
|
49
49
|
): ...
|
50
50
|
|
51
51
|
lookup: __lookup_spec
|
modal/exception.py
CHANGED
@@ -1,12 +1,6 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
2
|
import random
|
3
3
|
import signal
|
4
|
-
import sys
|
5
|
-
import warnings
|
6
|
-
from datetime import date
|
7
|
-
from typing import Iterable
|
8
|
-
|
9
|
-
from modal_proto import api_pb2
|
10
4
|
|
11
5
|
|
12
6
|
class Error(Exception):
|
@@ -129,45 +123,6 @@ class _CliUserExecutionError(Exception):
|
|
129
123
|
self.user_source = user_source
|
130
124
|
|
131
125
|
|
132
|
-
# TODO(erikbern): we have something similready in function_utils.py
|
133
|
-
_INTERNAL_MODULES = ["modal", "synchronicity"]
|
134
|
-
|
135
|
-
|
136
|
-
def _is_internal_frame(frame):
|
137
|
-
module = frame.f_globals["__name__"].split(".")[0]
|
138
|
-
return module in _INTERNAL_MODULES
|
139
|
-
|
140
|
-
|
141
|
-
def deprecation_error(deprecated_on: tuple[int, int, int], msg: str):
|
142
|
-
raise DeprecationError(f"Deprecated on {date(*deprecated_on)}: {msg}")
|
143
|
-
|
144
|
-
|
145
|
-
def deprecation_warning(
|
146
|
-
deprecated_on: tuple[int, int, int], msg: str, *, pending: bool = False, show_source: bool = True
|
147
|
-
) -> None:
|
148
|
-
"""Utility for getting the proper stack entry.
|
149
|
-
|
150
|
-
See the implementation of the built-in [warnings.warn](https://docs.python.org/3/library/warnings.html#available-functions).
|
151
|
-
"""
|
152
|
-
filename, lineno = "<unknown>", 0
|
153
|
-
if show_source:
|
154
|
-
# Find the last non-Modal line that triggered the warning
|
155
|
-
try:
|
156
|
-
frame = sys._getframe()
|
157
|
-
while frame is not None and _is_internal_frame(frame):
|
158
|
-
frame = frame.f_back
|
159
|
-
filename = frame.f_code.co_filename
|
160
|
-
lineno = frame.f_lineno
|
161
|
-
except ValueError:
|
162
|
-
# Use the defaults from above
|
163
|
-
pass
|
164
|
-
|
165
|
-
warning_cls: type = PendingDeprecationError if pending else DeprecationError
|
166
|
-
|
167
|
-
# This is a lower-level function that warnings.warn uses
|
168
|
-
warnings.warn_explicit(f"{date(*deprecated_on)}: {msg}", warning_cls, filename, lineno)
|
169
|
-
|
170
|
-
|
171
126
|
def _simulate_preemption_interrupt(signum, frame):
|
172
127
|
signal.alarm(30) # simulate a SIGKILL after 30s
|
173
128
|
raise KeyboardInterrupt("Simulated preemption interrupt from modal-client!")
|
@@ -224,12 +179,3 @@ class ClientClosed(Error):
|
|
224
179
|
|
225
180
|
class FilesystemExecutionError(Error):
|
226
181
|
"""Raised when an unknown error is thrown during a container filesystem operation."""
|
227
|
-
|
228
|
-
|
229
|
-
def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
|
230
|
-
# TODO(erikbern): move this to modal._utils.deprecation
|
231
|
-
for warning in server_warnings:
|
232
|
-
if warning.type == api_pb2.Warning.WARNING_TYPE_CLIENT_DEPRECATION:
|
233
|
-
warnings.warn_explicit(warning.message, DeprecationError, "<unknown>", 0)
|
234
|
-
else:
|
235
|
-
warnings.warn_explicit(warning.message, UserWarning, "<unknown>", 0)
|
modal/file_io.py
CHANGED
@@ -6,6 +6,8 @@ from typing import TYPE_CHECKING, AsyncIterator, Generic, Optional, Sequence, Ty
|
|
6
6
|
if TYPE_CHECKING:
|
7
7
|
import _typeshed
|
8
8
|
|
9
|
+
import json
|
10
|
+
|
9
11
|
from grpclib.exceptions import GRPCError, StreamTerminatedError
|
10
12
|
|
11
13
|
from modal._utils.grpc_utils import retry_transient_errors
|
@@ -267,12 +269,12 @@ class _FileIO(Generic[T]):
|
|
267
269
|
output = await self._make_read_request(None)
|
268
270
|
if self._binary:
|
269
271
|
lines_bytes = output.split(b"\n")
|
270
|
-
|
271
|
-
return cast(Sequence[T],
|
272
|
+
return_bytes = [line + b"\n" for line in lines_bytes[:-1]] + ([lines_bytes[-1]] if lines_bytes[-1] else [])
|
273
|
+
return cast(Sequence[T], return_bytes)
|
272
274
|
else:
|
273
275
|
lines = output.decode("utf-8").split("\n")
|
274
|
-
|
275
|
-
return cast(Sequence[T],
|
276
|
+
return_strs = [line + "\n" for line in lines[:-1]] + ([lines[-1]] if lines[-1] else [])
|
277
|
+
return cast(Sequence[T], return_strs)
|
276
278
|
|
277
279
|
async def write(self, data: Union[bytes, str]) -> None:
|
278
280
|
"""Write data to the current position.
|
@@ -337,6 +339,52 @@ class _FileIO(Generic[T]):
|
|
337
339
|
)
|
338
340
|
await self._wait(resp.exec_id)
|
339
341
|
|
342
|
+
@classmethod
|
343
|
+
async def ls(cls, path: str, client: _Client, task_id: str) -> list[str]:
|
344
|
+
"""List the contents of the provided directory."""
|
345
|
+
self = cls.__new__(cls)
|
346
|
+
self._client = client
|
347
|
+
self._task_id = task_id
|
348
|
+
resp = await self._make_request(
|
349
|
+
api_pb2.ContainerFilesystemExecRequest(
|
350
|
+
file_ls_request=api_pb2.ContainerFileLsRequest(path=path),
|
351
|
+
task_id=task_id,
|
352
|
+
)
|
353
|
+
)
|
354
|
+
output = await self._wait(resp.exec_id)
|
355
|
+
try:
|
356
|
+
return json.loads(output.decode("utf-8"))["paths"]
|
357
|
+
except json.JSONDecodeError:
|
358
|
+
raise FilesystemExecutionError("failed to parse list output")
|
359
|
+
|
360
|
+
@classmethod
|
361
|
+
async def mkdir(cls, path: str, client: _Client, task_id: str, parents: bool = False) -> None:
|
362
|
+
"""Create a new directory."""
|
363
|
+
self = cls.__new__(cls)
|
364
|
+
self._client = client
|
365
|
+
self._task_id = task_id
|
366
|
+
resp = await self._make_request(
|
367
|
+
api_pb2.ContainerFilesystemExecRequest(
|
368
|
+
file_mkdir_request=api_pb2.ContainerFileMkdirRequest(path=path, make_parents=parents),
|
369
|
+
task_id=self._task_id,
|
370
|
+
)
|
371
|
+
)
|
372
|
+
await self._wait(resp.exec_id)
|
373
|
+
|
374
|
+
@classmethod
|
375
|
+
async def rm(cls, path: str, client: _Client, task_id: str, recursive: bool = False) -> None:
|
376
|
+
"""Remove a file or directory in the Sandbox."""
|
377
|
+
self = cls.__new__(cls)
|
378
|
+
self._client = client
|
379
|
+
self._task_id = task_id
|
380
|
+
resp = await self._make_request(
|
381
|
+
api_pb2.ContainerFilesystemExecRequest(
|
382
|
+
file_rm_request=api_pb2.ContainerFileRmRequest(path=path, recursive=recursive),
|
383
|
+
task_id=self._task_id,
|
384
|
+
)
|
385
|
+
)
|
386
|
+
await self._wait(resp.exec_id)
|
387
|
+
|
340
388
|
async def _close(self) -> None:
|
341
389
|
# Buffer is flushed by the runner on close
|
342
390
|
resp = await self._make_request(
|
@@ -367,11 +415,10 @@ class _FileIO(Generic[T]):
|
|
367
415
|
if self._closed:
|
368
416
|
raise ValueError("I/O operation on closed file")
|
369
417
|
|
370
|
-
def
|
371
|
-
self._check_closed()
|
418
|
+
async def __aenter__(self) -> "_FileIO":
|
372
419
|
return self
|
373
420
|
|
374
|
-
async def
|
421
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> None:
|
375
422
|
await self._close()
|
376
423
|
|
377
424
|
|
modal/file_io.pyi
CHANGED
@@ -43,13 +43,19 @@ class _FileIO(typing.Generic[T]):
|
|
43
43
|
async def flush(self) -> None: ...
|
44
44
|
def _get_whence(self, whence: int): ...
|
45
45
|
async def seek(self, offset: int, whence: int = 0) -> None: ...
|
46
|
+
@classmethod
|
47
|
+
async def ls(cls, path: str, client: modal.client._Client, task_id: str) -> list[str]: ...
|
48
|
+
@classmethod
|
49
|
+
async def mkdir(cls, path: str, client: modal.client._Client, task_id: str, parents: bool = False) -> None: ...
|
50
|
+
@classmethod
|
51
|
+
async def rm(cls, path: str, client: modal.client._Client, task_id: str, recursive: bool = False) -> None: ...
|
46
52
|
async def _close(self) -> None: ...
|
47
53
|
async def close(self) -> None: ...
|
48
54
|
def _check_writable(self) -> None: ...
|
49
55
|
def _check_readable(self) -> None: ...
|
50
56
|
def _check_closed(self) -> None: ...
|
51
|
-
def
|
52
|
-
async def
|
57
|
+
async def __aenter__(self) -> _FileIO: ...
|
58
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> None: ...
|
53
59
|
|
54
60
|
class __delete_bytes_spec(typing_extensions.Protocol):
|
55
61
|
def __call__(self, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None: ...
|
@@ -161,6 +167,13 @@ class FileIO(typing.Generic[T]):
|
|
161
167
|
|
162
168
|
seek: __seek_spec
|
163
169
|
|
170
|
+
@classmethod
|
171
|
+
def ls(cls, path: str, client: modal.client.Client, task_id: str) -> list[str]: ...
|
172
|
+
@classmethod
|
173
|
+
def mkdir(cls, path: str, client: modal.client.Client, task_id: str, parents: bool = False) -> None: ...
|
174
|
+
@classmethod
|
175
|
+
def rm(cls, path: str, client: modal.client.Client, task_id: str, recursive: bool = False) -> None: ...
|
176
|
+
|
164
177
|
class ___close_spec(typing_extensions.Protocol):
|
165
178
|
def __call__(self) -> None: ...
|
166
179
|
async def aio(self) -> None: ...
|
@@ -177,9 +190,6 @@ class FileIO(typing.Generic[T]):
|
|
177
190
|
def _check_readable(self) -> None: ...
|
178
191
|
def _check_closed(self) -> None: ...
|
179
192
|
def __enter__(self) -> FileIO: ...
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
async def aio(self, exc_type, exc_value, traceback) -> None: ...
|
184
|
-
|
185
|
-
__exit__: ____exit___spec
|
193
|
+
async def __aenter__(self) -> FileIO: ...
|
194
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None: ...
|
195
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> None: ...
|
modal/file_pattern_matcher.py
CHANGED
@@ -10,13 +10,56 @@ then asking it whether file paths match any of its patterns.
|
|
10
10
|
"""
|
11
11
|
|
12
12
|
import os
|
13
|
+
from abc import abstractmethod
|
13
14
|
from pathlib import Path
|
14
|
-
from typing import Callable
|
15
|
+
from typing import Callable, Optional
|
15
16
|
|
16
17
|
from ._utils.pattern_utils import Pattern
|
17
18
|
|
18
19
|
|
19
|
-
class
|
20
|
+
class _AbstractPatternMatcher:
|
21
|
+
_custom_repr: Optional[str] = None
|
22
|
+
|
23
|
+
def __invert__(self) -> "_AbstractPatternMatcher":
|
24
|
+
"""Invert the filter. Returns a function that returns True if the path does not match any of the patterns.
|
25
|
+
|
26
|
+
Usage:
|
27
|
+
```python
|
28
|
+
from pathlib import Path
|
29
|
+
from modal import FilePatternMatcher
|
30
|
+
|
31
|
+
inverted_matcher = ~FilePatternMatcher("**/*.py")
|
32
|
+
|
33
|
+
assert not inverted_matcher(Path("foo.py"))
|
34
|
+
```
|
35
|
+
"""
|
36
|
+
return _CustomPatternMatcher(lambda path: not self(path))
|
37
|
+
|
38
|
+
def with_repr(self, custom_repr) -> "_AbstractPatternMatcher":
|
39
|
+
# use to give an instance of a matcher a custom name - useful for visualizing default values in signatures
|
40
|
+
self._custom_repr = custom_repr
|
41
|
+
return self
|
42
|
+
|
43
|
+
def __repr__(self) -> str:
|
44
|
+
if self._custom_repr:
|
45
|
+
return self._custom_repr
|
46
|
+
|
47
|
+
return super().__repr__()
|
48
|
+
|
49
|
+
@abstractmethod
|
50
|
+
def __call__(self, path: Path) -> bool:
|
51
|
+
...
|
52
|
+
|
53
|
+
|
54
|
+
class _CustomPatternMatcher(_AbstractPatternMatcher):
|
55
|
+
def __init__(self, predicate: Callable[[Path], bool]):
|
56
|
+
self._predicate = predicate
|
57
|
+
|
58
|
+
def __call__(self, path: Path) -> bool:
|
59
|
+
return self._predicate(path)
|
60
|
+
|
61
|
+
|
62
|
+
class FilePatternMatcher(_AbstractPatternMatcher):
|
20
63
|
"""Allows matching file paths against a list of patterns."""
|
21
64
|
|
22
65
|
def __init__(self, *pattern: str) -> None:
|
@@ -105,17 +148,7 @@ class FilePatternMatcher:
|
|
105
148
|
"""
|
106
149
|
return self._matches(str(file_path))
|
107
150
|
|
108
|
-
def __invert__(self) -> Callable[[Path], bool]:
|
109
|
-
"""Invert the filter. Returns a function that returns True if the path does not match any of the patterns.
|
110
|
-
|
111
|
-
Usage:
|
112
|
-
```python
|
113
|
-
from pathlib import Path
|
114
|
-
from modal import FilePatternMatcher
|
115
|
-
|
116
|
-
inverted_matcher = ~FilePatternMatcher("**/*.py")
|
117
151
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
return lambda path: not self(path)
|
152
|
+
# with_repr allows us to use this matcher as a default value in a function signature
|
153
|
+
# 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")
|
modal/functions.py
CHANGED
@@ -22,7 +22,6 @@ from grpclib import GRPCError, Status
|
|
22
22
|
from synchronicity.combined_types import MethodWithAio
|
23
23
|
from synchronicity.exceptions import UserCodeException
|
24
24
|
|
25
|
-
from modal._utils.async_utils import aclosing
|
26
25
|
from modal_proto import api_pb2
|
27
26
|
from modal_proto.modal_api_grpc import ModalClientModal
|
28
27
|
|
@@ -35,12 +34,14 @@ from ._serialization import serialize, serialize_proto_params
|
|
35
34
|
from ._traceback import print_server_warnings
|
36
35
|
from ._utils.async_utils import (
|
37
36
|
TaskContext,
|
37
|
+
aclosing,
|
38
38
|
async_merge,
|
39
39
|
callable_to_agen,
|
40
40
|
synchronize_api,
|
41
41
|
synchronizer,
|
42
42
|
warn_if_generator_is_not_consumed,
|
43
43
|
)
|
44
|
+
from ._utils.deprecation import deprecation_warning, renamed_parameter
|
44
45
|
from ._utils.function_utils import (
|
45
46
|
ATTEMPT_TIMEOUT_GRACE_PERIOD,
|
46
47
|
OUTPUTS_TIMEOUT,
|
@@ -58,14 +59,7 @@ from .call_graph import InputInfo, _reconstruct_call_graph
|
|
58
59
|
from .client import _Client
|
59
60
|
from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
|
60
61
|
from .config import config
|
61
|
-
from .exception import
|
62
|
-
ExecutionError,
|
63
|
-
FunctionTimeoutError,
|
64
|
-
InvalidError,
|
65
|
-
NotFoundError,
|
66
|
-
OutputExpiredError,
|
67
|
-
deprecation_warning,
|
68
|
-
)
|
62
|
+
from .exception import ExecutionError, FunctionTimeoutError, InvalidError, NotFoundError, OutputExpiredError
|
69
63
|
from .gpu import GPU_T, parse_gpu_config
|
70
64
|
from .image import _Image
|
71
65
|
from .mount import _get_client_mount, _Mount, get_auto_mounts
|
@@ -352,6 +346,7 @@ class _FunctionSpec:
|
|
352
346
|
memory: Optional[Union[int, tuple[int, int]]]
|
353
347
|
ephemeral_disk: Optional[int]
|
354
348
|
scheduler_placement: Optional[SchedulerPlacement]
|
349
|
+
proxy: Optional[_Proxy]
|
355
350
|
|
356
351
|
|
357
352
|
P = typing_extensions.ParamSpec("P")
|
@@ -536,6 +531,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
536
531
|
memory=memory,
|
537
532
|
ephemeral_disk=ephemeral_disk,
|
538
533
|
scheduler_placement=scheduler_placement,
|
534
|
+
proxy=proxy,
|
539
535
|
)
|
540
536
|
|
541
537
|
if info.user_cls and not is_auto_snapshot:
|
@@ -1028,10 +1024,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1028
1024
|
await retry_transient_errors(self._client.stub.FunctionUpdateSchedulingParams, request)
|
1029
1025
|
|
1030
1026
|
@classmethod
|
1027
|
+
@renamed_parameter((2024, 12, 18), "tag", "name")
|
1031
1028
|
def from_name(
|
1032
1029
|
cls: type["_Function"],
|
1033
1030
|
app_name: str,
|
1034
|
-
|
1031
|
+
name: str,
|
1035
1032
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
1036
1033
|
environment_name: Optional[str] = None,
|
1037
1034
|
) -> "_Function":
|
@@ -1050,7 +1047,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1050
1047
|
assert resolver.client and resolver.client.stub
|
1051
1048
|
request = api_pb2.FunctionGetRequest(
|
1052
1049
|
app_name=app_name,
|
1053
|
-
object_tag=
|
1050
|
+
object_tag=name,
|
1054
1051
|
namespace=namespace,
|
1055
1052
|
environment_name=_get_environment_name(environment_name, resolver) or "",
|
1056
1053
|
)
|
@@ -1070,9 +1067,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1070
1067
|
return cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
|
1071
1068
|
|
1072
1069
|
@staticmethod
|
1070
|
+
@renamed_parameter((2024, 12, 18), "tag", "name")
|
1073
1071
|
async def lookup(
|
1074
1072
|
app_name: str,
|
1075
|
-
|
1073
|
+
name: str,
|
1076
1074
|
namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
|
1077
1075
|
client: Optional[_Client] = None,
|
1078
1076
|
environment_name: Optional[str] = None,
|
@@ -1086,7 +1084,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
|
|
1086
1084
|
f = modal.Function.lookup("other-app", "function")
|
1087
1085
|
```
|
1088
1086
|
"""
|
1089
|
-
obj = _Function.from_name(app_name,
|
1087
|
+
obj = _Function.from_name(app_name, name, namespace=namespace, environment_name=environment_name)
|
1090
1088
|
if client is None:
|
1091
1089
|
client = await _Client.from_env()
|
1092
1090
|
resolver = Resolver(client=client)
|
modal/functions.pyi
CHANGED
@@ -100,6 +100,7 @@ class _FunctionSpec:
|
|
100
100
|
memory: typing.Union[int, tuple[int, int], None]
|
101
101
|
ephemeral_disk: typing.Optional[int]
|
102
102
|
scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement]
|
103
|
+
proxy: typing.Optional[modal.proxy._Proxy]
|
103
104
|
|
104
105
|
def __init__(
|
105
106
|
self,
|
@@ -121,6 +122,7 @@ class _FunctionSpec:
|
|
121
122
|
memory: typing.Union[int, tuple[int, int], None],
|
122
123
|
ephemeral_disk: typing.Optional[int],
|
123
124
|
scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement],
|
125
|
+
proxy: typing.Optional[modal.proxy._Proxy],
|
124
126
|
) -> None: ...
|
125
127
|
def __repr__(self): ...
|
126
128
|
def __eq__(self, other): ...
|
@@ -206,12 +208,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
|
|
206
208
|
async def keep_warm(self, warm_pool_size: int) -> None: ...
|
207
209
|
@classmethod
|
208
210
|
def from_name(
|
209
|
-
cls: type[_Function], app_name: str,
|
211
|
+
cls: type[_Function], app_name: str, name: str, namespace=1, environment_name: typing.Optional[str] = None
|
210
212
|
) -> _Function: ...
|
211
213
|
@staticmethod
|
212
214
|
async def lookup(
|
213
215
|
app_name: str,
|
214
|
-
|
216
|
+
name: str,
|
215
217
|
namespace=1,
|
216
218
|
client: typing.Optional[modal.client._Client] = None,
|
217
219
|
environment_name: typing.Optional[str] = None,
|
@@ -381,14 +383,14 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
381
383
|
|
382
384
|
@classmethod
|
383
385
|
def from_name(
|
384
|
-
cls: type[Function], app_name: str,
|
386
|
+
cls: type[Function], app_name: str, name: str, namespace=1, environment_name: typing.Optional[str] = None
|
385
387
|
) -> Function: ...
|
386
388
|
|
387
389
|
class __lookup_spec(typing_extensions.Protocol):
|
388
390
|
def __call__(
|
389
391
|
self,
|
390
392
|
app_name: str,
|
391
|
-
|
393
|
+
name: str,
|
392
394
|
namespace=1,
|
393
395
|
client: typing.Optional[modal.client.Client] = None,
|
394
396
|
environment_name: typing.Optional[str] = None,
|
@@ -396,7 +398,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
396
398
|
async def aio(
|
397
399
|
self,
|
398
400
|
app_name: str,
|
399
|
-
|
401
|
+
name: str,
|
400
402
|
namespace=1,
|
401
403
|
client: typing.Optional[modal.client.Client] = None,
|
402
404
|
environment_name: typing.Optional[str] = None,
|
@@ -448,7 +450,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
448
450
|
|
449
451
|
_call_function_nowait: ___call_function_nowait_spec
|
450
452
|
|
451
|
-
|
453
|
+
class ___call_generator_spec(typing_extensions.Protocol):
|
454
|
+
def __call__(self, args, kwargs): ...
|
455
|
+
def aio(self, args, kwargs): ...
|
456
|
+
|
457
|
+
_call_generator: ___call_generator_spec
|
452
458
|
|
453
459
|
class ___call_generator_nowait_spec(typing_extensions.Protocol):
|
454
460
|
def __call__(self, args, kwargs): ...
|
@@ -456,11 +462,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
456
462
|
|
457
463
|
_call_generator_nowait: ___call_generator_nowait_spec
|
458
464
|
|
459
|
-
class __remote_spec(typing_extensions.Protocol[
|
465
|
+
class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
|
460
466
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
461
467
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
|
462
468
|
|
463
|
-
remote: __remote_spec[
|
469
|
+
remote: __remote_spec[P, ReturnType]
|
464
470
|
|
465
471
|
class __remote_gen_spec(typing_extensions.Protocol):
|
466
472
|
def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
|
@@ -473,17 +479,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
|
|
473
479
|
def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
|
474
480
|
def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
|
475
481
|
|
476
|
-
class ___experimental_spawn_spec(typing_extensions.Protocol[
|
482
|
+
class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
|
477
483
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
478
484
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
479
485
|
|
480
|
-
_experimental_spawn: ___experimental_spawn_spec[
|
486
|
+
_experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
|
481
487
|
|
482
|
-
class __spawn_spec(typing_extensions.Protocol[
|
488
|
+
class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
|
483
489
|
def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
484
490
|
async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
|
485
491
|
|
486
|
-
spawn: __spawn_spec[
|
492
|
+
spawn: __spawn_spec[P, ReturnType]
|
487
493
|
|
488
494
|
def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
|
489
495
|
|
modal/image.py
CHANGED
@@ -30,14 +30,15 @@ from ._resolver import Resolver
|
|
30
30
|
from ._serialization import serialize
|
31
31
|
from ._utils.async_utils import synchronize_api
|
32
32
|
from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
|
33
|
+
from ._utils.deprecation import deprecation_error, deprecation_warning
|
33
34
|
from ._utils.function_utils import FunctionInfo
|
34
35
|
from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
|
35
36
|
from .client import _Client
|
36
37
|
from .cloud_bucket_mount import _CloudBucketMount
|
37
38
|
from .config import config, logger, user_config_path
|
38
39
|
from .environments import _get_environment_cached
|
39
|
-
from .exception import InvalidError, NotFoundError, RemoteError, VersionError
|
40
|
-
from .file_pattern_matcher import
|
40
|
+
from .exception import InvalidError, NotFoundError, RemoteError, VersionError
|
41
|
+
from .file_pattern_matcher import NON_PYTHON_FILES
|
41
42
|
from .gpu import GPU_T, parse_gpu_config
|
42
43
|
from .mount import _Mount, python_standalone_mount_name
|
43
44
|
from .network_file_system import _NetworkFileSystem
|
@@ -570,21 +571,6 @@ class _Image(_Object, type_prefix="im"):
|
|
570
571
|
obj.force_build = force_build
|
571
572
|
return obj
|
572
573
|
|
573
|
-
def extend(self, **kwargs) -> "_Image":
|
574
|
-
"""mdmd:hidden"""
|
575
|
-
deprecation_error(
|
576
|
-
(2024, 3, 7),
|
577
|
-
"`Image.extend` is deprecated; please use a higher-level method, such as `Image.dockerfile_commands`.",
|
578
|
-
)
|
579
|
-
|
580
|
-
def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
|
581
|
-
return DockerfileSpec(
|
582
|
-
commands=kwargs.pop("dockerfile_commands", []),
|
583
|
-
context_files=kwargs.pop("context_files", {}),
|
584
|
-
)
|
585
|
-
|
586
|
-
return _Image._from_args(base_images={"base": self}, dockerfile_function=build_dockerfile, **kwargs)
|
587
|
-
|
588
574
|
def copy_mount(self, mount: _Mount, remote_path: Union[str, Path] = ".") -> "_Image":
|
589
575
|
"""Copy the entire contents of a `modal.Mount` into an image.
|
590
576
|
Useful when files only available locally are required during the image
|
@@ -720,7 +706,9 @@ class _Image(_Object, type_prefix="im"):
|
|
720
706
|
context_mount=mount,
|
721
707
|
)
|
722
708
|
|
723
|
-
def add_local_python_source(
|
709
|
+
def add_local_python_source(
|
710
|
+
self, *modules: str, copy: bool = False, ignore: Union[Sequence[str], Callable[[Path], bool]] = NON_PYTHON_FILES
|
711
|
+
) -> "_Image":
|
724
712
|
"""Adds locally available Python packages/modules to containers
|
725
713
|
|
726
714
|
Adds all files from the specified Python package or module to containers running the Image.
|
@@ -738,9 +726,22 @@ class _Image(_Object, type_prefix="im"):
|
|
738
726
|
**Note:** This excludes all dot-prefixed subdirectories or files and all `.pyc`/`__pycache__` files.
|
739
727
|
To add full directories with finer control, use `.add_local_dir()` instead and specify `/root` as
|
740
728
|
the destination directory.
|
741
|
-
"""
|
742
729
|
|
743
|
-
|
730
|
+
By default only includes `.py`-files in the source modules. Set the `ignore` argument to a list of patterns
|
731
|
+
or a callable to override this behavior, e.g.:
|
732
|
+
|
733
|
+
```py
|
734
|
+
# includes everything except data.json
|
735
|
+
modal.Image.debian_slim().add_local_python_source("mymodule", ignore=["data.json"])
|
736
|
+
|
737
|
+
# exclude large files
|
738
|
+
modal.Image.debian_slim().add_local_python_source(
|
739
|
+
"mymodule",
|
740
|
+
ignore=lambda p: p.stat().st_size > 1e9
|
741
|
+
)
|
742
|
+
```
|
743
|
+
"""
|
744
|
+
mount = _Mount.from_local_python_packages(*modules, ignore=ignore)
|
744
745
|
return self._add_mount_layer_or_copy(mount, copy=copy)
|
745
746
|
|
746
747
|
def copy_local_dir(
|