modal 0.62.115__py3-none-any.whl → 0.72.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modal/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +402 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1025 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +196 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +943 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
- modal-0.72.13.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
modal/app.py
CHANGED
@@ -2,41 +2,50 @@
|
|
2
2
|
import inspect
|
3
3
|
import typing
|
4
4
|
import warnings
|
5
|
+
from collections.abc import AsyncGenerator, Coroutine, Sequence
|
5
6
|
from pathlib import PurePosixPath
|
6
|
-
from
|
7
|
+
from textwrap import dedent
|
8
|
+
from typing import (
|
9
|
+
Any,
|
10
|
+
Callable,
|
11
|
+
ClassVar,
|
12
|
+
Optional,
|
13
|
+
Union,
|
14
|
+
overload,
|
15
|
+
)
|
7
16
|
|
17
|
+
import typing_extensions
|
8
18
|
from google.protobuf.message import Message
|
9
19
|
from synchronicity.async_wrap import asynccontextmanager
|
10
20
|
|
11
21
|
from modal_proto import api_pb2
|
12
22
|
|
13
23
|
from ._ipython import is_notebook
|
14
|
-
from ._output import OutputManager
|
15
|
-
from ._resolver import Resolver
|
16
24
|
from ._utils.async_utils import synchronize_api
|
17
|
-
from ._utils.
|
25
|
+
from ._utils.deprecation import deprecation_error, deprecation_warning, renamed_parameter
|
26
|
+
from ._utils.function_utils import FunctionInfo, is_global_object, is_method_fn
|
27
|
+
from ._utils.grpc_utils import retry_transient_errors
|
18
28
|
from ._utils.mount_utils import validate_volumes
|
19
|
-
from .app_utils import ( # noqa: F401
|
20
|
-
_list_apps,
|
21
|
-
list_apps,
|
22
|
-
)
|
23
29
|
from .client import _Client
|
24
30
|
from .cloud_bucket_mount import _CloudBucketMount
|
25
|
-
from .cls import _Cls
|
31
|
+
from .cls import _Cls, parameter
|
26
32
|
from .config import logger
|
27
|
-
from .exception import
|
28
|
-
from .functions import _Function
|
33
|
+
from .exception import ExecutionError, InvalidError
|
34
|
+
from .functions import Function, _Function
|
29
35
|
from .gpu import GPU_T
|
30
36
|
from .image import _Image
|
31
37
|
from .mount import _Mount
|
32
38
|
from .network_file_system import _NetworkFileSystem
|
33
|
-
from .object import _Object
|
34
|
-
from .partial_function import
|
39
|
+
from .object import _get_environment_name, _Object
|
40
|
+
from .partial_function import (
|
41
|
+
PartialFunction,
|
42
|
+
_find_partial_methods_for_user_cls,
|
43
|
+
_PartialFunction,
|
44
|
+
_PartialFunctionFlags,
|
45
|
+
)
|
35
46
|
from .proxy import _Proxy
|
36
47
|
from .retries import Retries
|
37
|
-
from .runner import _run_app
|
38
48
|
from .running_app import RunningApp
|
39
|
-
from .sandbox import _Sandbox
|
40
49
|
from .schedule import Schedule
|
41
50
|
from .scheduler_placement import SchedulerPlacement
|
42
51
|
from .secret import _Secret
|
@@ -49,11 +58,11 @@ class _LocalEntrypoint:
|
|
49
58
|
_info: FunctionInfo
|
50
59
|
_app: "_App"
|
51
60
|
|
52
|
-
def __init__(self, info, app):
|
53
|
-
self._info = info
|
61
|
+
def __init__(self, info: FunctionInfo, app: "_App") -> None:
|
62
|
+
self._info = info
|
54
63
|
self._app = app
|
55
64
|
|
56
|
-
def __call__(self, *args, **kwargs):
|
65
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
57
66
|
return self._info.raw_f(*args, **kwargs)
|
58
67
|
|
59
68
|
@property
|
@@ -73,25 +82,67 @@ class _LocalEntrypoint:
|
|
73
82
|
LocalEntrypoint = synchronize_api(_LocalEntrypoint)
|
74
83
|
|
75
84
|
|
76
|
-
def check_sequence(items: typing.Sequence[typing.Any], item_type:
|
85
|
+
def check_sequence(items: typing.Sequence[typing.Any], item_type: type[typing.Any], error_msg: str) -> None:
|
77
86
|
if not isinstance(items, (list, tuple)):
|
78
87
|
raise InvalidError(error_msg)
|
79
88
|
if not all(isinstance(v, item_type) for v in items):
|
80
89
|
raise InvalidError(error_msg)
|
81
90
|
|
82
91
|
|
83
|
-
CLS_T = typing.TypeVar("CLS_T", bound=
|
92
|
+
CLS_T = typing.TypeVar("CLS_T", bound=type[Any])
|
93
|
+
|
94
|
+
|
95
|
+
P = typing_extensions.ParamSpec("P")
|
96
|
+
ReturnType = typing.TypeVar("ReturnType")
|
97
|
+
OriginalReturnType = typing.TypeVar("OriginalReturnType")
|
98
|
+
|
99
|
+
|
100
|
+
class _FunctionDecoratorType:
|
101
|
+
@overload
|
102
|
+
def __call__(
|
103
|
+
self, func: PartialFunction[P, ReturnType, OriginalReturnType]
|
104
|
+
) -> Function[P, ReturnType, OriginalReturnType]:
|
105
|
+
... # already wrapped by a modal decorator, e.g. web_endpoint
|
106
|
+
|
107
|
+
@overload
|
108
|
+
def __call__(
|
109
|
+
self, func: Callable[P, Coroutine[Any, Any, ReturnType]]
|
110
|
+
) -> Function[P, ReturnType, Coroutine[Any, Any, ReturnType]]:
|
111
|
+
... # decorated async function
|
112
|
+
|
113
|
+
@overload
|
114
|
+
def __call__(self, func: Callable[P, ReturnType]) -> Function[P, ReturnType, ReturnType]:
|
115
|
+
... # decorated non-async function
|
116
|
+
|
117
|
+
def __call__(self, func):
|
118
|
+
...
|
119
|
+
|
120
|
+
|
121
|
+
_app_attr_error = """\
|
122
|
+
App assignments of the form `app.x` or `app["x"]` are deprecated!
|
123
|
+
|
124
|
+
The only use cases for these assignments is in conjunction with `.new()`, which is now
|
125
|
+
in itself deprecated. If you are constructing objects with `.from_name(...)`, there is no
|
126
|
+
need to assign those objects to the app. Example:
|
127
|
+
|
128
|
+
```python
|
129
|
+
d = modal.Dict.from_name("my-dict", create_if_missing=True)
|
130
|
+
|
131
|
+
@app.function()
|
132
|
+
def f(x, y):
|
133
|
+
d[x] = y # Refer to d in global scope
|
134
|
+
```
|
135
|
+
"""
|
84
136
|
|
85
137
|
|
86
138
|
class _App:
|
87
|
-
"""A Modal
|
88
|
-
deployed together.
|
139
|
+
"""A Modal App is a group of functions and classes that are deployed together.
|
89
140
|
|
90
141
|
The app serves at least three purposes:
|
91
142
|
|
92
143
|
* A unit of deployment for functions and classes.
|
93
144
|
* Syncing of identities of (primarily) functions and classes across processes
|
94
|
-
(your local Python interpreter and every Modal
|
145
|
+
(your local Python interpreter and every Modal container active in your application).
|
95
146
|
* Manage log collection for everything that happens inside your code.
|
96
147
|
|
97
148
|
**Registering functions with an app**
|
@@ -103,7 +154,7 @@ class _App:
|
|
103
154
|
```python
|
104
155
|
import modal
|
105
156
|
|
106
|
-
app = modal.App()
|
157
|
+
app = modal.App()
|
107
158
|
|
108
159
|
@app.function(
|
109
160
|
secrets=[modal.Secret.from_name("some_secret")],
|
@@ -116,18 +167,25 @@ class _App:
|
|
116
167
|
In this example, the secret and schedule are registered with the app.
|
117
168
|
"""
|
118
169
|
|
170
|
+
_all_apps: ClassVar[dict[Optional[str], list["_App"]]] = {}
|
171
|
+
_container_app: ClassVar[Optional["_App"]] = None
|
172
|
+
|
119
173
|
_name: Optional[str]
|
120
174
|
_description: Optional[str]
|
121
|
-
|
122
|
-
|
175
|
+
_functions: dict[str, _Function]
|
176
|
+
_classes: dict[str, _Cls]
|
177
|
+
|
178
|
+
_image: Optional[_Image]
|
123
179
|
_mounts: Sequence[_Mount]
|
124
180
|
_secrets: Sequence[_Secret]
|
125
|
-
_volumes:
|
126
|
-
_web_endpoints:
|
127
|
-
_local_entrypoints:
|
128
|
-
|
181
|
+
_volumes: dict[Union[str, PurePosixPath], _Volume]
|
182
|
+
_web_endpoints: list[str] # Used by the CLI
|
183
|
+
_local_entrypoints: dict[str, _LocalEntrypoint]
|
184
|
+
|
185
|
+
# Running apps only (container apps or running local)
|
186
|
+
_app_id: Optional[str] # Kept after app finishes
|
187
|
+
_running_app: Optional[RunningApp] # Various app info
|
129
188
|
_client: Optional[_Client]
|
130
|
-
_all_apps: ClassVar[Dict[Optional[str], List["_App"]]] = {}
|
131
189
|
|
132
190
|
def __init__(
|
133
191
|
self,
|
@@ -136,19 +194,19 @@ class _App:
|
|
136
194
|
image: Optional[_Image] = None, # default image for all functions (default is `modal.Image.debian_slim()`)
|
137
195
|
mounts: Sequence[_Mount] = [], # default mounts for all functions
|
138
196
|
secrets: Sequence[_Secret] = [], # default secrets for all functions
|
139
|
-
volumes:
|
140
|
-
**kwargs: _Object, # DEPRECATED: passing additional objects to the stub as kwargs is no longer supported
|
197
|
+
volumes: dict[Union[str, PurePosixPath], _Volume] = {}, # default volumes for all functions
|
141
198
|
) -> None:
|
142
199
|
"""Construct a new app, optionally with default image, mounts, secrets, or volumes.
|
143
200
|
|
144
201
|
```python notest
|
145
202
|
image = modal.Image.debian_slim().pip_install(...)
|
146
|
-
mount = modal.Mount.from_local_dir("./config")
|
147
203
|
secret = modal.Secret.from_name("my-secret")
|
148
204
|
volume = modal.Volume.from_name("my-data")
|
149
|
-
app = modal.App(image=image,
|
205
|
+
app = modal.App(image=image, secrets=[secret], volumes={"/mnt/data": volume})
|
150
206
|
```
|
151
207
|
"""
|
208
|
+
if name is not None and not isinstance(name, str):
|
209
|
+
raise InvalidError("Invalid value for `name`: Must be string.")
|
152
210
|
|
153
211
|
self._name = name
|
154
212
|
self._description = name
|
@@ -160,27 +218,16 @@ class _App:
|
|
160
218
|
if image is not None and not isinstance(image, _Image):
|
161
219
|
raise InvalidError("image has to be a modal Image or AioImage object")
|
162
220
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
"Passing additional objects to the app constructor is deprecated."
|
167
|
-
f" Please remove the following parameters from your app definition: {', '.join(kwargs)}."
|
168
|
-
" In most cases, persistent (named) objects can just be defined in the global scope.",
|
169
|
-
)
|
170
|
-
|
171
|
-
for k, v in kwargs.items():
|
172
|
-
self._validate_blueprint_value(k, v)
|
173
|
-
|
174
|
-
self._indexed_objects = kwargs
|
175
|
-
if image is not None:
|
176
|
-
self._indexed_objects["image"] = image # backward compatibility since "image" used to be on the blueprint
|
177
|
-
|
221
|
+
self._functions = {}
|
222
|
+
self._classes = {}
|
223
|
+
self._image = image
|
178
224
|
self._mounts = mounts
|
179
|
-
|
180
225
|
self._secrets = secrets
|
181
226
|
self._volumes = volumes
|
182
227
|
self._local_entrypoints = {}
|
183
228
|
self._web_endpoints = []
|
229
|
+
|
230
|
+
self._app_id = None
|
184
231
|
self._running_app = None # Set inside container, OR during the time an app is running locally
|
185
232
|
self._client = None
|
186
233
|
|
@@ -203,17 +250,52 @@ class _App:
|
|
203
250
|
|
204
251
|
@property
|
205
252
|
def app_id(self) -> Optional[str]:
|
206
|
-
"""Return the app_id
|
207
|
-
|
208
|
-
return self._running_app.app_id
|
209
|
-
else:
|
210
|
-
return None
|
253
|
+
"""Return the app_id of a running or stopped app."""
|
254
|
+
return self._app_id
|
211
255
|
|
212
256
|
@property
|
213
257
|
def description(self) -> Optional[str]:
|
214
258
|
"""The App's `name`, if available, or a fallback descriptive identifier."""
|
215
259
|
return self._description
|
216
260
|
|
261
|
+
@staticmethod
|
262
|
+
@renamed_parameter((2024, 12, 18), "label", "name")
|
263
|
+
async def lookup(
|
264
|
+
name: str,
|
265
|
+
client: Optional[_Client] = None,
|
266
|
+
environment_name: Optional[str] = None,
|
267
|
+
create_if_missing: bool = False,
|
268
|
+
) -> "_App":
|
269
|
+
"""Look up an App with a given name, creating a new App if necessary.
|
270
|
+
|
271
|
+
Note that Apps created through this method will be in a deployed state,
|
272
|
+
but they will not have any associated Functions or Classes. This method
|
273
|
+
is mainly useful for creating an App to associate with a Sandbox:
|
274
|
+
|
275
|
+
```python
|
276
|
+
app = modal.App.lookup("my-app", create_if_missing=True)
|
277
|
+
modal.Sandbox.create("echo", "hi", app=app)
|
278
|
+
```
|
279
|
+
"""
|
280
|
+
if client is None:
|
281
|
+
client = await _Client.from_env()
|
282
|
+
|
283
|
+
environment_name = _get_environment_name(environment_name)
|
284
|
+
|
285
|
+
request = api_pb2.AppGetOrCreateRequest(
|
286
|
+
app_name=name,
|
287
|
+
environment_name=environment_name,
|
288
|
+
object_creation_type=(api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING if create_if_missing else None),
|
289
|
+
)
|
290
|
+
|
291
|
+
response = await retry_transient_errors(client.stub.AppGetOrCreate, request)
|
292
|
+
|
293
|
+
app = _App(name)
|
294
|
+
app._app_id = response.app_id
|
295
|
+
app._client = client
|
296
|
+
app._running_app = RunningApp(response.app_id, interactive=False)
|
297
|
+
return app
|
298
|
+
|
217
299
|
def set_description(self, description: str):
|
218
300
|
self._description = description
|
219
301
|
|
@@ -221,53 +303,22 @@ class _App:
|
|
221
303
|
if not isinstance(value, _Object):
|
222
304
|
raise InvalidError(f"App attribute `{key}` with value {value!r} is not a valid Modal object")
|
223
305
|
|
224
|
-
def _add_object(self, tag, obj):
|
225
|
-
if self._running_app:
|
226
|
-
# If this is inside a container, then objects can be defined after app initialization.
|
227
|
-
# So we may have to initialize objects once they get bound to the app.
|
228
|
-
if tag in self._running_app.tag_to_object_id:
|
229
|
-
object_id: str = self._running_app.tag_to_object_id[tag]
|
230
|
-
metadata: Message = self._running_app.object_handle_metadata[object_id]
|
231
|
-
obj._hydrate(object_id, self._client, metadata)
|
232
|
-
|
233
|
-
self._indexed_objects[tag] = obj
|
234
|
-
|
235
306
|
def __getitem__(self, tag: str):
|
236
|
-
|
237
|
-
|
238
|
-
The only use cases for these assignments is in conjunction with `.new()`, which is now
|
239
|
-
in itself deprecated. If you are constructing objects with `.from_name(...)`, there is no
|
240
|
-
need to assign those objects to the app. Example:
|
241
|
-
|
242
|
-
```python
|
243
|
-
d = modal.Dict.from_name("my-dict", create_if_missing=True)
|
244
|
-
|
245
|
-
@app.function()
|
246
|
-
def f(x, y):
|
247
|
-
d[x] = y # Refer to d in global scope
|
248
|
-
```
|
249
|
-
"""
|
250
|
-
deprecation_warning((2024, 3, 25), _App.__getitem__.__doc__)
|
251
|
-
return self._indexed_objects[tag]
|
307
|
+
deprecation_error((2024, 3, 25), _app_attr_error)
|
252
308
|
|
253
309
|
def __setitem__(self, tag: str, obj: _Object):
|
254
|
-
|
255
|
-
self._validate_blueprint_value(tag, obj)
|
256
|
-
# Deprecated ?
|
257
|
-
self._add_object(tag, obj)
|
310
|
+
deprecation_error((2024, 3, 25), _app_attr_error)
|
258
311
|
|
259
|
-
def __getattr__(self, tag: str)
|
312
|
+
def __getattr__(self, tag: str):
|
260
313
|
# TODO(erikbern): remove this method later
|
261
314
|
assert isinstance(tag, str)
|
262
315
|
if tag.startswith("__"):
|
263
316
|
# Hacky way to avoid certain issues, e.g. pickle will try to look this up
|
264
317
|
raise AttributeError(f"App has no member {tag}")
|
265
|
-
if tag not in self.
|
318
|
+
if tag not in self._functions or tag not in self._classes:
|
266
319
|
# Primarily to make hasattr work
|
267
320
|
raise AttributeError(f"App has no member {tag}")
|
268
|
-
|
269
|
-
deprecation_warning((2024, 3, 25), _App.__getitem__.__doc__)
|
270
|
-
return obj
|
321
|
+
deprecation_error((2024, 3, 25), _app_attr_error)
|
271
322
|
|
272
323
|
def __setattr__(self, tag: str, obj: _Object):
|
273
324
|
# TODO(erikbern): remove this method later
|
@@ -276,55 +327,44 @@ class _App:
|
|
276
327
|
if tag in self.__annotations__:
|
277
328
|
object.__setattr__(self, tag, obj)
|
278
329
|
elif tag == "image":
|
279
|
-
self.
|
330
|
+
self._image = obj
|
280
331
|
else:
|
281
|
-
|
282
|
-
deprecation_warning((2024, 3, 25), _App.__getitem__.__doc__)
|
283
|
-
self._add_object(tag, obj)
|
332
|
+
deprecation_error((2024, 3, 25), _app_attr_error)
|
284
333
|
|
285
334
|
@property
|
286
335
|
def image(self) -> _Image:
|
287
|
-
|
288
|
-
# Will also keep this one after we remove [get/set][item/attr]
|
289
|
-
return self._indexed_objects["image"]
|
336
|
+
return self._image
|
290
337
|
|
291
338
|
@image.setter
|
292
339
|
def image(self, value):
|
293
|
-
self.
|
340
|
+
self._image = value
|
294
341
|
|
295
342
|
def _uncreate_all_objects(self):
|
296
343
|
# TODO(erikbern): this doesn't unhydrate objects that aren't tagged
|
297
|
-
for obj in self.
|
344
|
+
for obj in self._functions.values():
|
345
|
+
obj._unhydrate()
|
346
|
+
for obj in self._classes.values():
|
298
347
|
obj._unhydrate()
|
299
|
-
|
300
|
-
def is_inside(self, image: Optional[_Image] = None):
|
301
|
-
"""Deprecated: use `Image.imports()` instead! Usage:
|
302
|
-
```
|
303
|
-
my_image = modal.Image.debian_slim().pip_install("torch")
|
304
|
-
with my_image.imports():
|
305
|
-
import torch
|
306
|
-
```
|
307
|
-
"""
|
308
|
-
deprecation_error((2023, 11, 8), _App.is_inside.__doc__)
|
309
348
|
|
310
349
|
@asynccontextmanager
|
311
|
-
async def _set_local_app(self, client: _Client,
|
350
|
+
async def _set_local_app(self, client: _Client, running_app: RunningApp) -> AsyncGenerator[None, None]:
|
351
|
+
self._app_id = running_app.app_id
|
352
|
+
self._running_app = running_app
|
312
353
|
self._client = client
|
313
|
-
self._running_app = app
|
314
354
|
try:
|
315
355
|
yield
|
316
356
|
finally:
|
317
|
-
self._client = None
|
318
357
|
self._running_app = None
|
358
|
+
self._client = None
|
359
|
+
self._uncreate_all_objects()
|
319
360
|
|
320
361
|
@asynccontextmanager
|
321
362
|
async def run(
|
322
363
|
self,
|
323
364
|
client: Optional[_Client] = None,
|
324
|
-
|
325
|
-
show_progress: bool = True,
|
365
|
+
show_progress: Optional[bool] = None,
|
326
366
|
detach: bool = False,
|
327
|
-
|
367
|
+
interactive: bool = False,
|
328
368
|
) -> AsyncGenerator["_App", None]:
|
329
369
|
"""Context manager that runs an app on Modal.
|
330
370
|
|
@@ -332,82 +372,163 @@ class _App:
|
|
332
372
|
to Modal functions should be made within the scope of this context
|
333
373
|
manager, and they will correspond to the current app.
|
334
374
|
|
375
|
+
**Example**
|
376
|
+
|
377
|
+
```python notest
|
378
|
+
with app.run():
|
379
|
+
some_modal_function.remote()
|
380
|
+
```
|
381
|
+
|
382
|
+
To enable output printing, use `modal.enable_output()`:
|
383
|
+
|
384
|
+
```python notest
|
385
|
+
with modal.enable_output():
|
386
|
+
with app.run():
|
387
|
+
some_modal_function.remote()
|
388
|
+
```
|
389
|
+
|
390
|
+
Note that you cannot invoke this in global scope of a file where you have
|
391
|
+
Modal functions or Classes, since that would run the block when the function
|
392
|
+
or class is imported in your containers as well. If you want to run it as
|
393
|
+
your entrypoint, consider wrapping it:
|
394
|
+
|
395
|
+
```python
|
396
|
+
if __name__ == "__main__":
|
397
|
+
with app.run():
|
398
|
+
some_modal_function.remote()
|
399
|
+
```
|
400
|
+
|
401
|
+
You can then run your script with:
|
402
|
+
|
403
|
+
```shell
|
404
|
+
python app_module.py
|
405
|
+
```
|
406
|
+
|
335
407
|
Note that this method used to return a separate "App" object. This is
|
336
408
|
no longer useful since you can use the app itself for access to all
|
337
409
|
objects. For backwards compatibility reasons, it returns the same app.
|
338
410
|
"""
|
339
|
-
#
|
340
|
-
|
411
|
+
from .runner import _run_app # Defer import of runner.py, which imports a lot from Rich
|
412
|
+
|
413
|
+
# See Github discussion here: https://github.com/modal-labs/modal-client/pull/2030#issuecomment-2237266186
|
414
|
+
|
415
|
+
if show_progress is True:
|
416
|
+
deprecation_error(
|
417
|
+
(2024, 11, 20),
|
418
|
+
"`show_progress=True` is no longer supported. Use `with modal.enable_output():` instead.",
|
419
|
+
)
|
420
|
+
elif show_progress is False:
|
421
|
+
deprecation_warning((2024, 11, 20), "`show_progress=False` is deprecated (and has no effect)")
|
422
|
+
|
423
|
+
async with _run_app(self, client=client, detach=detach, interactive=interactive):
|
341
424
|
yield self
|
342
425
|
|
343
426
|
def _get_default_image(self):
|
344
|
-
if
|
345
|
-
return self.
|
427
|
+
if self._image:
|
428
|
+
return self._image
|
346
429
|
else:
|
347
430
|
return _default_image
|
348
431
|
|
349
432
|
def _get_watch_mounts(self):
|
433
|
+
if not self._running_app:
|
434
|
+
raise ExecutionError("`_get_watch_mounts` requires a running app.")
|
435
|
+
|
350
436
|
all_mounts = [
|
351
437
|
*self._mounts,
|
352
438
|
]
|
353
439
|
for function in self.registered_functions.values():
|
354
|
-
all_mounts.extend(function.
|
440
|
+
all_mounts.extend(function._serve_mounts)
|
355
441
|
|
356
442
|
return [m for m in all_mounts if m.is_local()]
|
357
443
|
|
358
|
-
def _add_function(self, function: _Function):
|
359
|
-
if function.tag in self.
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
444
|
+
def _add_function(self, function: _Function, is_web_endpoint: bool):
|
445
|
+
if function.tag in self._functions:
|
446
|
+
if not is_notebook():
|
447
|
+
old_function: _Function = self._functions[function.tag]
|
448
|
+
logger.warning(
|
449
|
+
f"Warning: Tag '{function.tag}' collision!"
|
450
|
+
" Overriding existing function "
|
451
|
+
f"[{old_function._info.module_name}].{old_function._info.function_name}"
|
452
|
+
f" with new function [{function._info.module_name}].{function._info.function_name}"
|
453
|
+
)
|
454
|
+
if function.tag in self._classes:
|
455
|
+
logger.warning(f"Warning: tag {function.tag} exists but is overridden by function")
|
456
|
+
|
457
|
+
if self._running_app:
|
458
|
+
# If this is inside a container, then objects can be defined after app initialization.
|
459
|
+
# So we may have to initialize objects once they get bound to the app.
|
460
|
+
if function.tag in self._running_app.function_ids:
|
461
|
+
object_id: str = self._running_app.function_ids[function.tag]
|
462
|
+
metadata: Message = self._running_app.object_handle_metadata[object_id]
|
463
|
+
function._hydrate(object_id, self._client, metadata)
|
464
|
+
|
465
|
+
self._functions[function.tag] = function
|
466
|
+
if is_web_endpoint:
|
467
|
+
self._web_endpoints.append(function.tag)
|
468
|
+
|
469
|
+
def _add_class(self, tag: str, cls: _Cls):
|
470
|
+
if self._running_app:
|
471
|
+
# If this is inside a container, then objects can be defined after app initialization.
|
472
|
+
# So we may have to initialize objects once they get bound to the app.
|
473
|
+
if tag in self._running_app.class_ids:
|
474
|
+
object_id: str = self._running_app.class_ids[tag]
|
475
|
+
metadata: Message = self._running_app.object_handle_metadata[object_id]
|
476
|
+
cls._hydrate(object_id, self._client, metadata)
|
370
477
|
|
371
|
-
self.
|
478
|
+
self._classes[tag] = cls
|
372
479
|
|
373
480
|
def _init_container(self, client: _Client, running_app: RunningApp):
|
374
|
-
self.
|
481
|
+
self._app_id = running_app.app_id
|
375
482
|
self._running_app = running_app
|
483
|
+
self._client = client
|
376
484
|
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
485
|
+
_App._container_app = self
|
486
|
+
|
487
|
+
# Hydrate function objects
|
488
|
+
for tag, object_id in running_app.function_ids.items():
|
489
|
+
if tag in self._functions:
|
490
|
+
obj = self._functions[tag]
|
491
|
+
handle_metadata = running_app.object_handle_metadata[object_id]
|
492
|
+
obj._hydrate(object_id, client, handle_metadata)
|
493
|
+
|
494
|
+
# Hydrate class objects
|
495
|
+
for tag, object_id in running_app.class_ids.items():
|
496
|
+
if tag in self._classes:
|
497
|
+
obj = self._classes[tag]
|
381
498
|
handle_metadata = running_app.object_handle_metadata[object_id]
|
382
499
|
obj._hydrate(object_id, client, handle_metadata)
|
383
500
|
|
384
501
|
@property
|
385
|
-
def registered_functions(self) ->
|
502
|
+
def registered_functions(self) -> dict[str, _Function]:
|
386
503
|
"""All modal.Function objects registered on the app."""
|
387
|
-
return
|
504
|
+
return self._functions
|
388
505
|
|
389
506
|
@property
|
390
|
-
def registered_classes(self) ->
|
507
|
+
def registered_classes(self) -> dict[str, _Function]:
|
391
508
|
"""All modal.Cls objects registered on the app."""
|
392
|
-
return
|
509
|
+
return self._classes
|
393
510
|
|
394
511
|
@property
|
395
|
-
def registered_entrypoints(self) ->
|
512
|
+
def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]:
|
396
513
|
"""All local CLI entrypoints registered on the app."""
|
397
514
|
return self._local_entrypoints
|
398
515
|
|
399
516
|
@property
|
400
|
-
def indexed_objects(self) ->
|
401
|
-
|
517
|
+
def indexed_objects(self) -> dict[str, _Object]:
|
518
|
+
deprecation_warning(
|
519
|
+
(2024, 11, 25),
|
520
|
+
"`app.indexed_objects` is deprecated! Use `app.registered_functions` or `app.registered_classes` instead.",
|
521
|
+
)
|
522
|
+
return dict(**self._functions, **self._classes)
|
402
523
|
|
403
524
|
@property
|
404
|
-
def registered_web_endpoints(self) ->
|
525
|
+
def registered_web_endpoints(self) -> list[str]:
|
405
526
|
"""Names of web endpoint (ie. webhook) functions registered on the app."""
|
406
527
|
return self._web_endpoints
|
407
528
|
|
408
529
|
def local_entrypoint(
|
409
|
-
self, _warn_parentheses_missing=None, *, name: Optional[str] = None
|
410
|
-
) -> Callable[[Callable[..., Any]],
|
530
|
+
self, _warn_parentheses_missing: Any = None, *, name: Optional[str] = None
|
531
|
+
) -> Callable[[Callable[..., Any]], _LocalEntrypoint]:
|
411
532
|
"""Decorate a function to be used as a CLI entrypoint for a Modal App.
|
412
533
|
|
413
534
|
These functions can be used to define code that runs locally to set up the app,
|
@@ -443,7 +564,8 @@ class _App:
|
|
443
564
|
**Parsing Arguments**
|
444
565
|
|
445
566
|
If your entrypoint function take arguments with primitive types, `modal run` automatically parses them as
|
446
|
-
CLI options.
|
567
|
+
CLI options.
|
568
|
+
For example, the following function can be called with `modal run app_module.py --foo 1 --bar "hello"`:
|
447
569
|
|
448
570
|
```python
|
449
571
|
@app.local_entrypoint()
|
@@ -451,8 +573,8 @@ class _App:
|
|
451
573
|
some_modal_function.call(foo, bar)
|
452
574
|
```
|
453
575
|
|
454
|
-
Currently, `str`, `int`, `float`, `bool`, and `datetime.datetime` are supported.
|
455
|
-
information on usage.
|
576
|
+
Currently, `str`, `int`, `float`, `bool`, and `datetime.datetime` are supported.
|
577
|
+
Use `modal run app_module.py --help` for more information on usage.
|
456
578
|
|
457
579
|
"""
|
458
580
|
if _warn_parentheses_missing:
|
@@ -460,7 +582,7 @@ class _App:
|
|
460
582
|
if name is not None and not isinstance(name, str):
|
461
583
|
raise InvalidError("Invalid value for `name`: Must be string.")
|
462
584
|
|
463
|
-
def wrapped(raw_f: Callable[..., Any]) ->
|
585
|
+
def wrapped(raw_f: Callable[..., Any]) -> _LocalEntrypoint:
|
464
586
|
info = FunctionInfo(raw_f)
|
465
587
|
tag = name if name is not None else raw_f.__qualname__
|
466
588
|
if tag in self._local_entrypoints:
|
@@ -473,30 +595,36 @@ class _App:
|
|
473
595
|
|
474
596
|
def function(
|
475
597
|
self,
|
476
|
-
_warn_parentheses_missing=None,
|
598
|
+
_warn_parentheses_missing: Any = None,
|
477
599
|
*,
|
478
600
|
image: Optional[_Image] = None, # The image to run as the container for the function
|
479
601
|
schedule: Optional[Schedule] = None, # An optional Modal Schedule for the function
|
480
602
|
secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
|
481
|
-
gpu:
|
603
|
+
gpu: Union[
|
604
|
+
GPU_T, list[GPU_T]
|
605
|
+
] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
|
482
606
|
serialized: bool = False, # Whether to send the function over using cloudpickle.
|
483
607
|
mounts: Sequence[_Mount] = (), # Modal Mounts added to the container
|
484
|
-
network_file_systems:
|
608
|
+
network_file_systems: dict[
|
485
609
|
Union[str, PurePosixPath], _NetworkFileSystem
|
486
610
|
] = {}, # Mountpoints for Modal NetworkFileSystems
|
487
|
-
volumes:
|
611
|
+
volumes: dict[
|
488
612
|
Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
|
489
613
|
] = {}, # Mount points for Modal Volumes & CloudBucketMounts
|
490
614
|
allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
615
|
+
# Specify, in fractional CPU cores, how many CPU cores to request.
|
616
|
+
# Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
|
617
|
+
# CPU throttling will prevent a container from exceeding its specified limit.
|
618
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
619
|
+
# Specify, in MiB, a memory request which is the minimum memory required.
|
620
|
+
# Or, pass (request, limit) to additionally specify a hard limit in MiB.
|
621
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
622
|
+
ephemeral_disk: Optional[int] = None, # Specify, in MiB, the ephemeral disk size for the Function.
|
495
623
|
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
496
624
|
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
497
625
|
concurrency_limit: Optional[
|
498
626
|
int
|
499
|
-
] = None, # An optional maximum number of concurrent containers running the function (
|
627
|
+
] = None, # An optional maximum number of concurrent containers running the function (keep_warm sets minimum).
|
500
628
|
allow_concurrent_inputs: Optional[int] = None, # Number of inputs the container may fetch to run concurrently.
|
501
629
|
container_idle_timeout: Optional[int] = None, # Timeout for idle containers waiting for inputs to shut down.
|
502
630
|
timeout: Optional[int] = None, # Maximum execution time of the function in seconds.
|
@@ -508,82 +636,131 @@ class _App:
|
|
508
636
|
bool
|
509
637
|
] = None, # Set this to True if it's a non-generator function returning a [sync/async] generator object
|
510
638
|
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
639
|
+
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
|
511
640
|
enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
|
512
|
-
checkpointing_enabled: Optional[bool] = None, # Deprecated
|
513
641
|
block_network: bool = False, # Whether to block network access
|
514
|
-
|
515
|
-
|
516
|
-
] = None,
|
517
|
-
|
518
|
-
interactive: bool = False, # Deprecated: use the `modal.interact()` hook instead
|
519
|
-
secret: Optional[_Secret] = None, # Deprecated: use `secrets`
|
642
|
+
# Maximum number of inputs a container should handle before shutting down.
|
643
|
+
# With `max_inputs = 1`, containers will be single-use.
|
644
|
+
max_inputs: Optional[int] = None,
|
645
|
+
i6pn: Optional[bool] = None, # Whether to enable IPv6 container networking within the region.
|
520
646
|
# Parameters below here are experimental. Use with caution!
|
521
|
-
_allow_background_volume_commits: bool = False, # Experimental flag
|
522
|
-
_experimental_boost: bool = False, # Experimental flag for lower latency function execution (alpha).
|
523
|
-
_experimental_scheduler: bool = False, # Experimental flag for more fine-grained scheduling (alpha).
|
524
647
|
_experimental_scheduler_placement: Optional[
|
525
648
|
SchedulerPlacement
|
526
649
|
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
527
|
-
|
528
|
-
|
650
|
+
_experimental_buffer_containers: Optional[int] = None, # Number of additional, idle containers to keep around.
|
651
|
+
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
652
|
+
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
653
|
+
) -> _FunctionDecoratorType:
|
654
|
+
"""Decorator to register a new Modal [Function](/docs/reference/modal.Function) with this App."""
|
529
655
|
if isinstance(_warn_parentheses_missing, _Image):
|
530
656
|
# Handle edge case where maybe (?) some users passed image as a positional arg
|
531
657
|
raise InvalidError("`image` needs to be a keyword argument: `@app.function(image=image)`.")
|
532
658
|
if _warn_parentheses_missing:
|
533
659
|
raise InvalidError("Did you forget parentheses? Suggestion: `@app.function()`.")
|
534
660
|
|
535
|
-
if interactive:
|
536
|
-
deprecation_error(
|
537
|
-
(2024, 2, 29), "interactive=True has been deprecated. Set MODAL_INTERACTIVE_FUNCTIONS=1 instead."
|
538
|
-
)
|
539
|
-
|
540
661
|
if image is None:
|
541
662
|
image = self._get_default_image()
|
542
663
|
|
543
664
|
secrets = [*self._secrets, *secrets]
|
544
665
|
|
545
666
|
def wrapped(
|
546
|
-
f: Union[_PartialFunction, Callable[..., Any]],
|
547
|
-
_cls: Optional[type] = None, # Used for methods only
|
667
|
+
f: Union[_PartialFunction, Callable[..., Any], None],
|
548
668
|
) -> _Function:
|
549
|
-
nonlocal keep_warm, is_generator
|
669
|
+
nonlocal keep_warm, is_generator, cloud, serialized
|
670
|
+
|
671
|
+
# Check if the decorated object is a class
|
672
|
+
if inspect.isclass(f):
|
673
|
+
raise TypeError(
|
674
|
+
"The `@app.function` decorator cannot be used on a class. Please use `@app.cls` instead."
|
675
|
+
)
|
550
676
|
|
551
677
|
if isinstance(f, _PartialFunction):
|
678
|
+
# typically for @function-wrapped @web_endpoint, @asgi_app, or @batched
|
552
679
|
f.wrapped = True
|
553
|
-
|
680
|
+
|
681
|
+
# but we don't support @app.function wrapping a method.
|
682
|
+
if is_method_fn(f.raw_f.__qualname__):
|
683
|
+
raise InvalidError(
|
684
|
+
"The `@app.function` decorator cannot be used on class methods. "
|
685
|
+
"Swap with `@modal.method` or `@modal.web_endpoint`, or drop the `@app.function` decorator. "
|
686
|
+
"Example: "
|
687
|
+
"\n\n"
|
688
|
+
"```python\n"
|
689
|
+
"@app.cls()\n"
|
690
|
+
"class MyClass:\n"
|
691
|
+
" @modal.web_endpoint()\n"
|
692
|
+
" def f(self, x):\n"
|
693
|
+
" ...\n"
|
694
|
+
"```\n"
|
695
|
+
)
|
696
|
+
i6pn_enabled = i6pn or (f.flags & _PartialFunctionFlags.CLUSTERED)
|
697
|
+
cluster_size = f.cluster_size # Experimental: Clustered functions
|
698
|
+
|
699
|
+
info = FunctionInfo(f.raw_f, serialized=serialized, name_override=name)
|
554
700
|
raw_f = f.raw_f
|
555
701
|
webhook_config = f.webhook_config
|
556
702
|
is_generator = f.is_generator
|
557
703
|
keep_warm = f.keep_warm or keep_warm
|
558
|
-
|
559
|
-
|
560
|
-
if interactive:
|
561
|
-
raise InvalidError("interactive=True is not supported with web endpoint functions")
|
562
|
-
self._web_endpoints.append(info.get_tag())
|
704
|
+
batch_max_size = f.batch_max_size
|
705
|
+
batch_wait_ms = f.batch_wait_ms
|
563
706
|
else:
|
564
|
-
|
707
|
+
if not is_global_object(f.__qualname__) and not serialized:
|
708
|
+
raise InvalidError(
|
709
|
+
dedent(
|
710
|
+
"""
|
711
|
+
The `@app.function` decorator must apply to functions in global scope,
|
712
|
+
unless `serialize=True` is set.
|
713
|
+
If trying to apply additional decorators, they may need to use `functools.wraps`.
|
714
|
+
"""
|
715
|
+
)
|
716
|
+
)
|
717
|
+
|
718
|
+
if is_method_fn(f.__qualname__):
|
719
|
+
raise InvalidError(
|
720
|
+
dedent(
|
721
|
+
"""
|
722
|
+
The `@app.function` decorator cannot be used on class methods.
|
723
|
+
Please use `@app.cls` with `@modal.method` instead. Example:
|
724
|
+
|
725
|
+
```python
|
726
|
+
@app.cls()
|
727
|
+
class MyClass:
|
728
|
+
@modal.method()
|
729
|
+
def f(self, x):
|
730
|
+
...
|
731
|
+
```
|
732
|
+
"""
|
733
|
+
)
|
734
|
+
)
|
735
|
+
|
736
|
+
info = FunctionInfo(f, serialized=serialized, name_override=name)
|
565
737
|
webhook_config = None
|
738
|
+
batch_max_size = None
|
739
|
+
batch_wait_ms = None
|
566
740
|
raw_f = f
|
567
741
|
|
742
|
+
cluster_size = None # Experimental: Clustered functions
|
743
|
+
i6pn_enabled = i6pn
|
744
|
+
|
568
745
|
if info.function_name.endswith(".app"):
|
569
746
|
warnings.warn(
|
570
747
|
"Beware: the function name is `app`. Modal will soon rename `Stub` to `App`, "
|
571
748
|
"so you might run into issues if you have code like `app = modal.App()` in the same scope"
|
572
749
|
)
|
573
750
|
|
574
|
-
if not _cls and not info.is_serialized() and "." in info.function_name: # This is a method
|
575
|
-
raise InvalidError(
|
576
|
-
"`app.function` on methods is not allowed. See https://modal.com/docs/guide/lifecycle-functions instead"
|
577
|
-
)
|
578
|
-
|
579
751
|
if is_generator is None:
|
580
752
|
is_generator = inspect.isgeneratorfunction(raw_f) or inspect.isasyncgenfunction(raw_f)
|
581
753
|
|
754
|
+
scheduler_placement: Optional[SchedulerPlacement] = _experimental_scheduler_placement
|
755
|
+
if region:
|
756
|
+
if scheduler_placement:
|
757
|
+
raise InvalidError("`region` and `_experimental_scheduler_placement` cannot be used together")
|
758
|
+
scheduler_placement = SchedulerPlacement(region=region)
|
759
|
+
|
582
760
|
function = _Function.from_args(
|
583
761
|
info,
|
584
762
|
app=self,
|
585
763
|
image=image,
|
586
|
-
secret=secret,
|
587
764
|
secrets=secrets,
|
588
765
|
schedule=schedule,
|
589
766
|
is_generator=is_generator,
|
@@ -592,52 +769,63 @@ class _App:
|
|
592
769
|
network_file_systems=network_file_systems,
|
593
770
|
allow_cross_region_volumes=allow_cross_region_volumes,
|
594
771
|
volumes={**self._volumes, **volumes},
|
772
|
+
cpu=cpu,
|
595
773
|
memory=memory,
|
774
|
+
ephemeral_disk=ephemeral_disk,
|
596
775
|
proxy=proxy,
|
597
776
|
retries=retries,
|
598
777
|
concurrency_limit=concurrency_limit,
|
599
778
|
allow_concurrent_inputs=allow_concurrent_inputs,
|
779
|
+
batch_max_size=batch_max_size,
|
780
|
+
batch_wait_ms=batch_wait_ms,
|
600
781
|
container_idle_timeout=container_idle_timeout,
|
601
782
|
timeout=timeout,
|
602
|
-
cpu=cpu,
|
603
783
|
keep_warm=keep_warm,
|
604
784
|
cloud=cloud,
|
605
785
|
webhook_config=webhook_config,
|
606
786
|
enable_memory_snapshot=enable_memory_snapshot,
|
607
|
-
checkpointing_enabled=checkpointing_enabled,
|
608
|
-
allow_background_volume_commits=_allow_background_volume_commits,
|
609
787
|
block_network=block_network,
|
610
788
|
max_inputs=max_inputs,
|
611
|
-
|
612
|
-
|
613
|
-
|
789
|
+
scheduler_placement=scheduler_placement,
|
790
|
+
_experimental_buffer_containers=_experimental_buffer_containers,
|
791
|
+
_experimental_proxy_ip=_experimental_proxy_ip,
|
792
|
+
i6pn_enabled=i6pn_enabled,
|
793
|
+
cluster_size=cluster_size, # Experimental: Clustered functions
|
614
794
|
)
|
615
795
|
|
616
|
-
self._add_function(function)
|
796
|
+
self._add_function(function, webhook_config is not None)
|
797
|
+
|
617
798
|
return function
|
618
799
|
|
619
800
|
return wrapped
|
620
801
|
|
802
|
+
@typing_extensions.dataclass_transform(field_specifiers=(parameter,), kw_only_default=True)
|
621
803
|
def cls(
|
622
804
|
self,
|
623
|
-
_warn_parentheses_missing=None,
|
805
|
+
_warn_parentheses_missing: Optional[bool] = None,
|
624
806
|
*,
|
625
807
|
image: Optional[_Image] = None, # The image to run as the container for the function
|
626
808
|
secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
|
627
|
-
gpu:
|
809
|
+
gpu: Union[
|
810
|
+
GPU_T, list[GPU_T]
|
811
|
+
] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
|
628
812
|
serialized: bool = False, # Whether to send the function over using cloudpickle.
|
629
813
|
mounts: Sequence[_Mount] = (),
|
630
|
-
network_file_systems:
|
814
|
+
network_file_systems: dict[
|
631
815
|
Union[str, PurePosixPath], _NetworkFileSystem
|
632
816
|
] = {}, # Mountpoints for Modal NetworkFileSystems
|
633
|
-
volumes:
|
817
|
+
volumes: dict[
|
634
818
|
Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
|
635
819
|
] = {}, # Mount points for Modal Volumes & CloudBucketMounts
|
636
820
|
allow_cross_region_volumes: bool = False, # Whether using network file systems from other regions is allowed.
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
821
|
+
# Specify, in fractional CPU cores, how many CPU cores to request.
|
822
|
+
# Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
|
823
|
+
# CPU throttling will prevent a container from exceeding its specified limit.
|
824
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
825
|
+
# Specify, in MiB, a memory request which is the minimum memory required.
|
826
|
+
# Or, pass (request, limit) to additionally specify a hard limit in MiB.
|
827
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
828
|
+
ephemeral_disk: Optional[int] = None, # Specify, in MiB, the ephemeral disk size for the Function.
|
641
829
|
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
642
830
|
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
643
831
|
concurrency_limit: Optional[int] = None, # Limit for max concurrent containers running the function.
|
@@ -646,76 +834,101 @@ class _App:
|
|
646
834
|
timeout: Optional[int] = None, # Maximum execution time of the function in seconds.
|
647
835
|
keep_warm: Optional[int] = None, # An optional number of containers to always keep warm.
|
648
836
|
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
837
|
+
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
|
649
838
|
enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
|
650
|
-
checkpointing_enabled: Optional[bool] = None, # Deprecated
|
651
839
|
block_network: bool = False, # Whether to block network access
|
652
|
-
|
653
|
-
max_inputs
|
654
|
-
|
655
|
-
] = None, # Limits the number of inputs a container handles before shutting down. Use `max_inputs = 1` for single-use containers.
|
656
|
-
# The next group of parameters are deprecated; do not use in any new code
|
657
|
-
interactive: bool = False, # Deprecated: use the `modal.interact()` hook instead
|
658
|
-
secret: Optional[_Secret] = None, # Deprecated: use `secrets`
|
840
|
+
# Limits the number of inputs a container handles before shutting down.
|
841
|
+
# Use `max_inputs = 1` for single-use containers.
|
842
|
+
max_inputs: Optional[int] = None,
|
659
843
|
# Parameters below here are experimental. Use with caution!
|
660
|
-
_experimental_boost: bool = False, # Experimental flag for lower latency function execution (alpha).
|
661
|
-
_experimental_scheduler: bool = False, # Experimental flag for more fine-grained scheduling (alpha).
|
662
844
|
_experimental_scheduler_placement: Optional[
|
663
845
|
SchedulerPlacement
|
664
846
|
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
665
|
-
|
847
|
+
_experimental_buffer_containers: Optional[int] = None, # Number of additional, idle containers to keep around.
|
848
|
+
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
849
|
+
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
850
|
+
) -> Callable[[CLS_T], CLS_T]:
|
851
|
+
"""
|
852
|
+
Decorator to register a new Modal [Cls](/docs/reference/modal.Cls) with this App.
|
853
|
+
"""
|
666
854
|
if _warn_parentheses_missing:
|
667
855
|
raise InvalidError("Did you forget parentheses? Suggestion: `@app.cls()`.")
|
668
856
|
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
_experimental_scheduler=_experimental_scheduler,
|
697
|
-
_experimental_scheduler_placement=_experimental_scheduler_placement,
|
698
|
-
)
|
699
|
-
|
700
|
-
def wrapper(user_cls: CLS_T) -> _Cls:
|
701
|
-
cls: _Cls = _Cls.from_local(user_cls, self, decorator)
|
857
|
+
scheduler_placement = _experimental_scheduler_placement
|
858
|
+
if region:
|
859
|
+
if scheduler_placement:
|
860
|
+
raise InvalidError("`region` and `_experimental_scheduler_placement` cannot be used together")
|
861
|
+
scheduler_placement = SchedulerPlacement(region=region)
|
862
|
+
|
863
|
+
def wrapper(user_cls: CLS_T) -> CLS_T:
|
864
|
+
nonlocal keep_warm
|
865
|
+
|
866
|
+
# Check if the decorated object is a class
|
867
|
+
if not inspect.isclass(user_cls):
|
868
|
+
raise TypeError("The @app.cls decorator must be used on a class.")
|
869
|
+
|
870
|
+
batch_functions = _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.BATCHED)
|
871
|
+
if batch_functions:
|
872
|
+
if len(batch_functions) > 1:
|
873
|
+
raise InvalidError(f"Modal class {user_cls.__name__} can only have one batched function.")
|
874
|
+
if len(_find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.FUNCTION)) > 1:
|
875
|
+
raise InvalidError(
|
876
|
+
f"Modal class {user_cls.__name__} with a modal batched function cannot have other modal methods." # noqa
|
877
|
+
)
|
878
|
+
batch_function = next(iter(batch_functions.values()))
|
879
|
+
batch_max_size = batch_function.batch_max_size
|
880
|
+
batch_wait_ms = batch_function.batch_wait_ms
|
881
|
+
else:
|
882
|
+
batch_max_size = None
|
883
|
+
batch_wait_ms = None
|
702
884
|
|
703
885
|
if (
|
704
|
-
|
886
|
+
_find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.ENTER_PRE_SNAPSHOT)
|
705
887
|
and not enable_memory_snapshot
|
706
888
|
):
|
707
889
|
raise InvalidError("A class must have `enable_memory_snapshot=True` to use `snap=True` on its methods.")
|
708
890
|
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
)
|
891
|
+
info = FunctionInfo(None, serialized=serialized, user_cls=user_cls)
|
892
|
+
|
893
|
+
cls_func = _Function.from_args(
|
894
|
+
info,
|
895
|
+
app=self,
|
896
|
+
image=image or self._get_default_image(),
|
897
|
+
secrets=[*self._secrets, *secrets],
|
898
|
+
gpu=gpu,
|
899
|
+
mounts=[*self._mounts, *mounts],
|
900
|
+
network_file_systems=network_file_systems,
|
901
|
+
allow_cross_region_volumes=allow_cross_region_volumes,
|
902
|
+
volumes={**self._volumes, **volumes},
|
903
|
+
memory=memory,
|
904
|
+
ephemeral_disk=ephemeral_disk,
|
905
|
+
proxy=proxy,
|
906
|
+
retries=retries,
|
907
|
+
concurrency_limit=concurrency_limit,
|
908
|
+
allow_concurrent_inputs=allow_concurrent_inputs,
|
909
|
+
batch_max_size=batch_max_size,
|
910
|
+
batch_wait_ms=batch_wait_ms,
|
911
|
+
container_idle_timeout=container_idle_timeout,
|
912
|
+
timeout=timeout,
|
913
|
+
cpu=cpu,
|
914
|
+
keep_warm=keep_warm,
|
915
|
+
cloud=cloud,
|
916
|
+
enable_memory_snapshot=enable_memory_snapshot,
|
917
|
+
block_network=block_network,
|
918
|
+
max_inputs=max_inputs,
|
919
|
+
scheduler_placement=scheduler_placement,
|
920
|
+
_experimental_buffer_containers=_experimental_buffer_containers,
|
921
|
+
_experimental_proxy_ip=_experimental_proxy_ip,
|
922
|
+
_experimental_custom_scaling_factor=_experimental_custom_scaling_factor,
|
923
|
+
)
|
924
|
+
|
925
|
+
self._add_function(cls_func, is_web_endpoint=False)
|
926
|
+
|
927
|
+
cls: _Cls = _Cls.from_local(user_cls, self, cls_func)
|
715
928
|
|
716
929
|
tag: str = user_cls.__name__
|
717
|
-
self.
|
718
|
-
return cls
|
930
|
+
self._add_class(tag, cls)
|
931
|
+
return cls # type: ignore # a _Cls instance "simulates" being the user provided class
|
719
932
|
|
720
933
|
return wrapper
|
721
934
|
|
@@ -725,67 +938,42 @@ class _App:
|
|
725
938
|
image: Optional[_Image] = None, # The image to run as the container for the sandbox.
|
726
939
|
mounts: Sequence[_Mount] = (), # Mounts to attach to the sandbox.
|
727
940
|
secrets: Sequence[_Secret] = (), # Environment variables to inject into the sandbox.
|
728
|
-
network_file_systems:
|
941
|
+
network_file_systems: dict[Union[str, PurePosixPath], _NetworkFileSystem] = {},
|
729
942
|
timeout: Optional[int] = None, # Maximum execution time of the sandbox in seconds.
|
730
943
|
workdir: Optional[str] = None, # Working directory of the sandbox.
|
731
944
|
gpu: GPU_T = None,
|
732
945
|
cloud: Optional[str] = None,
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
946
|
+
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the sandbox on.
|
947
|
+
# Specify, in fractional CPU cores, how many CPU cores to request.
|
948
|
+
# Or, pass (request, limit) to additionally specify a hard limit in fractional CPU cores.
|
949
|
+
# CPU throttling will prevent a container from exceeding its specified limit.
|
950
|
+
cpu: Optional[Union[float, tuple[float, float]]] = None,
|
951
|
+
# Specify, in MiB, a memory request which is the minimum memory required.
|
952
|
+
# Or, pass (request, limit) to additionally specify a hard limit in MiB.
|
953
|
+
memory: Optional[Union[int, tuple[int, int]]] = None,
|
737
954
|
block_network: bool = False, # Whether to block network access
|
738
|
-
volumes:
|
955
|
+
volumes: dict[
|
739
956
|
Union[str, PurePosixPath], Union[_Volume, _CloudBucketMount]
|
740
|
-
] = {}, # Mount points for Modal Volumes
|
741
|
-
_allow_background_volume_commits: bool = False,
|
957
|
+
] = {}, # Mount points for Modal Volumes and CloudBucketMounts
|
742
958
|
pty_info: Optional[api_pb2.PTYInfo] = None,
|
743
|
-
_experimental_scheduler: bool = False, # Experimental flag for more fine-grained scheduling (alpha).
|
744
959
|
_experimental_scheduler_placement: Optional[
|
745
960
|
SchedulerPlacement
|
746
961
|
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
747
|
-
) ->
|
748
|
-
"""
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
app_id = self._running_app.app_id
|
756
|
-
environment_name = self._running_app.environment_name
|
757
|
-
client = self._client
|
758
|
-
else:
|
759
|
-
raise InvalidError("`app.spawn_sandbox` requires a running app.")
|
760
|
-
|
761
|
-
# TODO(erikbern): pulling a lot of app internals here, let's clean up shortly
|
762
|
-
resolver = Resolver(client, environment_name=environment_name, app_id=app_id)
|
763
|
-
obj = _Sandbox._new(
|
764
|
-
entrypoint_args,
|
765
|
-
image=image or _default_image,
|
766
|
-
mounts=mounts,
|
767
|
-
secrets=secrets,
|
768
|
-
timeout=timeout,
|
769
|
-
workdir=workdir,
|
770
|
-
gpu=gpu,
|
771
|
-
cloud=cloud,
|
772
|
-
cpu=cpu,
|
773
|
-
memory=memory,
|
774
|
-
network_file_systems=network_file_systems,
|
775
|
-
block_network=block_network,
|
776
|
-
volumes=volumes,
|
777
|
-
allow_background_volume_commits=_allow_background_volume_commits,
|
778
|
-
pty_info=pty_info,
|
779
|
-
_experimental_scheduler=_experimental_scheduler,
|
780
|
-
_experimental_scheduler_placement=_experimental_scheduler_placement,
|
962
|
+
) -> None:
|
963
|
+
"""mdmd:hidden"""
|
964
|
+
arglist = ", ".join(repr(s) for s in entrypoint_args)
|
965
|
+
message = (
|
966
|
+
"`App.spawn_sandbox` is deprecated.\n\n"
|
967
|
+
"Sandboxes can be created using the `Sandbox` object:\n\n"
|
968
|
+
f"```\nsb = Sandbox.create({arglist}, app=app)\n```\n\n"
|
969
|
+
"See https://modal.com/docs/guide/sandbox for more info on working with sandboxes."
|
781
970
|
)
|
782
|
-
|
783
|
-
return obj
|
971
|
+
deprecation_error((2024, 7, 5), message)
|
784
972
|
|
785
973
|
def include(self, /, other_app: "_App"):
|
786
|
-
"""Include another
|
974
|
+
"""Include another App's objects in this one.
|
787
975
|
|
788
|
-
Useful splitting up Modal
|
976
|
+
Useful for splitting up Modal Apps across different self-contained files.
|
789
977
|
|
790
978
|
```python
|
791
979
|
app_a = modal.App("a")
|
@@ -806,33 +994,83 @@ class _App:
|
|
806
994
|
bar.remote()
|
807
995
|
```
|
808
996
|
"""
|
809
|
-
for tag,
|
810
|
-
|
811
|
-
if
|
997
|
+
for tag, function in other_app._functions.items():
|
998
|
+
existing_function = self._functions.get(tag)
|
999
|
+
if existing_function and existing_function != function:
|
1000
|
+
logger.warning(
|
1001
|
+
f"Named app function {tag} with existing value {existing_function} is being "
|
1002
|
+
f"overwritten by a different function {function}"
|
1003
|
+
)
|
1004
|
+
|
1005
|
+
self._add_function(function, False) # TODO(erikbern): webhook config?
|
1006
|
+
|
1007
|
+
for tag, cls in other_app._classes.items():
|
1008
|
+
existing_cls = self._classes.get(tag)
|
1009
|
+
if existing_cls and existing_cls != cls:
|
812
1010
|
logger.warning(
|
813
|
-
f"Named app
|
1011
|
+
f"Named app class {tag} with existing value {existing_cls} is being "
|
1012
|
+
f"overwritten by a different class {cls}"
|
814
1013
|
)
|
815
1014
|
|
816
|
-
self.
|
1015
|
+
self._add_class(tag, cls)
|
1016
|
+
|
1017
|
+
async def _logs(self, client: Optional[_Client] = None) -> AsyncGenerator[str, None]:
|
1018
|
+
"""Stream logs from the app.
|
1019
|
+
|
1020
|
+
This method is considered private and its interface may change - use at your own risk!
|
1021
|
+
"""
|
1022
|
+
if not self._app_id:
|
1023
|
+
raise InvalidError("`app._logs` requires a running/stopped app.")
|
1024
|
+
|
1025
|
+
client = client or self._client or await _Client.from_env()
|
1026
|
+
|
1027
|
+
last_log_batch_entry_id: Optional[str] = None
|
1028
|
+
while True:
|
1029
|
+
request = api_pb2.AppGetLogsRequest(
|
1030
|
+
app_id=self._app_id,
|
1031
|
+
timeout=55,
|
1032
|
+
last_entry_id=last_log_batch_entry_id,
|
1033
|
+
)
|
1034
|
+
async for log_batch in client.stub.AppGetLogs.unary_stream(request):
|
1035
|
+
if log_batch.entry_id:
|
1036
|
+
# log_batch entry_id is empty for fd="server" messages from AppGetLogs
|
1037
|
+
last_log_batch_entry_id = log_batch.entry_id
|
1038
|
+
if log_batch.app_done:
|
1039
|
+
return
|
1040
|
+
for log in log_batch.items:
|
1041
|
+
if log.data:
|
1042
|
+
yield log.data
|
1043
|
+
|
1044
|
+
@classmethod
|
1045
|
+
def _get_container_app(cls) -> Optional["_App"]:
|
1046
|
+
"""Returns the `App` running inside a container.
|
1047
|
+
|
1048
|
+
This will return `None` outside of a Modal container."""
|
1049
|
+
return cls._container_app
|
1050
|
+
|
1051
|
+
@classmethod
|
1052
|
+
def _reset_container_app(cls):
|
1053
|
+
"""Only used for tests."""
|
1054
|
+
cls._container_app = None
|
817
1055
|
|
818
1056
|
|
819
1057
|
App = synchronize_api(_App)
|
820
1058
|
|
821
1059
|
|
822
1060
|
class _Stub(_App):
|
823
|
-
"""
|
1061
|
+
"""mdmd:hidden
|
1062
|
+
This enables using a "Stub" class instead of "App".
|
824
1063
|
|
825
1064
|
For most of Modal's history, the app class was called "Stub", so this exists for
|
826
1065
|
backwards compatibility, in order to facilitate moving from "Stub" to "App".
|
827
1066
|
"""
|
828
1067
|
|
829
1068
|
def __new__(cls, *args, **kwargs):
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
# )
|
1069
|
+
deprecation_warning(
|
1070
|
+
(2024, 4, 29),
|
1071
|
+
'The use of "Stub" has been deprecated in favor of "App".'
|
1072
|
+
" This is a pure name change with no other implications.",
|
1073
|
+
)
|
836
1074
|
return _App(*args, **kwargs)
|
837
1075
|
|
838
1076
|
|