modal 0.67.0__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 +81 -69
- 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.0.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.0.dist-info/RECORD +0 -168
- {modal-0.67.0.dist-info → modal-0.67.22.dist-info}/LICENSE +0 -0
- {modal-0.67.0.dist-info → modal-0.67.22.dist-info}/WHEEL +0 -0
- {modal-0.67.0.dist-info → modal-0.67.22.dist-info}/entry_points.txt +0 -0
- {modal-0.67.0.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
|
@@ -492,31 +489,37 @@ class _App:
|
|
492
489
|
|
493
490
|
_App._container_app = running_app
|
494
491
|
|
495
|
-
# Hydrate objects on app
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
492
|
+
# Hydrate objects on app -- hydrating functions first so that when a class is being hydrated its
|
493
|
+
# corresponding class service function is already hydrated.
|
494
|
+
def hydrate_objects(objects_dict):
|
495
|
+
for tag, object_id in running_app.tag_to_object_id.items():
|
496
|
+
if tag in objects_dict:
|
497
|
+
obj = objects_dict[tag]
|
498
|
+
handle_metadata = running_app.object_handle_metadata[object_id]
|
499
|
+
obj._hydrate(object_id, client, handle_metadata)
|
500
|
+
|
501
|
+
# Hydrate function objects
|
502
|
+
hydrate_objects(self._functions)
|
503
|
+
# Hydrate class objects
|
504
|
+
hydrate_objects(self._classes)
|
502
505
|
|
503
506
|
@property
|
504
|
-
def registered_functions(self) ->
|
507
|
+
def registered_functions(self) -> dict[str, _Function]:
|
505
508
|
"""All modal.Function objects registered on the app."""
|
506
509
|
return self._functions
|
507
510
|
|
508
511
|
@property
|
509
|
-
def registered_classes(self) ->
|
512
|
+
def registered_classes(self) -> dict[str, _Function]:
|
510
513
|
"""All modal.Cls objects registered on the app."""
|
511
514
|
return self._classes
|
512
515
|
|
513
516
|
@property
|
514
|
-
def registered_entrypoints(self) ->
|
517
|
+
def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]:
|
515
518
|
"""All local CLI entrypoints registered on the app."""
|
516
519
|
return self._local_entrypoints
|
517
520
|
|
518
521
|
@property
|
519
|
-
def indexed_objects(self) ->
|
522
|
+
def indexed_objects(self) -> dict[str, _Object]:
|
520
523
|
deprecation_warning(
|
521
524
|
(2024, 11, 25),
|
522
525
|
"`app.indexed_objects` is deprecated! Use `app.registered_functions` or `app.registered_classes` instead.",
|
@@ -524,7 +527,7 @@ class _App:
|
|
524
527
|
return dict(**self._functions, **self._classes)
|
525
528
|
|
526
529
|
@property
|
527
|
-
def registered_web_endpoints(self) ->
|
530
|
+
def registered_web_endpoints(self) -> list[str]:
|
528
531
|
"""Names of web endpoint (ie. webhook) functions registered on the app."""
|
529
532
|
return self._web_endpoints
|
530
533
|
|
@@ -603,24 +606,24 @@ class _App:
|
|
603
606
|
schedule: Optional[Schedule] = None, # An optional Modal Schedule for the function
|
604
607
|
secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
|
605
608
|
gpu: Union[
|
606
|
-
GPU_T,
|
609
|
+
GPU_T, list[GPU_T]
|
607
610
|
] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
|
608
611
|
serialized: bool = False, # Whether to send the function over using cloudpickle.
|
609
612
|
mounts: Sequence[_Mount] = (), # Modal Mounts added to the container
|
610
|
-
network_file_systems:
|
613
|
+
network_file_systems: dict[
|
611
614
|
Union[str, PurePosixPath], _NetworkFileSystem
|
612
615
|
] = {}, # Mountpoints for Modal NetworkFileSystems
|
613
|
-
volumes:
|
616
|
+
volumes: dict[
|
614
617
|
Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
|
615
618
|
] = {}, # Mount points for Modal Volumes & CloudBucketMounts
|
616
619
|
allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
|
617
620
|
# Specify, in fractional CPU cores, how many CPU cores to request.
|
618
621
|
# Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
|
619
622
|
# CPU throttling will prevent a container from exceeding its specified limit.
|
620
|
-
cpu: Optional[Union[float,
|
623
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
621
624
|
# Specify, in MiB, a memory request which is the minimum memory required.
|
622
625
|
# Or, pass (request, limit) to additionally specify a hard limit in MiB.
|
623
|
-
memory: Optional[Union[int,
|
626
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
624
627
|
ephemeral_disk: Optional[int] = None, # Specify, in MiB, the ephemeral disk size for the Function.
|
625
628
|
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
626
629
|
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
@@ -809,24 +812,24 @@ class _App:
|
|
809
812
|
image: Optional[_Image] = None, # The image to run as the container for the function
|
810
813
|
secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
|
811
814
|
gpu: Union[
|
812
|
-
GPU_T,
|
815
|
+
GPU_T, list[GPU_T]
|
813
816
|
] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
|
814
817
|
serialized: bool = False, # Whether to send the function over using cloudpickle.
|
815
818
|
mounts: Sequence[_Mount] = (),
|
816
|
-
network_file_systems:
|
819
|
+
network_file_systems: dict[
|
817
820
|
Union[str, PurePosixPath], _NetworkFileSystem
|
818
821
|
] = {}, # Mountpoints for Modal NetworkFileSystems
|
819
|
-
volumes:
|
822
|
+
volumes: dict[
|
820
823
|
Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
|
821
824
|
] = {}, # Mount points for Modal Volumes & CloudBucketMounts
|
822
825
|
allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
|
823
826
|
# Specify, in fractional CPU cores, how many CPU cores to request.
|
824
827
|
# Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
|
825
828
|
# CPU throttling will prevent a container from exceeding its specified limit.
|
826
|
-
cpu: Optional[Union[float,
|
829
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
827
830
|
# Specify, in MiB, a memory request which is the minimum memory required.
|
828
831
|
# Or, pass (request, limit) to additionally specify a hard limit in MiB.
|
829
|
-
memory: Optional[Union[int,
|
832
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
830
833
|
ephemeral_disk: Optional[int] = None, # Specify, in MiB, the ephemeral disk size for the Function.
|
831
834
|
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
832
835
|
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
@@ -929,7 +932,7 @@ class _App:
|
|
929
932
|
cls: _Cls = _Cls.from_local(user_cls, self, cls_func)
|
930
933
|
|
931
934
|
tag: str = user_cls.__name__
|
932
|
-
self.
|
935
|
+
self._add_class(tag, cls)
|
933
936
|
return cls # type: ignore # a _Cls instance "simulates" being the user provided class
|
934
937
|
|
935
938
|
return wrapper
|
@@ -940,7 +943,7 @@ class _App:
|
|
940
943
|
image: Optional[_Image] = None, # The image to run as the container for the sandbox.
|
941
944
|
mounts: Sequence[_Mount] = (), # Mounts to attach to the sandbox.
|
942
945
|
secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
|
943
|
-
network_file_systems:
|
946
|
+
network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
|
944
947
|
timeout: Optional[int] = None, # Maximum execution time of the sandbox in seconds.
|
945
948
|
workdir: Optional[str] = None, # Working directory of the sandbox.
|
946
949
|
gpu: GPU_T = None,
|
@@ -949,12 +952,12 @@ class _App:
|
|
949
952
|
# Specify, in fractional CPU cores, how many CPU cores to request.
|
950
953
|
# Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
|
951
954
|
# CPU throttling will prevent a container from exceeding its specified limit.
|
952
|
-
cpu: Optional[Union[float,
|
955
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
953
956
|
# Specify, in MiB, a memory request which is the minimum memory required.
|
954
957
|
# Or, pass (request, limit) to additionally specify a hard limit in MiB.
|
955
|
-
memory: Optional[Union[int,
|
958
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
956
959
|
block_network: bool = False, # Whether to block network access
|
957
|
-
volumes:
|
960
|
+
volumes: dict[
|
958
961
|
Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
|
959
962
|
] = {}, # Mount points for Modal Volumes and CloudBucketMounts
|
960
963
|
pty_info: Optional[api_pb2.PTYInfo] = None,
|
@@ -1016,16 +1019,25 @@ class _App:
|
|
1016
1019
|
bar.remote()
|
1017
1020
|
```
|
1018
1021
|
"""
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
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:
|
1023
1035
|
logger.warning(
|
1024
|
-
f"Named app
|
1025
|
-
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}"
|
1026
1038
|
)
|
1027
1039
|
|
1028
|
-
self.
|
1040
|
+
self._add_class(tag, cls)
|
1029
1041
|
|
1030
1042
|
async def _logs(self, client: Optional[_Client] = None) -> AsyncGenerator[str, None]:
|
1031
1043
|
"""Stream logs from the app.
|