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.

Files changed (160) hide show
  1. modal/__init__.py +0 -2
  2. modal/__main__.py +3 -4
  3. modal/_billing.py +80 -0
  4. modal/_clustered_functions.py +7 -3
  5. modal/_clustered_functions.pyi +15 -3
  6. modal/_container_entrypoint.py +51 -69
  7. modal/_functions.py +508 -240
  8. modal/_grpc_client.py +171 -0
  9. modal/_load_context.py +105 -0
  10. modal/_object.py +81 -21
  11. modal/_output.py +58 -45
  12. modal/_partial_function.py +48 -73
  13. modal/_pty.py +7 -3
  14. modal/_resolver.py +26 -46
  15. modal/_runtime/asgi.py +4 -3
  16. modal/_runtime/container_io_manager.py +358 -220
  17. modal/_runtime/container_io_manager.pyi +296 -101
  18. modal/_runtime/execution_context.py +18 -2
  19. modal/_runtime/execution_context.pyi +64 -7
  20. modal/_runtime/gpu_memory_snapshot.py +262 -57
  21. modal/_runtime/user_code_imports.py +28 -58
  22. modal/_serialization.py +90 -6
  23. modal/_traceback.py +42 -1
  24. modal/_tunnel.pyi +380 -12
  25. modal/_utils/async_utils.py +84 -29
  26. modal/_utils/auth_token_manager.py +111 -0
  27. modal/_utils/blob_utils.py +181 -58
  28. modal/_utils/deprecation.py +19 -0
  29. modal/_utils/function_utils.py +91 -47
  30. modal/_utils/grpc_utils.py +89 -66
  31. modal/_utils/mount_utils.py +26 -1
  32. modal/_utils/name_utils.py +17 -3
  33. modal/_utils/task_command_router_client.py +536 -0
  34. modal/_utils/time_utils.py +34 -6
  35. modal/app.py +256 -88
  36. modal/app.pyi +909 -92
  37. modal/billing.py +5 -0
  38. modal/builder/2025.06.txt +18 -0
  39. modal/builder/PREVIEW.txt +18 -0
  40. modal/builder/base-images.json +58 -0
  41. modal/cli/_download.py +19 -3
  42. modal/cli/_traceback.py +3 -2
  43. modal/cli/app.py +4 -4
  44. modal/cli/cluster.py +15 -7
  45. modal/cli/config.py +5 -3
  46. modal/cli/container.py +7 -6
  47. modal/cli/dict.py +22 -16
  48. modal/cli/entry_point.py +12 -5
  49. modal/cli/environment.py +5 -4
  50. modal/cli/import_refs.py +3 -3
  51. modal/cli/launch.py +102 -5
  52. modal/cli/network_file_system.py +11 -12
  53. modal/cli/profile.py +3 -2
  54. modal/cli/programs/launch_instance_ssh.py +94 -0
  55. modal/cli/programs/run_jupyter.py +1 -1
  56. modal/cli/programs/run_marimo.py +95 -0
  57. modal/cli/programs/vscode.py +1 -1
  58. modal/cli/queues.py +57 -26
  59. modal/cli/run.py +91 -23
  60. modal/cli/secret.py +48 -22
  61. modal/cli/token.py +7 -8
  62. modal/cli/utils.py +4 -7
  63. modal/cli/volume.py +31 -25
  64. modal/client.py +15 -85
  65. modal/client.pyi +183 -62
  66. modal/cloud_bucket_mount.py +5 -3
  67. modal/cloud_bucket_mount.pyi +197 -5
  68. modal/cls.py +200 -126
  69. modal/cls.pyi +446 -68
  70. modal/config.py +29 -11
  71. modal/container_process.py +319 -19
  72. modal/container_process.pyi +190 -20
  73. modal/dict.py +290 -71
  74. modal/dict.pyi +835 -83
  75. modal/environments.py +15 -27
  76. modal/environments.pyi +46 -24
  77. modal/exception.py +14 -2
  78. modal/experimental/__init__.py +194 -40
  79. modal/experimental/flash.py +618 -0
  80. modal/experimental/flash.pyi +380 -0
  81. modal/experimental/ipython.py +11 -7
  82. modal/file_io.py +29 -36
  83. modal/file_io.pyi +251 -53
  84. modal/file_pattern_matcher.py +56 -16
  85. modal/functions.pyi +673 -92
  86. modal/gpu.py +1 -1
  87. modal/image.py +528 -176
  88. modal/image.pyi +1572 -145
  89. modal/io_streams.py +458 -128
  90. modal/io_streams.pyi +433 -52
  91. modal/mount.py +216 -151
  92. modal/mount.pyi +225 -78
  93. modal/network_file_system.py +45 -62
  94. modal/network_file_system.pyi +277 -56
  95. modal/object.pyi +93 -17
  96. modal/parallel_map.py +942 -129
  97. modal/parallel_map.pyi +294 -15
  98. modal/partial_function.py +0 -2
  99. modal/partial_function.pyi +234 -19
  100. modal/proxy.py +17 -8
  101. modal/proxy.pyi +36 -3
  102. modal/queue.py +270 -65
  103. modal/queue.pyi +817 -57
  104. modal/runner.py +115 -101
  105. modal/runner.pyi +205 -49
  106. modal/sandbox.py +512 -136
  107. modal/sandbox.pyi +845 -111
  108. modal/schedule.py +1 -1
  109. modal/secret.py +300 -70
  110. modal/secret.pyi +589 -34
  111. modal/serving.py +7 -11
  112. modal/serving.pyi +7 -8
  113. modal/snapshot.py +11 -8
  114. modal/snapshot.pyi +25 -4
  115. modal/token_flow.py +4 -4
  116. modal/token_flow.pyi +28 -8
  117. modal/volume.py +416 -158
  118. modal/volume.pyi +1117 -121
  119. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
  120. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  121. modal_docs/mdmd/mdmd.py +17 -4
  122. modal_proto/api.proto +534 -79
  123. modal_proto/api_grpc.py +337 -1
  124. modal_proto/api_pb2.py +1522 -968
  125. modal_proto/api_pb2.pyi +1619 -134
  126. modal_proto/api_pb2_grpc.py +699 -4
  127. modal_proto/api_pb2_grpc.pyi +226 -14
  128. modal_proto/modal_api_grpc.py +175 -154
  129. modal_proto/sandbox_router.proto +145 -0
  130. modal_proto/sandbox_router_grpc.py +105 -0
  131. modal_proto/sandbox_router_pb2.py +149 -0
  132. modal_proto/sandbox_router_pb2.pyi +333 -0
  133. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  134. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  135. modal_proto/task_command_router.proto +144 -0
  136. modal_proto/task_command_router_grpc.py +105 -0
  137. modal_proto/task_command_router_pb2.py +149 -0
  138. modal_proto/task_command_router_pb2.pyi +333 -0
  139. modal_proto/task_command_router_pb2_grpc.py +203 -0
  140. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  141. modal_version/__init__.py +1 -1
  142. modal/requirements/PREVIEW.txt +0 -16
  143. modal/requirements/base-images.json +0 -26
  144. modal-1.0.3.dev10.dist-info/RECORD +0 -179
  145. modal_proto/modal_options_grpc.py +0 -3
  146. modal_proto/options.proto +0 -19
  147. modal_proto/options_grpc.py +0 -3
  148. modal_proto/options_pb2.py +0 -35
  149. modal_proto/options_pb2.pyi +0 -20
  150. modal_proto/options_pb2_grpc.py +0 -4
  151. modal_proto/options_pb2_grpc.pyi +0 -7
  152. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  153. /modal/{requirements → builder}/2023.12.txt +0 -0
  154. /modal/{requirements → builder}/2024.04.txt +0 -0
  155. /modal/{requirements → builder}/2024.10.txt +0 -0
  156. /modal/{requirements → builder}/README.md +0 -0
  157. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  158. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  159. {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  160. {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
- _image: Optional[_Image]
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
- _include_source_default: Optional[bool] = None
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
- image: Optional[_Image] = None, # default image for all functions (default is `modal.Image.debian_slim()`)
175
- secrets: Sequence[_Secret] = [], # default secrets for all functions
176
- volumes: dict[Union[str, PurePosixPath], _Volume] = {}, # default volumes for all functions
177
- include_source: Optional[bool] = None,
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._functions = {}
202
- self._classes = {}
203
- self._image = image
204
- self._secrets = secrets
205
- self._volumes = volumes
206
- self._local_entrypoints = {}
207
- self._web_endpoints = []
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
- """Whether the current app for the app is running in interactive mode."""
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 retry_transient_errors(client.stub.AppGetOrCreate, request)
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
- return self._image
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
- self._image = value
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
- for obj in self._functions.values():
351
+ local_state = self._local_state
352
+ for obj in local_state.functions.values():
298
353
  obj._unhydrate()
299
- for obj in self._classes.values():
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 will be visible in the deployment history
375
- client: Optional[_Client] = None, # Alternate client to use for RPCs
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
- if self._image:
436
- return self._image
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
- if old_function := self._functions.get(function.tag, None):
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 self._classes:
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
- self._functions[function.tag] = function
530
+ local_state.functions[function.tag] = function
474
531
  if is_web_endpoint:
475
- self._web_endpoints.append(function.tag)
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._classes[tag] = cls
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 self._functions:
498
- obj = self._functions[tag]
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 self._classes:
505
- obj = self._classes[tag]
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
- """All modal.Function objects registered on the app."""
512
- return self._functions
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
- """All modal.Cls objects registered on the app."""
517
- return self._classes
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
- """All local CLI entrypoints registered on the app."""
522
- return self._local_entrypoints
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
- """Names of web endpoint (ie. webhook) functions registered on the app."""
527
- return self._web_endpoints
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
- if tag in self._local_entrypoints:
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 = self._local_entrypoints[tag] = _LocalEntrypoint(info, self)
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: Any = None,
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
- secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
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: Optional[int] = None, # Maximum execution time of the function in seconds.
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 function's home package should be included in the image - defaults to True
643
- include_source: Optional[bool] = None, # When `False`, don't automatically add the App source to the container.
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
- _experimental_enable_gpu_snapshot: bool = False, # Experimentally enable GPU memory snapshots.
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 [Function](/docs/reference/modal.Function) with this App."""
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 = [*self._secrets, *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 `serialize=True` is set.
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={**self._volumes, **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 self._include_source_default,
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
- _experimental_enable_gpu_snapshot=_experimental_enable_gpu_snapshot,
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: Optional[bool] = None,
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
- secrets: Sequence[_Secret] = (), # Optional Modal Secret objects with environment variables for the container
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: Optional[int] = None, # Maximum execution time of the function in seconds.
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
- _experimental_enable_gpu_snapshot: bool = False, # Experimentally enable GPU memory snapshots.
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
- if allow_cross_region_volumes is not None:
903
- deprecation_warning((2025, 4, 23), "The `allow_cross_region_volumes` parameter no longer has any effect.")
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=[*self._secrets, *secrets],
1069
+ secrets=[*local_state.secrets_default, *secrets],
957
1070
  gpu=gpu,
958
1071
  network_file_systems=network_file_systems,
959
- volumes={**self._volumes, **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
- include_source=include_source if include_source is not None else self._include_source_default,
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
- _experimental_enable_gpu_snapshot=_experimental_enable_gpu_snapshot,
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
- for tag, function in other_app._functions.items():
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 other_app._classes.items():
1025
- existing_cls = self._classes.get(tag)
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