modal 1.0.6.dev58__py3-none-any.whl → 1.2.3.dev7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of modal might be problematic. Click here for more details.

Files changed (147) hide show
  1. modal/__main__.py +3 -4
  2. modal/_billing.py +80 -0
  3. modal/_clustered_functions.py +7 -3
  4. modal/_clustered_functions.pyi +4 -2
  5. modal/_container_entrypoint.py +41 -49
  6. modal/_functions.py +424 -195
  7. modal/_grpc_client.py +171 -0
  8. modal/_load_context.py +105 -0
  9. modal/_object.py +68 -20
  10. modal/_output.py +58 -45
  11. modal/_partial_function.py +36 -11
  12. modal/_pty.py +7 -3
  13. modal/_resolver.py +21 -35
  14. modal/_runtime/asgi.py +4 -3
  15. modal/_runtime/container_io_manager.py +301 -186
  16. modal/_runtime/container_io_manager.pyi +70 -61
  17. modal/_runtime/execution_context.py +18 -2
  18. modal/_runtime/execution_context.pyi +4 -1
  19. modal/_runtime/gpu_memory_snapshot.py +170 -63
  20. modal/_runtime/user_code_imports.py +28 -58
  21. modal/_serialization.py +57 -1
  22. modal/_utils/async_utils.py +33 -12
  23. modal/_utils/auth_token_manager.py +2 -5
  24. modal/_utils/blob_utils.py +110 -53
  25. modal/_utils/function_utils.py +49 -42
  26. modal/_utils/grpc_utils.py +80 -50
  27. modal/_utils/mount_utils.py +26 -1
  28. modal/_utils/name_utils.py +17 -3
  29. modal/_utils/task_command_router_client.py +536 -0
  30. modal/_utils/time_utils.py +34 -6
  31. modal/app.py +219 -83
  32. modal/app.pyi +229 -56
  33. modal/billing.py +5 -0
  34. modal/{requirements → builder}/2025.06.txt +1 -0
  35. modal/{requirements → builder}/PREVIEW.txt +1 -0
  36. modal/cli/_download.py +19 -3
  37. modal/cli/_traceback.py +3 -2
  38. modal/cli/app.py +4 -4
  39. modal/cli/cluster.py +15 -7
  40. modal/cli/config.py +5 -3
  41. modal/cli/container.py +7 -6
  42. modal/cli/dict.py +22 -16
  43. modal/cli/entry_point.py +12 -5
  44. modal/cli/environment.py +5 -4
  45. modal/cli/import_refs.py +3 -3
  46. modal/cli/launch.py +102 -5
  47. modal/cli/network_file_system.py +9 -13
  48. modal/cli/profile.py +3 -2
  49. modal/cli/programs/launch_instance_ssh.py +94 -0
  50. modal/cli/programs/run_jupyter.py +1 -1
  51. modal/cli/programs/run_marimo.py +95 -0
  52. modal/cli/programs/vscode.py +1 -1
  53. modal/cli/queues.py +57 -26
  54. modal/cli/run.py +58 -16
  55. modal/cli/secret.py +48 -22
  56. modal/cli/utils.py +3 -4
  57. modal/cli/volume.py +28 -25
  58. modal/client.py +13 -116
  59. modal/client.pyi +9 -91
  60. modal/cloud_bucket_mount.py +5 -3
  61. modal/cloud_bucket_mount.pyi +5 -1
  62. modal/cls.py +130 -102
  63. modal/cls.pyi +45 -85
  64. modal/config.py +29 -10
  65. modal/container_process.py +291 -13
  66. modal/container_process.pyi +95 -32
  67. modal/dict.py +282 -63
  68. modal/dict.pyi +423 -73
  69. modal/environments.py +15 -27
  70. modal/environments.pyi +5 -15
  71. modal/exception.py +8 -0
  72. modal/experimental/__init__.py +143 -38
  73. modal/experimental/flash.py +247 -78
  74. modal/experimental/flash.pyi +137 -9
  75. modal/file_io.py +14 -28
  76. modal/file_io.pyi +2 -2
  77. modal/file_pattern_matcher.py +25 -16
  78. modal/functions.pyi +134 -61
  79. modal/image.py +255 -86
  80. modal/image.pyi +300 -62
  81. modal/io_streams.py +436 -126
  82. modal/io_streams.pyi +236 -171
  83. modal/mount.py +62 -157
  84. modal/mount.pyi +45 -172
  85. modal/network_file_system.py +30 -53
  86. modal/network_file_system.pyi +16 -76
  87. modal/object.pyi +42 -8
  88. modal/parallel_map.py +821 -113
  89. modal/parallel_map.pyi +134 -0
  90. modal/partial_function.pyi +4 -1
  91. modal/proxy.py +16 -7
  92. modal/proxy.pyi +10 -2
  93. modal/queue.py +263 -61
  94. modal/queue.pyi +409 -66
  95. modal/runner.py +112 -92
  96. modal/runner.pyi +45 -27
  97. modal/sandbox.py +451 -124
  98. modal/sandbox.pyi +513 -67
  99. modal/secret.py +291 -67
  100. modal/secret.pyi +425 -19
  101. modal/serving.py +7 -11
  102. modal/serving.pyi +7 -8
  103. modal/snapshot.py +11 -8
  104. modal/token_flow.py +4 -4
  105. modal/volume.py +344 -98
  106. modal/volume.pyi +464 -68
  107. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +9 -8
  108. modal-1.2.3.dev7.dist-info/RECORD +195 -0
  109. modal_docs/mdmd/mdmd.py +11 -1
  110. modal_proto/api.proto +399 -67
  111. modal_proto/api_grpc.py +241 -1
  112. modal_proto/api_pb2.py +1395 -1000
  113. modal_proto/api_pb2.pyi +1239 -79
  114. modal_proto/api_pb2_grpc.py +499 -4
  115. modal_proto/api_pb2_grpc.pyi +162 -14
  116. modal_proto/modal_api_grpc.py +175 -160
  117. modal_proto/sandbox_router.proto +145 -0
  118. modal_proto/sandbox_router_grpc.py +105 -0
  119. modal_proto/sandbox_router_pb2.py +149 -0
  120. modal_proto/sandbox_router_pb2.pyi +333 -0
  121. modal_proto/sandbox_router_pb2_grpc.py +203 -0
  122. modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
  123. modal_proto/task_command_router.proto +144 -0
  124. modal_proto/task_command_router_grpc.py +105 -0
  125. modal_proto/task_command_router_pb2.py +149 -0
  126. modal_proto/task_command_router_pb2.pyi +333 -0
  127. modal_proto/task_command_router_pb2_grpc.py +203 -0
  128. modal_proto/task_command_router_pb2_grpc.pyi +75 -0
  129. modal_version/__init__.py +1 -1
  130. modal-1.0.6.dev58.dist-info/RECORD +0 -183
  131. modal_proto/modal_options_grpc.py +0 -3
  132. modal_proto/options.proto +0 -19
  133. modal_proto/options_grpc.py +0 -3
  134. modal_proto/options_pb2.py +0 -35
  135. modal_proto/options_pb2.pyi +0 -20
  136. modal_proto/options_pb2_grpc.py +0 -4
  137. modal_proto/options_pb2_grpc.pyi +0 -7
  138. /modal/{requirements → builder}/2023.12.312.txt +0 -0
  139. /modal/{requirements → builder}/2023.12.txt +0 -0
  140. /modal/{requirements → builder}/2024.04.txt +0 -0
  141. /modal/{requirements → builder}/2024.10.txt +0 -0
  142. /modal/{requirements → builder}/README.md +0 -0
  143. /modal/{requirements → builder}/base-images.json +0 -0
  144. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
  145. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
  146. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
  147. {modal-1.0.6.dev58.dist-info → modal-1.2.3.dev7.dist-info}/top_level.txt +0 -0
