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/runner.py CHANGED
@@ -8,8 +8,8 @@ import asyncio
8
8
  import dataclasses
9
9
  import os
10
10
  import time
11
- import typing
12
11
  from collections.abc import AsyncGenerator
12
+ from contextlib import nullcontext
13
13
  from multiprocessing.synchronize import Event
14
14
  from typing import TYPE_CHECKING, Any, Optional, TypeVar
15
15
 
@@ -18,6 +18,8 @@ from synchronicity.async_wrap import asynccontextmanager
18
18
 
19
19
  import modal._runtime.execution_context
20
20
  import modal_proto.api_pb2
21
+ from modal._load_context import LoadContext
22
+ from modal._utils.grpc_utils import Retry
21
23
  from modal_proto import api_pb2
22
24
 
23
25
  from ._functions import _Function
@@ -28,13 +30,12 @@ from ._traceback import print_server_warnings, traceback_contains_remote_call
28
30
  from ._utils.async_utils import TaskContext, gather_cancel_on_exc, synchronize_api
29
31
  from ._utils.deprecation import warn_if_passing_namespace
30
32
  from ._utils.git_utils import get_git_commit_info
31
- from ._utils.grpc_utils import retry_transient_errors
32
33
  from ._utils.name_utils import check_object_name, is_valid_tag
33
34
  from .client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
34
35
  from .cls import _Cls
35
36
  from .config import config, logger
36
37
  from .environments import _get_environment_cached
37
- from .exception import InteractiveTimeoutError, InvalidError, RemoteError, _CliUserExecutionError
38
+ from .exception import ConnectionError, InteractiveTimeoutError, InvalidError, RemoteError, _CliUserExecutionError
38
39
  from .output import _get_output_manager, enable_output
39
40
  from .running_app import RunningApp, running_app_from_layout
40
41
  from .sandbox import _Sandbox
@@ -42,9 +43,7 @@ from .secret import _Secret
42
43
  from .stream_type import StreamType
43
44
 
44
45
  if TYPE_CHECKING:
45
- from .app import _App
46
- else:
47
- _App = TypeVar("_App")
46
+ import modal.app
48
47
 
49
48
 
50
49
  V = TypeVar("V")
@@ -55,14 +54,14 @@ async def _heartbeat(client: _Client, app_id: str) -> None:
55
54
  # TODO(erikbern): we should capture exceptions here
56
55
  # * if request fails: destroy the client
57
56
  # * if server says the app is gone: print a helpful warning about detaching
58
- await retry_transient_errors(client.stub.AppHeartbeat, request, attempt_timeout=HEARTBEAT_TIMEOUT)
57
+ await client.stub.AppHeartbeat(request, retry=Retry(attempt_timeout=HEARTBEAT_TIMEOUT))
59
58
 
60
59
 
61
60
  async def _init_local_app_existing(client: _Client, existing_app_id: str, environment_name: str) -> RunningApp:
62
61
  # Get all the objects first
63
62
  obj_req = api_pb2.AppGetLayoutRequest(app_id=existing_app_id)
64
63
  obj_resp, _ = await gather_cancel_on_exc(
65
- retry_transient_errors(client.stub.AppGetLayout, obj_req),
64
+ client.stub.AppGetLayout(obj_req),
66
65
  # Cache the environment associated with the app now as we will use it later
67
66
  _get_environment_cached(environment_name, client),
68
67
  )
