modal 1.0.3.dev10__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/__init__.py +0 -2
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +15 -3
- modal/_container_entrypoint.py +51 -69
- modal/_functions.py +508 -240
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +81 -21
- modal/_output.py +58 -45
- modal/_partial_function.py +48 -73
- modal/_pty.py +7 -3
- modal/_resolver.py +26 -46
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +358 -220
- modal/_runtime/container_io_manager.pyi +296 -101
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +64 -7
- modal/_runtime/gpu_memory_snapshot.py +262 -57
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +90 -6
- modal/_traceback.py +42 -1
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +84 -29
- modal/_utils/auth_token_manager.py +111 -0
- modal/_utils/blob_utils.py +181 -58
- modal/_utils/deprecation.py +19 -0
- modal/_utils/function_utils.py +91 -47
- modal/_utils/grpc_utils.py +89 -66
- 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 +256 -88
- modal/app.pyi +909 -92
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +18 -0
- modal/builder/PREVIEW.txt +18 -0
- modal/builder/base-images.json +58 -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 +11 -12
- 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 +91 -23
- modal/cli/secret.py +48 -22
- modal/cli/token.py +7 -8
- modal/cli/utils.py +4 -7
- modal/cli/volume.py +31 -25
- modal/client.py +15 -85
- modal/client.pyi +183 -62
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +197 -5
- modal/cls.py +200 -126
- modal/cls.pyi +446 -68
- modal/config.py +29 -11
- modal/container_process.py +319 -19
- modal/container_process.pyi +190 -20
- modal/dict.py +290 -71
- modal/dict.pyi +835 -83
- modal/environments.py +15 -27
- modal/environments.pyi +46 -24
- modal/exception.py +14 -2
- modal/experimental/__init__.py +194 -40
- modal/experimental/flash.py +618 -0
- modal/experimental/flash.pyi +380 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.py +29 -36
- modal/file_io.pyi +251 -53
- modal/file_pattern_matcher.py +56 -16
- modal/functions.pyi +673 -92
- modal/gpu.py +1 -1
- modal/image.py +528 -176
- modal/image.pyi +1572 -145
- modal/io_streams.py +458 -128
- modal/io_streams.pyi +433 -52
- modal/mount.py +216 -151
- modal/mount.pyi +225 -78
- modal/network_file_system.py +45 -62
- modal/network_file_system.pyi +277 -56
- modal/object.pyi +93 -17
- modal/parallel_map.py +942 -129
- modal/parallel_map.pyi +294 -15
- modal/partial_function.py +0 -2
- modal/partial_function.pyi +234 -19
- modal/proxy.py +17 -8
- modal/proxy.pyi +36 -3
- modal/queue.py +270 -65
- modal/queue.pyi +817 -57
- modal/runner.py +115 -101
- modal/runner.pyi +205 -49
- modal/sandbox.py +512 -136
- modal/sandbox.pyi +845 -111
- modal/schedule.py +1 -1
- modal/secret.py +300 -70
- modal/secret.pyi +589 -34
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/snapshot.pyi +25 -4
- modal/token_flow.py +4 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +416 -158
- modal/volume.pyi +1117 -121
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +17 -4
- modal_proto/api.proto +534 -79
- modal_proto/api_grpc.py +337 -1
- modal_proto/api_pb2.py +1522 -968
- modal_proto/api_pb2.pyi +1619 -134
- modal_proto/api_pb2_grpc.py +699 -4
- modal_proto/api_pb2_grpc.pyi +226 -14
- modal_proto/modal_api_grpc.py +175 -154
- 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/requirements/PREVIEW.txt +0 -16
- modal/requirements/base-images.json +0 -26
- modal-1.0.3.dev10.dist-info/RECORD +0 -179
- 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-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {modal-1.0.3.dev10.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,40 +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
|
-
"""
|
|
512
|
-
|
|
568
|
+
"""mdmd:hidden
|
|
569
|
+
All modal.Function objects registered on the app.
|
|
570
|
+
|
|
571
|
+
Note: this property is populated only during the build phase, and it is not
|
|
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.
|
|
575
|
+
"""
|
|
576
|
+
return self._local_state.functions
|
|
513
577
|
|
|
514
578
|
@property
|
|
515
579
|
def registered_classes(self) -> dict[str, _Cls]:
|
|
516
|
-
"""
|
|
517
|
-
|
|
580
|
+
"""mdmd:hidden
|
|
581
|
+
All modal.Cls objects registered on the app.
|
|
582
|
+
|
|
583
|
+
Note: this property is populated only during the build phase, and it is not
|
|
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.
|
|
587
|
+
"""
|
|
588
|
+
return self._local_state.classes
|
|
518
589
|
|
|
519
590
|
@property
|
|
520
591
|
def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]:
|
|
521
|
-
"""
|
|
522
|
-
|
|
592
|
+
"""mdmd:hidden
|
|
593
|
+
All local CLI entrypoints registered on the app.
|
|
594
|
+
|
|
595
|
+
Note: this property is populated only during the build phase, and it is not
|
|
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.
|
|
598
|
+
"""
|
|
599
|
+
return self._local_state.local_entrypoints
|
|
523
600
|
|
|
524
601
|
@property
|
|
525
602
|
def registered_web_endpoints(self) -> list[str]:
|
|
526
|
-
"""
|
|
527
|
-
|
|
603
|
+
"""mdmd:hidden
|
|
604
|
+
Names of web endpoint (ie. webhook) functions registered on the app.
|
|
605
|
+
|
|
606
|
+
Note: this property is populated only during the build phase, and it is not
|
|
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.
|
|
610
|
+
"""
|
|
611
|
+
return self._local_state.web_endpoints
|
|
528
612
|
|
|
529
613
|
def local_entrypoint(
|
|
530
614
|
self, _warn_parentheses_missing: Any = None, *, name: Optional[str] = None
|
|
@@ -550,8 +634,8 @@ class _App:
|
|
|
550
634
|
modal run app_module.py
|
|
551
635
|
```
|
|
552
636
|
|
|
553
|
-
Note that an explicit [`app.run()`](/docs/reference/modal.App#run) is not needed, as an
|
|
554
|
-
[app](/docs/guide/apps) is automatically created for you.
|
|
637
|
+
Note that an explicit [`app.run()`](https://modal.com/docs/reference/modal.App#run) is not needed, as an
|
|
638
|
+
[app](https://modal.com/docs/guide/apps) is automatically created for you.
|
|
555
639
|
|
|
556
640
|
**Multiple Entrypoints**
|
|
557
641
|
|
|
@@ -585,10 +669,11 @@ class _App:
|
|
|
585
669
|
def wrapped(raw_f: Callable[..., Any]) -> _LocalEntrypoint:
|
|
586
670
|
info = FunctionInfo(raw_f)
|
|
587
671
|
tag = name if name is not None else raw_f.__qualname__
|
|
588
|
-
|
|
672
|
+
local_state = self._local_state
|
|
673
|
+
if tag in local_state.local_entrypoints:
|
|
589
674
|
# TODO: get rid of this limitation.
|
|
590
675
|
raise InvalidError(f"Duplicate local entrypoint name: {tag}. Local entrypoint names must be unique.")
|
|
591
|
-
entrypoint =
|
|
676
|
+
entrypoint = local_state.local_entrypoints[tag] = _LocalEntrypoint(info, self)
|
|
592
677
|
return entrypoint
|
|
593
678
|
|
|
594
679
|
return wrapped
|
|
@@ -596,11 +681,12 @@ class _App:
|
|
|
596
681
|
@warn_on_renamed_autoscaler_settings
|
|
597
682
|
def function(
|
|
598
683
|
self,
|
|
599
|
-
_warn_parentheses_missing
|
|
684
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
600
685
|
*,
|
|
601
686
|
image: Optional[_Image] = None, # The image to run as the container for the function
|
|
602
687
|
schedule: Optional[Schedule] = None, # An optional Modal Schedule for the function
|
|
603
|
-
|
|
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
|
|
604
690
|
gpu: Union[
|
|
605
691
|
GPU_T, list[GPU_T]
|
|
606
692
|
] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
|
|
@@ -625,7 +711,8 @@ class _App:
|
|
|
625
711
|
scaledown_window: Optional[int] = None, # Max time (in seconds) a container can remain idle while scaling down.
|
|
626
712
|
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
|
627
713
|
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
|
628
|
-
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`.
|
|
629
716
|
name: Optional[str] = None, # Sets the Modal name of the function within the app
|
|
630
717
|
is_generator: Optional[
|
|
631
718
|
bool
|
|
@@ -639,8 +726,9 @@ class _App:
|
|
|
639
726
|
# With `max_inputs = 1`, containers will be single-use.
|
|
640
727
|
max_inputs: Optional[int] = None,
|
|
641
728
|
i6pn: Optional[bool] = None, # Whether to enable IPv6 container networking within the region.
|
|
642
|
-
# Whether the
|
|
643
|
-
|
|
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,
|
|
644
732
|
experimental_options: Optional[dict[str, Any]] = None,
|
|
645
733
|
# Parameters below here are experimental. Use with caution!
|
|
646
734
|
_experimental_scheduler_placement: Optional[
|
|
@@ -648,16 +736,15 @@ class _App:
|
|
|
648
736
|
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
|
649
737
|
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
650
738
|
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
|
651
|
-
|
|
739
|
+
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
652
740
|
# Parameters below here are deprecated. Please update your code as suggested
|
|
653
741
|
keep_warm: Optional[int] = None, # Replaced with `min_containers`
|
|
654
742
|
concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
|
|
655
743
|
container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
|
|
656
744
|
allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
|
|
657
745
|
_experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
|
|
658
|
-
allow_cross_region_volumes: Optional[bool] = None, # Always True on the Modal backend now
|
|
659
746
|
) -> _FunctionDecoratorType:
|
|
660
|
-
"""Decorator to register a new Modal
|
|
747
|
+
"""Decorator to register a new Modal Function with this App."""
|
|
661
748
|
if isinstance(_warn_parentheses_missing, _Image):
|
|
662
749
|
# Handle edge case where maybe (?) some users passed image as a positional arg
|
|
663
750
|
raise InvalidError("`image` needs to be a keyword argument: `@app.function(image=image)`.")
|
|
@@ -674,10 +761,12 @@ class _App:
|
|
|
674
761
|
" Please use the `@modal.concurrent` decorator instead."
|
|
675
762
|
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
676
763
|
)
|
|
677
|
-
if allow_cross_region_volumes is not None:
|
|
678
|
-
deprecation_warning((2025, 4, 23), "The `allow_cross_region_volumes` parameter no longer has any effect.")
|
|
679
764
|
|
|
680
|
-
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]
|
|
681
770
|
|
|
682
771
|
def wrapped(
|
|
683
772
|
f: Union[_PartialFunction, Callable[..., Any], None],
|
|
@@ -720,6 +809,7 @@ class _App:
|
|
|
720
809
|
batch_max_size = f.params.batch_max_size
|
|
721
810
|
batch_wait_ms = f.params.batch_wait_ms
|
|
722
811
|
if f.flags & _PartialFunctionFlags.CONCURRENT:
|
|
812
|
+
verify_concurrent_params(params=f.params, is_flash=is_flash_object(experimental_options))
|
|
723
813
|
max_concurrent_inputs = f.params.max_concurrent_inputs
|
|
724
814
|
target_concurrent_inputs = f.params.target_concurrent_inputs
|
|
725
815
|
else:
|
|
@@ -731,7 +821,7 @@ class _App:
|
|
|
731
821
|
dedent(
|
|
732
822
|
"""
|
|
733
823
|
The `@app.function` decorator must apply to functions in global scope,
|
|
734
|
-
unless `
|
|
824
|
+
unless `serialized=True` is set.
|
|
735
825
|
If trying to apply additional decorators, they may need to use `functools.wraps`.
|
|
736
826
|
"""
|
|
737
827
|
)
|
|
@@ -785,7 +875,7 @@ class _App:
|
|
|
785
875
|
is_generator=is_generator,
|
|
786
876
|
gpu=gpu,
|
|
787
877
|
network_file_systems=network_file_systems,
|
|
788
|
-
volumes={**
|
|
878
|
+
volumes={**local_state.volumes_default, **volumes},
|
|
789
879
|
cpu=cpu,
|
|
790
880
|
memory=memory,
|
|
791
881
|
ephemeral_disk=ephemeral_disk,
|
|
@@ -800,6 +890,7 @@ class _App:
|
|
|
800
890
|
batch_max_size=batch_max_size,
|
|
801
891
|
batch_wait_ms=batch_wait_ms,
|
|
802
892
|
timeout=timeout,
|
|
893
|
+
startup_timeout=startup_timeout or timeout,
|
|
803
894
|
cloud=cloud,
|
|
804
895
|
webhook_config=webhook_config,
|
|
805
896
|
enable_memory_snapshot=enable_memory_snapshot,
|
|
@@ -810,10 +901,10 @@ class _App:
|
|
|
810
901
|
i6pn_enabled=i6pn_enabled,
|
|
811
902
|
cluster_size=cluster_size, # Experimental: Clustered functions
|
|
812
903
|
rdma=rdma,
|
|
813
|
-
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,
|
|
814
905
|
experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
|
|
815
906
|
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
816
|
-
|
|
907
|
+
restrict_output=_experimental_restrict_output,
|
|
817
908
|
)
|
|
818
909
|
|
|
819
910
|
self._add_function(function, webhook_config is not None)
|
|
@@ -826,10 +917,11 @@ class _App:
|
|
|
826
917
|
@warn_on_renamed_autoscaler_settings
|
|
827
918
|
def cls(
|
|
828
919
|
self,
|
|
829
|
-
_warn_parentheses_missing
|
|
920
|
+
_warn_parentheses_missing=None, # mdmd:line-hidden
|
|
830
921
|
*,
|
|
831
922
|
image: Optional[_Image] = None, # The image to run as the container for the function
|
|
832
|
-
|
|
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
|
|
833
925
|
gpu: Union[
|
|
834
926
|
GPU_T, list[GPU_T]
|
|
835
927
|
] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
|
|
@@ -854,7 +946,8 @@ class _App:
|
|
|
854
946
|
scaledown_window: Optional[int] = None, # Max time (in seconds) a container can remain idle while scaling down.
|
|
855
947
|
proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
|
|
856
948
|
retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
|
|
857
|
-
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`.
|
|
858
951
|
cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
|
|
859
952
|
region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
|
|
860
953
|
enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
|
|
@@ -863,6 +956,7 @@ class _App:
|
|
|
863
956
|
# Limits the number of inputs a container handles before shutting down.
|
|
864
957
|
# Use `max_inputs = 1` for single-use containers.
|
|
865
958
|
max_inputs: Optional[int] = None,
|
|
959
|
+
i6pn: Optional[bool] = None, # Whether to enable IPv6 container networking within the region.
|
|
866
960
|
include_source: Optional[bool] = None, # When `False`, don't automatically add the App source to the container.
|
|
867
961
|
experimental_options: Optional[dict[str, Any]] = None,
|
|
868
962
|
# Parameters below here are experimental. Use with caution!
|
|
@@ -871,17 +965,16 @@ class _App:
|
|
|
871
965
|
] = None, # Experimental controls over fine-grained scheduling (alpha).
|
|
872
966
|
_experimental_proxy_ip: Optional[str] = None, # IP address of proxy
|
|
873
967
|
_experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
|
|
874
|
-
|
|
968
|
+
_experimental_restrict_output: bool = False, # Don't use pickle for return values
|
|
875
969
|
# Parameters below here are deprecated. Please update your code as suggested
|
|
876
970
|
keep_warm: Optional[int] = None, # Replaced with `min_containers`
|
|
877
971
|
concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
|
|
878
972
|
container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
|
|
879
973
|
allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
|
|
880
974
|
_experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
|
|
881
|
-
allow_cross_region_volumes: Optional[bool] = None, # Always True on the Modal backend now
|
|
882
975
|
) -> Callable[[Union[CLS_T, _PartialFunction]], CLS_T]:
|
|
883
976
|
"""
|
|
884
|
-
Decorator to register a new Modal [Cls](/docs/reference/modal.Cls) with this App.
|
|
977
|
+
Decorator to register a new Modal [Cls](https://modal.com/docs/reference/modal.Cls) with this App.
|
|
885
978
|
"""
|
|
886
979
|
if _warn_parentheses_missing:
|
|
887
980
|
raise InvalidError("Did you forget parentheses? Suggestion: `@app.cls()`.")
|
|
@@ -899,27 +992,45 @@ class _App:
|
|
|
899
992
|
" Please use the `@modal.concurrent` decorator instead."
|
|
900
993
|
"\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
|
|
901
994
|
)
|
|
902
|
-
|
|
903
|
-
|
|
995
|
+
|
|
996
|
+
secrets = secrets or []
|
|
997
|
+
if env:
|
|
998
|
+
secrets = [*secrets, _Secret.from_dict(env)]
|
|
904
999
|
|
|
905
1000
|
def wrapper(wrapped_cls: Union[CLS_T, _PartialFunction]) -> CLS_T:
|
|
1001
|
+
local_state = self._local_state
|
|
906
1002
|
# Check if the decorated object is a class
|
|
907
1003
|
if isinstance(wrapped_cls, _PartialFunction):
|
|
908
1004
|
wrapped_cls.registered = True
|
|
909
1005
|
user_cls = wrapped_cls.user_cls
|
|
910
1006
|
if wrapped_cls.flags & _PartialFunctionFlags.CONCURRENT:
|
|
1007
|
+
verify_concurrent_params(params=wrapped_cls.params, is_flash=is_flash_object(experimental_options))
|
|
911
1008
|
max_concurrent_inputs = wrapped_cls.params.max_concurrent_inputs
|
|
912
1009
|
target_concurrent_inputs = wrapped_cls.params.target_concurrent_inputs
|
|
913
1010
|
else:
|
|
914
1011
|
max_concurrent_inputs = allow_concurrent_inputs
|
|
915
1012
|
target_concurrent_inputs = None
|
|
1013
|
+
|
|
1014
|
+
if wrapped_cls.flags & _PartialFunctionFlags.CLUSTERED:
|
|
1015
|
+
cluster_size = wrapped_cls.params.cluster_size
|
|
1016
|
+
rdma = wrapped_cls.params.rdma
|
|
1017
|
+
else:
|
|
1018
|
+
cluster_size = None
|
|
1019
|
+
rdma = None
|
|
916
1020
|
else:
|
|
917
1021
|
user_cls = wrapped_cls
|
|
918
1022
|
max_concurrent_inputs = allow_concurrent_inputs
|
|
919
1023
|
target_concurrent_inputs = None
|
|
1024
|
+
cluster_size = None
|
|
1025
|
+
rdma = None
|
|
920
1026
|
if not inspect.isclass(user_cls):
|
|
921
1027
|
raise TypeError("The @app.cls decorator must be used on a class.")
|
|
922
1028
|
|
|
1029
|
+
interface_methods = _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.interface_flags())
|
|
1030
|
+
if cluster_size:
|
|
1031
|
+
if len(interface_methods) > 1:
|
|
1032
|
+
raise InvalidError(f"Modal class {user_cls.__name__} cannot have multiple methods when clustered.")
|
|
1033
|
+
|
|
923
1034
|
batch_functions = _find_partial_methods_for_user_cls(user_cls, _PartialFunctionFlags.BATCHED)
|
|
924
1035
|
if batch_functions:
|
|
925
1036
|
if len(batch_functions) > 1:
|
|
@@ -949,14 +1060,16 @@ class _App:
|
|
|
949
1060
|
|
|
950
1061
|
info = FunctionInfo(None, serialized=serialized, user_cls=user_cls)
|
|
951
1062
|
|
|
1063
|
+
i6pn_enabled = i6pn or cluster_size is not None
|
|
1064
|
+
|
|
952
1065
|
cls_func = _Function.from_local(
|
|
953
1066
|
info,
|
|
954
1067
|
app=self,
|
|
955
1068
|
image=image or self._get_default_image(),
|
|
956
|
-
secrets=[*
|
|
1069
|
+
secrets=[*local_state.secrets_default, *secrets],
|
|
957
1070
|
gpu=gpu,
|
|
958
1071
|
network_file_systems=network_file_systems,
|
|
959
|
-
volumes={**
|
|
1072
|
+
volumes={**local_state.volumes_default, **volumes},
|
|
960
1073
|
cpu=cpu,
|
|
961
1074
|
memory=memory,
|
|
962
1075
|
ephemeral_disk=ephemeral_disk,
|
|
@@ -971,22 +1084,31 @@ class _App:
|
|
|
971
1084
|
batch_max_size=batch_max_size,
|
|
972
1085
|
batch_wait_ms=batch_wait_ms,
|
|
973
1086
|
timeout=timeout,
|
|
1087
|
+
startup_timeout=startup_timeout or timeout,
|
|
974
1088
|
cloud=cloud,
|
|
975
1089
|
enable_memory_snapshot=enable_memory_snapshot,
|
|
976
1090
|
block_network=block_network,
|
|
977
1091
|
restrict_modal_access=restrict_modal_access,
|
|
978
1092
|
max_inputs=max_inputs,
|
|
979
1093
|
scheduler_placement=scheduler_placement,
|
|
980
|
-
|
|
1094
|
+
i6pn_enabled=i6pn_enabled,
|
|
1095
|
+
cluster_size=cluster_size,
|
|
1096
|
+
rdma=rdma,
|
|
1097
|
+
include_source=include_source if include_source is not None else local_state.include_source_default,
|
|
981
1098
|
experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
|
|
982
1099
|
_experimental_proxy_ip=_experimental_proxy_ip,
|
|
983
1100
|
_experimental_custom_scaling_factor=_experimental_custom_scaling_factor,
|
|
984
|
-
|
|
1101
|
+
restrict_output=_experimental_restrict_output,
|
|
985
1102
|
)
|
|
986
1103
|
|
|
987
1104
|
self._add_function(cls_func, is_web_endpoint=False)
|
|
988
1105
|
|
|
989
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
|
|
990
1112
|
|
|
991
1113
|
tag: str = user_cls.__name__
|
|
992
1114
|
self._add_class(tag, cls)
|
|
@@ -994,7 +1116,7 @@ class _App:
|
|
|
994
1116
|
|
|
995
1117
|
return wrapper
|
|
996
1118
|
|
|
997
|
-
def include(self, /, other_app: "_App") -> typing_extensions.Self:
|
|
1119
|
+
def include(self, /, other_app: "_App", inherit_tags: bool = True) -> typing_extensions.Self:
|
|
998
1120
|
"""Include another App's objects in this one.
|
|
999
1121
|
|
|
1000
1122
|
Useful for splitting up Modal Apps across different self-contained files.
|
|
@@ -1017,12 +1139,19 @@ class _App:
|
|
|
1017
1139
|
# use function declared on the included app
|
|
1018
1140
|
bar.remote()
|
|
1019
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
|
+
|
|
1020
1146
|
"""
|
|
1021
|
-
|
|
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():
|
|
1022
1151
|
self._add_function(function, False) # TODO(erikbern): webhook config?
|
|
1023
1152
|
|
|
1024
|
-
for tag, cls in
|
|
1025
|
-
existing_cls =
|
|
1153
|
+
for tag, cls in other_app_local_state.classes.items():
|
|
1154
|
+
existing_cls = this_local_state.classes.get(tag)
|
|
1026
1155
|
if existing_cls and existing_cls != cls:
|
|
1027
1156
|
logger.warning(
|
|
1028
1157
|
f"Named app class {tag} with existing value {existing_cls} is being "
|
|
@@ -1030,8 +1159,47 @@ class _App:
|
|
|
1030
1159
|
)
|
|
1031
1160
|
|
|
1032
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
|
+
|
|
1033
1166
|
return self
|
|
1034
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
|
+
|
|
1035
1203
|
async def _logs(self, client: Optional[_Client] = None) -> AsyncGenerator[str, None]:
|
|
1036
1204
|
"""Stream logs from the app.
|
|
1037
1205
|
|