modal 0.62.115__py3-none-any.whl → 0.72.13__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/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +402 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1025 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +196 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +943 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
- modal-0.72.13.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
modal/partial_function.py
CHANGED
@@ -1,65 +1,114 @@
|
|
1
1
|
# Copyright Modal Labs 2023
|
2
2
|
import enum
|
3
|
+
import inspect
|
4
|
+
import typing
|
5
|
+
from collections.abc import Coroutine, Iterable
|
3
6
|
from typing import (
|
4
7
|
Any,
|
5
8
|
Callable,
|
6
|
-
Dict,
|
7
|
-
Iterable,
|
8
|
-
List,
|
9
9
|
Optional,
|
10
|
-
Type,
|
11
10
|
Union,
|
12
11
|
)
|
13
12
|
|
13
|
+
import typing_extensions
|
14
|
+
|
14
15
|
from modal_proto import api_pb2
|
15
16
|
|
16
17
|
from ._utils.async_utils import synchronize_api, synchronizer
|
17
|
-
from ._utils.
|
18
|
+
from ._utils.deprecation import deprecation_error, deprecation_warning
|
19
|
+
from ._utils.function_utils import callable_has_non_self_non_default_params, callable_has_non_self_params
|
18
20
|
from .config import logger
|
19
|
-
from .exception import InvalidError
|
21
|
+
from .exception import InvalidError
|
20
22
|
from .functions import _Function
|
21
23
|
|
24
|
+
MAX_MAX_BATCH_SIZE = 1000
|
25
|
+
MAX_BATCH_WAIT_MS = 10 * 60 * 1000 # 10 minutes
|
26
|
+
|
22
27
|
|
23
28
|
class _PartialFunctionFlags(enum.IntFlag):
|
24
29
|
FUNCTION: int = 1
|
25
30
|
BUILD: int = 2
|
26
|
-
|
27
|
-
|
31
|
+
ENTER_PRE_SNAPSHOT: int = 4
|
32
|
+
ENTER_POST_SNAPSHOT: int = 8
|
28
33
|
EXIT: int = 16
|
34
|
+
BATCHED: int = 32
|
35
|
+
CLUSTERED: int = 64 # Experimental: Clustered functions
|
36
|
+
|
37
|
+
@staticmethod
|
38
|
+
def all() -> int:
|
39
|
+
return ~_PartialFunctionFlags(0)
|
40
|
+
|
41
|
+
|
42
|
+
P = typing_extensions.ParamSpec("P")
|
43
|
+
ReturnType = typing_extensions.TypeVar("ReturnType", covariant=True)
|
44
|
+
OriginalReturnType = typing_extensions.TypeVar("OriginalReturnType", covariant=True)
|
29
45
|
|
30
46
|
|
31
|
-
class _PartialFunction:
|
32
|
-
"""Intermediate function, produced by @method or @
|
47
|
+
class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
48
|
+
"""Intermediate function, produced by @enter, @build, @method, @web_endpoint, or @batched"""
|
33
49
|
|
34
|
-
raw_f: Callable[
|
50
|
+
raw_f: Callable[P, ReturnType]
|
35
51
|
flags: _PartialFunctionFlags
|
36
52
|
webhook_config: Optional[api_pb2.WebhookConfig]
|
37
|
-
is_generator:
|
53
|
+
is_generator: bool
|
38
54
|
keep_warm: Optional[int]
|
55
|
+
batch_max_size: Optional[int]
|
56
|
+
batch_wait_ms: Optional[int]
|
57
|
+
force_build: bool
|
58
|
+
cluster_size: Optional[int] # Experimental: Clustered functions
|
59
|
+
build_timeout: Optional[int]
|
39
60
|
|
40
61
|
def __init__(
|
41
62
|
self,
|
42
|
-
raw_f: Callable[
|
63
|
+
raw_f: Callable[P, ReturnType],
|
43
64
|
flags: _PartialFunctionFlags,
|
44
65
|
webhook_config: Optional[api_pb2.WebhookConfig] = None,
|
45
66
|
is_generator: Optional[bool] = None,
|
46
67
|
keep_warm: Optional[int] = None,
|
68
|
+
batch_max_size: Optional[int] = None,
|
69
|
+
batch_wait_ms: Optional[int] = None,
|
70
|
+
cluster_size: Optional[int] = None, # Experimental: Clustered functions
|
71
|
+
force_build: bool = False,
|
72
|
+
build_timeout: Optional[int] = None,
|
47
73
|
):
|
48
74
|
self.raw_f = raw_f
|
49
75
|
self.flags = flags
|
50
76
|
self.webhook_config = webhook_config
|
51
|
-
|
77
|
+
if is_generator is None:
|
78
|
+
# auto detect - doesn't work if the function *returns* a generator
|
79
|
+
final_is_generator = inspect.isgeneratorfunction(raw_f) or inspect.isasyncgenfunction(raw_f)
|
80
|
+
else:
|
81
|
+
final_is_generator = is_generator
|
82
|
+
|
83
|
+
self.is_generator = final_is_generator
|
52
84
|
self.keep_warm = keep_warm
|
53
85
|
self.wrapped = False # Make sure that this was converted into a FunctionHandle
|
86
|
+
self.batch_max_size = batch_max_size
|
87
|
+
self.batch_wait_ms = batch_wait_ms
|
88
|
+
self.cluster_size = cluster_size # Experimental: Clustered functions
|
89
|
+
self.force_build = force_build
|
90
|
+
self.build_timeout = build_timeout
|
54
91
|
|
55
|
-
def __get__(self, obj, objtype=None) -> _Function:
|
56
|
-
# This only happens inside user methods when they refer to other methods
|
92
|
+
def __get__(self, obj, objtype=None) -> _Function[P, ReturnType, OriginalReturnType]:
|
57
93
|
k = self.raw_f.__name__
|
58
|
-
if obj: #
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
94
|
+
if obj: # accessing the method on an instance of a class, e.g. `MyClass().fun``
|
95
|
+
if hasattr(obj, "_modal_functions"):
|
96
|
+
# This happens inside "local" user methods when they refer to other methods,
|
97
|
+
# e.g. Foo().parent_method.remote() calling self.other_method.remote()
|
98
|
+
return getattr(obj, "_modal_functions")[k]
|
99
|
+
else:
|
100
|
+
# special edge case: referencing a method of an instance of an
|
101
|
+
# unwrapped class (not using app.cls()) with @methods
|
102
|
+
# not sure what would be useful here, but let's return a bound version of the underlying function,
|
103
|
+
# since the class is just a vanilla class at this point
|
104
|
+
# This wouldn't let the user access `.remote()` and `.local()` etc. on the function
|
105
|
+
return self.raw_f.__get__(obj, objtype)
|
106
|
+
|
107
|
+
else: # accessing a method directly on the class, e.g. `MyClass.fun`
|
108
|
+
# This happens mainly during serialization of the wrapped underlying class of a Cls
|
109
|
+
# since we don't have the instance info here we just return the PartialFunction itself
|
110
|
+
# to let it be bound to a variable and become a Function later on
|
111
|
+
return self # type: ignore # this returns a PartialFunction in a special internal case
|
63
112
|
|
64
113
|
def __del__(self):
|
65
114
|
if (self.flags & _PartialFunctionFlags.FUNCTION) and self.wrapped is False:
|
@@ -76,76 +125,70 @@ class _PartialFunction:
|
|
76
125
|
flags=(self.flags | flags),
|
77
126
|
webhook_config=self.webhook_config,
|
78
127
|
keep_warm=self.keep_warm,
|
128
|
+
batch_max_size=self.batch_max_size,
|
129
|
+
batch_wait_ms=self.batch_wait_ms,
|
130
|
+
force_build=self.force_build,
|
131
|
+
build_timeout=self.build_timeout,
|
79
132
|
)
|
80
133
|
|
81
134
|
|
82
135
|
PartialFunction = synchronize_api(_PartialFunction)
|
83
136
|
|
84
137
|
|
85
|
-
def
|
86
|
-
"""Grabs all method on a user class"""
|
87
|
-
|
88
|
-
|
138
|
+
def _find_partial_methods_for_user_cls(user_cls: type[Any], flags: int) -> dict[str, _PartialFunction]:
|
139
|
+
"""Grabs all method on a user class, and returns partials. Includes legacy methods."""
|
140
|
+
|
141
|
+
partial_functions: dict[str, _PartialFunction] = {}
|
142
|
+
for parent_cls in reversed(user_cls.mro()):
|
89
143
|
if parent_cls is not object:
|
90
144
|
for k, v in parent_cls.__dict__.items():
|
91
|
-
if isinstance(v, PartialFunction):
|
92
|
-
|
93
|
-
if
|
94
|
-
partial_functions[k] =
|
145
|
+
if isinstance(v, PartialFunction): # type: ignore[reportArgumentType] # synchronicity wrapper types
|
146
|
+
_partial_function: _PartialFunction = typing.cast(_PartialFunction, synchronizer._translate_in(v))
|
147
|
+
if _partial_function.flags & flags:
|
148
|
+
partial_functions[k] = _partial_function
|
95
149
|
|
96
150
|
return partial_functions
|
97
151
|
|
98
152
|
|
99
|
-
def
|
100
|
-
"""Grabs all
|
101
|
-
|
102
|
-
|
103
|
-
# Build up a list of legacy attributes to check
|
104
|
-
check_attrs: List[str] = []
|
105
|
-
if flags & _PartialFunctionFlags.BUILD:
|
106
|
-
check_attrs += ["__build__", "__abuild__"]
|
107
|
-
if flags & _PartialFunctionFlags.ENTER_POST_CHECKPOINT:
|
108
|
-
check_attrs += ["__enter__", "__aenter__"]
|
109
|
-
if flags & _PartialFunctionFlags.EXIT:
|
110
|
-
check_attrs += ["__exit__", "__aexit__"]
|
111
|
-
|
112
|
-
# Grab legacy lifecycle methods
|
113
|
-
for attr in check_attrs:
|
114
|
-
if hasattr(user_cls, attr):
|
115
|
-
suggested = attr.strip("_")
|
116
|
-
if is_async := suggested.startswith("a"):
|
117
|
-
suggested = suggested[1:]
|
118
|
-
async_suggestion = " (on an async method)" if is_async else ""
|
119
|
-
message = (
|
120
|
-
f"Using `{attr}` methods for class lifecycle management is deprecated."
|
121
|
-
f" Please try using the `modal.{suggested}` decorator{async_suggestion} instead."
|
122
|
-
" See https://modal.com/docs/guide/lifecycle-functions for more information."
|
123
|
-
)
|
124
|
-
deprecation_warning((2024, 2, 21), message, show_source=True)
|
125
|
-
functions[attr] = getattr(user_cls, attr)
|
153
|
+
def _find_callables_for_obj(user_obj: Any, flags: int) -> dict[str, Callable[..., Any]]:
|
154
|
+
"""Grabs all methods for an object, and binds them to the class"""
|
155
|
+
user_cls: type = type(user_obj)
|
156
|
+
return {k: pf.raw_f.__get__(user_obj) for k, pf in _find_partial_methods_for_user_cls(user_cls, flags).items()}
|
126
157
|
|
127
|
-
# Grab new decorator-based methods
|
128
|
-
for k, pf in _find_partial_methods_for_cls(user_cls, flags).items():
|
129
|
-
functions[k] = pf.raw_f
|
130
158
|
|
131
|
-
|
159
|
+
class _MethodDecoratorType:
|
160
|
+
@typing.overload
|
161
|
+
def __call__(
|
162
|
+
self, func: PartialFunction[typing_extensions.Concatenate[Any, P], ReturnType, OriginalReturnType]
|
163
|
+
) -> PartialFunction[P, ReturnType, OriginalReturnType]:
|
164
|
+
...
|
132
165
|
|
166
|
+
@typing.overload
|
167
|
+
def __call__(
|
168
|
+
self, func: Callable[typing_extensions.Concatenate[Any, P], Coroutine[Any, Any, ReturnType]]
|
169
|
+
) -> PartialFunction[P, ReturnType, Coroutine[Any, Any, ReturnType]]:
|
170
|
+
...
|
133
171
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
172
|
+
@typing.overload
|
173
|
+
def __call__(
|
174
|
+
self, func: Callable[typing_extensions.Concatenate[Any, P], ReturnType]
|
175
|
+
) -> PartialFunction[P, ReturnType, ReturnType]:
|
176
|
+
...
|
177
|
+
|
178
|
+
def __call__(self, func):
|
179
|
+
...
|
138
180
|
|
139
181
|
|
182
|
+
# TODO(elias): fix support for coroutine type unwrapping for methods (static typing)
|
140
183
|
def _method(
|
141
184
|
_warn_parentheses_missing=None,
|
142
185
|
*,
|
143
186
|
# Set this to True if it's a non-generator function returning
|
144
187
|
# a [sync/async] generator object
|
145
188
|
is_generator: Optional[bool] = None,
|
146
|
-
keep_warm: Optional[int] = None, #
|
147
|
-
) ->
|
148
|
-
"""Decorator for methods that should be transformed into a Modal Function registered against this class's
|
189
|
+
keep_warm: Optional[int] = None, # Deprecated: Use keep_warm on @app.cls() instead
|
190
|
+
) -> _MethodDecoratorType:
|
191
|
+
"""Decorator for methods that should be transformed into a Modal Function registered against this class's App.
|
149
192
|
|
150
193
|
**Usage:**
|
151
194
|
|
@@ -158,23 +201,42 @@ def _method(
|
|
158
201
|
...
|
159
202
|
```
|
160
203
|
"""
|
161
|
-
if _warn_parentheses_missing:
|
204
|
+
if _warn_parentheses_missing is not None:
|
162
205
|
raise InvalidError("Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@method()`.")
|
163
206
|
|
207
|
+
if keep_warm is not None:
|
208
|
+
deprecation_warning(
|
209
|
+
(2024, 6, 10),
|
210
|
+
(
|
211
|
+
"`keep_warm=` is no longer supported per-method on Modal classes. "
|
212
|
+
"All methods and web endpoints of a class use the same set of containers now. "
|
213
|
+
"Use keep_warm via the @app.cls() decorator instead. "
|
214
|
+
),
|
215
|
+
pending=True,
|
216
|
+
)
|
217
|
+
|
164
218
|
def wrapper(raw_f: Callable[..., Any]) -> _PartialFunction:
|
219
|
+
nonlocal is_generator
|
165
220
|
if isinstance(raw_f, _PartialFunction) and raw_f.webhook_config:
|
166
221
|
raw_f.wrapped = True # suppress later warning
|
167
222
|
raise InvalidError(
|
168
|
-
"Web endpoints on classes should not be wrapped by `@method`.
|
223
|
+
"Web endpoints on classes should not be wrapped by `@method`. "
|
224
|
+
"Suggestion: remove the `@method` decorator."
|
225
|
+
)
|
226
|
+
if isinstance(raw_f, _PartialFunction) and raw_f.batch_max_size is not None:
|
227
|
+
raw_f.wrapped = True # suppress later warning
|
228
|
+
raise InvalidError(
|
229
|
+
"Batched function on classes should not be wrapped by `@method`. "
|
230
|
+
"Suggestion: remove the `@method` decorator."
|
169
231
|
)
|
170
232
|
return _PartialFunction(raw_f, _PartialFunctionFlags.FUNCTION, is_generator=is_generator, keep_warm=keep_warm)
|
171
233
|
|
172
234
|
return wrapper
|
173
235
|
|
174
236
|
|
175
|
-
def _parse_custom_domains(custom_domains: Optional[Iterable[str]] = None) ->
|
237
|
+
def _parse_custom_domains(custom_domains: Optional[Iterable[str]] = None) -> list[api_pb2.CustomDomainConfig]:
|
176
238
|
assert not isinstance(custom_domains, str), "custom_domains must be `Iterable[str]` but is `str` instead."
|
177
|
-
_custom_domains:
|
239
|
+
_custom_domains: list[api_pb2.CustomDomainConfig] = []
|
178
240
|
if custom_domains is not None:
|
179
241
|
for custom_domain in custom_domains:
|
180
242
|
_custom_domains.append(api_pb2.CustomDomainConfig(name=custom_domain))
|
@@ -187,11 +249,13 @@ def _web_endpoint(
|
|
187
249
|
*,
|
188
250
|
method: str = "GET", # REST method for the created endpoint.
|
189
251
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
190
|
-
|
252
|
+
docs: bool = False, # Whether to enable interactive documentation for this endpoint at /docs.
|
191
253
|
custom_domains: Optional[
|
192
254
|
Iterable[str]
|
193
255
|
] = None, # Create an endpoint using a custom domain fully-qualified domain name (FQDN).
|
194
|
-
|
256
|
+
requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
|
257
|
+
wait_for_response: bool = True, # DEPRECATED: this must always be True now
|
258
|
+
) -> Callable[[Callable[P, ReturnType]], _PartialFunction[P, ReturnType, ReturnType]]:
|
195
259
|
"""Register a basic web endpoint with this application.
|
196
260
|
|
197
261
|
This is the simple way to create a web endpoint on Modal. The function
|
@@ -209,7 +273,7 @@ def _web_endpoint(
|
|
209
273
|
if isinstance(_warn_parentheses_missing, str):
|
210
274
|
# Probably passing the method string as a positional argument.
|
211
275
|
raise InvalidError('Positional arguments are not allowed. Suggestion: `@web_endpoint(method="GET")`.')
|
212
|
-
elif _warn_parentheses_missing:
|
276
|
+
elif _warn_parentheses_missing is not None:
|
213
277
|
raise InvalidError(
|
214
278
|
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@web_endpoint()`."
|
215
279
|
)
|
@@ -222,9 +286,11 @@ def _web_endpoint(
|
|
222
286
|
"@app.function()\n@app.web_endpoint()\ndef my_webhook():\n ..."
|
223
287
|
)
|
224
288
|
if not wait_for_response:
|
225
|
-
|
226
|
-
|
227
|
-
|
289
|
+
deprecation_error(
|
290
|
+
(2024, 5, 13),
|
291
|
+
"wait_for_response=False has been deprecated on web endpoints. See "
|
292
|
+
"https://modal.com/docs/guide/webhook-timeouts#polling-solutions for alternatives.",
|
293
|
+
)
|
228
294
|
|
229
295
|
# self._loose_webhook_configs.add(raw_f)
|
230
296
|
|
@@ -234,9 +300,11 @@ def _web_endpoint(
|
|
234
300
|
api_pb2.WebhookConfig(
|
235
301
|
type=api_pb2.WEBHOOK_TYPE_FUNCTION,
|
236
302
|
method=method,
|
303
|
+
web_endpoint_docs=docs,
|
237
304
|
requested_suffix=label,
|
238
|
-
async_mode=
|
305
|
+
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
239
306
|
custom_domains=_parse_custom_domains(custom_domains),
|
307
|
+
requires_proxy_auth=requires_proxy_auth,
|
240
308
|
),
|
241
309
|
)
|
242
310
|
|
@@ -247,8 +315,9 @@ def _asgi_app(
|
|
247
315
|
_warn_parentheses_missing=None,
|
248
316
|
*,
|
249
317
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
250
|
-
wait_for_response: bool = True, # Whether requests should wait for and return the function response.
|
251
318
|
custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
|
319
|
+
requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
|
320
|
+
wait_for_response: bool = True, # DEPRECATED: this must always be True now
|
252
321
|
) -> Callable[[Callable[..., Any]], _PartialFunction]:
|
253
322
|
"""Decorator for registering an ASGI app with a Modal function.
|
254
323
|
|
@@ -273,16 +342,35 @@ def _asgi_app(
|
|
273
342
|
"""
|
274
343
|
if isinstance(_warn_parentheses_missing, str):
|
275
344
|
raise InvalidError('Positional arguments are not allowed. Suggestion: `@asgi_app(label="foo")`.')
|
276
|
-
elif _warn_parentheses_missing:
|
345
|
+
elif _warn_parentheses_missing is not None:
|
277
346
|
raise InvalidError(
|
278
347
|
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@asgi_app()`."
|
279
348
|
)
|
280
349
|
|
281
350
|
def wrapper(raw_f: Callable[..., Any]) -> _PartialFunction:
|
351
|
+
if callable_has_non_self_params(raw_f):
|
352
|
+
if callable_has_non_self_non_default_params(raw_f):
|
353
|
+
raise InvalidError(
|
354
|
+
f"ASGI app function {raw_f.__name__} can't have parameters. See https://modal.com/docs/guide/webhooks#asgi."
|
355
|
+
)
|
356
|
+
else:
|
357
|
+
deprecation_warning(
|
358
|
+
(2024, 9, 4),
|
359
|
+
f"ASGI app function {raw_f.__name__} has default parameters, but shouldn't have any parameters - "
|
360
|
+
f"Modal will drop support for default parameters in a future release.",
|
361
|
+
)
|
362
|
+
|
363
|
+
if inspect.iscoroutinefunction(raw_f):
|
364
|
+
raise InvalidError(
|
365
|
+
f"ASGI app function {raw_f.__name__} is an async function. Only sync Python functions are supported."
|
366
|
+
)
|
367
|
+
|
282
368
|
if not wait_for_response:
|
283
|
-
|
284
|
-
|
285
|
-
|
369
|
+
deprecation_error(
|
370
|
+
(2024, 5, 13),
|
371
|
+
"wait_for_response=False has been deprecated on web endpoints. See "
|
372
|
+
"https://modal.com/docs/guide/webhook-timeouts#polling-solutions for alternatives",
|
373
|
+
)
|
286
374
|
|
287
375
|
return _PartialFunction(
|
288
376
|
raw_f,
|
@@ -290,8 +378,9 @@ def _asgi_app(
|
|
290
378
|
api_pb2.WebhookConfig(
|
291
379
|
type=api_pb2.WEBHOOK_TYPE_ASGI_APP,
|
292
380
|
requested_suffix=label,
|
293
|
-
async_mode=
|
381
|
+
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
294
382
|
custom_domains=_parse_custom_domains(custom_domains),
|
383
|
+
requires_proxy_auth=requires_proxy_auth,
|
295
384
|
),
|
296
385
|
)
|
297
386
|
|
@@ -302,14 +391,16 @@ def _wsgi_app(
|
|
302
391
|
_warn_parentheses_missing=None,
|
303
392
|
*,
|
304
393
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
305
|
-
wait_for_response: bool = True, # Whether requests should wait for and return the function response.
|
306
394
|
custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
|
395
|
+
requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
|
396
|
+
wait_for_response: bool = True, # DEPRECATED: this must always be True now
|
307
397
|
) -> Callable[[Callable[..., Any]], _PartialFunction]:
|
308
398
|
"""Decorator for registering a WSGI app with a Modal function.
|
309
399
|
|
310
400
|
Web Server Gateway Interface (WSGI) is a standard for synchronous Python web apps.
|
311
|
-
It has been [succeeded by the ASGI interface](https://asgi.readthedocs.io/en/latest/introduction.html#wsgi-compatibility)
|
312
|
-
additional functionality such as web sockets.
|
401
|
+
It has been [succeeded by the ASGI interface](https://asgi.readthedocs.io/en/latest/introduction.html#wsgi-compatibility)
|
402
|
+
which is compatible with ASGI and supports additional functionality such as web sockets.
|
403
|
+
Modal supports ASGI via [`asgi_app`](/docs/reference/modal.asgi_app).
|
313
404
|
|
314
405
|
**Usage:**
|
315
406
|
|
@@ -327,16 +418,35 @@ def _wsgi_app(
|
|
327
418
|
"""
|
328
419
|
if isinstance(_warn_parentheses_missing, str):
|
329
420
|
raise InvalidError('Positional arguments are not allowed. Suggestion: `@wsgi_app(label="foo")`.')
|
330
|
-
elif _warn_parentheses_missing:
|
421
|
+
elif _warn_parentheses_missing is not None:
|
331
422
|
raise InvalidError(
|
332
423
|
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@wsgi_app()`."
|
333
424
|
)
|
334
425
|
|
335
426
|
def wrapper(raw_f: Callable[..., Any]) -> _PartialFunction:
|
427
|
+
if callable_has_non_self_params(raw_f):
|
428
|
+
if callable_has_non_self_non_default_params(raw_f):
|
429
|
+
raise InvalidError(
|
430
|
+
f"WSGI app function {raw_f.__name__} can't have parameters. See https://modal.com/docs/guide/webhooks#wsgi."
|
431
|
+
)
|
432
|
+
else:
|
433
|
+
deprecation_warning(
|
434
|
+
(2024, 9, 4),
|
435
|
+
f"WSGI app function {raw_f.__name__} has default parameters, but shouldn't have any parameters - "
|
436
|
+
f"Modal will drop support for default parameters in a future release.",
|
437
|
+
)
|
438
|
+
|
439
|
+
if inspect.iscoroutinefunction(raw_f):
|
440
|
+
raise InvalidError(
|
441
|
+
f"WSGI app function {raw_f.__name__} is an async function. Only sync Python functions are supported."
|
442
|
+
)
|
443
|
+
|
336
444
|
if not wait_for_response:
|
337
|
-
|
338
|
-
|
339
|
-
|
445
|
+
deprecation_error(
|
446
|
+
(2024, 5, 13),
|
447
|
+
"wait_for_response=False has been deprecated on web endpoints. See "
|
448
|
+
"https://modal.com/docs/guide/webhook-timeouts#polling-solutions for alternatives",
|
449
|
+
)
|
340
450
|
|
341
451
|
return _PartialFunction(
|
342
452
|
raw_f,
|
@@ -344,8 +454,9 @@ def _wsgi_app(
|
|
344
454
|
api_pb2.WebhookConfig(
|
345
455
|
type=api_pb2.WEBHOOK_TYPE_WSGI_APP,
|
346
456
|
requested_suffix=label,
|
347
|
-
async_mode=
|
457
|
+
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
348
458
|
custom_domains=_parse_custom_domains(custom_domains),
|
459
|
+
requires_proxy_auth=requires_proxy_auth,
|
349
460
|
),
|
350
461
|
)
|
351
462
|
|
@@ -358,6 +469,7 @@ def _web_server(
|
|
358
469
|
startup_timeout: float = 5.0, # Maximum number of seconds to wait for the web server to start.
|
359
470
|
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
360
471
|
custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
|
472
|
+
requires_proxy_auth: bool = False, # Require Proxy-Authorization HTTP Headers on requests
|
361
473
|
) -> Callable[[Callable[..., Any]], _PartialFunction]:
|
362
474
|
"""Decorator that registers an HTTP web server inside the container.
|
363
475
|
|
@@ -401,6 +513,7 @@ def _web_server(
|
|
401
513
|
custom_domains=_parse_custom_domains(custom_domains),
|
402
514
|
web_server_port=port,
|
403
515
|
web_server_startup_timeout=startup_timeout,
|
516
|
+
requires_proxy_auth=requires_proxy_auth,
|
404
517
|
),
|
405
518
|
)
|
406
519
|
|
@@ -414,7 +527,7 @@ def _disallow_wrapping_method(f: _PartialFunction, wrapper: str) -> None:
|
|
414
527
|
|
415
528
|
|
416
529
|
def _build(
|
417
|
-
_warn_parentheses_missing=None,
|
530
|
+
_warn_parentheses_missing=None, *, force: bool = False, timeout: int = 86400
|
418
531
|
) -> Callable[[Union[Callable[[Any], Any], _PartialFunction]], _PartialFunction]:
|
419
532
|
"""
|
420
533
|
Decorator for methods that should execute at _build time_ to create a new layer
|
@@ -436,15 +549,17 @@ def _build(
|
|
436
549
|
LlamaTokenizer.from_pretrained(base_model)
|
437
550
|
```
|
438
551
|
"""
|
439
|
-
if _warn_parentheses_missing:
|
552
|
+
if _warn_parentheses_missing is not None:
|
440
553
|
raise InvalidError("Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@build()`.")
|
441
554
|
|
442
555
|
def wrapper(f: Union[Callable[[Any], Any], _PartialFunction]) -> _PartialFunction:
|
443
556
|
if isinstance(f, _PartialFunction):
|
444
557
|
_disallow_wrapping_method(f, "build")
|
558
|
+
f.force_build = force
|
559
|
+
f.build_timeout = timeout
|
445
560
|
return f.add_flags(_PartialFunctionFlags.BUILD)
|
446
561
|
else:
|
447
|
-
return _PartialFunction(f, _PartialFunctionFlags.BUILD)
|
562
|
+
return _PartialFunction(f, _PartialFunctionFlags.BUILD, force_build=force, build_timeout=timeout)
|
448
563
|
|
449
564
|
return wrapper
|
450
565
|
|
@@ -457,13 +572,13 @@ def _enter(
|
|
457
572
|
"""Decorator for methods which should be executed when a new container is started.
|
458
573
|
|
459
574
|
See the [lifeycle function guide](https://modal.com/docs/guide/lifecycle-functions#enter) for more information."""
|
460
|
-
if _warn_parentheses_missing:
|
575
|
+
if _warn_parentheses_missing is not None:
|
461
576
|
raise InvalidError("Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@enter()`.")
|
462
577
|
|
463
578
|
if snap:
|
464
|
-
flag = _PartialFunctionFlags.
|
579
|
+
flag = _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
|
465
580
|
else:
|
466
|
-
flag = _PartialFunctionFlags.
|
581
|
+
flag = _PartialFunctionFlags.ENTER_POST_SNAPSHOT
|
467
582
|
|
468
583
|
def wrapper(f: Union[Callable[[Any], Any], _PartialFunction]) -> _PartialFunction:
|
469
584
|
if isinstance(f, _PartialFunction):
|
@@ -476,10 +591,12 @@ def _enter(
|
|
476
591
|
|
477
592
|
|
478
593
|
ExitHandlerType = Union[
|
594
|
+
# NOTE: return types of these callables should be `Union[None, Awaitable[None]]` but
|
595
|
+
# synchronicity type stubs would strip Awaitable so we use Any for now
|
479
596
|
# Original, __exit__ style method signature (now deprecated)
|
480
|
-
Callable[[Any, Optional[
|
597
|
+
Callable[[Any, Optional[type[BaseException]], Optional[BaseException], Any], Any],
|
481
598
|
# Forward-looking unparameterized method
|
482
|
-
Callable[[Any],
|
599
|
+
Callable[[Any], Any],
|
483
600
|
]
|
484
601
|
|
485
602
|
|
@@ -487,23 +604,70 @@ def _exit(_warn_parentheses_missing=None) -> Callable[[ExitHandlerType], _Partia
|
|
487
604
|
"""Decorator for methods which should be executed when a container is about to exit.
|
488
605
|
|
489
606
|
See the [lifeycle function guide](https://modal.com/docs/guide/lifecycle-functions#exit) for more information."""
|
490
|
-
if _warn_parentheses_missing:
|
607
|
+
if _warn_parentheses_missing is not None:
|
491
608
|
raise InvalidError("Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@exit()`.")
|
492
609
|
|
493
610
|
def wrapper(f: ExitHandlerType) -> _PartialFunction:
|
494
611
|
if isinstance(f, _PartialFunction):
|
495
612
|
_disallow_wrapping_method(f, "exit")
|
496
|
-
|
497
|
-
message = (
|
498
|
-
"Support for decorating parameterized methods with `@exit` has been deprecated."
|
499
|
-
" To avoid future errors, please update your code by removing the parameters."
|
500
|
-
)
|
501
|
-
deprecation_warning((2024, 2, 23), message)
|
613
|
+
|
502
614
|
return _PartialFunction(f, _PartialFunctionFlags.EXIT)
|
503
615
|
|
504
616
|
return wrapper
|
505
617
|
|
506
618
|
|
619
|
+
def _batched(
|
620
|
+
_warn_parentheses_missing=None,
|
621
|
+
*,
|
622
|
+
max_batch_size: int,
|
623
|
+
wait_ms: int,
|
624
|
+
) -> Callable[[Callable[..., Any]], _PartialFunction]:
|
625
|
+
"""Decorator for functions or class methods that should be batched.
|
626
|
+
|
627
|
+
**Usage**
|
628
|
+
|
629
|
+
```python notest
|
630
|
+
@app.function()
|
631
|
+
@modal.batched(max_batch_size=4, wait_ms=1000)
|
632
|
+
async def batched_multiply(xs: list[int], ys: list[int]) -> list[int]:
|
633
|
+
return [x * y for x, y in zip(xs, xs)]
|
634
|
+
|
635
|
+
# call batched_multiply with individual inputs
|
636
|
+
batched_multiply.remote.aio(2, 100)
|
637
|
+
```
|
638
|
+
|
639
|
+
See the [dynamic batching guide](https://modal.com/docs/guide/dynamic-batching) for more information.
|
640
|
+
"""
|
641
|
+
if _warn_parentheses_missing is not None:
|
642
|
+
raise InvalidError(
|
643
|
+
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@batched()`."
|
644
|
+
)
|
645
|
+
if max_batch_size < 1:
|
646
|
+
raise InvalidError("max_batch_size must be a positive integer.")
|
647
|
+
if max_batch_size >= MAX_MAX_BATCH_SIZE:
|
648
|
+
raise InvalidError(f"max_batch_size must be less than {MAX_MAX_BATCH_SIZE}.")
|
649
|
+
if wait_ms < 0:
|
650
|
+
raise InvalidError("wait_ms must be a non-negative integer.")
|
651
|
+
if wait_ms >= MAX_BATCH_WAIT_MS:
|
652
|
+
raise InvalidError(f"wait_ms must be less than {MAX_BATCH_WAIT_MS}.")
|
653
|
+
|
654
|
+
def wrapper(raw_f: Callable[..., Any]) -> _PartialFunction:
|
655
|
+
if isinstance(raw_f, _Function):
|
656
|
+
raw_f = raw_f.get_raw_f()
|
657
|
+
raise InvalidError(
|
658
|
+
f"Applying decorators for {raw_f} in the wrong order!\nUsage:\n\n"
|
659
|
+
"@app.function()\n@modal.batched()\ndef batched_function():\n ..."
|
660
|
+
)
|
661
|
+
return _PartialFunction(
|
662
|
+
raw_f,
|
663
|
+
_PartialFunctionFlags.FUNCTION | _PartialFunctionFlags.BATCHED,
|
664
|
+
batch_max_size=max_batch_size,
|
665
|
+
batch_wait_ms=wait_ms,
|
666
|
+
)
|
667
|
+
|
668
|
+
return wrapper
|
669
|
+
|
670
|
+
|
507
671
|
method = synchronize_api(_method)
|
508
672
|
web_endpoint = synchronize_api(_web_endpoint)
|
509
673
|
asgi_app = synchronize_api(_asgi_app)
|
@@ -512,3 +676,4 @@ web_server = synchronize_api(_web_server)
|
|
512
676
|
build = synchronize_api(_build)
|
513
677
|
enter = synchronize_api(_enter)
|
514
678
|
exit = synchronize_api(_exit)
|
679
|
+
batched = synchronize_api(_batched)
|