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.
Files changed (220) hide show
  1. modal/__init__.py +13 -9
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +402 -398
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -60
  11. modal/_resources.py +26 -7
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1025 -0
  15. modal/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +3 -3
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +196 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +943 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
  128. modal-0.72.13.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
  220. {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.function_utils import method_has_params
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, deprecation_warning
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
- ENTER_PRE_CHECKPOINT: int = 4
27
- ENTER_POST_CHECKPOINT: int = 8
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 @web_endpoint"""
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[..., Any]
50
+ raw_f: Callable[P, ReturnType]
35
51
  flags: _PartialFunctionFlags
36
52
  webhook_config: Optional[api_pb2.WebhookConfig]
37
- is_generator: Optional[bool]
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[..., Any],
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
- self.is_generator = is_generator
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: # Cls().fun
59
- function = getattr(obj, "_modal_functions")[k]
60
- else: # Cls.fun
61
- function = getattr(objtype, "_modal_functions")[k]
62
- return function
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 _find_partial_methods_for_cls(user_cls: Type, flags: _PartialFunctionFlags) -> Dict[str, _PartialFunction]:
86
- """Grabs all method on a user class"""
87
- partial_functions: Dict[str, PartialFunction] = {}
88
- for parent_cls in user_cls.mro():
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
- partial_function = synchronizer._translate_in(v) # TODO: remove need for?
93
- if partial_function.flags & flags:
94
- partial_functions[k] = partial_function
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 _find_callables_for_cls(user_cls: Type, flags: _PartialFunctionFlags) -> Dict[str, Callable]:
100
- """Grabs all method on a user class, and returns callables. Includes legacy methods."""
101
- functions: Dict[str, Callable] = {}
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
- return functions
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
- def _find_callables_for_obj(user_obj: Any, flags: _PartialFunctionFlags) -> Dict[str, Callable]:
135
- """Grabs all methods for an object, and binds them to the class"""
136
- user_cls: Type = type(user_obj)
137
- return {k: meth.__get__(user_obj) for k, meth in _find_callables_for_cls(user_cls, flags).items()}
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, # An optional number of containers to always keep warm.
147
- ) -> Callable[[Callable[..., Any]], _PartialFunction]:
148
- """Decorator for methods that should be transformed into a Modal Function registered against this class's app.
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`. Suggestion: remove the `@method` decorator."
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) -> List[api_pb2.CustomDomainConfig]:
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: List[api_pb2.CustomDomainConfig] = []
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
- wait_for_response: bool = True, # Whether requests should wait for and return the function response.
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
- ) -> Callable[[Callable[..., Any]], _PartialFunction]:
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
- _response_mode = api_pb2.WEBHOOK_ASYNC_MODE_TRIGGER
226
- else:
227
- _response_mode = api_pb2.WEBHOOK_ASYNC_MODE_AUTO # the default
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=_response_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
- _response_mode = api_pb2.WEBHOOK_ASYNC_MODE_TRIGGER
284
- else:
285
- _response_mode = api_pb2.WEBHOOK_ASYNC_MODE_AUTO # the default
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=_response_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) which is compatible with ASGI and supports
312
- additional functionality such as web sockets. Modal supports ASGI via [`asgi_app`](/docs/reference/modal.asgi_app).
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
- _response_mode = api_pb2.WEBHOOK_ASYNC_MODE_TRIGGER
338
- else:
339
- _response_mode = api_pb2.WEBHOOK_ASYNC_MODE_AUTO # the default
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=_response_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.ENTER_PRE_CHECKPOINT
579
+ flag = _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
465
580
  else:
466
- flag = _PartialFunctionFlags.ENTER_POST_CHECKPOINT
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[Type[BaseException]], Optional[BaseException], Any], None],
597
+ Callable[[Any, Optional[type[BaseException]], Optional[BaseException], Any], Any],
481
598
  # Forward-looking unparameterized method
482
- Callable[[Any], None],
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
- if method_has_params(f):
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)