@@ -77,6 +76,7 @@ async def _init_local_app_existing(client: _Client, existing_app_id: str, enviro
77
76
  async def _init_local_app_new(
78
77
  client: _Client,
79
78
  description: str,
79
+ tags: dict[str, str],
80
80
  app_state: int, # ValueType
81
81
  environment_name: str = "",
82
82
  interactive: bool = False,
@@ -85,9 +85,10 @@ async def _init_local_app_new(
85
85
  description=description,
86
86
  environment_name=environment_name,
87
87
  app_state=app_state, # type: ignore
88
+ tags=tags,
88
89
  )
89
90
  app_resp, _ = await gather_cancel_on_exc( # TODO: use TaskGroup?
90
- retry_transient_errors(client.stub.AppCreate, app_req),
91
+ client.stub.AppCreate(app_req),
91
92
  # Cache the environment associated with the app now as we will use it later
92
93
  _get_environment_cached(environment_name, client),
93
94
  )
@@ -103,6 +104,7 @@ async def _init_local_app_new(
103
104
  async def _init_local_app_from_name(
104
105
  client: _Client,
105
106
  name: str,
107
+ tags: dict[str, str],
106
108
  environment_name: str = "",
107
109
  ) -> RunningApp:
108
110
  # Look up any existing deployment
@@ -110,7 +112,7 @@ async def _init_local_app_from_name(
110
112
  name=name,
111
113
  environment_name=environment_name,
112
114
  )
113
- app_resp = await retry_transient_errors(client.stub.AppGetByDeploymentName, app_req)
115
+ app_resp = await client.stub.AppGetByDeploymentName(app_req)
114
116
  existing_app_id = app_resp.app_id or None
115
117
 
116
118
  # Grab the app
@@ -118,24 +120,19 @@ async def _init_local_app_from_name(
118
120
  return await _init_local_app_existing(client, existing_app_id, environment_name)
119
121
  else:
120
122
  return await _init_local_app_new(
121
- client, name, api_pb2.APP_STATE_INITIALIZING, environment_name=environment_name
123
+ client, name, tags, api_pb2.APP_STATE_INITIALIZING, environment_name=environment_name
122
124
  )
123
125
 
124
126
 
125
127
  async def _create_all_objects(
126
- client: _Client,
127
128
  running_app: RunningApp,
128
- functions: dict[str, _Function],
129
- classes: dict[str, _Cls],
130
- environment_name: str,
129
+ local_app_state: "modal.app._LocalAppState",
130
+ load_context: LoadContext,
131
131
  ) -> None:
132
132
  """Create objects that have been defined but not created on the server."""
133
- indexed_objects: dict[str, _Object] = {**functions, **classes}
134
- resolver = Resolver(
135
- client,
136
- environment_name=environment_name,
137
- app_id=running_app.app_id,
138
- )
133
+ indexed_objects: dict[str, _Object] = {**local_app_state.functions, **local_app_state.classes}
134
+
135
+ resolver = Resolver()
139
136
  with resolver.display():
140
137
  # Get current objects, and reset all objects
141
138
  tag_to_object_id = {**running_app.function_ids, **running_app.class_ids}
@@ -158,7 +155,7 @@ async def _create_all_objects(
158
155
  # Note: preload only currently implemented for Functions, returns None otherwise
159
156
  # this is to ensure that directly referenced functions from the global scope has
160
157
  # ids associated with them when they are serialized into other functions
161
- await resolver.preload(obj, existing_object_id)
158
+ await resolver.preload(obj, load_context, existing_object_id)
162
159
  if obj.is_hydrated:
163
160
  tag_to_object_id[tag] = obj.object_id
164
161
 
@@ -166,7 +163,8 @@ async def _create_all_objects(
166
163
 
167
164
  async def _load(tag, obj):
168
165
  existing_object_id = tag_to_object_id.get(tag)
169
- await resolver.load(obj, existing_object_id)
166
+ # Pass load_context so dependencies can inherit app_id, client, etc.
167
+ await resolver.load(obj, load_context, existing_object_id=existing_object_id)
170
168
  if _Function._is_id_type(obj.object_id):
171
169
  running_app.function_ids[tag] = obj.object_id
172
170
  elif _Cls._is_id_type(obj.object_id):
@@ -181,29 +179,29 @@ async def _publish_app(
181
179
  client: _Client,
182
180
  running_app: RunningApp,
183
181
  app_state: int, # api_pb2.AppState.value
184
- functions: dict[str, _Function],
185
- classes: dict[str, _Cls],
186
- name: str = "", # Only relevant for deployments
187
- tag: str = "", # Only relevant for deployments
182
+ app_local_state: "modal.app._LocalAppState",
183
+ name: str = "",
184
+ deployment_tag: str = "", # Only relevant for deployments
188
185
  commit_info: Optional[api_pb2.CommitInfo] = None, # Git commit information
189
186
  ) -> tuple[str, list[api_pb2.Warning]]:
190
187
  """Wrapper for AppPublish RPC."""
191
-
188
+ functions = app_local_state.functions
192
189
  definition_ids = {obj.object_id: obj._get_metadata().definition_id for obj in functions.values()} # type: ignore
193
190
 
194
191
  request = api_pb2.AppPublishRequest(
195
192
  app_id=running_app.app_id,
196
193
  name=name,
197
- deployment_tag=tag,
194
+ tags=app_local_state.tags,
195
+ deployment_tag=deployment_tag,
196
+ commit_info=commit_info,
198
197
  app_state=app_state, # type: ignore : should be a api_pb2.AppState.value
199
198
  function_ids=running_app.function_ids,
200
199
  class_ids=running_app.class_ids,
201
200
  definition_ids=definition_ids,
202
- commit_info=commit_info,
203
201
  )
204
202
 
205
203
  try:
206
- response = await retry_transient_errors(client.stub.AppPublish, request)
204
+ response = await client.stub.AppPublish(request)
207
205
  except GRPCError as exc:
208
206
  if exc.status == Status.INVALID_ARGUMENT or exc.status == Status.FAILED_PRECONDITION:
209
207
  raise InvalidError(exc.message)
@@ -227,7 +225,7 @@ async def _disconnect(
227
225
 
228
226
  logger.debug("Sending app disconnect/stop request")
229
227
  req_disconnect = api_pb2.AppClientDisconnectRequest(app_id=app_id, reason=reason, exception=exc_str)
230
- await retry_transient_errors(client.stub.AppClientDisconnect, req_disconnect)
228
+ await client.stub.AppClientDisconnect(req_disconnect)
231
229
  logger.debug("App disconnected")
232
230
 
233
231
 
@@ -257,16 +255,17 @@ async def _status_based_disconnect(client: _Client, app_id: str, exc_info: Optio
257
255
 
258
256
  @asynccontextmanager
259
257
  async def _run_app(
260
- app: _App,
258
+ app: "modal.app._App",
261
259
  *,
262
260
  client: Optional[_Client] = None,
263
261
  detach: bool = False,
264
262
  environment_name: Optional[str] = None,
265
263
  interactive: bool = False,
266
- ) -> AsyncGenerator[_App, None]:
264
+ ) -> AsyncGenerator["modal.app._App", None]:
267
265
  """mdmd:hidden"""
268
- if environment_name is None:
269
- environment_name = typing.cast(str, config.get("environment"))
266
+ load_context = await app._root_load_context.reset().in_place_upgrade(
267
+ client=client, environment_name=environment_name
268
+ )
270
269
 
271
270
  if modal._runtime.execution_context._is_currently_importing:
272
271
  raise InvalidError("Can not run an app in global scope within a container")
@@ -287,9 +286,6 @@ async def _run_app(
287
286
  # https://docs.python.org/3/library/__main__.html#import-main
288
287
  app.set_description(__main__.__name__)
289
288
 
290
- if client is None:
291
- client = await _Client.from_env()
292
-
293
289
  app_state = api_pb2.APP_STATE_DETACHED if detach else api_pb2.APP_STATE_EPHEMERAL
294
290
 
295
291
  output_mgr = _get_output_manager()
@@ -297,21 +293,25 @@ async def _run_app(
297
293
  msg = "Interactive mode requires output to be enabled. (Use the the `modal.enable_output()` context manager.)"
298
294
  raise InvalidError(msg)
299
295
 
296
+ local_app_state = app._local_state
297
+
300
298
  running_app: RunningApp = await _init_local_app_new(
301
- client,
299
+ load_context.client,
302
300
  app.description or "",
303
- environment_name=environment_name or "",
301
+ local_app_state.tags,
302
+ environment_name=load_context.environment_name,
304
303
  app_state=app_state,
305
304
  interactive=interactive,
306
305
  )
306
+ await load_context.in_place_upgrade(app_id=running_app.app_id)
307
307
 
308
308
  logs_timeout = config["logs_timeout"]
309
- async with app._set_local_app(client, running_app), TaskContext(grace=logs_timeout) as tc:
309
+ async with app._set_local_app(load_context.client, running_app), TaskContext(grace=logs_timeout) as tc:
310
310
  # Start heartbeats loop to keep the client alive
311
311
  # we don't log heartbeat exceptions in detached mode
312
312
  # as losing the local connection will not affect the running app
313
313
  def heartbeat():
314
- return _heartbeat(client, running_app.app_id)
314
+ return _heartbeat(load_context.client, running_app.app_id)
315
315
 
316
316
  heartbeat_loop = tc.infinite_loop(heartbeat, sleep=HEARTBEAT_INTERVAL, log_exception=not detach)
317
317
  logs_loop: Optional[asyncio.Task] = None
@@ -332,26 +332,37 @@ async def _run_app(
332
332
  # Start logs loop
333
333
 
334
334
  logs_loop = tc.create_task(
335
- get_app_logs_loop(client, output_mgr, app_id=running_app.app_id, app_logs_url=running_app.app_logs_url)
335
+ get_app_logs_loop(
336
+ load_context.client, output_mgr, app_id=running_app.app_id, app_logs_url=running_app.app_logs_url
337
+ )
336
338
  )
337
339
 
338
340
  try:
339
341
  # Create all members
340
- await _create_all_objects(client, running_app, app._functions, app._classes, environment_name)
342
+ await _create_all_objects(running_app, local_app_state, load_context)
341
343
 
342
344
  # Publish the app
343
- await _publish_app(client, running_app, app_state, app._functions, app._classes)
345
+ await _publish_app(load_context.client, running_app, app_state, local_app_state)
344
346
  except asyncio.CancelledError as e:
345
347
  # this typically happens on sigint/ctrl-C during setup (the KeyboardInterrupt happens in the main thread)
346
348
  if output_mgr := _get_output_manager():
347
349
  output_mgr.print("Aborting app initialization...\n")
348
350
 
349
- await _status_based_disconnect(client, running_app.app_id, e)
351
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
350
352
  raise
351
353
  except BaseException as e:
352
- await _status_based_disconnect(client, running_app.app_id, e)
354
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
353
355
  raise
354
356
 
357
+ detached_disconnect_msg = (
358
+ "The detached App will keep running. You can track its progress on the Dashboard: "
359
+ f"[magenta underline]{running_app.app_page_url}[/magenta underline]"
360
+ "\n\nStream App logs:\n"
361
+ f"[green]modal app logs {running_app.app_id}[/green]"
362
+ "\n\nStop the App:\n"
363
+ f"[green]modal app stop {running_app.app_id}[/green]"
364
+ )
365
+
355
366
  try:
356
367
  # Show logs from dynamically created images.
357
368
  # TODO: better way to do this
@@ -360,32 +371,30 @@ async def _run_app(
360
371
 
361
372
  # Yield to context
362
373
  if output_mgr := _get_output_manager():
363
- with output_mgr.show_status_spinner():
374
+ # Don't show status spinner in interactive mode to avoid interfering with breakpoints
375
+ spinner_ctx = nullcontext() if interactive else output_mgr.show_status_spinner()
376
+ with spinner_ctx:
364
377
  yield app
365
378
  else:
366
379
  yield app
367
380
  # successful completion!
368
381
  heartbeat_loop.cancel()
369
- await _status_based_disconnect(client, running_app.app_id, exc_info=None)
382
+ await _status_based_disconnect(load_context.client, running_app.app_id, exc_info=None)
370
383
  except KeyboardInterrupt as e:
371
384
  # this happens only if sigint comes in during the yield block above
372
385
  if detach:
373
386
  if output_mgr := _get_output_manager():
374
387
  output_mgr.print(output_mgr.step_completed("Shutting down Modal client."))
375
- output_mgr.print(
376
- "The detached app keeps running. You can track its progress at: "
377
- f"[magenta]{running_app.app_page_url}[/magenta]"
378
- ""
379
- )
388
+ output_mgr.print(detached_disconnect_msg)
380
389
  if logs_loop:
381
390
  logs_loop.cancel()
382
- await _status_based_disconnect(client, running_app.app_id, e)
391
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
383
392
  else:
384
393
  if output_mgr := _get_output_manager():
385
394
  output_mgr.print(
386
395
  "Disconnecting from Modal - This will terminate your Modal app in a few seconds.\n"
387
396
  )
388
- await _status_based_disconnect(client, running_app.app_id, e)
397
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
389
398
  if logs_loop:
390
399
  try:
391
400
  await asyncio.wait_for(logs_loop, timeout=logs_timeout)
@@ -400,9 +409,17 @@ async def _run_app(
400
409
  )
401
410
  )
402
411
  return
412
+ except ConnectionError as e:
413
+ # If we lose connection to the server after a detached App has started running, it will continue
414
+ # I think we can only exit "nicely" if we are able to print output though, otherwise we should raise
415
+ if detach and (output_mgr := _get_output_manager()):
416
+ output_mgr.print(":white_exclamation_mark: Connection lost!")
417
+ output_mgr.print(detached_disconnect_msg)
418
+ return
419
+ raise
403
420
  except BaseException as e:
404
421
  logger.info("Exception during app run")
405
- await _status_based_disconnect(client, running_app.app_id, e)
422
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
406
423
  raise
407
424
 
408
425
  # wait for logs gracefully, even though the task context would do the same
@@ -423,28 +440,28 @@ async def _run_app(
423
440
 
424
441
 
425
442
  async def _serve_update(
426
- app: _App,
443
+ app: "modal.app._App",
427
444
  existing_app_id: str,
428
445
  is_ready: Event,
429
446
  environment_name: str,
430
447
  ) -> None:
431
448
  """mdmd:hidden"""
432
449
  # Used by child process to reinitialize a served app
433
- client = await _Client.from_env()
450
+ load_context = await app._root_load_context.reset().in_place_upgrade(environment_name=environment_name)
434
451
  try:
435
- running_app: RunningApp = await _init_local_app_existing(client, existing_app_id, environment_name)
436
-
452
+ running_app: RunningApp = await _init_local_app_existing(load_context.client, existing_app_id, environment_name)
453
+ await load_context.in_place_upgrade(app_id=running_app.app_id)
454
+ local_app_state = app._local_state
437
455
  # Create objects
438
- await _create_all_objects(
439
- client,
440
- running_app,
441
- app._functions,
442
- app._classes,
443
- environment_name,
444
- )
456
+ await _create_all_objects(running_app, local_app_state, load_context)
445
457
 
446
458
  # Publish the updated app
447
- await _publish_app(client, running_app, api_pb2.APP_STATE_UNSPECIFIED, app._functions, app._classes)
459
+ await _publish_app(
460
+ load_context.client,
461
+ running_app,
462
+ app_state=api_pb2.APP_STATE_UNSPECIFIED,
463
+ app_local_state=local_app_state,
464
+ )
448
465
 
449
466
  # Communicate to the parent process
450
467
  is_ready.set()
@@ -464,7 +481,7 @@ class DeployResult:
464
481
 
465
482
 
466
483
  async def _deploy_app(
467
- app: _App,
484
+ app: "modal.app._App",
468
485
  name: Optional[str] = None,
469
486
  namespace: Any = None, # mdmd:line-hidden
470
487
  client: Optional[_Client] = None,
@@ -475,9 +492,6 @@ async def _deploy_app(
475
492
 
476
493
  Users should prefer the `modal deploy` CLI or the `App.deploy` method.
477
494
  """
478
- if environment_name is None:
479
- environment_name = typing.cast(str, config.get("environment"))
480
-
481
495
  warn_if_passing_namespace(namespace, "modal.runner.deploy_app")
482
496
 
483
497
  name = name or app.name or ""
@@ -493,7 +507,7 @@ async def _deploy_app(
493
507
  else:
494
508
  check_object_name(name, "App")
495
509
 
496
- if tag and not is_valid_tag(tag):
510
+ if tag and not is_valid_tag(tag, max_length=50):
497
511
  raise InvalidError(
498
512
  f"Deployment tag {tag!r} is invalid."
499
513
  "\n\nTags may only contain alphanumeric characters, dashes, periods, and underscores, "
@@ -503,12 +517,25 @@ async def _deploy_app(
503
517
  if client is None:
504
518
  client = await _Client.from_env()
505
519
 
520
+ local_app_state = app._local_state
506
521
  t0 = time.time()
507
522
 
508
523
  # Get git information to track deployment history
509
524
  commit_info_task = asyncio.create_task(get_git_commit_info())
510
525
 
511
- running_app: RunningApp = await _init_local_app_from_name(client, name, environment_name=environment_name)
526
+ # We need to do in-place replacement of fields in self._root_load_context in case it has already "spread"
527
+ # to with_options() instances or similar before load
528
+ root_load_context = await app._root_load_context.reset().in_place_upgrade(
529
+ client=client,
530
+ environment_name=environment_name,
531
+ )
532
+ running_app: RunningApp = await _init_local_app_from_name(
533
+ root_load_context.client, name, local_app_state.tags, environment_name=root_load_context.environment_name
534
+ )
535
+
536
+ await root_load_context.in_place_upgrade(
537
+ app_id=running_app.app_id,
538
+ )
512
539
 
513
540
  async with TaskContext(0) as tc:
514
541
  # Start heartbeats loop to keep the client alive
@@ -519,13 +546,7 @@ async def _deploy_app(
519
546
 
520
547
  try:
521
548
  # Create all members
522
- await _create_all_objects(
523
- client,
524
- running_app,
525
- app._functions,
526
- app._classes,
527
- environment_name=environment_name,
528
- )
549
+ await _create_all_objects(running_app, local_app_state, root_load_context)
529
550
 
530
551
  commit_info = None
531
552
  try:
@@ -537,11 +558,10 @@ async def _deploy_app(
537
558
  client,
538
559
  running_app,
539
560
  api_pb2.APP_STATE_DEPLOYED,
540
- app._functions,
541
- app._classes,
542
- name,
543
- tag,
544
- commit_info,
561
+ local_app_state,
562
+ name=name,
563
+ deployment_tag=tag,
564
+ commit_info=commit_info,
545
565
  )
546
566
  except Exception as e:
547
567
  # Note that AppClientDisconnect only stops the app if it's still initializing, and is a no-op otherwise.
@@ -561,7 +581,7 @@ async def _deploy_app(
561
581
 
562
582
 
563
583
  async def _interactive_shell(
564
- _app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: Any
584
+ _app: "modal.app._App", cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: Any
565
585
  ) -> None:
566
586
  """Run an interactive shell (like `bash`) within the image for this app.
567
587
 
@@ -607,19 +627,19 @@ async def _interactive_shell(
607
627
 
608
628
  try:
609
629
  if pty:
610
- container_process = await sandbox.exec(
630
+ container_process = await sandbox._exec(
611
631
  *sandbox_cmds, pty_info=get_pty_info(shell=True) if pty else None
612
632
  )
613
633
  await container_process.attach()
614
634
  else:
615
- container_process = await sandbox.exec(
635
+ container_process = await sandbox._exec(
616
636
  *sandbox_cmds, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
617
637
  )
618
638
  await container_process.wait()
619
639
  except InteractiveTimeoutError:
620
640
  # Check on status of Sandbox. It may have crashed, causing connection failure.
621
641
  req = api_pb2.SandboxWaitRequest(sandbox_id=sandbox._object_id, timeout=0)
622
- resp = await retry_transient_errors(sandbox._client.stub.SandboxWait, req)
642
+ resp = await sandbox._client.stub.SandboxWait(req)
623
643
  if resp.result.exception:
624
644
  raise RemoteError(resp.result.exception)
625
645
  else: