modal 0.73.27__py3-none-any.whl → 0.73.29__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/__main__.py +1 -1
- modal/_container_entrypoint.py +4 -4
- modal/_functions.py +6 -5
- modal/_partial_function.py +691 -0
- modal/_resolver.py +1 -2
- modal/_runtime/container_io_manager.py +1 -1
- modal/_runtime/user_code_imports.py +3 -4
- modal/_utils/async_utils.py +3 -6
- modal/_utils/blob_utils.py +1 -1
- modal/_utils/function_utils.py +1 -2
- modal/app.py +12 -14
- modal/cli/entry_point.py +1 -1
- modal/cli/run.py +2 -3
- modal/cli/secret.py +1 -1
- modal/cli/volume.py +1 -2
- modal/client.pyi +2 -2
- modal/cls.py +7 -8
- modal/cls.pyi +2 -1
- modal/environments.py +1 -3
- modal/experimental.py +1 -1
- modal/file_pattern_matcher.py +1 -2
- modal/mount.py +4 -8
- modal/output.py +1 -0
- modal/partial_function.py +26 -696
- modal/partial_function.pyi +19 -157
- modal/sandbox.py +4 -8
- modal/token_flow.py +1 -1
- {modal-0.73.27.dist-info → modal-0.73.29.dist-info}/METADATA +1 -1
- {modal-0.73.27.dist-info → modal-0.73.29.dist-info}/RECORD +35 -34
- modal_docs/mdmd/mdmd.py +1 -0
- modal_version/_version_generated.py +1 -1
- {modal-0.73.27.dist-info → modal-0.73.29.dist-info}/LICENSE +0 -0
- {modal-0.73.27.dist-info → modal-0.73.29.dist-info}/WHEEL +0 -0
- {modal-0.73.27.dist-info → modal-0.73.29.dist-info}/entry_points.txt +0 -0
- {modal-0.73.27.dist-info → modal-0.73.29.dist-info}/top_level.txt +0 -0
modal/partial_function.py
CHANGED
@@ -1,698 +1,28 @@
|
|
1
|
-
# Copyright Modal Labs
|
2
|
-
import
|
3
|
-
|
4
|
-
import
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
1
|
+
# Copyright Modal Labs 2025
|
2
|
+
from modal._utils.async_utils import synchronize_api
|
3
|
+
|
4
|
+
from ._partial_function import (
|
5
|
+
_asgi_app,
|
6
|
+
_batched,
|
7
|
+
_build,
|
8
|
+
_enter,
|
9
|
+
_exit,
|
10
|
+
_method,
|
11
|
+
_PartialFunction,
|
12
|
+
_web_endpoint,
|
13
|
+
_web_server,
|
14
|
+
_wsgi_app,
|
11
15
|
)
|
12
16
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
MAX_BATCH_WAIT_MS = 10 * 60 * 1000 # 10 minutes
|
26
|
-
|
27
|
-
|
28
|
-
class _PartialFunctionFlags(enum.IntFlag):
|
29
|
-
FUNCTION = 1
|
30
|
-
BUILD = 2
|
31
|
-
ENTER_PRE_SNAPSHOT = 4
|
32
|
-
ENTER_POST_SNAPSHOT = 8
|
33
|
-
EXIT = 16
|
34
|
-
BATCHED = 32
|
35
|
-
CLUSTERED = 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)
|
45
|
-
|
46
|
-
|
47
|
-
class _PartialFunction(typing.Generic[P, ReturnType, OriginalReturnType]):
|
48
|
-
"""Intermediate function, produced by @enter, @build, @method, @web_endpoint, or @batched"""
|
49
|
-
|
50
|
-
raw_f: Callable[P, ReturnType]
|
51
|
-
flags: _PartialFunctionFlags
|
52
|
-
webhook_config: Optional[api_pb2.WebhookConfig]
|
53
|
-
is_generator: bool
|
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]
|
60
|
-
|
61
|
-
def __init__(
|
62
|
-
self,
|
63
|
-
raw_f: Callable[P, ReturnType],
|
64
|
-
flags: _PartialFunctionFlags,
|
65
|
-
webhook_config: Optional[api_pb2.WebhookConfig] = None,
|
66
|
-
is_generator: Optional[bool] = None,
|
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,
|
73
|
-
):
|
74
|
-
self.raw_f = raw_f
|
75
|
-
self.flags = flags
|
76
|
-
self.webhook_config = webhook_config
|
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
|
84
|
-
self.keep_warm = keep_warm
|
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
|
91
|
-
|
92
|
-
def _get_raw_f(self) -> Callable[P, ReturnType]:
|
93
|
-
return self.raw_f
|
94
|
-
|
95
|
-
def _is_web_endpoint(self) -> bool:
|
96
|
-
if self.webhook_config is None:
|
97
|
-
return False
|
98
|
-
return self.webhook_config.type != api_pb2.WEBHOOK_TYPE_UNSPECIFIED
|
99
|
-
|
100
|
-
def __get__(self, obj, objtype=None) -> _Function[P, ReturnType, OriginalReturnType]:
|
101
|
-
k = self.raw_f.__name__
|
102
|
-
if obj: # accessing the method on an instance of a class, e.g. `MyClass().fun``
|
103
|
-
if hasattr(obj, "_modal_functions"):
|
104
|
-
# This happens inside "local" user methods when they refer to other methods,
|
105
|
-
# e.g. Foo().parent_method.remote() calling self.other_method.remote()
|
106
|
-
return getattr(obj, "_modal_functions")[k]
|
107
|
-
else:
|
108
|
-
# special edge case: referencing a method of an instance of an
|
109
|
-
# unwrapped class (not using app.cls()) with @methods
|
110
|
-
# not sure what would be useful here, but let's return a bound version of the underlying function,
|
111
|
-
# since the class is just a vanilla class at this point
|
112
|
-
# This wouldn't let the user access `.remote()` and `.local()` etc. on the function
|
113
|
-
return self.raw_f.__get__(obj, objtype)
|
114
|
-
|
115
|
-
else: # accessing a method directly on the class, e.g. `MyClass.fun`
|
116
|
-
# This happens mainly during serialization of the wrapped underlying class of a Cls
|
117
|
-
# since we don't have the instance info here we just return the PartialFunction itself
|
118
|
-
# to let it be bound to a variable and become a Function later on
|
119
|
-
return self # type: ignore # this returns a PartialFunction in a special internal case
|
120
|
-
|
121
|
-
def __del__(self):
|
122
|
-
if (self.flags & _PartialFunctionFlags.FUNCTION) and self.wrapped is False:
|
123
|
-
logger.warning(
|
124
|
-
f"Method or web function {self.raw_f} was never turned into a function."
|
125
|
-
" Did you forget a @app.function or @app.cls decorator?"
|
126
|
-
)
|
127
|
-
|
128
|
-
def add_flags(self, flags) -> "_PartialFunction":
|
129
|
-
# Helper method used internally when stacking decorators
|
130
|
-
self.wrapped = True
|
131
|
-
return _PartialFunction(
|
132
|
-
raw_f=self.raw_f,
|
133
|
-
flags=(self.flags | flags),
|
134
|
-
webhook_config=self.webhook_config,
|
135
|
-
keep_warm=self.keep_warm,
|
136
|
-
batch_max_size=self.batch_max_size,
|
137
|
-
batch_wait_ms=self.batch_wait_ms,
|
138
|
-
force_build=self.force_build,
|
139
|
-
build_timeout=self.build_timeout,
|
140
|
-
)
|
141
|
-
|
142
|
-
|
143
|
-
PartialFunction = synchronize_api(_PartialFunction)
|
144
|
-
|
145
|
-
|
146
|
-
def _find_partial_methods_for_user_cls(user_cls: type[Any], flags: int) -> dict[str, _PartialFunction]:
|
147
|
-
"""Grabs all method on a user class, and returns partials. Includes legacy methods."""
|
148
|
-
|
149
|
-
partial_functions: dict[str, _PartialFunction] = {}
|
150
|
-
for parent_cls in reversed(user_cls.mro()):
|
151
|
-
if parent_cls is not object:
|
152
|
-
for k, v in parent_cls.__dict__.items():
|
153
|
-
if isinstance(v, PartialFunction): # type: ignore[reportArgumentType] # synchronicity wrapper types
|
154
|
-
_partial_function: _PartialFunction = typing.cast(_PartialFunction, synchronizer._translate_in(v))
|
155
|
-
if _partial_function.flags & flags:
|
156
|
-
partial_functions[k] = _partial_function
|
157
|
-
|
158
|
-
return partial_functions
|
159
|
-
|
160
|
-
|
161
|
-
def _find_callables_for_obj(user_obj: Any, flags: int) -> dict[str, Callable[..., Any]]:
|
162
|
-
"""Grabs all methods for an object, and binds them to the class"""
|
163
|
-
user_cls: type = type(user_obj)
|
164
|
-
return {k: pf.raw_f.__get__(user_obj) for k, pf in _find_partial_methods_for_user_cls(user_cls, flags).items()}
|
165
|
-
|
166
|
-
|
167
|
-
class _MethodDecoratorType:
|
168
|
-
@typing.overload
|
169
|
-
def __call__(
|
170
|
-
self, func: PartialFunction[typing_extensions.Concatenate[Any, P], ReturnType, OriginalReturnType]
|
171
|
-
) -> PartialFunction[P, ReturnType, OriginalReturnType]:
|
172
|
-
...
|
173
|
-
|
174
|
-
@typing.overload
|
175
|
-
def __call__(
|
176
|
-
self, func: Callable[typing_extensions.Concatenate[Any, P], Coroutine[Any, Any, ReturnType]]
|
177
|
-
) -> PartialFunction[P, ReturnType, Coroutine[Any, Any, ReturnType]]:
|
178
|
-
...
|
179
|
-
|
180
|
-
@typing.overload
|
181
|
-
def __call__(
|
182
|
-
self, func: Callable[typing_extensions.Concatenate[Any, P], ReturnType]
|
183
|
-
) -> PartialFunction[P, ReturnType, ReturnType]:
|
184
|
-
...
|
185
|
-
|
186
|
-
def __call__(self, func):
|
187
|
-
...
|
188
|
-
|
189
|
-
|
190
|
-
# TODO(elias): fix support for coroutine type unwrapping for methods (static typing)
|
191
|
-
def _method(
|
192
|
-
_warn_parentheses_missing=None,
|
193
|
-
*,
|
194
|
-
# Set this to True if it's a non-generator function returning
|
195
|
-
# a [sync/async] generator object
|
196
|
-
is_generator: Optional[bool] = None,
|
197
|
-
keep_warm: Optional[int] = None, # Deprecated: Use keep_warm on @app.cls() instead
|
198
|
-
) -> _MethodDecoratorType:
|
199
|
-
"""Decorator for methods that should be transformed into a Modal Function registered against this class's App.
|
200
|
-
|
201
|
-
**Usage:**
|
202
|
-
|
203
|
-
```python
|
204
|
-
@app.cls(cpu=8)
|
205
|
-
class MyCls:
|
206
|
-
|
207
|
-
@modal.method()
|
208
|
-
def f(self):
|
209
|
-
...
|
210
|
-
```
|
211
|
-
"""
|
212
|
-
if _warn_parentheses_missing is not None:
|
213
|
-
raise InvalidError("Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@method()`.")
|
214
|
-
|
215
|
-
if keep_warm is not None:
|
216
|
-
deprecation_warning(
|
217
|
-
(2024, 6, 10),
|
218
|
-
(
|
219
|
-
"`keep_warm=` is no longer supported per-method on Modal classes. "
|
220
|
-
"All methods and web endpoints of a class use the same set of containers now. "
|
221
|
-
"Use keep_warm via the @app.cls() decorator instead. "
|
222
|
-
),
|
223
|
-
pending=True,
|
224
|
-
)
|
225
|
-
|
226
|
-
def wrapper(raw_f: Callable[..., Any]) -> _PartialFunction:
|
227
|
-
nonlocal is_generator
|
228
|
-
if isinstance(raw_f, _PartialFunction) and raw_f.webhook_config:
|
229
|
-
raw_f.wrapped = True # suppress later warning
|
230
|
-
raise InvalidError(
|
231
|
-
"Web endpoints on classes should not be wrapped by `@method`. "
|
232
|
-
"Suggestion: remove the `@method` decorator."
|
233
|
-
)
|
234
|
-
if isinstance(raw_f, _PartialFunction) and raw_f.batch_max_size is not None:
|
235
|
-
raw_f.wrapped = True # suppress later warning
|
236
|
-
raise InvalidError(
|
237
|
-
"Batched function on classes should not be wrapped by `@method`. "
|
238
|
-
"Suggestion: remove the `@method` decorator."
|
239
|
-
)
|
240
|
-
return _PartialFunction(raw_f, _PartialFunctionFlags.FUNCTION, is_generator=is_generator, keep_warm=keep_warm)
|
241
|
-
|
242
|
-
return wrapper
|
243
|
-
|
244
|
-
|
245
|
-
def _parse_custom_domains(custom_domains: Optional[Iterable[str]] = None) -> list[api_pb2.CustomDomainConfig]:
|
246
|
-
assert not isinstance(custom_domains, str), "custom_domains must be `Iterable[str]` but is `str` instead."
|
247
|
-
_custom_domains: list[api_pb2.CustomDomainConfig] = []
|
248
|
-
if custom_domains is not None:
|
249
|
-
for custom_domain in custom_domains:
|
250
|
-
_custom_domains.append(api_pb2.CustomDomainConfig(name=custom_domain))
|
251
|
-
|
252
|
-
return _custom_domains
|
253
|
-
|
254
|
-
|
255
|
-
def _web_endpoint(
|
256
|
-
_warn_parentheses_missing=None,
|
257
|
-
*,
|
258
|
-
method: str = "GET", # REST method for the created endpoint.
|
259
|
-
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
260
|
-
docs: bool = False, # Whether to enable interactive documentation for this endpoint at /docs.
|
261
|
-
custom_domains: Optional[
|
262
|
-
Iterable[str]
|
263
|
-
] = None, # Create an endpoint using a custom domain fully-qualified domain name (FQDN).
|
264
|
-
requires_proxy_auth: bool = False, # Require Modal-Key and Modal-Secret HTTP Headers on requests.
|
265
|
-
wait_for_response: bool = True, # DEPRECATED: this must always be True now
|
266
|
-
) -> Callable[[Callable[P, ReturnType]], _PartialFunction[P, ReturnType, ReturnType]]:
|
267
|
-
"""Register a basic web endpoint with this application.
|
268
|
-
|
269
|
-
This is the simple way to create a web endpoint on Modal. The function
|
270
|
-
behaves as a [FastAPI](https://fastapi.tiangolo.com/) handler and should
|
271
|
-
return a response object to the caller.
|
272
|
-
|
273
|
-
Endpoints created with `@app.web_endpoint` are meant to be simple, single
|
274
|
-
request handlers and automatically have
|
275
|
-
[CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) enabled.
|
276
|
-
For more flexibility, use `@app.asgi_app`.
|
277
|
-
|
278
|
-
To learn how to use Modal with popular web frameworks, see the
|
279
|
-
[guide on web endpoints](https://modal.com/docs/guide/webhooks).
|
280
|
-
"""
|
281
|
-
if isinstance(_warn_parentheses_missing, str):
|
282
|
-
# Probably passing the method string as a positional argument.
|
283
|
-
raise InvalidError('Positional arguments are not allowed. Suggestion: `@web_endpoint(method="GET")`.')
|
284
|
-
elif _warn_parentheses_missing is not None:
|
285
|
-
raise InvalidError(
|
286
|
-
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@web_endpoint()`."
|
287
|
-
)
|
288
|
-
|
289
|
-
def wrapper(raw_f: Callable[..., Any]) -> _PartialFunction:
|
290
|
-
if isinstance(raw_f, _Function):
|
291
|
-
raw_f = raw_f.get_raw_f()
|
292
|
-
raise InvalidError(
|
293
|
-
f"Applying decorators for {raw_f} in the wrong order!\nUsage:\n\n"
|
294
|
-
"@app.function()\n@app.web_endpoint()\ndef my_webhook():\n ..."
|
295
|
-
)
|
296
|
-
if not wait_for_response:
|
297
|
-
deprecation_error(
|
298
|
-
(2024, 5, 13),
|
299
|
-
"wait_for_response=False has been deprecated on web endpoints. See "
|
300
|
-
"https://modal.com/docs/guide/webhook-timeouts#polling-solutions for alternatives.",
|
301
|
-
)
|
302
|
-
|
303
|
-
# self._loose_webhook_configs.add(raw_f)
|
304
|
-
|
305
|
-
return _PartialFunction(
|
306
|
-
raw_f,
|
307
|
-
_PartialFunctionFlags.FUNCTION,
|
308
|
-
api_pb2.WebhookConfig(
|
309
|
-
type=api_pb2.WEBHOOK_TYPE_FUNCTION,
|
310
|
-
method=method,
|
311
|
-
web_endpoint_docs=docs,
|
312
|
-
requested_suffix=label,
|
313
|
-
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
314
|
-
custom_domains=_parse_custom_domains(custom_domains),
|
315
|
-
requires_proxy_auth=requires_proxy_auth,
|
316
|
-
),
|
317
|
-
)
|
318
|
-
|
319
|
-
return wrapper
|
320
|
-
|
321
|
-
|
322
|
-
def _asgi_app(
|
323
|
-
_warn_parentheses_missing=None,
|
324
|
-
*,
|
325
|
-
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
326
|
-
custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
|
327
|
-
requires_proxy_auth: bool = False, # Require Modal-Key and Modal-Secret HTTP Headers on requests.
|
328
|
-
wait_for_response: bool = True, # DEPRECATED: this must always be True now
|
329
|
-
) -> Callable[[Callable[..., Any]], _PartialFunction]:
|
330
|
-
"""Decorator for registering an ASGI app with a Modal function.
|
331
|
-
|
332
|
-
Asynchronous Server Gateway Interface (ASGI) is a standard for Python
|
333
|
-
synchronous and asynchronous apps, supported by all popular Python web
|
334
|
-
libraries. This is an advanced decorator that gives full flexibility in
|
335
|
-
defining one or more web endpoints on Modal.
|
336
|
-
|
337
|
-
**Usage:**
|
338
|
-
|
339
|
-
```python
|
340
|
-
from typing import Callable
|
341
|
-
|
342
|
-
@app.function()
|
343
|
-
@modal.asgi_app()
|
344
|
-
def create_asgi() -> Callable:
|
345
|
-
...
|
346
|
-
```
|
347
|
-
|
348
|
-
To learn how to use Modal with popular web frameworks, see the
|
349
|
-
[guide on web endpoints](https://modal.com/docs/guide/webhooks).
|
350
|
-
"""
|
351
|
-
if isinstance(_warn_parentheses_missing, str):
|
352
|
-
raise InvalidError('Positional arguments are not allowed. Suggestion: `@asgi_app(label="foo")`.')
|
353
|
-
elif _warn_parentheses_missing is not None:
|
354
|
-
raise InvalidError(
|
355
|
-
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@asgi_app()`."
|
356
|
-
)
|
357
|
-
|
358
|
-
def wrapper(raw_f: Callable[..., Any]) -> _PartialFunction:
|
359
|
-
if callable_has_non_self_params(raw_f):
|
360
|
-
if callable_has_non_self_non_default_params(raw_f):
|
361
|
-
raise InvalidError(
|
362
|
-
f"ASGI app function {raw_f.__name__} can't have parameters. See https://modal.com/docs/guide/webhooks#asgi."
|
363
|
-
)
|
364
|
-
else:
|
365
|
-
deprecation_warning(
|
366
|
-
(2024, 9, 4),
|
367
|
-
f"ASGI app function {raw_f.__name__} has default parameters, but shouldn't have any parameters - "
|
368
|
-
f"Modal will drop support for default parameters in a future release.",
|
369
|
-
)
|
370
|
-
|
371
|
-
if inspect.iscoroutinefunction(raw_f):
|
372
|
-
raise InvalidError(
|
373
|
-
f"ASGI app function {raw_f.__name__} is an async function. Only sync Python functions are supported."
|
374
|
-
)
|
375
|
-
|
376
|
-
if not wait_for_response:
|
377
|
-
deprecation_error(
|
378
|
-
(2024, 5, 13),
|
379
|
-
"wait_for_response=False has been deprecated on web endpoints. See "
|
380
|
-
"https://modal.com/docs/guide/webhook-timeouts#polling-solutions for alternatives",
|
381
|
-
)
|
382
|
-
|
383
|
-
return _PartialFunction(
|
384
|
-
raw_f,
|
385
|
-
_PartialFunctionFlags.FUNCTION,
|
386
|
-
api_pb2.WebhookConfig(
|
387
|
-
type=api_pb2.WEBHOOK_TYPE_ASGI_APP,
|
388
|
-
requested_suffix=label,
|
389
|
-
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
390
|
-
custom_domains=_parse_custom_domains(custom_domains),
|
391
|
-
requires_proxy_auth=requires_proxy_auth,
|
392
|
-
),
|
393
|
-
)
|
394
|
-
|
395
|
-
return wrapper
|
396
|
-
|
397
|
-
|
398
|
-
def _wsgi_app(
|
399
|
-
_warn_parentheses_missing=None,
|
400
|
-
*,
|
401
|
-
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
402
|
-
custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
|
403
|
-
requires_proxy_auth: bool = False, # Require Modal-Key and Modal-Secret HTTP Headers on requests.
|
404
|
-
wait_for_response: bool = True, # DEPRECATED: this must always be True now
|
405
|
-
) -> Callable[[Callable[..., Any]], _PartialFunction]:
|
406
|
-
"""Decorator for registering a WSGI app with a Modal function.
|
407
|
-
|
408
|
-
Web Server Gateway Interface (WSGI) is a standard for synchronous Python web apps.
|
409
|
-
It has been [succeeded by the ASGI interface](https://asgi.readthedocs.io/en/latest/introduction.html#wsgi-compatibility)
|
410
|
-
which is compatible with ASGI and supports additional functionality such as web sockets.
|
411
|
-
Modal supports ASGI via [`asgi_app`](/docs/reference/modal.asgi_app).
|
412
|
-
|
413
|
-
**Usage:**
|
414
|
-
|
415
|
-
```python
|
416
|
-
from typing import Callable
|
417
|
-
|
418
|
-
@app.function()
|
419
|
-
@modal.wsgi_app()
|
420
|
-
def create_wsgi() -> Callable:
|
421
|
-
...
|
422
|
-
```
|
423
|
-
|
424
|
-
To learn how to use this decorator with popular web frameworks, see the
|
425
|
-
[guide on web endpoints](https://modal.com/docs/guide/webhooks).
|
426
|
-
"""
|
427
|
-
if isinstance(_warn_parentheses_missing, str):
|
428
|
-
raise InvalidError('Positional arguments are not allowed. Suggestion: `@wsgi_app(label="foo")`.')
|
429
|
-
elif _warn_parentheses_missing is not None:
|
430
|
-
raise InvalidError(
|
431
|
-
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@wsgi_app()`."
|
432
|
-
)
|
433
|
-
|
434
|
-
def wrapper(raw_f: Callable[..., Any]) -> _PartialFunction:
|
435
|
-
if callable_has_non_self_params(raw_f):
|
436
|
-
if callable_has_non_self_non_default_params(raw_f):
|
437
|
-
raise InvalidError(
|
438
|
-
f"WSGI app function {raw_f.__name__} can't have parameters. See https://modal.com/docs/guide/webhooks#wsgi."
|
439
|
-
)
|
440
|
-
else:
|
441
|
-
deprecation_warning(
|
442
|
-
(2024, 9, 4),
|
443
|
-
f"WSGI app function {raw_f.__name__} has default parameters, but shouldn't have any parameters - "
|
444
|
-
f"Modal will drop support for default parameters in a future release.",
|
445
|
-
)
|
446
|
-
|
447
|
-
if inspect.iscoroutinefunction(raw_f):
|
448
|
-
raise InvalidError(
|
449
|
-
f"WSGI app function {raw_f.__name__} is an async function. Only sync Python functions are supported."
|
450
|
-
)
|
451
|
-
|
452
|
-
if not wait_for_response:
|
453
|
-
deprecation_error(
|
454
|
-
(2024, 5, 13),
|
455
|
-
"wait_for_response=False has been deprecated on web endpoints. See "
|
456
|
-
"https://modal.com/docs/guide/webhook-timeouts#polling-solutions for alternatives",
|
457
|
-
)
|
458
|
-
|
459
|
-
return _PartialFunction(
|
460
|
-
raw_f,
|
461
|
-
_PartialFunctionFlags.FUNCTION,
|
462
|
-
api_pb2.WebhookConfig(
|
463
|
-
type=api_pb2.WEBHOOK_TYPE_WSGI_APP,
|
464
|
-
requested_suffix=label,
|
465
|
-
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
466
|
-
custom_domains=_parse_custom_domains(custom_domains),
|
467
|
-
requires_proxy_auth=requires_proxy_auth,
|
468
|
-
),
|
469
|
-
)
|
470
|
-
|
471
|
-
return wrapper
|
472
|
-
|
473
|
-
|
474
|
-
def _web_server(
|
475
|
-
port: int,
|
476
|
-
*,
|
477
|
-
startup_timeout: float = 5.0, # Maximum number of seconds to wait for the web server to start.
|
478
|
-
label: Optional[str] = None, # Label for created endpoint. Final subdomain will be <workspace>--<label>.modal.run.
|
479
|
-
custom_domains: Optional[Iterable[str]] = None, # Deploy this endpoint on a custom domain.
|
480
|
-
requires_proxy_auth: bool = False, # Require Modal-Key and Modal-Secret HTTP Headers on requests.
|
481
|
-
) -> Callable[[Callable[..., Any]], _PartialFunction]:
|
482
|
-
"""Decorator that registers an HTTP web server inside the container.
|
483
|
-
|
484
|
-
This is similar to `@asgi_app` and `@wsgi_app`, but it allows you to expose a full HTTP server
|
485
|
-
listening on a container port. This is useful for servers written in other languages like Rust,
|
486
|
-
as well as integrating with non-ASGI frameworks like aiohttp and Tornado.
|
487
|
-
|
488
|
-
**Usage:**
|
489
|
-
|
490
|
-
```python
|
491
|
-
import subprocess
|
492
|
-
|
493
|
-
@app.function()
|
494
|
-
@modal.web_server(8000)
|
495
|
-
def my_file_server():
|
496
|
-
subprocess.Popen("python -m http.server -d / 8000", shell=True)
|
497
|
-
```
|
498
|
-
|
499
|
-
The above example starts a simple file server, displaying the contents of the root directory.
|
500
|
-
Here, requests to the web endpoint will go to external port 8000 on the container. The
|
501
|
-
`http.server` module is included with Python, but you could run anything here.
|
502
|
-
|
503
|
-
Internally, the web server is transparently converted into a web endpoint by Modal, so it has
|
504
|
-
the same serverless autoscaling behavior as other web endpoints.
|
505
|
-
|
506
|
-
For more info, see the [guide on web endpoints](https://modal.com/docs/guide/webhooks).
|
507
|
-
"""
|
508
|
-
if not isinstance(port, int) or port < 1 or port > 65535:
|
509
|
-
raise InvalidError("First argument of `@web_server` must be a local port, such as `@web_server(8000)`.")
|
510
|
-
if startup_timeout <= 0:
|
511
|
-
raise InvalidError("The `startup_timeout` argument of `@web_server` must be positive.")
|
512
|
-
|
513
|
-
def wrapper(raw_f: Callable[..., Any]) -> _PartialFunction:
|
514
|
-
return _PartialFunction(
|
515
|
-
raw_f,
|
516
|
-
_PartialFunctionFlags.FUNCTION,
|
517
|
-
api_pb2.WebhookConfig(
|
518
|
-
type=api_pb2.WEBHOOK_TYPE_WEB_SERVER,
|
519
|
-
requested_suffix=label,
|
520
|
-
async_mode=api_pb2.WEBHOOK_ASYNC_MODE_AUTO,
|
521
|
-
custom_domains=_parse_custom_domains(custom_domains),
|
522
|
-
web_server_port=port,
|
523
|
-
web_server_startup_timeout=startup_timeout,
|
524
|
-
requires_proxy_auth=requires_proxy_auth,
|
525
|
-
),
|
526
|
-
)
|
527
|
-
|
528
|
-
return wrapper
|
529
|
-
|
530
|
-
|
531
|
-
def _disallow_wrapping_method(f: _PartialFunction, wrapper: str) -> None:
|
532
|
-
if f.flags & _PartialFunctionFlags.FUNCTION:
|
533
|
-
f.wrapped = True # Hack to avoid warning about not using @app.cls()
|
534
|
-
raise InvalidError(f"Cannot use `@{wrapper}` decorator with `@method`.")
|
535
|
-
|
536
|
-
|
537
|
-
def _build(
|
538
|
-
_warn_parentheses_missing=None, *, force: bool = False, timeout: int = 86400
|
539
|
-
) -> Callable[[Union[Callable[[Any], Any], _PartialFunction]], _PartialFunction]:
|
540
|
-
"""
|
541
|
-
Decorator for methods that execute at _build time_ to create a new Image layer.
|
542
|
-
|
543
|
-
**Deprecated**: This function is deprecated. We recommend using `modal.Volume`
|
544
|
-
to store large assets (such as model weights) instead of writing them to the
|
545
|
-
Image during the build process. For other use cases, you can replace this
|
546
|
-
decorator with the `Image.run_function` method.
|
547
|
-
|
548
|
-
**Usage**
|
549
|
-
|
550
|
-
```python notest
|
551
|
-
@app.cls(gpu="A10G")
|
552
|
-
class AlpacaLoRAModel:
|
553
|
-
@build()
|
554
|
-
def download_models(self):
|
555
|
-
model = LlamaForCausalLM.from_pretrained(
|
556
|
-
base_model,
|
557
|
-
)
|
558
|
-
PeftModel.from_pretrained(model, lora_weights)
|
559
|
-
LlamaTokenizer.from_pretrained(base_model)
|
560
|
-
```
|
561
|
-
"""
|
562
|
-
if _warn_parentheses_missing is not None:
|
563
|
-
raise InvalidError("Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@build()`.")
|
564
|
-
|
565
|
-
deprecation_warning(
|
566
|
-
(2025, 1, 15),
|
567
|
-
"The `@modal.build` decorator is deprecated and will be removed in a future release."
|
568
|
-
"\n\nWe now recommend storing large assets (such as model weights) using a `modal.Volume`"
|
569
|
-
" instead of writing them directly into the `modal.Image` filesystem."
|
570
|
-
" For other use cases we recommend using `Image.run_function` instead."
|
571
|
-
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
572
|
-
)
|
573
|
-
|
574
|
-
def wrapper(f: Union[Callable[[Any], Any], _PartialFunction]) -> _PartialFunction:
|
575
|
-
if isinstance(f, _PartialFunction):
|
576
|
-
_disallow_wrapping_method(f, "build")
|
577
|
-
f.force_build = force
|
578
|
-
f.build_timeout = timeout
|
579
|
-
return f.add_flags(_PartialFunctionFlags.BUILD)
|
580
|
-
else:
|
581
|
-
return _PartialFunction(f, _PartialFunctionFlags.BUILD, force_build=force, build_timeout=timeout)
|
582
|
-
|
583
|
-
return wrapper
|
584
|
-
|
585
|
-
|
586
|
-
def _enter(
|
587
|
-
_warn_parentheses_missing=None,
|
588
|
-
*,
|
589
|
-
snap: bool = False,
|
590
|
-
) -> Callable[[Union[Callable[[Any], Any], _PartialFunction]], _PartialFunction]:
|
591
|
-
"""Decorator for methods which should be executed when a new container is started.
|
592
|
-
|
593
|
-
See the [lifeycle function guide](https://modal.com/docs/guide/lifecycle-functions#enter) for more information."""
|
594
|
-
if _warn_parentheses_missing is not None:
|
595
|
-
raise InvalidError("Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@enter()`.")
|
596
|
-
|
597
|
-
if snap:
|
598
|
-
flag = _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
|
599
|
-
else:
|
600
|
-
flag = _PartialFunctionFlags.ENTER_POST_SNAPSHOT
|
601
|
-
|
602
|
-
def wrapper(f: Union[Callable[[Any], Any], _PartialFunction]) -> _PartialFunction:
|
603
|
-
if isinstance(f, _PartialFunction):
|
604
|
-
_disallow_wrapping_method(f, "enter")
|
605
|
-
return f.add_flags(flag)
|
606
|
-
else:
|
607
|
-
return _PartialFunction(f, flag)
|
608
|
-
|
609
|
-
return wrapper
|
610
|
-
|
611
|
-
|
612
|
-
ExitHandlerType = Union[
|
613
|
-
# NOTE: return types of these callables should be `Union[None, Awaitable[None]]` but
|
614
|
-
# synchronicity type stubs would strip Awaitable so we use Any for now
|
615
|
-
# Original, __exit__ style method signature (now deprecated)
|
616
|
-
Callable[[Any, Optional[type[BaseException]], Optional[BaseException], Any], Any],
|
617
|
-
# Forward-looking unparametrized method
|
618
|
-
Callable[[Any], Any],
|
619
|
-
]
|
620
|
-
|
621
|
-
|
622
|
-
def _exit(_warn_parentheses_missing=None) -> Callable[[ExitHandlerType], _PartialFunction]:
|
623
|
-
"""Decorator for methods which should be executed when a container is about to exit.
|
624
|
-
|
625
|
-
See the [lifeycle function guide](https://modal.com/docs/guide/lifecycle-functions#exit) for more information."""
|
626
|
-
if _warn_parentheses_missing is not None:
|
627
|
-
raise InvalidError("Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@exit()`.")
|
628
|
-
|
629
|
-
def wrapper(f: ExitHandlerType) -> _PartialFunction:
|
630
|
-
if isinstance(f, _PartialFunction):
|
631
|
-
_disallow_wrapping_method(f, "exit")
|
632
|
-
|
633
|
-
return _PartialFunction(f, _PartialFunctionFlags.EXIT)
|
634
|
-
|
635
|
-
return wrapper
|
636
|
-
|
637
|
-
|
638
|
-
def _batched(
|
639
|
-
_warn_parentheses_missing=None,
|
640
|
-
*,
|
641
|
-
max_batch_size: int,
|
642
|
-
wait_ms: int,
|
643
|
-
) -> Callable[[Callable[..., Any]], _PartialFunction]:
|
644
|
-
"""Decorator for functions or class methods that should be batched.
|
645
|
-
|
646
|
-
**Usage**
|
647
|
-
|
648
|
-
```python notest
|
649
|
-
@app.function()
|
650
|
-
@modal.batched(max_batch_size=4, wait_ms=1000)
|
651
|
-
async def batched_multiply(xs: list[int], ys: list[int]) -> list[int]:
|
652
|
-
return [x * y for x, y in zip(xs, xs)]
|
653
|
-
|
654
|
-
# call batched_multiply with individual inputs
|
655
|
-
batched_multiply.remote.aio(2, 100)
|
656
|
-
```
|
657
|
-
|
658
|
-
See the [dynamic batching guide](https://modal.com/docs/guide/dynamic-batching) for more information.
|
659
|
-
"""
|
660
|
-
if _warn_parentheses_missing is not None:
|
661
|
-
raise InvalidError(
|
662
|
-
"Positional arguments are not allowed. Did you forget parentheses? Suggestion: `@batched()`."
|
663
|
-
)
|
664
|
-
if max_batch_size < 1:
|
665
|
-
raise InvalidError("max_batch_size must be a positive integer.")
|
666
|
-
if max_batch_size >= MAX_MAX_BATCH_SIZE:
|
667
|
-
raise InvalidError(f"max_batch_size must be less than {MAX_MAX_BATCH_SIZE}.")
|
668
|
-
if wait_ms < 0:
|
669
|
-
raise InvalidError("wait_ms must be a non-negative integer.")
|
670
|
-
if wait_ms >= MAX_BATCH_WAIT_MS:
|
671
|
-
raise InvalidError(f"wait_ms must be less than {MAX_BATCH_WAIT_MS}.")
|
672
|
-
|
673
|
-
def wrapper(raw_f: Callable[..., Any]) -> _PartialFunction:
|
674
|
-
if isinstance(raw_f, _Function):
|
675
|
-
raw_f = raw_f.get_raw_f()
|
676
|
-
raise InvalidError(
|
677
|
-
f"Applying decorators for {raw_f} in the wrong order!\nUsage:\n\n"
|
678
|
-
"@app.function()\n@modal.batched()\ndef batched_function():\n ..."
|
679
|
-
)
|
680
|
-
return _PartialFunction(
|
681
|
-
raw_f,
|
682
|
-
_PartialFunctionFlags.FUNCTION | _PartialFunctionFlags.BATCHED,
|
683
|
-
batch_max_size=max_batch_size,
|
684
|
-
batch_wait_ms=wait_ms,
|
685
|
-
)
|
686
|
-
|
687
|
-
return wrapper
|
688
|
-
|
689
|
-
|
690
|
-
method = synchronize_api(_method)
|
691
|
-
web_endpoint = synchronize_api(_web_endpoint)
|
692
|
-
asgi_app = synchronize_api(_asgi_app)
|
693
|
-
wsgi_app = synchronize_api(_wsgi_app)
|
694
|
-
web_server = synchronize_api(_web_server)
|
695
|
-
build = synchronize_api(_build)
|
696
|
-
enter = synchronize_api(_enter)
|
697
|
-
exit = synchronize_api(_exit)
|
698
|
-
batched = synchronize_api(_batched)
|
17
|
+
# The only reason these are wrapped is to get translated type stubs, they
|
18
|
+
# don't actually run any async code as of 2025-02-04:
|
19
|
+
PartialFunction = synchronize_api(_PartialFunction, target_module=__name__)
|
20
|
+
method = synchronize_api(_method, target_module=__name__)
|
21
|
+
web_endpoint = synchronize_api(_web_endpoint, target_module=__name__)
|
22
|
+
asgi_app = synchronize_api(_asgi_app, target_module=__name__)
|
23
|
+
wsgi_app = synchronize_api(_wsgi_app, target_module=__name__)
|
24
|
+
web_server = synchronize_api(_web_server, target_module=__name__)
|
25
|
+
build = synchronize_api(_build, target_module=__name__)
|
26
|
+
enter = synchronize_api(_enter, target_module=__name__)
|
27
|
+
exit = synchronize_api(_exit, target_module=__name__)
|
28
|
+
batched = synchronize_api(_batched, target_module=__name__)
|