modal/app.py CHANGED
@@ -1,7 +1,8 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import inspect
3
3
  import typing
4
- from collections.abc import AsyncGenerator, Coroutine, Sequence
4
+ from collections.abc import AsyncGenerator, Collection, Coroutine, Mapping, Sequence
5
+ from dataclasses import dataclass
5
6
  from pathlib import PurePosixPath
6
7
  from textwrap import dedent
7
8
  from typing import (
@@ -21,21 +22,22 @@ from modal_proto import api_pb2
21
22
 
22
23
  from ._functions import _Function
23
24
  from ._ipython import is_notebook
25
+ from ._load_context import LoadContext
24
26
  from ._object import _get_environment_name, _Object
25
27
  from ._partial_function import (
26
28
  _find_partial_methods_for_user_cls,
27
29
  _PartialFunction,
28
30
  _PartialFunctionFlags,
31
+ verify_concurrent_params,
29
32
  )
30
33
  from ._utils.async_utils import synchronize_api
31
34
  from ._utils.deprecation import (
32
35
  deprecation_warning,
33
36
  warn_on_renamed_autoscaler_settings,
34
37
  )
35
- from ._utils.function_utils import FunctionInfo, is_global_object, is_method_fn
36
- from ._utils.grpc_utils import retry_transient_errors
38
+ from ._utils.function_utils import FunctionInfo, is_flash_object, is_global_object, is_method_fn
37
39
  from ._utils.mount_utils import validate_volumes
38
- from ._utils.name_utils import check_object_name
40
+ from ._utils.name_utils import check_object_name, check_tag_dict
39
41
  from .client import _Client
40
42
  from .cloud_bucket_mount import _CloudBucketMount
41
43
  from .cls import _Cls, parameter
@@ -114,6 +116,22 @@ class _FunctionDecoratorType:
114
116
  def __call__(self, func): ...
115
117
 
116
118
 
119
+ @dataclass()
120
+ class _LocalAppState:
121
+ """All state for apps that's part of the local/definition state"""
122
+
123
+ functions: dict[str, _Function]
124
+ classes: dict[str, _Cls]
125
+ image_default: Optional[_Image]
126
+ web_endpoints: list[str] # Used by the CLI
127
+ local_entrypoints: dict[str, _LocalEntrypoint]
128
+ tags: dict[str, str]
129
+
130
+ include_source_default: bool
131
+ secrets_default: Sequence[_Secret]
132
+ volumes_default: dict[Union[str, PurePosixPath], _Volume]
133
+
134
+
117
135
  class _App:
118
136
  """A Modal App is a group of functions and classes that are deployed together.
119
137
 
@@ -151,30 +169,35 @@ class _App:
151
169
 
152
170
  _name: Optional[str]
153
171
  _description: Optional[str]
154
- _functions: dict[str, _Function]
155
- _classes: dict[str, _Cls]
156
172
 
157
- _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,56 +548,67 @@ class _App:
491
548
  self._client = client
492
549
 
493
550
  _App._container_app = self
494
-
551
+ local_state = self._local_state
495
552
  # Hydrate function objects
496
553
  for tag, object_id in running_app.function_ids.items():
497
- if tag in 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.
568
+ """mdmd:hidden
569
+ All modal.Function objects registered on the app.
512
570
 
513
571
  Note: this property is populated only during the build phase, and it is not
514
572
  expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
573
+ This method is likely to be deprecated in the future in favor of a different
574
+ approach for retrieving the layout of a deployed App.
515
575
  """
516
- return self._functions
576
+ return self._local_state.functions
517
577
 
518
578
  @property
519
579
  def registered_classes(self) -> dict[str, _Cls]:
520
- """All modal.Cls objects registered on the app.
580
+ """mdmd:hidden
581
+ All modal.Cls objects registered on the app.
521
582
 
522
583
  Note: this property is populated only during the build phase, and it is not
523
584
  expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
585
+ This method is likely to be deprecated in the future in favor of a different
586
+ approach for retrieving the layout of a deployed App.
524
587
  """
525
- return self._classes
588
+ return self._local_state.classes
526
589
 
527
590
  @property
528
591
  def registered_entrypoints(self) -> dict[str, _LocalEntrypoint]:
529
- """All local CLI entrypoints registered on the app.
592
+ """mdmd:hidden
593
+ All local CLI entrypoints registered on the app.
530
594
 
531
595
  Note: this property is populated only during the build phase, and it is not
532
596
  expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
597
+ This method is likely to be deprecated in the future.
533
598
  """
534
- return self._local_entrypoints
599
+ return self._local_state.local_entrypoints
535
600
 
536
601
  @property
537
602
  def registered_web_endpoints(self) -> list[str]:
538
- """Names of web endpoint (ie. webhook) functions registered on the app.
603
+ """mdmd:hidden
604
+ Names of web endpoint (ie. webhook) functions registered on the app.
539
605
 
540
606
  Note: this property is populated only during the build phase, and it is not
541
607
  expected to work when a deplyoed App has been retrieved via `modal.App.lookup`.
608
+ This method is likely to be deprecated in the future in favor of a different
609
+ approach for retrieving the layout of a deployed App.
542
610
  """
543
- return self._web_endpoints
611
+ return self._local_state.web_endpoints
544
612
 
545
613
  def local_entrypoint(
546
614
  self, _warn_parentheses_missing: Any = None, *, name: Optional[str] = None
@@ -601,10 +669,11 @@ class _App:
601
669
  def wrapped(raw_f: Callable[..., Any]) -> _LocalEntrypoint:
602
670
  info = FunctionInfo(raw_f)
603
671
  tag = name if name is not None else raw_f.__qualname__
604
- if tag in self._local_entrypoints:
672
+ local_state = self._local_state
673
+ if tag in local_state.local_entrypoints:
605
674
  # TODO: get rid of this limitation.
606
675
  raise InvalidError(f"Duplicate local entrypoint name: {tag}. Local entrypoint names must be unique.")
607
- entrypoint = self._local_entrypoints[tag] = _LocalEntrypoint(info, self)
676
+ entrypoint = local_state.local_entrypoints[tag] = _LocalEntrypoint(info, self)
608
677
  return entrypoint
609
678
 
610
679
  return wrapped
@@ -612,11 +681,12 @@ class _App:
612
681
  @warn_on_renamed_autoscaler_settings
613
682
  def function(
614
683
  self,
615
- _warn_parentheses_missing: Any = None,
684
+ _warn_parentheses_missing=None, # mdmd:line-hidden
616
685
  *,
617
686
  image: Optional[_Image] = None, # The image to run as the container for the function
618
687
  schedule: Optional[Schedule] = None, # An optional Modal Schedule for the function
619
- 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
620
690
  gpu: Union[
621
691
  GPU_T, list[GPU_T]
622
692
  ] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
@@ -641,7 +711,8 @@ class _App:
641
711
  scaledown_window: Optional[int] = None, # Max time (in seconds) a container can remain idle while scaling down.
642
712
  proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
643
713
  retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
644
- timeout: 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`.
645
716
  name: Optional[str] = None, # Sets the Modal name of the function within the app
646
717
  is_generator: Optional[
647
718
  bool
@@ -655,8 +726,9 @@ class _App:
655
726
  # With `max_inputs = 1`, containers will be single-use.
656
727
  max_inputs: Optional[int] = None,
657
728
  i6pn: Optional[bool] = None, # Whether to enable IPv6 container networking within the region.
658
- # Whether the function's home package should be included in the image - defaults to True
659
- 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,
660
732
  experimental_options: Optional[dict[str, Any]] = None,
661
733
  # Parameters below here are experimental. Use with caution!
662
734
  _experimental_scheduler_placement: Optional[
@@ -664,14 +736,13 @@ class _App:
664
736
  ] = None, # Experimental controls over fine-grained scheduling (alpha).
665
737
  _experimental_proxy_ip: Optional[str] = None, # IP address of proxy
666
738
  _experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
667
- _experimental_enable_gpu_snapshot: bool = False, # Experimentally enable GPU memory snapshots.
739
+ _experimental_restrict_output: bool = False, # Don't use pickle for return values
668
740
  # Parameters below here are deprecated. Please update your code as suggested
669
741
  keep_warm: Optional[int] = None, # Replaced with `min_containers`
670
742
  concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
671
743
  container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
672
744
  allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
673
745
  _experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
674
- allow_cross_region_volumes: Optional[bool] = None, # Always True on the Modal backend now
675
746
  ) -> _FunctionDecoratorType:
676
747
  """Decorator to register a new Modal Function with this App."""
677
748
  if isinstance(_warn_parentheses_missing, _Image):
@@ -690,10 +761,12 @@ class _App:
690
761
  " Please use the `@modal.concurrent` decorator instead."
691
762
  "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
692
763
  )
693
- if allow_cross_region_volumes is not None:
694
- deprecation_warning((2025, 4, 23), "The `allow_cross_region_volumes` parameter no longer has any effect.")
695
764
 
696
- secrets = [*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]
697
770
 
698
771
  def wrapped(
699
772
  f: Union[_PartialFunction, Callable[..., Any], None],
@@ -736,6 +809,7 @@ class _App:
736
809
  batch_max_size = f.params.batch_max_size
737
810
  batch_wait_ms = f.params.batch_wait_ms
738
811
  if f.flags & _PartialFunctionFlags.CONCURRENT:
812
+ verify_concurrent_params(params=f.params, is_flash=is_flash_object(experimental_options))
739
813
  max_concurrent_inputs = f.params.max_concurrent_inputs
740
814
  target_concurrent_inputs = f.params.target_concurrent_inputs
741
815
  else:
@@ -801,7 +875,7 @@ class _App:
801
875
  is_generator=is_generator,
802
876
  gpu=gpu,
803
877
  network_file_systems=network_file_systems,
804
- volumes={**self._volumes, **volumes},
878
+ volumes={**local_state.volumes_default, **volumes},
805
879
  cpu=cpu,
806
880
  memory=memory,
807
881
  ephemeral_disk=ephemeral_disk,
@@ -816,6 +890,7 @@ class _App:
816
890
  batch_max_size=batch_max_size,
817
891
  batch_wait_ms=batch_wait_ms,
818
892
  timeout=timeout,
893
+ startup_timeout=startup_timeout or timeout,
819
894
  cloud=cloud,
820
895
  webhook_config=webhook_config,
821
896
  enable_memory_snapshot=enable_memory_snapshot,
@@ -826,10 +901,10 @@ class _App:
826
901
  i6pn_enabled=i6pn_enabled,
827
902
  cluster_size=cluster_size, # Experimental: Clustered functions
828
903
  rdma=rdma,
829
- include_source=include_source if include_source is not None else self._include_source_default,
904
+ include_source=include_source if include_source is not None else local_state.include_source_default,
830
905
  experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
831
906
  _experimental_proxy_ip=_experimental_proxy_ip,
832
- _experimental_enable_gpu_snapshot=_experimental_enable_gpu_snapshot,
907
+ restrict_output=_experimental_restrict_output,
833
908
  )
834
909
 
835
910
  self._add_function(function, webhook_config is not None)
@@ -842,10 +917,11 @@ class _App:
842
917
  @warn_on_renamed_autoscaler_settings
843
918
  def cls(
844
919
  self,
845
- _warn_parentheses_missing: Optional[bool] = None,
920
+ _warn_parentheses_missing=None, # mdmd:line-hidden
846
921
  *,
847
922
  image: Optional[_Image] = None, # The image to run as the container for the function
848
- 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
849
925
  gpu: Union[
850
926
  GPU_T, list[GPU_T]
851
927
  ] = None, # GPU request as string ("any", "T4", ...), object (`modal.GPU.A100()`, ...), or a list of either
@@ -870,7 +946,8 @@ class _App:
870
946
  scaledown_window: Optional[int] = None, # Max time (in seconds) a container can remain idle while scaling down.
871
947
  proxy: Optional[_Proxy] = None, # Reference to a Modal Proxy to use in front of this function.
872
948
  retries: Optional[Union[int, Retries]] = None, # Number of times to retry each input in case of failure.
873
- timeout: 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`.
874
951
  cloud: Optional[str] = None, # Cloud provider to run the function on. Possible values are aws, gcp, oci, auto.
875
952
  region: Optional[Union[str, Sequence[str]]] = None, # Region or regions to run the function on.
876
953
  enable_memory_snapshot: bool = False, # Enable memory checkpointing for faster cold starts.
@@ -888,14 +965,13 @@ class _App:
888
965
  ] = None, # Experimental controls over fine-grained scheduling (alpha).
889
966
  _experimental_proxy_ip: Optional[str] = None, # IP address of proxy
890
967
  _experimental_custom_scaling_factor: Optional[float] = None, # Custom scaling factor
891
- _experimental_enable_gpu_snapshot: bool = False, # Experimentally enable GPU memory snapshots.
968
+ _experimental_restrict_output: bool = False, # Don't use pickle for return values
892
969
  # Parameters below here are deprecated. Please update your code as suggested
893
970
  keep_warm: Optional[int] = None, # Replaced with `min_containers`
894
971
  concurrency_limit: Optional[int] = None, # Replaced with `max_containers`
895
972
  container_idle_timeout: Optional[int] = None, # Replaced with `scaledown_window`
896
973
  allow_concurrent_inputs: Optional[int] = None, # Replaced with the `@modal.concurrent` decorator
897
974
  _experimental_buffer_containers: Optional[int] = None, # Now stable API with `buffer_containers`
898
- allow_cross_region_volumes: Optional[bool] = None, # Always True on the Modal backend now
899
975
  ) -> Callable[[Union[CLS_T, _PartialFunction]], CLS_T]:
900
976
  """
901
977
  Decorator to register a new Modal [Cls](https://modal.com/docs/reference/modal.Cls) with this App.
@@ -916,15 +992,19 @@ class _App:
916
992
  " Please use the `@modal.concurrent` decorator instead."
917
993
  "\n\nSee https://modal.com/docs/guide/modal-1-0-migration for more information.",
918
994
  )
919
- if allow_cross_region_volumes is not None:
920
- 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)]
921
999
 
922
1000
  def wrapper(wrapped_cls: Union[CLS_T, _PartialFunction]) -> CLS_T:
1001
+ local_state = self._local_state
923
1002
  # Check if the decorated object is a class
924
1003
  if isinstance(wrapped_cls, _PartialFunction):
925
1004
  wrapped_cls.registered = True
926
1005
  user_cls = wrapped_cls.user_cls
927
1006
  if wrapped_cls.flags & _PartialFunctionFlags.CONCURRENT:
1007
+ verify_concurrent_params(params=wrapped_cls.params, is_flash=is_flash_object(experimental_options))
928
1008
  max_concurrent_inputs = wrapped_cls.params.max_concurrent_inputs
929
1009
  target_concurrent_inputs = wrapped_cls.params.target_concurrent_inputs
930
1010
  else:
@@ -933,13 +1013,16 @@ class _App:
933
1013
 
934
1014
  if wrapped_cls.flags & _PartialFunctionFlags.CLUSTERED:
935
1015
  cluster_size = wrapped_cls.params.cluster_size
1016
+ rdma = wrapped_cls.params.rdma
936
1017
  else:
937
1018
  cluster_size = None
1019
+ rdma = None
938
1020
  else:
939
1021
  user_cls = wrapped_cls
940
1022
  max_concurrent_inputs = allow_concurrent_inputs
941
1023
  target_concurrent_inputs = None
942
1024
  cluster_size = None
1025
+ rdma = None
943
1026
  if not inspect.isclass(user_cls):
944
1027
  raise TypeError("The @app.cls decorator must be used on a class.")
945
1028
 
@@ -983,10 +1066,10 @@ class _App:
983
1066
  info,
984
1067
  app=self,
985
1068
  image=image or self._get_default_image(),
986
- secrets=[*self._secrets, *secrets],
1069
+ secrets=[*local_state.secrets_default, *secrets],
987
1070
  gpu=gpu,
988
1071
  network_file_systems=network_file_systems,
989
- volumes={**self._volumes, **volumes},
1072
+ volumes={**local_state.volumes_default, **volumes},
990
1073
  cpu=cpu,
991
1074
  memory=memory,
992
1075
  ephemeral_disk=ephemeral_disk,
@@ -1001,6 +1084,7 @@ class _App:
1001
1084
  batch_max_size=batch_max_size,
1002
1085
  batch_wait_ms=batch_wait_ms,
1003
1086
  timeout=timeout,
1087
+ startup_timeout=startup_timeout or timeout,
1004
1088
  cloud=cloud,
1005
1089
  enable_memory_snapshot=enable_memory_snapshot,
1006
1090
  block_network=block_network,
@@ -1009,16 +1093,22 @@ class _App:
1009
1093
  scheduler_placement=scheduler_placement,
1010
1094
  i6pn_enabled=i6pn_enabled,
1011
1095
  cluster_size=cluster_size,
1012
- include_source=include_source if include_source is not None else self._include_source_default,
1096
+ rdma=rdma,
1097
+ include_source=include_source if include_source is not None else local_state.include_source_default,
1013
1098
  experimental_options={k: str(v) for k, v in (experimental_options or {}).items()},
1014
1099
  _experimental_proxy_ip=_experimental_proxy_ip,
1015
1100
  _experimental_custom_scaling_factor=_experimental_custom_scaling_factor,
1016
- _experimental_enable_gpu_snapshot=_experimental_enable_gpu_snapshot,
1101
+ restrict_output=_experimental_restrict_output,
1017
1102
  )
1018
1103
 
1019
1104
  self._add_function(cls_func, is_web_endpoint=False)
1020
1105
 
1021
1106
  cls: _Cls = _Cls.from_local(user_cls, self, cls_func)
1107
+ for method_name, partial_function in cls._method_partials.items():
1108
+ if partial_function.params.webhook_config is not None:
1109
+ full_name = f"{user_cls.__name__}.{method_name}"
1110
+ local_state.web_endpoints.append(full_name)
1111
+ partial_function.registered = True
1022
1112
 
1023
1113
  tag: str = user_cls.__name__
1024
1114
  self._add_class(tag, cls)
@@ -1026,7 +1116,7 @@ class _App:
1026
1116
 
1027
1117
  return wrapper
1028
1118
 
1029
- def include(self, /, other_app: "_App") -> typing_extensions.Self:
1119
+ def include(self, /, other_app: "_App", inherit_tags: bool = True) -> typing_extensions.Self:
1030
1120
  """Include another App's objects in this one.
1031
1121
 
1032
1122
  Useful for splitting up Modal Apps across different self-contained files.
@@ -1049,12 +1139,19 @@ class _App:
1049
1139
  # use function declared on the included app
1050
1140
  bar.remote()
1051
1141
  ```
1142
+
1143
+ When `inherit_tags=True` any tags set on the other App will be inherited by this App
1144
+ (with this App's tags taking precedence in the case of conflicts).
1145
+
1052
1146
  """
1053
- 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():
1054
1151
  self._add_function(function, False) # TODO(erikbern): webhook config?
1055
1152
 
1056
- for tag, cls in other_app._classes.items():
1057
- 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)
1058
1155
  if existing_cls and existing_cls != cls:
1059
1156
  logger.warning(
1060
1157
  f"Named app class {tag} with existing value {existing_cls} is being "
@@ -1062,8 +1159,47 @@ class _App:
1062
1159
  )
1063
1160
 
1064
1161
  self._add_class(tag, cls)
1162
+
1163
+ if inherit_tags:
1164
+ this_local_state.tags = {**other_app_local_state.tags, **this_local_state.tags}
1165
+
1065
1166
  return self
1066
1167
 
1168
+ async def set_tags(self, tags: Mapping[str, str], *, client: Optional[_Client] = None) -> None:
1169
+ """Attach key-value metadata to the App.
1170
+
1171
+ Tag metadata can be used to add organization-specific context to the App and can be
1172
+ included in billing reports and other informational APIs. Tags can also be set in
1173
+ the App constructor.
1174
+
1175
+ Any tags set on the App before calling this method will be removed if they are not
1176
+ included in the argument (i.e., this method does not have `.update()` semantics).
1177
+
1178
+ """
1179
+ # Note that we are requiring the App to be "running" before we set the tags.
1180
+ # Alternatively, we could hold onto the tags (i.e. in `self._local_state.tags`) and then pass
1181
+ # then up when AppPublish gets called. I'm not certain we want to support it, though.
1182
+ # It might not be obvious to users that `.set_tags()` is eager and has immediate effect
1183
+ # when the App is running, but lazy (and potentially ignored) otherwise. There would be
1184
+ # other complications, like what do you do with any tags set in the constructor, and
1185
+ # what should `.get_tags()` do when it's called before the App is running?
1186
+ if self._app_id is None:
1187
+ raise InvalidError("`App.set_tags` cannot be called before the App is running.")
1188
+ check_tag_dict(tags)
1189
+ req = api_pb2.AppSetTagsRequest(app_id=self._app_id, tags=tags)
1190
+
1191
+ client = client or self._client or await _Client.from_env()
1192
+ await client.stub.AppSetTags(req)
1193
+
1194
+ async def get_tags(self, *, client: Optional[_Client] = None) -> dict[str, str]:
1195
+ """Get the tags that are currently attached to the App."""
1196
+ if self._app_id is None:
1197
+ raise InvalidError("`App.get_tags` cannot be called before the App is running.")
1198
+ req = api_pb2.AppGetTagsRequest(app_id=self._app_id)
1199
+ client = client or self._client or await _Client.from_env()
1200
+ resp = await client.stub.AppGetTags(req)
1201
+ return dict(resp.tags)
1202
+
1067
1203
  async def _logs(self, client: Optional[_Client] = None) -> AsyncGenerator[str, None]:
1068
1204
  """Stream logs from the app.
1069
1205