modal 1.0.6.dev58__py3-none-any.whl → 1.2.3.dev7__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.
Potentially problematic release.
This version of modal might be problematic. Click here for more details.
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +4 -2
- modal/_container_entrypoint.py +41 -49
- modal/_functions.py +424 -195
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +68 -20
- modal/_output.py +58 -45
- modal/_partial_function.py +36 -11
- modal/_pty.py +7 -3
- modal/_resolver.py +21 -35
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +301 -186
- modal/_runtime/container_io_manager.pyi +70 -61
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +4 -1
- modal/_runtime/gpu_memory_snapshot.py +170 -63
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +57 -1
- modal/_utils/async_utils.py +33 -12
- modal/_utils/auth_token_manager.py +2 -5
- modal/_utils/blob_utils.py +110 -53
- modal/_utils/function_utils.py +49 -42
- modal/_utils/grpc_utils.py +80 -50
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +219 -83
- modal/app.pyi +229 -56
- modal/billing.py +5 -0
- modal/{requirements → builder}/2025.06.txt +1 -0
- modal/{requirements → builder}/PREVIEW.txt +1 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +9 -13
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +58 -16
- modal/cli/secret.py +48 -22
- modal/cli/utils.py +3 -4
- modal/cli/volume.py +28 -25
- modal/client.py +13 -116
- modal/client.pyi +9 -91
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +5 -1
- modal/cls.py +130 -102
- modal/cls.pyi +45 -85
- modal/config.py +29 -10
- modal/container_process.py +291 -13
- modal/container_process.pyi +95 -32
- modal/dict.py +282 -63
- modal/dict.pyi +423 -73
- modal/environments.py +15 -27
- modal/environments.pyi +5 -15
- modal/exception.py +8 -0
- modal/experimental/__init__.py +143 -38
- modal/experimental/flash.py +247 -78
- modal/experimental/flash.pyi +137 -9
- modal/file_io.py +14 -28
- modal/file_io.pyi +2 -2
- modal/file_pattern_matcher.py +25 -16
- modal/functions.pyi +134 -61
- modal/image.py +255 -86
- modal/image.pyi +300 -62
- modal/io_streams.py +436 -126
- modal/io_streams.pyi +236 -171
- modal/mount.py +62 -157
- modal/mount.pyi +45 -172
- modal/network_file_system.py +30 -53
- modal/network_file_system.pyi +16 -76
- modal/object.pyi +42 -8
- modal/parallel_map.py +821 -113
- modal/parallel_map.pyi +134 -0
- modal/partial_function.pyi +4 -1
- modal/proxy.py +16 -7
- modal/proxy.pyi +10 -2
- modal/queue.py +263 -61
- modal/queue.pyi +409 -66
- modal/runner.py +112 -92
- modal/runner.pyi +45 -27
- modal/sandbox.py +451 -124
- modal/sandbox.pyi +513 -67
- modal/secret.py +291 -67
- modal/secret.pyi +425 -19
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/token_flow.py +4 -4
- modal/volume.py +344 -98
- modal/volume.pyi +464 -68
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +9 -8
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +11 -1
- modal_proto/api.proto +399 -67
- modal_proto/api_grpc.py +241 -1
- modal_proto/api_pb2.py +1395 -1000
- modal_proto/api_pb2.pyi +1239 -79
- modal_proto/api_pb2_grpc.py +499 -4
- modal_proto/api_pb2_grpc.pyi +162 -14
- modal_proto/modal_api_grpc.py +175 -160
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal-1.0.6.dev58.dist-info/RECORD +0 -183
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- /modal/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- /modal/{requirements → builder}/base-images.json +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/app.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Copyright Modal Labs 2022
|
|
2
2
|
import inspect
|
|
3
3
|
import typing
|
|
4
|
-
from collections.abc import AsyncGenerator, Coroutine, Sequence
|
|
4
|
+
from collections.abc import AsyncGenerator, Collection, Coroutine, Mapping, Sequence
|
|
5
|
+
from dataclasses import dataclass
|
|
5
6
|
from pathlib import PurePosixPath
|
|
6
7
|
from textwrap import dedent
|
|
7
8
|
from typing import (
|
|
@@ -21,21 +22,22 @@ from modal_proto import api_pb2
|
|
|
21
22
|
|
|
22
23
|
from ._functions import _Function
|
|
23
24
|
from ._ipython import is_notebook
|
|
25
|
+
from ._load_context import LoadContext
|
|
24
26
|
from ._object import _get_environment_name, _Object
|
|
25
27
|
from ._partial_function import (
|
|
26
28
|
_find_partial_methods_for_user_cls,
|
|
27
29
|
_PartialFunction,
|
|
28
30
|
_PartialFunctionFlags,
|
|
31
|
+
verify_concurrent_params,
|
|
29
32
|
)
|
|
30
33
|
from ._utils.async_utils import synchronize_api
|
|
31
34
|
from ._utils.deprecation import (
|
|
32
35
|
deprecation_warning,
|
|
33
36
|
warn_on_renamed_autoscaler_settings,
|
|
34
37
|
)
|
|
35
|
-
from ._utils.function_utils import FunctionInfo, is_global_object, is_method_fn
|
|
36
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
38
|
+
from ._utils.function_utils import FunctionInfo, is_flash_object, is_global_object, is_method_fn
|
|
37
39
|
from ._utils.mount_utils import validate_volumes
|
|
38
|
-
from ._utils.name_utils import check_object_name
|
|
40
|
+
from ._utils.name_utils import check_object_name, check_tag_dict
|
|
39
41
|
from .client import _Client
|
|
40
42
|
from .cloud_bucket_mount import _CloudBucketMount
|
|
41
43
|
from .cls import _Cls, parameter
|
|
@@ -114,6 +116,22 @@ class _FunctionDecoratorType:
|
|
|
114
116
|
def __call__(self, func): ...
|
|
115
117
|
|
|
116
118
|
|
|
119
|
+
@dataclass()
|
|
120
|
+
class _LocalAppState:
|
|
121
|
+
"""All state for apps that's part of the local/definition state"""
|
|
122
|
+
|
|
123
|
+
functions: dict[str, _Function]
|
|
124
|
+
classes: dict[str, _Cls]
|
|
125
|
+
image_default: Optional[_Image]
|
|
126
|
+
web_endpoints: list[str] # Used by the CLI
|
|
127
|
+
local_entrypoints: dict[str, _LocalEntrypoint]
|
|
128
|
+
tags: dict[str, str]
|
|
129
|
+
|
|
130
|
+
include_source_default: bool
|
|
131
|
+
secrets_default: Sequence[_Secret]
|
|
132
|
+
volumes_default: dict[Union[str, PurePosixPath], _Volume]
|
|
133
|
+
|
|
134
|
+
|
|
117
135
|
class _App:
|
|
118
136
|
"""A Modal App is a group of functions and classes that are deployed together.
|
|
119
137
|
|
|
@@ -151,30 +169,35 @@ class _App:
|
|
|
151
169
|
|
|
152
170
|
_name: Optional[str]
|
|
153
171
|
_description: Optional[str]
|
|
154
|
-
_functions: dict[str, _Function]
|
|
155
|
-
_classes: dict[str, _Cls]
|
|
156
172
|
|
|
157
|
-
|
|
158
|
-
_secrets: Sequence[_Secret]
|
|
159
|
-
_volumes: dict[Union[str, PurePosixPath], _Volume]
|
|
160
|
-
_web_endpoints: list[str] # Used by the CLI
|
|
161
|
-
_local_entrypoints: dict[str, _LocalEntrypoint]
|
|
173
|
+
_local_state_attr: Optional[_LocalAppState] = None
|
|
162
174
|
|
|
163
175
|
# Running apps only (container apps or running local)
|
|
164
176
|
_app_id: Optional[str] # Kept after app finishes
|
|
165
177
|
_running_app: Optional[RunningApp] # Various app info
|
|
166
178
|
_client: Optional[_Client]
|
|
167
179
|
|
|
168
|
-
|
|
180
|
+
# Metadata for loading objects within this app
|
|
181
|
+
# passed by reference to functions and classes so it can be updated by run()/deploy()
|
|
182
|
+
_root_load_context: LoadContext
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def _local_state(self) -> _LocalAppState:
|
|
186
|
+
"""For internal use only. Do not use this property directly."""
|
|
187
|
+
|
|
188
|
+
if self._local_state_attr is None:
|
|
189
|
+
raise AttributeError("Local state is not initialized - app is not locally available")
|
|
190
|
+
return self._local_state_attr
|
|
169
191
|
|
|
170
192
|
def __init__(
|
|
171
193
|
self,
|
|
172
194
|
name: Optional[str] = None,
|
|
173
195
|
*,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
196
|
+
tags: Optional[dict[str, str]] = None, # Additional metadata to set on the App
|
|
197
|
+
image: Optional[_Image] = None, # Default Image for the App (otherwise default to `modal.Image.debian_slim()`)
|
|
198
|
+
secrets: Sequence[_Secret] = [], # Secrets to add for all Functions in the App
|
|
199
|
+
volumes: dict[Union[str, PurePosixPath], _Volume] = {}, # Volume mounts to use for all Functions
|
|
200
|
+
include_source: bool = True, # Default configuration for adding Function source file(s) to the Modal container
|
|
178
201
|
) -> None:
|
|
179
202
|
"""Construct a new app, optionally with default image, mounts, secrets, or volumes.
|
|
180
203
|
|
|
@@ -188,9 +211,11 @@ class _App:
|
|
|
188
211
|
if name is not None and not isinstance(name, str):
|
|
189
212
|
raise InvalidError("Invalid value for `name`: Must be string.")
|
|
190
213
|
|
|
214
|
+
if tags is not None:
|
|
215
|
+
check_tag_dict(tags)
|
|
216
|
+
|
|
191
217
|
self._name = name
|
|
192
218
|
self._description = name
|
|
193
|
-
self._include_source_default = include_source
|
|
194
219
|
|
|
195
220
|
check_sequence(secrets, _Secret, "`secrets=` has to be a list or tuple of `modal.Secret` objects")
|
|
196
221
|
validate_volumes(volumes)
|
|
@@ -198,17 +223,26 @@ class _App:
|
|
|
198
223
|
if image is not None and not isinstance(image, _Image):
|
|
199
224
|
raise InvalidError("`image=` has to be a `modal.Image` object")
|
|
200
225
|
|
|
201
|
-
self.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
226
|
+
self._local_state_attr = _LocalAppState(
|
|
227
|
+
functions={},
|
|
228
|
+
classes={},
|
|
229
|
+
image_default=image,
|
|
230
|
+
secrets_default=secrets,
|
|
231
|
+
volumes_default=volumes,
|
|
232
|
+
include_source_default=include_source,
|
|
233
|
+
web_endpoints=[],
|
|
234
|
+
local_entrypoints={},
|
|
235
|
+
tags=tags or {},
|
|
236
|
+
)
|
|
208
237
|
|
|
238
|
+
# Running apps only
|
|
209
239
|
self._app_id = None
|
|
210
240
|
self._running_app = None # Set inside container, OR during the time an app is running locally
|
|
241
|
+
|
|
242
|
+
# Client is special - needed to be set just before the app is "hydrated" or running at the latest
|
|
243
|
+
# Guaranteed to be set for running apps, but also needed to actually *hydrate* the app and make it running
|
|
211
244
|
self._client = None
|
|
245
|
+
self._root_load_context = LoadContext.empty()
|
|
212
246
|
|
|
213
247
|
# Register this app. This is used to look up the app in the container, when we can't get it from the function
|
|
214
248
|
_App._all_apps.setdefault(self._name, []).append(self)
|
|
@@ -220,7 +254,12 @@ class _App:
|
|
|
220
254
|
|
|
221
255
|
@property
|
|
222
256
|
def is_interactive(self) -> bool:
|
|
223
|
-
"""
|
|
257
|
+
"""mdmd:hidden
|
|
258
|
+
Whether the current app for the app is running in interactive mode.
|
|
259
|
+
|
|
260
|
+
Note: this method will likely be deprecated in the future.
|
|
261
|
+
|
|
262
|
+
"""
|
|
224
263
|
# return self._name
|
|
225
264
|
if self._running_app:
|
|
226
265
|
return self._running_app.interactive
|
|
@@ -269,15 +308,22 @@ class _App:
|
|
|
269
308
|
object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
|
|
270
309
|
)
|
|
271
310
|
|
|
272
|
-
response = await
|
|
311
|
+
response = await client.stub.AppGetOrCreate(request)
|
|
273
312
|
|
|
274
|
-
app = _App(name)
|
|
313
|
+
app = _App(name) # TODO: this should probably be a distinct constructor, possibly even a distinct type
|
|
314
|
+
app._local_state_attr = None # this is not a locally defined App, so no local state
|
|
275
315
|
app._app_id = response.app_id
|
|
276
316
|
app._client = client
|
|
317
|
+
app._root_load_context = LoadContext(client=client, environment_name=environment_name, app_id=response.app_id)
|
|
277
318
|
app._running_app = RunningApp(response.app_id, interactive=False)
|
|
278
319
|
return app
|
|
279
320
|
|
|
280
321
|
def set_description(self, description: str):
|
|
322
|
+
"""mdmd:hidden
|
|
323
|
+
Set the description of the App before it starts running.
|
|
324
|
+
|
|
325
|
+
Note: we don't recommend using the method and may deprecate it in the future.
|
|
326
|
+
"""
|
|
281
327
|
self._description = description
|
|
282
328
|
|
|
283
329
|
def _validate_blueprint_value(self, key: str, value: Any):
|
|
@@ -286,17 +332,26 @@ class _App:
|
|
|
286
332
|
|
|
287
333
|
@property
|
|
288
334
|
def image(self) -> _Image:
|
|
289
|
-
|
|
335
|
+
"""mdmd:hidden
|
|
336
|
+
Retrieve the Image that will be used as the default for any Functions registered to the App.
|
|
337
|
+
|
|
338
|
+
Note: This property is only relevant in the build phase and won't be populated on a deployed
|
|
339
|
+
App that is retrieved via `modal.App.lookup`. It is likely to be deprecated in the future.
|
|
340
|
+
|
|
341
|
+
"""
|
|
342
|
+
return self._local_state.image_default
|
|
290
343
|
|
|
291
344
|
@image.setter
|
|
292
345
|
def image(self, value):
|
|
293
|
-
|
|
346
|
+
"""mdmd:hidden"""
|
|
347
|
+
self._local_state.image_default = value
|
|
294
348
|
|
|
295
349
|
def _uncreate_all_objects(self):
|
|
296
350
|
# TODO(erikbern): this doesn't unhydrate objects that aren't tagged
|
|
297
|
-
|
|
351
|
+
local_state = self._local_state
|
|
352
|
+
for obj in local_state.functions.values():
|
|
298
353
|
obj._unhydrate()
|
|
299
|
-
for obj in
|
|
354
|
+
for obj in local_state.classes.values():
|
|
300
355
|
obj._unhydrate()
|
|
301
356
|
|
|
302
357
|
@asynccontextmanager
|
|
@@ -371,8 +426,8 @@ class _App:
|
|
|
371
426
|
*,
|
|
372
427
|
name: Optional[str] = None, # Name for the deployment, overriding any set on the App
|
|
373
428
|
environment_name: Optional[str] = None, # Environment to deploy the App in
|
|
374
|
-
tag: str = "", # Optional metadata that
|
|
375
|
-
client: Optional[_Client] = None, # Alternate client to use for
|
|
429
|
+
tag: str = "", # Optional metadata that is specific to this deployment
|
|
430
|
+
client: Optional[_Client] = None, # Alternate client to use for communication with the server
|
|
376
431
|
) -> typing_extensions.Self:
|
|
377
432
|
"""Deploy the App so that it is available persistently.
|
|
378
433
|
|
|
@@ -432,8 +487,9 @@ class _App:
|
|
|
432
487
|
return self
|
|
433
488
|
|
|
434
489
|
def _get_default_image(self):
|
|
435
|
-
|
|
436
|
-
|
|
490
|
+
local_state = self._local_state
|
|
491
|
+
if local_state.image_default:
|
|
492
|
+
return local_state.image_default
|
|
437
493
|
else:
|
|
438
494
|
return _default_image
|
|
439
495
|
|
|
@@ -448,7 +504,8 @@ class _App:
|
|
|
448
504
|
return [m for m in all_mounts if m.is_local()]
|
|
449
505
|
|
|
450
506
|
def _add_function(self, function: _Function, is_web_endpoint: bool):
|
|
451
|
-
|
|
507
|
+
local_state = self._local_state
|
|
508
|
+
if old_function := local_state.functions.get(function.tag, None):
|
|
452
509
|
if old_function is function:
|
|
453
510
|
return # already added the same exact instance, ignore
|
|
454
511
|
|
|
@@ -459,7 +516,7 @@ class _App:
|
|
|
459
516
|
f"[{old_function._info.module_name}].{old_function._info.function_name}"
|
|
460
517
|
f" with new function [{function._info.module_name}].{function._info.function_name}"
|
|
461
518
|
)
|
|
462
|
-
if function.tag in
|
|
519
|
+
if function.tag in local_state.classes:
|
|
463
520
|
logger.warning(f"Warning: tag {function.tag} exists but is overridden by function")
|
|
464
521
|
|
|
465
522
|
if self._running_app:
|
|
@@ -470,9 +527,9 @@ class _App:
|
|
|
470
527
|
metadata: Message = self._running_app.object_handle_metadata[object_id]
|
|
471
528
|
function._hydrate(object_id, self._client, metadata)
|
|
472
529
|
|
|
473
|
-
|
|
530
|
+
local_state.functions[function.tag] = function
|
|
474
531
|
if is_web_endpoint:
|
|
475
|
-
|
|
532
|
+
local_state.web_endpoints.append(function.tag)
|
|
476
533
|
|
|
477
534
|
def _add_class(self, tag: str, cls: _Cls):
|
|
478
535
|
if self._running_app:
|
|
@@ -483,7 +540,7 @@ class _App:
|
|
|
483
540
|
metadata: Message = self._running_app.object_handle_metadata[object_id]
|
|
484
541
|
cls._hydrate(object_id, self._client, metadata)
|
|
485
542
|
|
|
486
|
-
self.
|
|
543
|
+
self._local_state.classes[tag] = cls
|
|
487
544
|
|
|
488
545
|
def _init_container(self, client: _Client, running_app: RunningApp):
|
|
489
546
|
self._app_id = running_app.app_id
|
|
@@ -491,56 +548,67 @@ class _App:
|
|
|
491
548
|
self._client = client
|
|
492
549
|
|
|
493
550
|
_App._container_app = self
|
|
494
|
-
|
|
551
|
+
local_state = self._local_state
|
|
495
552
|
# Hydrate function objects
|
|
496
553
|
for tag, object_id in running_app.function_ids.items():
|
|
497
|
-
if tag in
|
|
498
|
-
obj =
|
|
554
|
+
if tag in local_state.functions:
|
|
555
|
+
obj = local_state.functions[tag]
|
|
499
556
|
handle_metadata = running_app.object_handle_metadata[object_id]
|
|
500
557
|
obj._hydrate(object_id, client, handle_metadata)
|
|
501
558
|
|
|
502
559
|
# Hydrate class objects
|
|
503
560
|
for tag, object_id in running_app.class_ids.items():
|
|
504
|
-
if tag in
|
|
505
|
-
obj =
|
|
561
|
+
if tag in local_state.classes:
|
|
562
|
+
obj = local_state.classes[tag]
|
|
506
563
|
handle_metadata = running_app.object_handle_metadata[object_id]
|
|
507
564
|
obj._hydrate(object_id, client, handle_metadata)
|
|
508
565
|
|
|
509
566
|
@property
|
|
510
567
|
def registered_functions(self) -> dict[str, _Function]:
|
|
511
|
-
"""
|
|
568
|
+
"""mdmd:hidden
|
|
569
|
+
All modal.Function objects registered on the app.
|
|
512
570
|
|
|
513
571
|
Note: this property is populated only during the build phase, and it is not
|
|
514
572
|
expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
|
|
573
|
+
This method is likely to be deprecated in the future in favor of a different
|
|
574
|
+
approach for retrieving the layout of a deployed App.
|
|
515
575
|
"""
|
|
516
|
-
return self.
|
|
576
|
+
return self._local_state.functions
|
|
517
577
|
|
|
518
578
|
@property
|
|
519
579
|
def registered_classes(self) -> dict[str, _Cls]:
|
|
520
|
-
"""
|
|
580
|
+
"""mdmd:hidden
|
|
581
|
+
All modal.Cls objects registered on the app.
|
|
521
582
|
|
|
522
583
|
Note: this property is populated only during the build phase, and it is not
|
|
523
584
|
expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
|
|
585
|
+
This method is likely to be deprecated in the future in favor of a different
|
|
586
|
+
approach for retrieving the layout of a deployed App.
|
|
524
587
|
"""
|
|
525
|
-
return self.
|
|
588
|
+
return self._local_state.classes
|
|
526
589
|
|
|
527
590
|
@property
|
|
528
591
|
def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]:
|
|
529
|
-
"""
|
|
592
|
+
"""mdmd:hidden
|
|
593
|
+
All local CLI entrypoints registered on the app.
|
|
530
594
|
|
|
531
595
|
Note: this property is populated only during the build phase, and it is not
|
|
532
596
|
expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
|
|
597
|
+
This method is likely to be deprecated in the future.
|
|
533
598
|
"""
|
|
534
|
-
return self.
|
|
599
|
+
return self._local_state.local_entrypoints
|
|
535
600
|
|
|
536
601
|
@property
|
|
537
602
|
def registered_web_endpoints(self) -> list[str]:
|
|
538
|
-
"""
|
|
603
|
+
"""mdmd:hidden
|
|
604
|
+
Names of web endpoint (ie. webhook) functions registered on the app.
|
|
539
605
|
|
|
540
606
|
Note: this property is populated only during the build phase, and it is not
|
|
541
607
|
expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
|
|
608
|
+
This method is likely to be deprecated in the future in favor of a different
|
|
609
|
+
approach for retrieving the layout of a deployed App.
|
|
542
610
|
"""
|
|
543
|
-
return self.
|
|
611
|
+
return self._local_state.web_endpoints
|
|
544
612
|
|
|
545
613
|
def local_entrypoint(
|
|
546
614
|
self, _warn_parentheses_missing: Any = None, *, name: Optional[str] = None
|
|
@@ -601,10 +669,11 @@ class _App:
|
|
|
601
669
|
def wrapped(raw_f: Callable[..., Any]) -> _LocalEntrypoint:
|
|
602
670
|
info = FunctionInfo(raw_f)
|
|
603
671
|
tag = name if name is not None else raw_f.__qualname__
|
|
604
|
-
|
|
672
|
+
local_state = self._local_state
|
|
673
|
+
if tag in local_state.local_entrypoints:
|
|
605
674
|
# TODO: get rid of this limitation.
|
|
606
675
|
raise InvalidError(f"Duplicate local entrypoint name: {tag}. Local entrypoint names must be unique.")
|
|
607
|
-
entrypoint =
|
|
676
|
+
entrypoint = local_state.local_entrypoints[tag] = _LocalEntrypoint(info, self)
|
|
608
677
|
return entrypoint
|
|
609
678
|
|
|
610
679
|
return wrapped
|
|
@@ -612,11 +681,12 @@ class _App:
|
|
|
612
681
|
@warn_on_renamed_autoscaler_settings
|
|
613
682
|
def function(
|
|
614
683
|
self,
|
|
615
|
-
_warn_parentheses_missing
|
|
684
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
616
685
|
*,
|
|
617
686
|
image: Optional[_Image] = None, # The image to run as the container for the function
|
|
618
687
|
schedule: Optional[Schedule] = None, # An optional Modal Schedule for the function
|
|
619
|
-
|
|
688
|
+
env: Optional[dict[str, Optional[str]]] = None, # Environment variables to set in the container
|
|
689
|
+
secrets: Optional[Collection[_Secret]] = None, # Secrets to inject into the container as environment variables
|
|
620
690
|
gpu: Union[
|
|
621
691
|
GPU_T, list[GPU_T]
|
|
622
692
|
] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
|
|
@@ -641,7 +711,8 @@ class _App:
|
|
|
641
711
|
scaledown_window: Optional[int] = None, # Max time (in seconds) a container can remain idle while scaling down.
|
|
642
712
|
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
|
643
713
|
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
|
644
|
-
timeout:
|
|
714
|
+
timeout: int = 300, # Maximum execution time for inputs and startup time in seconds.
|
|
715
|
+
startup_timeout: Optional[int] = None, # Maximum startup time in seconds with higher precedence than `timeout`.
|
|
645
716
|
name: Optional[str] = None, # Sets the Modal name of the function within the app
|
|
646
717
|
is_generator: Optional[
|
|
647
718
|
bool
|
|
@@ -655,8 +726,9 @@ class _App:
|
|
|
655
726
|
# With `max_inputs = 1`, containers will be single-use.
|
|
656
727
|
max_inputs: Optional[int] = None,
|
|
657
728
|
i6pn: Optional[bool] = None, # Whether to enable IPv6 container networking within the region.
|
|
658
|
-
# Whether the
|
|
659
|
-
|
|
729
|
+
# Whether the file or directory containing the Function's source should automatically be included
|
|
730
|
+
# in the container. When unset, falls back to the App-level configuration, or is otherwise True by default.
|
|
731
|
+
include_source: Optional[bool] = None,
|
|
660
732
|
experimental_options: Optional[dict[str, Any]] = None,
|
|
661
733
|
# Parameters below here are experimental. Use with caution!
|
|
662
734
|
_experimental_scheduler_placement: Optional[
|
|
@@ -664,14 +736,13 @@ class _App:
|
|
|
664
736
|
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
|
665
737
|
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
666
738
|
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
|
667
|
-
|
|
739
|
+
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
668
740
|
# Parameters below here are deprecated. Please update your code as suggested
|
|
669
741
|
keep_warm: Optional[int] = None, # Replaced with `min_containers`
|
|
670
742
|
concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
|
|
671
743
|
container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
|
|
672
744
|
allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
|
|
673
745
|
_experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
|
|
674
|
-
allow_cross_region_volumes: Optional[bool] = None, # Always True on the Modal backend now
|
|
675
746
|
) -> _FunctionDecoratorType:
|
|
676
747
|
"""Decorator to register a new Modal Function with this App."""
|
|
677
748
|
if isinstance(_warn_parentheses_missing, _Image):
|
|
@@ -690,10 +761,12 @@ class _App:
|
|
|
690
761
|
" Please use the `@modal.concurrent` decorator instead."
|
|
691
762
|
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
692
763
|
)
|
|
693
|
-
if allow_cross_region_volumes is not None:
|
|
694
|
-
deprecation_warning((2025, 4, 23), "The `allow_cross_region_volumes` parameter no longer has any effect.")
|
|
695
764
|
|
|
696
|
-
secrets = [
|
|
765
|
+
secrets = secrets or []
|
|
766
|
+
if env:
|
|
767
|
+
secrets = [*secrets, _Secret.from_dict(env)]
|
|
768
|
+
local_state = self._local_state
|
|
769
|
+
secrets = [*local_state.secrets_default, *secrets]
|
|
697
770
|
|
|
698
771
|
def wrapped(
|
|
699
772
|
f: Union[_PartialFunction, Callable[..., Any], None],
|
|
@@ -736,6 +809,7 @@ class _App:
|
|
|
736
809
|
batch_max_size = f.params.batch_max_size
|
|
737
810
|
batch_wait_ms = f.params.batch_wait_ms
|
|
738
811
|
if f.flags & _PartialFunctionFlags.CONCURRENT:
|
|
812
|
+
verify_concurrent_params(params=f.params, is_flash=is_flash_object(experimental_options))
|
|
739
813
|
max_concurrent_inputs = f.params.max_concurrent_inputs
|
|
740
814
|
target_concurrent_inputs = f.params.target_concurrent_inputs
|
|
741
815
|
else:
|
|
@@ -801,7 +875,7 @@ class _App:
|
|
|
801
875
|
is_generator=is_generator,
|
|
802
876
|
gpu=gpu,
|
|
803
877
|
network_file_systems=network_file_systems,
|
|
804
|
-
volumes={**
|
|
878
|
+
volumes={**local_state.volumes_default, **volumes},
|
|
805
879
|
cpu=cpu,
|
|
806
880
|
memory=memory,
|
|
807
881
|
ephemeral_disk=ephemeral_disk,
|
|
@@ -816,6 +890,7 @@ class _App:
|
|
|
816
890
|
batch_max_size=batch_max_size,
|
|
817
891
|
batch_wait_ms=batch_wait_ms,
|
|
818
892
|
timeout=timeout,
|
|
893
|
+
startup_timeout=startup_timeout or timeout,
|
|
819
894
|
cloud=cloud,
|
|
820
895
|
webhook_config=webhook_config,
|
|
821
896
|
enable_memory_snapshot=enable_memory_snapshot,
|
|
@@ -826,10 +901,10 @@ class _App:
|
|
|
826
901
|
i6pn_enabled=i6pn_enabled,
|
|
827
902
|
cluster_size=cluster_size, # Experimental: Clustered functions
|
|
828
903
|
rdma=rdma,
|
|
829
|
-
include_source=include_source if include_source is not None else
|
|
904
|
+
include_source=include_source if include_source is not None else local_state.include_source_default,
|
|
830
905
|
experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
|
|
831
906
|
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
832
|
-
|
|
907
|
+
restrict_output=_experimental_restrict_output,
|
|
833
908
|
)
|
|
834
909
|
|
|
835
910
|
self._add_function(function, webhook_config is not None)
|
|
@@ -842,10 +917,11 @@ class _App:
|
|
|
842
917
|
@warn_on_renamed_autoscaler_settings
|
|
843
918
|
def cls(
|
|
844
919
|
self,
|
|
845
|
-
_warn_parentheses_missing
|
|
920
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
846
921
|
*,
|
|
847
922
|
image: Optional[_Image] = None, # The image to run as the container for the function
|
|
848
|
-
|
|
923
|
+
env: Optional[dict[str, Optional[str]]] = None, # Environment variables to set in the container
|
|
924
|
+
secrets: Optional[Collection[_Secret]] = None, # Secrets to inject into the container as environment variables
|
|
849
925
|
gpu: Union[
|
|
850
926
|
GPU_T, list[GPU_T]
|
|
851
927
|
] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
|
|
@@ -870,7 +946,8 @@ class _App:
|
|
|
870
946
|
scaledown_window: Optional[int] = None, # Max time (in seconds) a container can remain idle while scaling down.
|
|
871
947
|
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
|
872
948
|
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
|
873
|
-
timeout:
|
|
949
|
+
timeout: int = 300, # Maximum execution time for inputs and startup time in seconds.
|
|
950
|
+
startup_timeout: Optional[int] = None, # Maximum startup time in seconds with higher precedence than `timeout`.
|
|
874
951
|
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
|
875
952
|
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
|
|
876
953
|
enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
|
|
@@ -888,14 +965,13 @@ class _App:
|
|
|
888
965
|
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
|
889
966
|
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
890
967
|
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
|
891
|
-
|
|
968
|
+
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
892
969
|
# Parameters below here are deprecated. Please update your code as suggested
|
|
893
970
|
keep_warm: Optional[int] = None, # Replaced with `min_containers`
|
|
894
971
|
concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
|
|
895
972
|
container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
|
|
896
973
|
allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
|
|
897
974
|
_experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
|
|
898
|
-
allow_cross_region_volumes: Optional[bool] = None, # Always True on the Modal backend now
|
|
899
975
|
) -> Callable[[Union[CLS_T, _PartialFunction]], CLS_T]:
|
|
900
976
|
"""
|
|
901
977
|
Decorator to register a new Modal [Cls](https://modal.com/docs/reference/modal.Cls) with this App.
|
|
@@ -916,15 +992,19 @@ class _App:
|
|
|
916
992
|
" Please use the `@modal.concurrent` decorator instead."
|
|
917
993
|
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
918
994
|
)
|
|
919
|
-
|
|
920
|
-
|
|
995
|
+
|
|
996
|
+
secrets = secrets or []
|
|
997
|
+
if env:
|
|
998
|
+
secrets = [*secrets, _Secret.from_dict(env)]
|
|
921
999
|
|
|
922
1000
|
def wrapper(wrapped_cls: Union[CLS_T, _PartialFunction]) -> CLS_T:
|
|
1001
|
+
local_state = self._local_state
|
|
923
1002
|
# Check if the decorated object is a class
|
|
924
1003
|
if isinstance(wrapped_cls, _PartialFunction):
|
|
925
1004
|
wrapped_cls.registered = True
|
|
926
1005
|
user_cls = wrapped_cls.user_cls
|
|
927
1006
|
if wrapped_cls.flags & _PartialFunctionFlags.CONCURRENT:
|
|
1007
|
+
verify_concurrent_params(params=wrapped_cls.params, is_flash=is_flash_object(experimental_options))
|
|
928
1008
|
max_concurrent_inputs = wrapped_cls.params.max_concurrent_inputs
|
|
929
1009
|
target_concurrent_inputs = wrapped_cls.params.target_concurrent_inputs
|
|
930
1010
|
else:
|
|
@@ -933,13 +1013,16 @@ class _App:
|
|
|
933
1013
|
|
|
934
1014
|
if wrapped_cls.flags & _PartialFunctionFlags.CLUSTERED:
|
|
935
1015
|
cluster_size = wrapped_cls.params.cluster_size
|
|
1016
|
+
rdma = wrapped_cls.params.rdma
|
|
936
1017
|
else:
|
|
937
1018
|
cluster_size = None
|
|
1019
|
+
rdma = None
|
|
938
1020
|
else:
|
|
939
1021
|
user_cls = wrapped_cls
|
|
940
1022
|
max_concurrent_inputs = allow_concurrent_inputs
|
|
941
1023
|
target_concurrent_inputs = None
|
|
942
1024
|
cluster_size = None
|
|
1025
|
+
rdma = None
|
|
943
1026
|
if not inspect.isclass(user_cls):
|
|
944
1027
|
raise TypeError("The @app.cls decorator must be used on a class.")
|
|
945
1028
|
|
|
@@ -983,10 +1066,10 @@ class _App:
|
|
|
983
1066
|
info,
|
|
984
1067
|
app=self,
|
|
985
1068
|
image=image or self._get_default_image(),
|
|
986
|
-
secrets=[*
|
|
1069
|
+
secrets=[*local_state.secrets_default, *secrets],
|
|
987
1070
|
gpu=gpu,
|
|
988
1071
|
network_file_systems=network_file_systems,
|
|
989
|
-
volumes={**
|
|
1072
|
+
volumes={**local_state.volumes_default, **volumes},
|
|
990
1073
|
cpu=cpu,
|
|
991
1074
|
memory=memory,
|
|
992
1075
|
ephemeral_disk=ephemeral_disk,
|
|
@@ -1001,6 +1084,7 @@ class _App:
|
|
|
1001
1084
|
batch_max_size=batch_max_size,
|
|
1002
1085
|
batch_wait_ms=batch_wait_ms,
|
|
1003
1086
|
timeout=timeout,
|
|
1087
|
+
startup_timeout=startup_timeout or timeout,
|
|
1004
1088
|
cloud=cloud,
|
|
1005
1089
|
enable_memory_snapshot=enable_memory_snapshot,
|
|
1006
1090
|
block_network=block_network,
|
|
@@ -1009,16 +1093,22 @@ class _App:
|
|
|
1009
1093
|
scheduler_placement=scheduler_placement,
|
|
1010
1094
|
i6pn_enabled=i6pn_enabled,
|
|
1011
1095
|
cluster_size=cluster_size,
|
|
1012
|
-
|
|
1096
|
+
rdma=rdma,
|
|
1097
|
+
include_source=include_source if include_source is not None else local_state.include_source_default,
|
|
1013
1098
|
experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
|
|
1014
1099
|
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
1015
1100
|
_experimental_custom_scaling_factor=_experimental_custom_scaling_factor,
|
|
1016
|
-
|
|
1101
|
+
restrict_output=_experimental_restrict_output,
|
|
1017
1102
|
)
|
|
1018
1103
|
|
|
1019
1104
|
self._add_function(cls_func, is_web_endpoint=False)
|
|
1020
1105
|
|
|
1021
1106
|
cls: _Cls = _Cls.from_local(user_cls, self, cls_func)
|
|
1107
|
+
for method_name, partial_function in cls._method_partials.items():
|
|
1108
|
+
if partial_function.params.webhook_config is not None:
|
|
1109
|
+
full_name = f"{user_cls.__name__}.{method_name}"
|
|
1110
|
+
local_state.web_endpoints.append(full_name)
|
|
1111
|
+
partial_function.registered = True
|
|
1022
1112
|
|
|
1023
1113
|
tag: str = user_cls.__name__
|
|
1024
1114
|
self._add_class(tag, cls)
|
|
@@ -1026,7 +1116,7 @@ class _App:
|
|
|
1026
1116
|
|
|
1027
1117
|
return wrapper
|
|
1028
1118
|
|
|
1029
|
-
def include(self, /, other_app: "_App") -> typing_extensions.Self:
|
|
1119
|
+
def include(self, /, other_app: "_App", inherit_tags: bool = True) -> typing_extensions.Self:
|
|
1030
1120
|
"""Include another App's objects in this one.
|
|
1031
1121
|
|
|
1032
1122
|
Useful for splitting up Modal Apps across different self-contained files.
|
|
@@ -1049,12 +1139,19 @@ class _App:
|
|
|
1049
1139
|
# use function declared on the included app
|
|
1050
1140
|
bar.remote()
|
|
1051
1141
|
```
|
|
1142
|
+
|
|
1143
|
+
When `inherit_tags=True` any tags set on the other App will be inherited by this App
|
|
1144
|
+
(with this App's tags taking precedence in the case of conflicts).
|
|
1145
|
+
|
|
1052
1146
|
"""
|
|
1053
|
-
|
|
1147
|
+
other_app_local_state = other_app._local_state
|
|
1148
|
+
this_local_state = self._local_state
|
|
1149
|
+
|
|
1150
|
+
for tag, function in other_app_local_state.functions.items():
|
|
1054
1151
|
self._add_function(function, False) # TODO(erikbern): webhook config?
|
|
1055
1152
|
|
|
1056
|
-
for tag, cls in
|
|
1057
|
-
existing_cls =
|
|
1153
|
+
for tag, cls in other_app_local_state.classes.items():
|
|
1154
|
+
existing_cls = this_local_state.classes.get(tag)
|
|
1058
1155
|
if existing_cls and existing_cls != cls:
|
|
1059
1156
|
logger.warning(
|
|
1060
1157
|
f"Named app class {tag} with existing value {existing_cls} is being "
|
|
@@ -1062,8 +1159,47 @@ class _App:
|
|
|
1062
1159
|
)
|
|
1063
1160
|
|
|
1064
1161
|
self._add_class(tag, cls)
|
|
1162
|
+
|
|
1163
|
+
if inherit_tags:
|
|
1164
|
+
this_local_state.tags = {**other_app_local_state.tags, **this_local_state.tags}
|
|
1165
|
+
|
|
1065
1166
|
return self
|
|
1066
1167
|
|
|
1168
|
+
async def set_tags(self, tags: Mapping[str, str], *, client: Optional[_Client] = None) -> None:
|
|
1169
|
+
"""Attach key-value metadata to the App.
|
|
1170
|
+
|
|
1171
|
+
Tag metadata can be used to add organization-specific context to the App and can be
|
|
1172
|
+
included in billing reports and other informational APIs. Tags can also be set in
|
|
1173
|
+
the App constructor.
|
|
1174
|
+
|
|
1175
|
+
Any tags set on the App before calling this method will be removed if they are not
|
|
1176
|
+
included in the argument (i.e., this method does not have `.update()` semantics).
|
|
1177
|
+
|
|
1178
|
+
"""
|
|
1179
|
+
# Note that we are requiring the App to be "running" before we set the tags.
|
|
1180
|
+
# Alternatively, we could hold onto the tags (i.e. in `self._local_state.tags`) and then pass
|
|
1181
|
+
# then up when AppPublish gets called. I'm not certain we want to support it, though.
|
|
1182
|
+
# It might not be obvious to users that `.set_tags()` is eager and has immediate effect
|
|
1183
|
+
# when the App is running, but lazy (and potentially ignored) otherwise. There would be
|
|
1184
|
+
# other complications, like what do you do with any tags set in the constructor, and
|
|
1185
|
+
# what should `.get_tags()` do when it's called before the App is running?
|
|
1186
|
+
if self._app_id is None:
|
|
1187
|
+
raise InvalidError("`App.set_tags` cannot be called before the App is running.")
|
|
1188
|
+
check_tag_dict(tags)
|
|
1189
|
+
req = api_pb2.AppSetTagsRequest(app_id=self._app_id, tags=tags)
|
|
1190
|
+
|
|
1191
|
+
client = client or self._client or await _Client.from_env()
|
|
1192
|
+
await client.stub.AppSetTags(req)
|
|
1193
|
+
|
|
1194
|
+
async def get_tags(self, *, client: Optional[_Client] = None) -> dict[str, str]:
|
|
1195
|
+
"""Get the tags that are currently attached to the App."""
|
|
1196
|
+
if self._app_id is None:
|
|
1197
|
+
raise InvalidError("`App.get_tags` cannot be called before the App is running.")
|
|
1198
|
+
req = api_pb2.AppGetTagsRequest(app_id=self._app_id)
|
|
1199
|
+
client = client or self._client or await _Client.from_env()
|
|
1200
|
+
resp = await client.stub.AppGetTags(req)
|
|
1201
|
+
return dict(resp.tags)
|
|
1202
|
+
|
|
1067
1203
|
async def _logs(self, client: Optional[_Client] = None) -> AsyncGenerator[str, None]:
|
|
1068
1204
|
"""Stream logs from the app.
|
|
1069
1205
|
|