modal 0.62.16__py3-none-any.whl → 0.72.11__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 +17 -13
  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 +420 -937
  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 -59
  11. modal/_resources.py +51 -0
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1036 -0
  15. modal/_runtime/execution_context.py +89 -0
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +134 -9
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +52 -16
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +479 -100
  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 +460 -171
  29. modal/_utils/grpc_testing.py +47 -31
  30. modal/_utils/grpc_utils.py +62 -109
  31. modal/_utils/hash_utils.py +61 -19
  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 +5 -7
  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 +14 -12
  43. modal/app.py +1003 -314
  44. modal/app.pyi +540 -264
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +63 -53
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +205 -45
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +62 -14
  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 +64 -58
  55. modal/cli/launch.py +32 -18
  56. modal/cli/network_file_system.py +64 -83
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +35 -10
  59. modal/cli/programs/vscode.py +60 -10
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +234 -131
  62. modal/cli/secret.py +8 -7
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +79 -10
  65. modal/cli/volume.py +110 -109
  66. modal/client.py +250 -144
  67. modal/client.pyi +157 -118
  68. modal/cloud_bucket_mount.py +108 -34
  69. modal/cloud_bucket_mount.pyi +32 -38
  70. modal/cls.py +535 -148
  71. modal/cls.pyi +190 -146
  72. modal/config.py +41 -19
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +111 -65
  76. modal/dict.pyi +136 -131
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +34 -43
  80. modal/experimental.py +61 -2
  81. modal/extensions/ipython.py +5 -5
  82. modal/file_io.py +537 -0
  83. modal/file_io.pyi +235 -0
  84. modal/file_pattern_matcher.py +197 -0
  85. modal/functions.py +906 -911
  86. modal/functions.pyi +466 -430
  87. modal/gpu.py +57 -44
  88. modal/image.py +1089 -479
  89. modal/image.pyi +584 -228
  90. modal/io_streams.py +434 -0
  91. modal/io_streams.pyi +122 -0
  92. modal/mount.py +314 -101
  93. modal/mount.pyi +241 -235
  94. modal/network_file_system.py +92 -92
  95. modal/network_file_system.pyi +152 -110
  96. modal/object.py +67 -36
  97. modal/object.pyi +166 -143
  98. modal/output.py +63 -0
  99. modal/parallel_map.py +434 -0
  100. modal/parallel_map.pyi +75 -0
  101. modal/partial_function.py +282 -117
  102. modal/partial_function.pyi +222 -129
  103. modal/proxy.py +15 -12
  104. modal/proxy.pyi +3 -8
  105. modal/queue.py +182 -65
  106. modal/queue.pyi +218 -118
  107. modal/requirements/2024.04.txt +29 -0
  108. modal/requirements/2024.10.txt +16 -0
  109. modal/requirements/README.md +21 -0
  110. modal/requirements/base-images.json +22 -0
  111. modal/retries.py +48 -7
  112. modal/runner.py +459 -156
  113. modal/runner.pyi +135 -71
  114. modal/running_app.py +38 -0
  115. modal/sandbox.py +514 -236
  116. modal/sandbox.pyi +397 -169
  117. modal/schedule.py +4 -4
  118. modal/scheduler_placement.py +20 -3
  119. modal/secret.py +56 -31
  120. modal/secret.pyi +62 -42
  121. modal/serving.py +51 -56
  122. modal/serving.pyi +44 -36
  123. modal/stream_type.py +15 -0
  124. modal/token_flow.py +5 -3
  125. modal/token_flow.pyi +37 -32
  126. modal/volume.py +285 -157
  127. modal/volume.pyi +249 -184
  128. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/METADATA +7 -7
  129. modal-0.72.11.dist-info/RECORD +174 -0
  130. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/top_level.txt +0 -1
  131. modal_docs/gen_reference_docs.py +3 -1
  132. modal_docs/mdmd/mdmd.py +0 -1
  133. modal_docs/mdmd/signatures.py +5 -2
  134. modal_global_objects/images/base_images.py +28 -0
  135. modal_global_objects/mounts/python_standalone.py +2 -2
  136. modal_proto/__init__.py +1 -1
  137. modal_proto/api.proto +1288 -533
  138. modal_proto/api_grpc.py +856 -456
  139. modal_proto/api_pb2.py +2165 -1157
  140. modal_proto/api_pb2.pyi +8859 -0
  141. modal_proto/api_pb2_grpc.py +1674 -855
  142. modal_proto/api_pb2_grpc.pyi +1416 -0
  143. modal_proto/modal_api_grpc.py +149 -0
  144. modal_proto/modal_options_grpc.py +3 -0
  145. modal_proto/options_pb2.pyi +20 -0
  146. modal_proto/options_pb2_grpc.pyi +7 -0
  147. modal_proto/py.typed +0 -0
  148. modal_version/__init__.py +1 -1
  149. modal_version/_version_generated.py +2 -2
  150. modal/_asgi.py +0 -370
  151. modal/_container_entrypoint.pyi +0 -378
  152. modal/_container_exec.py +0 -128
  153. modal/_sandbox_shell.py +0 -49
  154. modal/shared_volume.py +0 -23
  155. modal/shared_volume.pyi +0 -24
  156. modal/stub.py +0 -783
  157. modal/stub.pyi +0 -332
  158. modal-0.62.16.dist-info/RECORD +0 -198
  159. modal_global_objects/images/conda.py +0 -15
  160. modal_global_objects/images/debian_slim.py +0 -15
  161. modal_global_objects/images/micromamba.py +0 -15
  162. test/__init__.py +0 -1
  163. test/aio_test.py +0 -12
  164. test/async_utils_test.py +0 -262
  165. test/blob_test.py +0 -67
  166. test/cli_imports_test.py +0 -149
  167. test/cli_test.py +0 -659
  168. test/client_test.py +0 -194
  169. test/cls_test.py +0 -630
  170. test/config_test.py +0 -137
  171. test/conftest.py +0 -1420
  172. test/container_app_test.py +0 -32
  173. test/container_test.py +0 -1389
  174. test/cpu_test.py +0 -23
  175. test/decorator_test.py +0 -85
  176. test/deprecation_test.py +0 -34
  177. test/dict_test.py +0 -33
  178. test/e2e_test.py +0 -68
  179. test/error_test.py +0 -7
  180. test/function_serialization_test.py +0 -32
  181. test/function_test.py +0 -653
  182. test/function_utils_test.py +0 -101
  183. test/gpu_test.py +0 -159
  184. test/grpc_utils_test.py +0 -141
  185. test/helpers.py +0 -42
  186. test/image_test.py +0 -669
  187. test/live_reload_test.py +0 -80
  188. test/lookup_test.py +0 -70
  189. test/mdmd_test.py +0 -329
  190. test/mount_test.py +0 -162
  191. test/mounted_files_test.py +0 -329
  192. test/network_file_system_test.py +0 -181
  193. test/notebook_test.py +0 -66
  194. test/object_test.py +0 -41
  195. test/package_utils_test.py +0 -25
  196. test/queue_test.py +0 -97
  197. test/resolver_test.py +0 -58
  198. test/retries_test.py +0 -67
  199. test/runner_test.py +0 -85
  200. test/sandbox_test.py +0 -191
  201. test/schedule_test.py +0 -15
  202. test/scheduler_placement_test.py +0 -29
  203. test/secret_test.py +0 -78
  204. test/serialization_test.py +0 -42
  205. test/stub_composition_test.py +0 -10
  206. test/stub_test.py +0 -360
  207. test/test_asgi_wrapper.py +0 -234
  208. test/token_flow_test.py +0 -18
  209. test/traceback_test.py +0 -135
  210. test/tunnel_test.py +0 -29
  211. test/utils_test.py +0 -88
  212. test/version_test.py +0 -14
  213. test/volume_test.py +0 -341
  214. test/watcher_test.py +0 -30
  215. test/webhook_test.py +0 -146
  216. /modal/{requirements.312.txt → requirements/2023.12.312.txt} +0 -0
  217. /modal/{requirements.txt → requirements/2023.12.txt} +0 -0
  218. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/LICENSE +0 -0
  219. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/WHEEL +0 -0
  220. {modal-0.62.16.dist-info → modal-0.72.11.dist-info}/entry_points.txt +0 -0
