modal 0.67.1__py3-none-any.whl → 0.67.22__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/_clustered_functions.py +2 -2
- modal/_clustered_functions.pyi +2 -2
- modal/_container_entrypoint.py +5 -4
- modal/_output.py +29 -28
- modal/_pty.py +2 -2
- modal/_resolver.py +6 -5
- modal/_resources.py +3 -3
- modal/_runtime/asgi.py +46 -6
- modal/_runtime/container_io_manager.py +22 -26
- modal/_runtime/execution_context.py +2 -2
- modal/_runtime/telemetry.py +1 -2
- modal/_runtime/user_code_imports.py +12 -14
- modal/_serialization.py +3 -7
- modal/_traceback.py +5 -5
- modal/_tunnel.py +5 -4
- modal/_tunnel.pyi +2 -2
- modal/_utils/async_utils.py +53 -17
- modal/_utils/blob_utils.py +22 -7
- modal/_utils/function_utils.py +14 -10
- modal/_utils/grpc_testing.py +7 -6
- modal/_utils/grpc_utils.py +2 -3
- modal/_utils/hash_utils.py +2 -2
- modal/_utils/mount_utils.py +5 -4
- modal/_utils/package_utils.py +2 -3
- modal/_utils/pattern_matcher.py +6 -6
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +2 -1
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +8 -7
- modal/app.py +68 -62
- modal/app.pyi +104 -99
- modal/call_graph.py +6 -6
- modal/cli/_download.py +3 -2
- modal/cli/_traceback.py +4 -4
- modal/cli/app.py +4 -4
- modal/cli/container.py +4 -4
- modal/cli/dict.py +1 -1
- modal/cli/environment.py +2 -3
- modal/cli/import_refs.py +1 -1
- modal/cli/launch.py +2 -2
- modal/cli/network_file_system.py +1 -1
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +2 -2
- modal/cli/programs/vscode.py +3 -3
- modal/cli/queues.py +1 -1
- modal/cli/run.py +6 -6
- modal/cli/secret.py +3 -3
- modal/cli/utils.py +2 -1
- modal/cli/volume.py +3 -3
- modal/client.py +6 -11
- modal/client.pyi +18 -27
- modal/cloud_bucket_mount.py +3 -3
- modal/cloud_bucket_mount.pyi +2 -2
- modal/cls.py +32 -32
- modal/cls.pyi +35 -34
- modal/config.py +3 -2
- modal/container_process.py +6 -2
- modal/dict.py +6 -3
- modal/dict.pyi +10 -9
- modal/environments.py +3 -3
- modal/environments.pyi +3 -3
- modal/exception.py +2 -3
- modal/functions.py +111 -40
- modal/functions.pyi +71 -48
- modal/image.py +46 -49
- modal/image.pyi +102 -101
- modal/io_streams.py +20 -12
- modal/io_streams.pyi +24 -14
- modal/mount.py +24 -24
- modal/mount.pyi +28 -29
- modal/network_file_system.py +14 -11
- modal/network_file_system.pyi +12 -11
- modal/object.py +9 -8
- modal/object.pyi +47 -34
- modal/output.py +2 -1
- modal/parallel_map.py +4 -4
- modal/partial_function.py +10 -14
- modal/partial_function.pyi +17 -18
- modal/queue.py +11 -8
- modal/queue.pyi +23 -22
- modal/retries.py +38 -0
- modal/runner.py +8 -7
- modal/runner.pyi +8 -14
- modal/running_app.py +3 -3
- modal/sandbox.py +20 -13
- modal/sandbox.pyi +73 -72
- modal/scheduler_placement.py +2 -1
- modal/secret.py +7 -7
- modal/secret.pyi +12 -12
- modal/serving.py +4 -3
- modal/serving.pyi +5 -4
- modal/token_flow.py +3 -2
- modal/token_flow.pyi +3 -3
- modal/volume.py +16 -23
- modal/volume.pyi +17 -16
- {modal-0.67.1.dist-info → modal-0.67.22.dist-info}/METADATA +2 -2
- modal-0.67.22.dist-info/RECORD +168 -0
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/mounts/python_standalone.py +1 -1
- modal_proto/api.proto +13 -0
- modal_proto/api_grpc.py +16 -0
- modal_proto/api_pb2.py +241 -221
- modal_proto/api_pb2.pyi +41 -0
- modal_proto/api_pb2_grpc.py +33 -0
- modal_proto/api_pb2_grpc.pyi +10 -0
- modal_proto/modal_api_grpc.py +1 -0
- modal_version/_version_generated.py +1 -1
- modal-0.67.1.dist-info/RECORD +0 -168
- {modal-0.67.1.dist-info → modal-0.67.22.dist-info}/LICENSE +0 -0
- {modal-0.67.1.dist-info → modal-0.67.22.dist-info}/WHEEL +0 -0
- {modal-0.67.1.dist-info → modal-0.67.22.dist-info}/entry_points.txt +0 -0
- {modal-0.67.1.dist-info → modal-0.67.22.dist-info}/top_level.txt +0 -0
modal/_vendor/a2wsgi_wsgi.py
CHANGED
@@ -35,10 +35,8 @@ from concurrent.futures import ThreadPoolExecutor
|
|
35
35
|
from types import TracebackType
|
36
36
|
from typing import (
|
37
37
|
Any,
|
38
|
-
Awaitable,
|
39
38
|
Callable,
|
40
39
|
Dict,
|
41
|
-
Iterable,
|
42
40
|
List,
|
43
41
|
Literal,
|
44
42
|
Optional,
|
@@ -48,6 +46,7 @@ from typing import (
|
|
48
46
|
TypedDict,
|
49
47
|
Union,
|
50
48
|
)
|
49
|
+
from collections.abc import Awaitable, Iterable
|
51
50
|
|
52
51
|
|
53
52
|
## BEGIN a2wsgi/asgi_typing.py
|
@@ -73,11 +72,11 @@ class HTTPScope(TypedDict):
|
|
73
72
|
raw_path: NotRequired[bytes]
|
74
73
|
query_string: bytes
|
75
74
|
root_path: str
|
76
|
-
headers: Iterable[
|
77
|
-
client: NotRequired[
|
78
|
-
server: NotRequired[
|
79
|
-
state: NotRequired[
|
80
|
-
extensions: NotRequired[
|
75
|
+
headers: Iterable[tuple[bytes, bytes]]
|
76
|
+
client: NotRequired[tuple[str, int]]
|
77
|
+
server: NotRequired[tuple[str, Optional[int]]]
|
78
|
+
state: NotRequired[dict[str, Any]]
|
79
|
+
extensions: NotRequired[dict[str, dict[object, object]]]
|
81
80
|
|
82
81
|
|
83
82
|
class WebSocketScope(TypedDict):
|
@@ -89,18 +88,18 @@ class WebSocketScope(TypedDict):
|
|
89
88
|
raw_path: bytes
|
90
89
|
query_string: bytes
|
91
90
|
root_path: str
|
92
|
-
headers: Iterable[
|
93
|
-
client: NotRequired[
|
94
|
-
server: NotRequired[
|
91
|
+
headers: Iterable[tuple[bytes, bytes]]
|
92
|
+
client: NotRequired[tuple[str, int]]
|
93
|
+
server: NotRequired[tuple[str, Optional[int]]]
|
95
94
|
subprotocols: Iterable[str]
|
96
|
-
state: NotRequired[
|
97
|
-
extensions: NotRequired[
|
95
|
+
state: NotRequired[dict[str, Any]]
|
96
|
+
extensions: NotRequired[dict[str, dict[object, object]]]
|
98
97
|
|
99
98
|
|
100
99
|
class LifespanScope(TypedDict):
|
101
100
|
type: Literal["lifespan"]
|
102
101
|
asgi: ASGIVersions
|
103
|
-
state: NotRequired[
|
102
|
+
state: NotRequired[dict[str, Any]]
|
104
103
|
|
105
104
|
|
106
105
|
WWWScope = Union[HTTPScope, WebSocketScope]
|
@@ -116,7 +115,7 @@ class HTTPRequestEvent(TypedDict):
|
|
116
115
|
class HTTPResponseStartEvent(TypedDict):
|
117
116
|
type: Literal["http.response.start"]
|
118
117
|
status: int
|
119
|
-
headers: NotRequired[Iterable[
|
118
|
+
headers: NotRequired[Iterable[tuple[bytes, bytes]]]
|
120
119
|
trailers: NotRequired[bool]
|
121
120
|
|
122
121
|
|
@@ -137,7 +136,7 @@ class WebSocketConnectEvent(TypedDict):
|
|
137
136
|
class WebSocketAcceptEvent(TypedDict):
|
138
137
|
type: Literal["websocket.accept"]
|
139
138
|
subprotocol: NotRequired[str]
|
140
|
-
headers: NotRequired[Iterable[
|
139
|
+
headers: NotRequired[Iterable[tuple[bytes, bytes]]]
|
141
140
|
|
142
141
|
|
143
142
|
class WebSocketReceiveEvent(TypedDict):
|
@@ -223,56 +222,47 @@ ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]]
|
|
223
222
|
|
224
223
|
## BEGIN a2wsgi/wsgi_typing.py
|
225
224
|
|
226
|
-
CGIRequiredDefined
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
# The contents of any Content-Type fields in the HTTP request. May be empty
|
268
|
-
# or absent.
|
269
|
-
"CONTENT_TYPE": str,
|
270
|
-
# The contents of any Content-Length fields in the HTTP request. May be empty
|
271
|
-
# or absent.
|
272
|
-
"CONTENT_LENGTH": str,
|
273
|
-
},
|
274
|
-
total=False,
|
275
|
-
)
|
225
|
+
class CGIRequiredDefined(TypedDict):
|
226
|
+
# The HTTP request method, such as GET or POST. This cannot ever be an
|
227
|
+
# empty string, and so is always required.
|
228
|
+
REQUEST_METHOD: str
|
229
|
+
# When HTTP_HOST is not set, these variables can be combined to determine
|
230
|
+
# a default.
|
231
|
+
# SERVER_NAME and SERVER_PORT are required strings and must never be empty.
|
232
|
+
SERVER_NAME: str
|
233
|
+
SERVER_PORT: str
|
234
|
+
# The version of the protocol the client used to send the request.
|
235
|
+
# Typically this will be something like "HTTP/1.0" or "HTTP/1.1" and
|
236
|
+
# may be used by the application to determine how to treat any HTTP
|
237
|
+
# request headers. (This variable should probably be called REQUEST_PROTOCOL,
|
238
|
+
# since it denotes the protocol used in the request, and is not necessarily
|
239
|
+
# the protocol that will be used in the server's response. However, for
|
240
|
+
# compatibility with CGI we have to keep the existing name.)
|
241
|
+
SERVER_PROTOCOL: str
|
242
|
+
|
243
|
+
class CGIOptionalDefined(TypedDict, total=False):
|
244
|
+
REQUEST_URI: str
|
245
|
+
REMOTE_ADDR: str
|
246
|
+
REMOTE_PORT: str
|
247
|
+
# The initial portion of the request URL’s “path” that corresponds to the
|
248
|
+
# application object, so that the application knows its virtual “location”.
|
249
|
+
# This may be an empty string, if the application corresponds to the “root”
|
250
|
+
# of the server.
|
251
|
+
SCRIPT_NAME: str
|
252
|
+
# The remainder of the request URL’s “path”, designating the virtual
|
253
|
+
# “location” of the request’s target within the application. This may be an
|
254
|
+
# empty string, if the request URL targets the application root and does
|
255
|
+
# not have a trailing slash.
|
256
|
+
PATH_INFO: str
|
257
|
+
# The portion of the request URL that follows the “?”, if any. May be empty
|
258
|
+
# or absent.
|
259
|
+
QUERY_STRING: str
|
260
|
+
# The contents of any Content-Type fields in the HTTP request. May be empty
|
261
|
+
# or absent.
|
262
|
+
CONTENT_TYPE: str
|
263
|
+
# The contents of any Content-Length fields in the HTTP request. May be empty
|
264
|
+
# or absent.
|
265
|
+
CONTENT_LENGTH: str
|
276
266
|
|
277
267
|
|
278
268
|
class InputStream(Protocol):
|
@@ -308,7 +298,7 @@ class InputStream(Protocol):
|
|
308
298
|
"""
|
309
299
|
raise NotImplementedError
|
310
300
|
|
311
|
-
def readlines(self, hint: int = -1, /) ->
|
301
|
+
def readlines(self, hint: int = -1, /) -> list[bytes]:
|
312
302
|
"""
|
313
303
|
Note that the hint argument to readlines() is optional for both caller and
|
314
304
|
implementer. The application is free not to supply it, and the server or gateway
|
@@ -349,14 +339,14 @@ class ErrorStream(Protocol):
|
|
349
339
|
def write(self, s: str, /) -> Any:
|
350
340
|
raise NotImplementedError
|
351
341
|
|
352
|
-
def writelines(self, seq:
|
342
|
+
def writelines(self, seq: list[str], /) -> Any:
|
353
343
|
raise NotImplementedError
|
354
344
|
|
355
345
|
|
356
346
|
WSGIDefined = TypedDict(
|
357
347
|
"WSGIDefined",
|
358
348
|
{
|
359
|
-
"wsgi.version":
|
349
|
+
"wsgi.version": tuple[int, int], # e.g. (1, 0)
|
360
350
|
"wsgi.url_scheme": str, # e.g. "http" or "https"
|
361
351
|
"wsgi.input": InputStream,
|
362
352
|
"wsgi.errors": ErrorStream,
|
@@ -381,7 +371,7 @@ class Environ(CGIRequiredDefined, CGIOptionalDefined, WSGIDefined):
|
|
381
371
|
"""
|
382
372
|
|
383
373
|
|
384
|
-
ExceptionInfo =
|
374
|
+
ExceptionInfo = tuple[type[BaseException], BaseException, Optional[TracebackType]]
|
385
375
|
|
386
376
|
# https://peps.python.org/pep-3333/#the-write-callable
|
387
377
|
WriteCallable = Callable[[bytes], None]
|
@@ -391,7 +381,7 @@ class StartResponse(Protocol):
|
|
391
381
|
def __call__(
|
392
382
|
self,
|
393
383
|
status: str,
|
394
|
-
response_headers:
|
384
|
+
response_headers: list[tuple[str, str]],
|
395
385
|
exc_info: Optional[ExceptionInfo] = None,
|
396
386
|
/,
|
397
387
|
) -> WriteCallable:
|
@@ -460,7 +450,7 @@ class Body:
|
|
460
450
|
self.buffer.clear()
|
461
451
|
return result
|
462
452
|
|
463
|
-
def readlines(self, hint: int = -1) ->
|
453
|
+
def readlines(self, hint: int = -1) -> list[bytes]:
|
464
454
|
if not self.has_more:
|
465
455
|
return []
|
466
456
|
if hint == -1:
|
@@ -626,7 +616,7 @@ class WSGIResponder:
|
|
626
616
|
def start_response(
|
627
617
|
self,
|
628
618
|
status: str,
|
629
|
-
response_headers:
|
619
|
+
response_headers: list[tuple[str, str]],
|
630
620
|
exc_info: typing.Optional[ExceptionInfo] = None,
|
631
621
|
) -> WriteCallable:
|
632
622
|
self.exc_info = exc_info
|
modal/_vendor/cloudpickle.py
CHANGED
@@ -256,7 +256,7 @@ def _should_pickle_by_reference(obj, name=None):
|
|
256
256
|
return False
|
257
257
|
return obj.__name__ in sys.modules
|
258
258
|
else:
|
259
|
-
raise TypeError("cannot check importability of {
|
259
|
+
raise TypeError(f"cannot check importability of {type(obj).__name__} instances")
|
260
260
|
|
261
261
|
|
262
262
|
def _lookup_module_and_qualname(obj, name=None):
|
modal/_watcher.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# Copyright Modal Labs 2022
|
2
2
|
from collections import defaultdict
|
3
|
+
from collections.abc import AsyncGenerator
|
3
4
|
from pathlib import Path
|
4
|
-
from typing import
|
5
|
+
from typing import Optional
|
5
6
|
|
6
7
|
from rich.tree import Tree
|
7
8
|
from watchfiles import Change, DefaultFilter, awatch
|
@@ -21,7 +22,7 @@ class AppFilesFilter(DefaultFilter):
|
|
21
22
|
# Watching specific files is discouraged on Linux, so to watch a file we watch its
|
22
23
|
# containing directory and then filter that directory's changes for relevant files.
|
23
24
|
# https://github.com/notify-rs/notify/issues/394
|
24
|
-
dir_filters:
|
25
|
+
dir_filters: dict[Path, Optional[set[Path]]],
|
25
26
|
) -> None:
|
26
27
|
self.dir_filters = dir_filters
|
27
28
|
super().__init__()
|
@@ -54,7 +55,7 @@ class AppFilesFilter(DefaultFilter):
|
|
54
55
|
return super().__call__(change, path)
|
55
56
|
|
56
57
|
|
57
|
-
async def _watch_paths(paths:
|
58
|
+
async def _watch_paths(paths: set[Path], watch_filter: AppFilesFilter) -> AsyncGenerator[set[str], None]:
|
58
59
|
try:
|
59
60
|
async for changes in awatch(*paths, step=500, watch_filter=watch_filter):
|
60
61
|
changed_paths = {stringpath for _, stringpath in changes}
|
@@ -64,7 +65,7 @@ async def _watch_paths(paths: Set[Path], watch_filter: AppFilesFilter) -> AsyncG
|
|
64
65
|
pass
|
65
66
|
|
66
67
|
|
67
|
-
def _print_watched_paths(paths:
|
68
|
+
def _print_watched_paths(paths: set[Path]):
|
68
69
|
msg = "️️⚡️ Serving... hit Ctrl-C to stop!"
|
69
70
|
|
70
71
|
output_tree = Tree(msg, guide_style="gray50")
|
@@ -76,9 +77,9 @@ def _print_watched_paths(paths: Set[Path]):
|
|
76
77
|
output_mgr.print(output_tree)
|
77
78
|
|
78
79
|
|
79
|
-
def _watch_args_from_mounts(mounts:
|
80
|
+
def _watch_args_from_mounts(mounts: list[_Mount]) -> tuple[set[Path], AppFilesFilter]:
|
80
81
|
paths = set()
|
81
|
-
dir_filters:
|
82
|
+
dir_filters: dict[Path, Optional[set[Path]]] = defaultdict(set)
|
82
83
|
for mount in mounts:
|
83
84
|
# TODO(elias): Make this part of the mount class instead, since it uses so much internals
|
84
85
|
for entry in mount._entries:
|
@@ -94,7 +95,7 @@ def _watch_args_from_mounts(mounts: List[_Mount]) -> Tuple[Set[Path], AppFilesFi
|
|
94
95
|
return paths, watch_filter
|
95
96
|
|
96
97
|
|
97
|
-
async def watch(mounts:
|
98
|
+
async def watch(mounts: list[_Mount]) -> AsyncGenerator[set[str], None]:
|
98
99
|
paths, watch_filter = _watch_args_from_mounts(mounts)
|
99
100
|
|
100
101
|
_print_watched_paths(paths)
|
modal/app.py
CHANGED
@@ -2,19 +2,14 @@
|
|
2
2
|
import inspect
|
3
3
|
import typing
|
4
4
|
import warnings
|
5
|
+
from collections.abc import AsyncGenerator, Coroutine, Sequence
|
5
6
|
from pathlib import PurePosixPath
|
6
7
|
from textwrap import dedent
|
7
8
|
from typing import (
|
8
9
|
Any,
|
9
|
-
AsyncGenerator,
|
10
10
|
Callable,
|
11
11
|
ClassVar,
|
12
|
-
Coroutine,
|
13
|
-
Dict,
|
14
|
-
List,
|
15
12
|
Optional,
|
16
|
-
Sequence,
|
17
|
-
Tuple,
|
18
13
|
Union,
|
19
14
|
overload,
|
20
15
|
)
|
@@ -87,14 +82,14 @@ class _LocalEntrypoint:
|
|
87
82
|
LocalEntrypoint = synchronize_api(_LocalEntrypoint)
|
88
83
|
|
89
84
|
|
90
|
-
def check_sequence(items: typing.Sequence[typing.Any], item_type:
|
85
|
+
def check_sequence(items: typing.Sequence[typing.Any], item_type: type[typing.Any], error_msg: str) -> None:
|
91
86
|
if not isinstance(items, (list, tuple)):
|
92
87
|
raise InvalidError(error_msg)
|
93
88
|
if not all(isinstance(v, item_type) for v in items):
|
94
89
|
raise InvalidError(error_msg)
|
95
90
|
|
96
91
|
|
97
|
-
CLS_T = typing.TypeVar("CLS_T", bound=
|
92
|
+
CLS_T = typing.TypeVar("CLS_T", bound=type[Any])
|
98
93
|
|
99
94
|
|
100
95
|
P = typing_extensions.ParamSpec("P")
|
@@ -172,20 +167,20 @@ class _App:
|
|
172
167
|
In this example, the secret and schedule are registered with the app.
|
173
168
|
"""
|
174
169
|
|
175
|
-
_all_apps: ClassVar[
|
170
|
+
_all_apps: ClassVar[dict[Optional[str], list["_App"]]] = {}
|
176
171
|
_container_app: ClassVar[Optional[RunningApp]] = None
|
177
172
|
|
178
173
|
_name: Optional[str]
|
179
174
|
_description: Optional[str]
|
180
|
-
_functions:
|
181
|
-
_classes:
|
175
|
+
_functions: dict[str, _Function]
|
176
|
+
_classes: dict[str, _Cls]
|
182
177
|
|
183
178
|
_image: Optional[_Image]
|
184
179
|
_mounts: Sequence[_Mount]
|
185
180
|
_secrets: Sequence[_Secret]
|
186
|
-
_volumes:
|
187
|
-
_web_endpoints:
|
188
|
-
_local_entrypoints:
|
181
|
+
_volumes: dict[Union[str, PurePosixPath], _Volume]
|
182
|
+
_web_endpoints: list[str] # Used by the CLI
|
183
|
+
_local_entrypoints: dict[str, _LocalEntrypoint]
|
189
184
|
|
190
185
|
# Running apps only (container apps or running local)
|
191
186
|
_app_id: Optional[str] # Kept after app finishes
|
@@ -199,7 +194,7 @@ class _App:
|
|
199
194
|
image: Optional[_Image] = None, # default image for all functions (default is `modal.Image.debian_slim()`)
|
200
195
|
mounts: Sequence[_Mount] = [], # default mounts for all functions
|
201
196
|
secrets: Sequence[_Secret] = [], # default secrets for all functions
|
202
|
-
volumes:
|
197
|
+
volumes: dict[Union[str, PurePosixPath], _Volume] = {}, # default volumes for all functions
|
203
198
|
) -> None:
|
204
199
|
"""Construct a new app, optionally with default image, mounts, secrets, or volumes.
|
205
200
|
|
@@ -313,23 +308,6 @@ class _App:
|
|
313
308
|
if not isinstance(value, _Object):
|
314
309
|
raise InvalidError(f"App attribute `{key}` with value {value!r} is not a valid Modal object")
|
315
310
|
|
316
|
-
def _add_object(self, tag, obj):
|
317
|
-
# TODO(erikbern): replace this with _add_function and _add_class
|
318
|
-
if self._running_app:
|
319
|
-
# If this is inside a container, then objects can be defined after app initialization.
|
320
|
-
# So we may have to initialize objects once they get bound to the app.
|
321
|
-
if tag in self._running_app.tag_to_object_id:
|
322
|
-
object_id: str = self._running_app.tag_to_object_id[tag]
|
323
|
-
metadata: Message = self._running_app.object_handle_metadata[object_id]
|
324
|
-
obj._hydrate(object_id, self._client, metadata)
|
325
|
-
|
326
|
-
if isinstance(obj, _Function):
|
327
|
-
self._functions[tag] = obj
|
328
|
-
elif isinstance(obj, _Cls):
|
329
|
-
self._classes[tag] = obj
|
330
|
-
else:
|
331
|
-
raise RuntimeError(f"Expected `obj` to be a _Function or _Cls (got {type(obj)}")
|
332
|
-
|
333
311
|
def __getitem__(self, tag: str):
|
334
312
|
deprecation_error((2024, 3, 25), _app_attr_error)
|
335
313
|
|
@@ -401,14 +379,14 @@ class _App:
|
|
401
379
|
|
402
380
|
**Example**
|
403
381
|
|
404
|
-
```python
|
382
|
+
```python notest
|
405
383
|
with app.run():
|
406
384
|
some_modal_function.remote()
|
407
385
|
```
|
408
386
|
|
409
387
|
To enable output printing, use `modal.enable_output()`:
|
410
388
|
|
411
|
-
```python
|
389
|
+
```python notest
|
412
390
|
with modal.enable_output():
|
413
391
|
with app.run():
|
414
392
|
some_modal_function.remote()
|
@@ -481,10 +459,29 @@ class _App:
|
|
481
459
|
if function.tag in self._classes:
|
482
460
|
logger.warning(f"Warning: tag {function.tag} exists but is overridden by function")
|
483
461
|
|
484
|
-
self.
|
462
|
+
if self._running_app:
|
463
|
+
# If this is inside a container, then objects can be defined after app initialization.
|
464
|
+
# So we may have to initialize objects once they get bound to the app.
|
465
|
+
if function.tag in self._running_app.tag_to_object_id:
|
466
|
+
object_id: str = self._running_app.tag_to_object_id[function.tag]
|
467
|
+
metadata: Message = self._running_app.object_handle_metadata[object_id]
|
468
|
+
function._hydrate(object_id, self._client, metadata)
|
469
|
+
|
470
|
+
self._functions[function.tag] = function
|
485
471
|
if is_web_endpoint:
|
486
472
|
self._web_endpoints.append(function.tag)
|
487
473
|
|
474
|
+
def _add_class(self, tag: str, cls: _Cls):
|
475
|
+
if self._running_app:
|
476
|
+
# If this is inside a container, then objects can be defined after app initialization.
|
477
|
+
# So we may have to initialize objects once they get bound to the app.
|
478
|
+
if tag in self._running_app.tag_to_object_id:
|
479
|
+
object_id: str = self._running_app.tag_to_object_id[tag]
|
480
|
+
metadata: Message = self._running_app.object_handle_metadata[object_id]
|
481
|
+
cls._hydrate(object_id, self._client, metadata)
|
482
|
+
|
483
|
+
self._classes[tag] = cls
|
484
|
+
|
488
485
|
def _init_container(self, client: _Client, running_app: RunningApp):
|
489
486
|
self._app_id = running_app.app_id
|
490
487
|
self._running_app = running_app
|
@@ -507,22 +504,22 @@ class _App:
|
|
507
504
|
hydrate_objects(self._classes)
|
508
505
|
|
509
506
|
@property
|
510
|
-
def registered_functions(self) ->
|
507
|
+
def registered_functions(self) -> dict[str, _Function]:
|
511
508
|
"""All modal.Function objects registered on the app."""
|
512
509
|
return self._functions
|
513
510
|
|
514
511
|
@property
|
515
|
-
def registered_classes(self) ->
|
512
|
+
def registered_classes(self) -> dict[str, _Function]:
|
516
513
|
"""All modal.Cls objects registered on the app."""
|
517
514
|
return self._classes
|
518
515
|
|
519
516
|
@property
|
520
|
-
def registered_entrypoints(self) ->
|
517
|
+
def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]:
|
521
518
|
"""All local CLI entrypoints registered on the app."""
|
522
519
|
return self._local_entrypoints
|
523
520
|
|
524
521
|
@property
|
525
|
-
def indexed_objects(self) ->
|
522
|
+
def indexed_objects(self) -> dict[str, _Object]:
|
526
523
|
deprecation_warning(
|
527
524
|
(2024, 11, 25),
|
528
525
|
"`app.indexed_objects` is deprecated! Use `app.registered_functions` or `app.registered_classes` instead.",
|
@@ -530,7 +527,7 @@ class _App:
|
|
530
527
|
return dict(**self._functions, **self._classes)
|
531
528
|
|
532
529
|
@property
|
533
|
-
def registered_web_endpoints(self) ->
|
530
|
+
def registered_web_endpoints(self) -> list[str]:
|
534
531
|
"""Names of web endpoint (ie. webhook) functions registered on the app."""
|
535
532
|
return self._web_endpoints
|
536
533
|
|
@@ -609,24 +606,24 @@ class _App:
|
|
609
606
|
schedule: Optional[Schedule] = None, # An optional Modal Schedule for the function
|
610
607
|
secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
|
611
608
|
gpu: Union[
|
612
|
-
GPU_T,
|
609
|
+
GPU_T, list[GPU_T]
|
613
610
|
] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
|
614
611
|
serialized: bool = False, # Whether to send the function over using cloudpickle.
|
615
612
|
mounts: Sequence[_Mount] = (), # Modal Mounts added to the container
|
616
|
-
network_file_systems:
|
613
|
+
network_file_systems: dict[
|
617
614
|
Union[str, PurePosixPath], _NetworkFileSystem
|
618
615
|
] = {}, # Mountpoints for Modal NetworkFileSystems
|
619
|
-
volumes:
|
616
|
+
volumes: dict[
|
620
617
|
Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
|
621
618
|
] = {}, # Mount points for Modal Volumes & CloudBucketMounts
|
622
619
|
allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
|
623
620
|
# Specify, in fractional CPU cores, how many CPU cores to request.
|
624
621
|
# Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
|
625
622
|
# CPU throttling will prevent a container from exceeding its specified limit.
|
626
|
-
cpu: Optional[Union[float,
|
623
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
627
624
|
# Specify, in MiB, a memory request which is the minimum memory required.
|
628
625
|
# Or, pass (request, limit) to additionally specify a hard limit in MiB.
|
629
|
-
memory: Optional[Union[int,
|
626
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
630
627
|
ephemeral_disk: Optional[int] = None, # Specify, in MiB, the ephemeral disk size for the Function.
|
631
628
|
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
632
629
|
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
@@ -815,24 +812,24 @@ class _App:
|
|
815
812
|
image: Optional[_Image] = None, # The image to run as the container for the function
|
816
813
|
secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
|
817
814
|
gpu: Union[
|
818
|
-
GPU_T,
|
815
|
+
GPU_T, list[GPU_T]
|
819
816
|
] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
|
820
817
|
serialized: bool = False, # Whether to send the function over using cloudpickle.
|
821
818
|
mounts: Sequence[_Mount] = (),
|
822
|
-
network_file_systems:
|
819
|
+
network_file_systems: dict[
|
823
820
|
Union[str, PurePosixPath], _NetworkFileSystem
|
824
821
|
] = {}, # Mountpoints for Modal NetworkFileSystems
|
825
|
-
volumes:
|
822
|
+
volumes: dict[
|
826
823
|
Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
|
827
824
|
] = {}, # Mount points for Modal Volumes & CloudBucketMounts
|
828
825
|
allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
|
829
826
|
# Specify, in fractional CPU cores, how many CPU cores to request.
|
830
827
|
# Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
|
831
828
|
# CPU throttling will prevent a container from exceeding its specified limit.
|
832
|
-
cpu: Optional[Union[float,
|
829
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
833
830
|
# Specify, in MiB, a memory request which is the minimum memory required.
|
834
831
|
# Or, pass (request, limit) to additionally specify a hard limit in MiB.
|
835
|
-
memory: Optional[Union[int,
|
832
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
836
833
|
ephemeral_disk: Optional[int] = None, # Specify, in MiB, the ephemeral disk size for the Function.
|
837
834
|
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
838
835
|
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
@@ -935,7 +932,7 @@ class _App:
|
|
935
932
|
cls: _Cls = _Cls.from_local(user_cls, self, cls_func)
|
936
933
|
|
937
934
|
tag: str = user_cls.__name__
|
938
|
-
self.
|
935
|
+
self._add_class(tag, cls)
|
939
936
|
return cls # type: ignore # a _Cls instance "simulates" being the user provided class
|
940
937
|
|
941
938
|
return wrapper
|
@@ -946,7 +943,7 @@ class _App:
|
|
946
943
|
image: Optional[_Image] = None, # The image to run as the container for the sandbox.
|
947
944
|
mounts: Sequence[_Mount] = (), # Mounts to attach to the sandbox.
|
948
945
|
secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
|
949
|
-
network_file_systems:
|
946
|
+
network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
|
950
947
|
timeout: Optional[int] = None, # Maximum execution time of the sandbox in seconds.
|
951
948
|
workdir: Optional[str] = None, # Working directory of the sandbox.
|
952
949
|
gpu: GPU_T = None,
|
@@ -955,12 +952,12 @@ class _App:
|
|
955
952
|
# Specify, in fractional CPU cores, how many CPU cores to request.
|
956
953
|
# Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
|
957
954
|
# CPU throttling will prevent a container from exceeding its specified limit.
|
958
|
-
cpu: Optional[Union[float,
|
955
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
959
956
|
# Specify, in MiB, a memory request which is the minimum memory required.
|
960
957
|
# Or, pass (request, limit) to additionally specify a hard limit in MiB.
|
961
|
-
memory: Optional[Union[int,
|
958
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
962
959
|
block_network: bool = False, # Whether to block network access
|
963
|
-
volumes:
|
960
|
+
volumes: dict[
|
964
961
|
Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
|
965
962
|
] = {}, # Mount points for Modal Volumes and CloudBucketMounts
|
966
963
|
pty_info: Optional[api_pb2.PTYInfo] = None,
|
@@ -1022,16 +1019,25 @@ class _App:
|
|
1022
1019
|
bar.remote()
|
1023
1020
|
```
|
1024
1021
|
"""
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1022
|
+
for tag, function in other_app._functions.items():
|
1023
|
+
existing_function = self._functions.get(tag)
|
1024
|
+
if existing_function and existing_function != function:
|
1025
|
+
logger.warning(
|
1026
|
+
f"Named app function {tag} with existing value {existing_function} is being "
|
1027
|
+
f"overwritten by a different function {function}"
|
1028
|
+
)
|
1029
|
+
|
1030
|
+
self._add_function(function, False) # TODO(erikbern): webhook config?
|
1031
|
+
|
1032
|
+
for tag, cls in other_app._classes.items():
|
1033
|
+
existing_cls = self._classes.get(tag)
|
1034
|
+
if existing_cls and existing_cls != cls:
|
1029
1035
|
logger.warning(
|
1030
|
-
f"Named app
|
1031
|
-
f"overwritten by a different
|
1036
|
+
f"Named app class {tag} with existing value {existing_cls} is being "
|
1037
|
+
f"overwritten by a different class {cls}"
|
1032
1038
|
)
|
1033
1039
|
|
1034
|
-
self.
|
1040
|
+
self._add_class(tag, cls)
|
1035
1041
|
|
1036
1042
|
async def _logs(self, client: Optional[_Client] = None) -> AsyncGenerator[str, None]:
|
1037
1043
|
"""Stream logs from the app.
|