modal/stub.py DELETED
@@ -1,783 +0,0 @@
1
- # Copyright Modal Labs 2022
2
- import inspect
3
- import os
4
- import typing
5
- from pathlib import PurePosixPath
6
- from typing import Any, AsyncGenerator, Callable, ClassVar, Dict, List, Optional, Sequence, Tuple, Union
7
-
8
- from synchronicity.async_wrap import asynccontextmanager
9
-
10
- from modal_proto import api_pb2
11
-
12
- from ._ipython import is_notebook
13
- from ._output import OutputManager
14
- from ._resolver import Resolver
15
- from ._utils.async_utils import synchronize_api
16
- from ._utils.function_utils import FunctionInfo
17
- from ._utils.mount_utils import validate_volumes
18
- from .app import _ContainerApp, _LocalApp
19
- from .client import _Client
20
- from .cls import _Cls
21
- from .config import logger
22
- from .exception import InvalidError, deprecation_error, deprecation_warning
23
- from .functions import _Function
24
- from .gpu import GPU_T
25
- from .image import _Image
26
- from .mount import _Mount
27
- from .network_file_system import _NetworkFileSystem
28
- from .object import _Object
29
- from .partial_function import PartialFunction, _find_callables_for_cls, _PartialFunction, _PartialFunctionFlags
30
- from .proxy import _Proxy
31
- from .retries import Retries
32
- from .runner import _run_stub
33
- from .sandbox import _Sandbox
34
- from .schedule import Schedule
35
- from .scheduler_placement import SchedulerPlacement
36
- from .secret import _Secret
37
- from .volume import _Volume
38
-
39
- _default_image: _Image = _Image.debian_slim()
40
-
41
-
42
- class _LocalEntrypoint:
43
- _info: FunctionInfo
44
- _stub: "_Stub"
45
-
46
- def __init__(self, info, stub):
47
- self._info = info # type: ignore
48
- self._stub = stub
49
-
50
- def __call__(self, *args, **kwargs):
51
- return self._info.raw_f(*args, **kwargs)
52
-
53
- @property
54
- def info(self) -> FunctionInfo:
55
- return self._info
56
-
57
- @property
58
- def stub(self) -> "_Stub":
59
- return self._stub
60
-
61
-
62
- LocalEntrypoint = synchronize_api(_LocalEntrypoint)
63
-
64
-
65
- def check_sequence(items: typing.Sequence[typing.Any], item_type: typing.Type[typing.Any], error_msg: str):
66
- if not isinstance(items, (list, tuple)):
67
- raise InvalidError(error_msg)
68
- if not all(isinstance(v, item_type) for v in items):
69
- raise InvalidError(error_msg)
70
-
71
-
72
- CLS_T = typing.TypeVar("CLS_T", bound=typing.Type)
73
-
74
-
75
- class _Stub:
76
- """A `Stub` is a description of how to create a Modal application.
77
-
78
- The stub object principally describes Modal objects (`Function`, `Image`,
79
- `Secret`, etc.) associated with the application. It has three responsibilities:
80
-
81
- * Syncing of identities across processes (your local Python interpreter and
82
- every Modal worker active in your application).
83
- * Making Objects stay alive and not be garbage collected for as long as the
84
- app lives (see App lifetime below).
85
- * Manage log collection for everything that happens inside your code.
86
-
87
- **Registering functions with an app**
88
-
89
- The most common way to explicitly register an Object with an app is through the
90
- `@stub.function()` decorator. It both registers the annotated function itself and
91
- other passed objects, like schedules and secrets, with the app:
92
-
93
- ```python
94
- import modal
95
-
96
- stub = modal.Stub()
97
-
98
- @stub.function(
99
- secrets=[modal.Secret.from_name("some_secret")],
100
- schedule=modal.Period(days=1),
101
- )
102
- def foo():
103
- pass
104
- ```
105
-
106
- In this example, the secret and schedule are registered with the app.
107
- """
108
-
109
- _name: Optional[str]
110
- _description: Optional[str]
111
- _indexed_objects: Dict[str, _Object]
112
- _function_mounts: Dict[str, _Mount]
113
- _mounts: Sequence[_Mount]
114
- _secrets: Sequence[_Secret]
115
- _volumes: Dict[Union[str, PurePosixPath], _Volume]
116
- _web_endpoints: List[str] # Used by the CLI
117
- _local_entrypoints: Dict[str, _LocalEntrypoint]
118
- _container_app: Optional[_ContainerApp]
119
- _local_app: Optional[_LocalApp]
120
- _all_stubs: ClassVar[Dict[Optional[str], List["_Stub"]]] = {}
121
-
122
- def __init__(
123
- self,
124
- name: Optional[str] = None,
125
- *,
126
- image: Optional[_Image] = None, # default image for all functions (default is `modal.Image.debian_slim()`)
127
- mounts: Sequence[_Mount] = [], # default mounts for all functions
128
- secrets: Sequence[_Secret] = [], # default secrets for all functions
129
- volumes: Dict[Union[str, PurePosixPath], _Volume] = {}, # default volumes for all functions
130
- **indexed_objects: _Object, # any Modal Object dependencies (Dict, Queue, etc.)
131
- ) -> None:
132
- """Construct a new app stub, optionally with default image, mounts, secrets
133
-
134
- Any "indexed_objects" objects are loaded as part of running or deploying the app,
135
- and are accessible by name on the running container app, e.g.:
136
- ```python
137
- stub = modal.Stub(key_value_store=modal.Dict.new())
138
-
139
- @stub.function()
140
- def store_something(key: str, value: str):
141
- stub.app.key_value_store.put(key, value)
142
- ```
143
- """
144
-
145
- self._name = name
146
- self._description = name
147
-
148
- check_sequence(mounts, _Mount, "mounts has to be a list or tuple of Mount objects")
149
- check_sequence(secrets, _Secret, "secrets has to be a list or tuple of Secret objects")
150
- validate_volumes(volumes)
151
-
152
- if image is not None and not isinstance(image, _Image):
153
- raise InvalidError("image has to be a modal Image or AioImage object")
154
-
155
- if indexed_objects:
156
- deprecation_warning(
157
- (2023, 12, 13),
158
- "Passing **kwargs to a stub is deprecated. In most cases, you can just define the objects in global scope.",
159
- )
160
-
161
- for k, v in indexed_objects.items():
162
- self._validate_blueprint_value(k, v)
163
-
164
- self._indexed_objects = indexed_objects
165
- if image is not None:
166
- self._indexed_objects["image"] = image # backward compatibility since "image" used to be on the blueprint
167
-
168
- self._mounts = mounts
169
-
170
- self._secrets = secrets
171
- self._volumes = volumes
172
- self._local_entrypoints = {}
173
- self._web_endpoints = []
174
- self._local_app = None # when this is the launcher process
175
- self._container_app = None # when this is inside a container
176
-
177
- # Register this stub. This is used to look up the stub in the container, when we can't get it from the function
178
- _Stub._all_stubs.setdefault(self._name, []).append(self)
179
-
180
- @property
181
- def name(self) -> Optional[str]:
182
- """The user-provided name of the Stub."""
183
- return self._name
184
-
185
- @property
186
- def is_interactive(self) -> bool:
187
- """Whether the current app for the stub is running in interactive mode."""
188
- # return self._name
189
- if self._local_app:
190
- return self._local_app.is_interactive
191
- else:
192
- return False
193
-
194
- @property
195
- def app_id(self) -> Optional[str]:
196
- """Return the app_id, if the stub is running."""
197
- if self._container_app:
198
- return self._container_app._app_id
199
- elif self._local_app:
200
- return self._local_app._app_id
201
- else:
202
- return None
203
-
204
- @property
205
- def description(self) -> Optional[str]:
206
- """The Stub's `name`, if available, or a fallback descriptive identifier."""
207
- return self._description
208
-
209
- def set_description(self, description: str):
210
- self._description = description
211
-
212
- def _validate_blueprint_value(self, key: str, value: Any):
213
- if not isinstance(value, _Object):
214
- raise InvalidError(f"Stub attribute {key} with value {value} is not a valid Modal object")
215
-
216
- def _add_object(self, tag, obj):
217
- if self._container_app:
218
- # If this is inside a container, then objects can be defined after app initialization.
219
- # So we may have to initialize objects once they get bound to the stub.
220
- if self._container_app._has_object(tag):
221
- self._container_app._hydrate_object(obj, tag)
222
-
223
- self._indexed_objects[tag] = obj
224
-
225
- def __getitem__(self, tag: str):
226
- """Stub assignments of the form `stub.x` or `stub["x"]` are deprecated!
227
-
228
- The only use cases for these assignments is in conjunction with `.new()`, which is now
229
- in itself deprecated. If you are constructing objects with `.from_name(...)`, there is no
230
- need to assign those objects to the stub. Example:
231
-
232
- ```python
233
- d = modal.Dict.from_name("my-dict", create_if_missing=True)
234
-
235
- @stub.function()
236
- def f(x, y):
237
- d[x] = y # Refer to d in global scope
238
- ```
239
- """
240
- deprecation_warning((2024, 3, 25), _Stub.__getitem__.__doc__)
241
- return self._indexed_objects[tag]
242
-
243
- def __setitem__(self, tag: str, obj: _Object):
244
- deprecation_warning((2024, 3, 25), _Stub.__getitem__.__doc__)
245
- self._validate_blueprint_value(tag, obj)
246
- # Deprecated ?
247
- self._add_object(tag, obj)
248
-
249
- def __getattr__(self, tag: str) -> _Object:
250
- # TODO(erikbern): remove this method later
251
- assert isinstance(tag, str)
252
- if tag.startswith("__"):
253
- # Hacky way to avoid certain issues, e.g. pickle will try to look this up
254
- raise AttributeError(f"Stub has no member {tag}")
255
- if tag not in self._indexed_objects:
256
- # Primarily to make hasattr work
257
- raise AttributeError(f"Stub has no member {tag}")
258
- obj: _Object = self._indexed_objects[tag]
259
- deprecation_warning((2024, 3, 25), _Stub.__getitem__.__doc__)
260
- return obj
261
-
262
- def __setattr__(self, tag: str, obj: _Object):
263
- # TODO(erikbern): remove this method later
264
- # Note that only attributes defined in __annotations__ are set on the object itself,
265
- # everything else is registered on the indexed_objects
266
- if tag in self.__annotations__:
267
- object.__setattr__(self, tag, obj)
268
- elif tag == "image":
269
- self._indexed_objects["image"] = obj
270
- else:
271
- self._validate_blueprint_value(tag, obj)
272
- deprecation_warning((2024, 3, 25), _Stub.__getitem__.__doc__)
273
- self._add_object(tag, obj)
274
-
275
- @property
276
- def image(self) -> _Image:
277
- # Exists to get the type inference working for `stub.image`
278
- # Will also keep this one after we remove [get/set][item/attr]
279
- return self._indexed_objects["image"]
280
-
281
- @image.setter
282
- def image(self, value):
283
- self._indexed_objects["image"] = value
284
-
285
- def get_objects(self) -> List[Tuple[str, _Object]]:
286
- """Used by the container app to initialize objects."""
287
- return list(self._indexed_objects.items())
288
-
289
- def _uncreate_all_objects(self):
290
- # TODO(erikbern): this doesn't unhydrate objects that aren't tagged
291
- for obj in self._indexed_objects.values():
292
- obj._unhydrate()
293
-
294
- def is_inside(self, image: Optional[_Image] = None):
295
- """Deprecated: use `Image.imports()` instead! Usage:
296
- ```
297
- my_image = modal.Image.debian_slim().pip_install("torch")
298
- with my_image.imports():
299
- import torch
300
- ```
301
- """
302
- deprecation_error((2023, 11, 8), _Stub.is_inside.__doc__)
303
-
304
- @asynccontextmanager
305
- async def _set_local_app(self, app: _LocalApp) -> AsyncGenerator[None, None]:
306
- self._local_app = app
307
- try:
308
- yield
309
- finally:
310
- self._local_app = None
311
-
312
- @asynccontextmanager
313
- async def run(
314
- self,
315
- client: Optional[_Client] = None,
316
- stdout=None,
317
- show_progress: bool = True,
318
- detach: bool = False,
319
- output_mgr: Optional[OutputManager] = None,
320
- ) -> AsyncGenerator["_Stub", None]:
321
- """Context manager that runs an app on Modal.
322
-
323
- Use this as the main entry point for your Modal application. All calls
324
- to Modal functions should be made within the scope of this context
325
- manager, and they will correspond to the current app.
326
-
327
- Note that this method used to return a separate "App" object. This is
328
- no longer useful since you can use the stub itself for access to all
329
- objects. For backwards compatibility reasons, it returns the same stub.
330
- """
331
- # TODO(erikbern): deprecate this one too?
332
- async with _run_stub(self, client, stdout, show_progress, detach, output_mgr):
333
- yield self
334
-
335
- def _get_default_image(self):
336
- if "image" in self._indexed_objects:
337
- return self._indexed_objects["image"]
338
- else:
339
- return _default_image
340
-
341
- def _get_watch_mounts(self):
342
- all_mounts = [
343
- *self._mounts,
344
- ]
345
- for function in self.registered_functions.values():
346
- all_mounts.extend(function._all_mounts)
347
-
348
- return [m for m in all_mounts if m.is_local()]
349
-
350
- def _add_function(self, function: _Function):
351
- if function.tag in self._indexed_objects:
352
- old_function = self._indexed_objects[function.tag]
353
- if isinstance(old_function, _Function):
354
- if not is_notebook():
355
- logger.warning(
356
- f"Warning: Tag '{function.tag}' collision!"
357
- f" Overriding existing function [{old_function._info.module_name}].{old_function._info.function_name}"
358
- f" with new function [{function._info.module_name}].{function._info.function_name}"
359
- )
360
- else:
361
- logger.warning(f"Warning: tag {function.tag} exists but is overridden by function")
362
-
363
- self._add_object(function.tag, function)
364
-
365
- @property
366
- def registered_functions(self) -> Dict[str, _Function]:
367
- """All modal.Function objects registered on the stub."""
368
- return {tag: obj for tag, obj in self._indexed_objects.items() if isinstance(obj, _Function)}
369
-
370
- @property
371
- def registered_classes(self) -> Dict[str, _Function]:
372
- """All modal.Cls objects registered on the stub."""
373
- return {tag: obj for tag, obj in self._indexed_objects.items() if isinstance(obj, _Cls)}
374
-
375
- @property
376
- def registered_entrypoints(self) -> Dict[str, _LocalEntrypoint]:
377
- """All local CLI entrypoints registered on the stub."""
378
- return self._local_entrypoints
379
-
380
- @property
381
- def indexed_objects(self) -> Dict[str, _Object]:
382
- return self._indexed_objects
383
-
384
- @property
385
- def registered_web_endpoints(self) -> List[str]:
386
- """Names of web endpoint (ie. webhook) functions registered on the stub."""
387
- return self._web_endpoints
388
-
389
- def local_entrypoint(
390
- self, _warn_parentheses_missing=None, *, name: Optional[str] = None
391
- ) -> Callable[[Callable[..., Any]], None]:
392
- """Decorate a function to be used as a CLI entrypoint for a Modal App.
393
-
394
- These functions can be used to define code that runs locally to set up the app,
395
- and act as an entrypoint to start Modal functions from. Note that regular
396
- Modal functions can also be used as CLI entrypoints, but unlike `local_entrypoint`,
397
- those functions are executed remotely directly.
398
-
399
- **Example**
400
-
401
- ```python
402
- @stub.local_entrypoint()
403
- def main():
404
- some_modal_function.remote()
405
- ```
406
-
407
- You can call the function using `modal run` directly from the CLI:
408
-
409
- ```shell
410
- modal run stub_module.py
411
- ```
412
-
413
- Note that an explicit [`stub.run()`](/docs/reference/modal.Stub#run) is not needed, as an
414
- [app](/docs/guide/apps) is automatically created for you.
415
-
416
- **Multiple Entrypoints**
417
-
418
- If you have multiple `local_entrypoint` functions, you can qualify the name of your stub and function:
419
-
420
- ```shell
421
- modal run stub_module.py::stub.some_other_function
422
- ```
423
-
424
- **Parsing Arguments**
425
-
426
- If your entrypoint function take arguments with primitive types, `modal run` automatically parses them as
427
- CLI options. For example, the following function can be called with `modal run stub_module.py --foo 1 --bar "hello"`:
428
-
429
- ```python
430
- @stub.local_entrypoint()
431
- def main(foo: int, bar: str):
432
- some_modal_function.call(foo, bar)
433
- ```
434
-
435
- Currently, `str`, `int`, `float`, `bool`, and `datetime.datetime` are supported. Use `modal run stub_module.py --help` for more
436
- information on usage.
437
-
438
- """
439
- if _warn_parentheses_missing:
440
- raise InvalidError("Did you forget parentheses? Suggestion: `@stub.local_entrypoint()`.")
441
- if name is not None and not isinstance(name, str):
442
- raise InvalidError("Invalid value for `name`: Must be string.")
443
-
444
- def wrapped(raw_f: Callable[..., Any]) -> None:
445
- info = FunctionInfo(raw_f)
446
- tag = name if name is not None else raw_f.__qualname__
447
- if tag in self._local_entrypoints:
448
- # TODO: get rid of this limitation.
449
- raise InvalidError(f"Duplicate local entrypoint name: {tag}. Local entrypoint names must be unique.")
450
- entrypoint = self._local_entrypoints[tag] = _LocalEntrypoint(info, self)
451
- return entrypoint
452
-
453
- return wrapped
454
-
455
- def function(
456
- self,
457
- _warn_parentheses_missing=None,
458
- *,
459
- image: Optional[_Image] = None, # The image to run as the container for the function
460
- schedule: Optional[Schedule] = None, # An optional Modal Schedule for the function
461
- secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
462
- gpu: GPU_T = None, # GPU specification as string ("any", "T4", "A10G", ...) or object (`modal.GPU.A100()`, ...)
463
- serialized: bool = False, # Whether to send the function over using cloudpickle.
464
- mounts: Sequence[_Mount] = (), # Modal Mounts added to the container
465
- network_file_systems: Dict[
466
- Union[str, PurePosixPath], _NetworkFileSystem
467
- ] = {}, # Mountpoints for Modal NetworkFileSystems
468
- volumes: Dict[Union[str, PurePosixPath], _Volume] = {}, # Mountpoints for Modal Volumes
469
- allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
470
- cpu: Optional[float] = None, # How many CPU cores to request. This is a soft limit.
471
- memory: Optional[int] = None, # How much memory to request, in MiB. This is a soft limit.
472
- proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
473
- retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
474
- concurrency_limit: Optional[
475
- int
476
- ] = None, # An optional maximum number of concurrent containers running the function (use keep_warm for minimum).
477
- allow_concurrent_inputs: Optional[int] = None, # Number of inputs the container may fetch to run concurrently.
478
- container_idle_timeout: Optional[int] = None, # Timeout for idle containers waiting for inputs to shut down.
479
- timeout: Optional[int] = None, # Maximum execution time of the function in seconds.
480
- keep_warm: Optional[
481
- int
482
- ] = None, # An optional minimum number of containers to always keep warm (use concurrency_limit for maximum).
483
- name: Optional[str] = None, # Sets the Modal name of the function within the stub
484
- is_generator: Optional[
485
- bool
486
- ] = None, # Set this to True if it's a non-generator function returning a [sync/async] generator object
487
- cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
488
- enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
489
- checkpointing_enabled: Optional[bool] = None, # Deprecated
490
- block_network: bool = False, # Whether to block network access
491
- max_inputs: Optional[
492
- int
493
- ] = None, # Maximum number of inputs a container should handle before shutting down. With `max_inputs = 1`, containers will be single-use.
494
- # The next group of parameters are deprecated; do not use in any new code
495
- interactive: bool = False, # Deprecated: use the `modal.interact()` hook instead
496
- secret: Optional[_Secret] = None, # Deprecated: use `secrets`
497
- # Parameters below here are experimental. Use with caution!
498
- _allow_background_volume_commits: bool = False, # Experimental flag
499
- _experimental_boost: bool = False, # Experimental flag for lower latency function execution (alpha).
500
- _experimental_scheduler: bool = False, # Experimental flag for more fine-grained scheduling (alpha).
501
- _experimental_scheduler_placement: Optional[
502
- SchedulerPlacement
503
- ] = None, # Experimental controls over fine-grained scheduling (alpha).
504
- ) -> Callable[..., _Function]:
505
- """Decorator to register a new Modal function with this stub."""
506
- if isinstance(_warn_parentheses_missing, _Image):
507
- # Handle edge case where maybe (?) some users passed image as a positional arg
508
- raise InvalidError("`image` needs to be a keyword argument: `@stub.function(image=image)`.")
509
- if _warn_parentheses_missing:
510
- raise InvalidError("Did you forget parentheses? Suggestion: `@stub.function()`.")
511
-
512
- if interactive:
513
- deprecation_error(
514
- (2024, 2, 29), "interactive=True has been deprecated. Set MODAL_INTERACTIVE_FUNCTIONS=1 instead."
515
- )
516
-
517
- if image is None:
518
- image = self._get_default_image()
519
-
520
- secrets = [*self._secrets, *secrets]
521
-
522
- def wrapped(
523
- f: Union[_PartialFunction, Callable[..., Any]],
524
- _cls: Optional[type] = None, # Used for methods only
525
- ) -> _Function:
526
- nonlocal keep_warm, is_generator
527
-
528
- if isinstance(f, _PartialFunction):
529
- f.wrapped = True
530
- info = FunctionInfo(f.raw_f, serialized=serialized, name_override=name, cls=_cls)
531
- raw_f = f.raw_f
532
- webhook_config = f.webhook_config
533
- is_generator = f.is_generator
534
- keep_warm = f.keep_warm or keep_warm
535
-
536
- if webhook_config:
537
- if interactive:
538
- raise InvalidError("interactive=True is not supported with web endpoint functions")
539
- self._web_endpoints.append(info.get_tag())
540
- else:
541
- info = FunctionInfo(f, serialized=serialized, name_override=name, cls=_cls)
542
- webhook_config = None
543
- raw_f = f
544
-
545
- if not _cls and not info.is_serialized() and "." in info.function_name: # This is a method
546
- raise InvalidError(
547
- "`stub.function` on methods is not allowed. See https://modal.com/docs/guide/lifecycle-functions instead"
548
- )
549
-
550
- if is_generator is None:
551
- is_generator = inspect.isgeneratorfunction(raw_f) or inspect.isasyncgenfunction(raw_f)
552
-
553
- function = _Function.from_args(
554
- info,
555
- stub=self,
556
- image=image,
557
- secret=secret,
558
- secrets=secrets,
559
- schedule=schedule,
560
- is_generator=is_generator,
561
- gpu=gpu,
562
- mounts=[*self._mounts, *mounts],
563
- network_file_systems=network_file_systems,
564
- allow_cross_region_volumes=allow_cross_region_volumes,
565
- volumes={**self._volumes, **volumes},
566
- memory=memory,
567
- proxy=proxy,
568
- retries=retries,
569
- concurrency_limit=concurrency_limit,
570
- allow_concurrent_inputs=allow_concurrent_inputs,
571
- container_idle_timeout=container_idle_timeout,
572
- timeout=timeout,
573
- cpu=cpu,
574
- keep_warm=keep_warm,
575
- cloud=cloud,
576
- webhook_config=webhook_config,
577
- enable_memory_snapshot=enable_memory_snapshot,
578
- checkpointing_enabled=checkpointing_enabled,
579
- allow_background_volume_commits=_allow_background_volume_commits,
580
- block_network=block_network,
581
- max_inputs=max_inputs,
582
- _experimental_boost=_experimental_boost,
583
- _experimental_scheduler=_experimental_scheduler,
584
- _experimental_scheduler_placement=_experimental_scheduler_placement,
585
- )
586
-
587
- self._add_function(function)
588
- return function
589
-
590
- return wrapped
591
-
592
- def cls(
593
- self,
594
- _warn_parentheses_missing=None,
595
- *,
596
- image: Optional[_Image] = None, # The image to run as the container for the function
597
- secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
598
- gpu: GPU_T = None, # GPU specification as string ("any", "T4", "A10G", ...) or object (`modal.GPU.A100()`, ...)
599
- serialized: bool = False, # Whether to send the function over using cloudpickle.
600
- mounts: Sequence[_Mount] = (),
601
- network_file_systems: Dict[
602
- Union[str, PurePosixPath], _NetworkFileSystem
603
- ] = {}, # Mountpoints for Modal NetworkFileSystems
604
- volumes: Dict[Union[str, PurePosixPath], _Volume] = {}, # Mountpoints for Modal Volumes
605
- allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
606
- cpu: Optional[float] = None, # How many CPU cores to request. This is a soft limit.
607
- memory: Optional[int] = None, # How much memory to request, in MiB. This is a soft limit.
608
- proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
609
- retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
610
- concurrency_limit: Optional[int] = None, # Limit for max concurrent containers running the function.
611
- allow_concurrent_inputs: Optional[int] = None, # Number of inputs the container may fetch to run concurrently.
612
- container_idle_timeout: Optional[int] = None, # Timeout for idle containers waiting for inputs to shut down.
613
- timeout: Optional[int] = None, # Maximum execution time of the function in seconds.
614
- keep_warm: Optional[int] = None, # An optional number of containers to always keep warm.
615
- cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
616
- enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
617
- checkpointing_enabled: Optional[bool] = None, # Deprecated
618
- block_network: bool = False, # Whether to block network access
619
- _allow_background_volume_commits: bool = False,
620
- max_inputs: Optional[
621
- int
622
- ] = None, # Limits the number of inputs a container handles before shutting down. Use `max_inputs = 1` for single-use containers.
623
- # The next group of parameters are deprecated; do not use in any new code
624
- interactive: bool = False, # Deprecated: use the `modal.interact()` hook instead
625
- secret: Optional[_Secret] = None, # Deprecated: use `secrets`
626
- # Parameters below here are experimental. Use with caution!
627
- _experimental_boost: bool = False, # Experimental flag for lower latency function execution (alpha).
628
- _experimental_scheduler: bool = False, # Experimental flag for more fine-grained scheduling (alpha).
629
- _experimental_scheduler_placement: Optional[
630
- SchedulerPlacement
631
- ] = None, # Experimental controls over fine-grained scheduling (alpha).
632
- ) -> Callable[[CLS_T], _Cls]:
633
- if _warn_parentheses_missing:
634
- raise InvalidError("Did you forget parentheses? Suggestion: `@stub.cls()`.")
635
-
636
- decorator: Callable[[PartialFunction, type], _Function] = self.function(
637
- image=image,
638
- secret=secret,
639
- secrets=secrets,
640
- gpu=gpu,
641
- serialized=serialized,
642
- mounts=mounts,
643
- network_file_systems=network_file_systems,
644
- allow_cross_region_volumes=allow_cross_region_volumes,
645
- volumes=volumes,
646
- cpu=cpu,
647
- memory=memory,
648
- proxy=proxy,
649
- retries=retries,
650
- concurrency_limit=concurrency_limit,
651
- allow_concurrent_inputs=allow_concurrent_inputs,
652
- container_idle_timeout=container_idle_timeout,
653
- timeout=timeout,
654
- interactive=interactive,
655
- keep_warm=keep_warm,
656
- cloud=cloud,
657
- enable_memory_snapshot=enable_memory_snapshot,
658
- checkpointing_enabled=checkpointing_enabled,
659
- block_network=block_network,
660
- _allow_background_volume_commits=_allow_background_volume_commits,
661
- max_inputs=max_inputs,
662
- _experimental_boost=_experimental_boost,
663
- _experimental_scheduler=_experimental_scheduler,
664
- _experimental_scheduler_placement=_experimental_scheduler_placement,
665
- )
666
-
667
- def wrapper(user_cls: CLS_T) -> _Cls:
668
- cls: _Cls = _Cls.from_local(user_cls, self, decorator)
669
-
670
- if (
671
- _find_callables_for_cls(user_cls, _PartialFunctionFlags.ENTER_PRE_CHECKPOINT)
672
- and not enable_memory_snapshot
673
- ):
674
- raise InvalidError("A class must have `enable_memory_snapshot=True` to use `snap=True` on its methods.")
675
-
676
- if len(cls._functions) > 1 and keep_warm is not None:
677
- deprecation_warning(
678
- (2023, 10, 20),
679
- "`@stub.cls(keep_warm=...)` is deprecated when there is more than 1 method."
680
- " Use `@method(keep_warm=...)` on each method instead!",
681
- )
682
-
683
- tag: str = user_cls.__name__
684
- self._add_object(tag, cls)
685
- return cls
686
-
687
- return wrapper
688
-
689
- async def spawn_sandbox(
690
- self,
691
- *entrypoint_args: str,
692
- image: Optional[_Image] = None, # The image to run as the container for the sandbox.
693
- mounts: Sequence[_Mount] = (), # Mounts to attach to the sandbox.
694
- secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
695
- network_file_systems: Dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
696
- timeout: Optional[int] = None, # Maximum execution time of the sandbox in seconds.
697
- workdir: Optional[str] = None, # Working directory of the sandbox.
698
- gpu: GPU_T = None,
699
- cloud: Optional[str] = None,
700
- cpu: Optional[float] = None, # How many CPU cores to request. This is a soft limit.
701
- memory: Optional[int] = None, # How much memory to request, in MiB. This is a soft limit.
702
- block_network: bool = False, # Whether to block network access
703
- volumes: Dict[Union[str, os.PathLike], _Volume] = {}, # Volumes to mount in the sandbox.
704
- _allow_background_volume_commits: bool = False,
705
- pty_info: Optional[api_pb2.PTYInfo] = None,
706
- ) -> _Sandbox:
707
- """Sandboxes are a way to run arbitrary commands in dynamically defined environments.
708
-
709
- This function returns a [SandboxHandle](/docs/reference/modal.Sandbox#modalsandboxsandbox), which can be used to interact with the running sandbox.
710
-
711
- Refer to the [docs](/docs/guide/sandbox) on how to spawn and use sandboxes.
712
- """
713
- from .sandbox import _Sandbox
714
- from .stub import _default_image
715
-
716
- if self._local_app:
717
- app_id = self._local_app.app_id
718
- environment_name = self._local_app._environment_name
719
- client = self._local_app.client
720
- elif self._container_app:
721
- app_id = self._container_app.app_id
722
- environment_name = self._container_app._environment_name
723
- client = self._container_app.client
724
- else:
725
- raise InvalidError("`stub.spawn_sandbox` requires a running app.")
726
-
727
- # TODO(erikbern): pulling a lot of app internals here, let's clean up shortly
728
- resolver = Resolver(client, environment_name=environment_name, app_id=app_id)
729
- obj = _Sandbox._new(
730
- entrypoint_args,
731
- image=image or _default_image,
732
- mounts=mounts,
733
- secrets=secrets,
734
- timeout=timeout,
735
- workdir=workdir,
736
- gpu=gpu,
737
- cloud=cloud,
738
- cpu=cpu,
739
- memory=memory,
740
- network_file_systems=network_file_systems,
741
- block_network=block_network,
742
- volumes=volumes,
743
- allow_background_volume_commits=_allow_background_volume_commits,
744
- pty_info=pty_info,
745
- )
746
- await resolver.load(obj)
747
- return obj
748
-
749
- def include(self, /, other_stub: "_Stub"):
750
- """Include another stub's objects in this one.
751
-
752
- Useful splitting up Modal apps across different self-contained files
753
-
754
- ```python
755
- stub_a = modal.Stub("a")
756
- @stub.function()
757
- def foo():
758
- ...
759
-
760
- stub_b = modal.Stub("b")
761
- @stub.function()
762
- def bar():
763
- ...
764
-
765
- stub_a.include(stub_b)
766
-
767
- @stub_a.local_entrypoint()
768
- def main():
769
- # use function declared on the included stub
770
- bar.remote()
771
- ```
772
- """
773
- for tag, object in other_stub._indexed_objects.items():
774
- existing_object = self._indexed_objects.get(tag)
775
- if existing_object and existing_object != object:
776
- logger.warning(
777
- f"Named app object {tag} with existing value {existing_object} is being overwritten by a different object {object}"
778
- )
779
-
780
- self._add_object(tag, object)
781
-
782
-
783
- Stub = synchronize_api(_Stub)