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/runner.py CHANGED
@@ -8,9 +8,8 @@ import asyncio
8
8
  import dataclasses
9
9
  import os
10
10
  import time
11
- import typing
12
- import warnings
13
11
  from collections.abc import AsyncGenerator
12
+ from contextlib import nullcontext
14
13
  from multiprocessing.synchronize import Event
15
14
  from typing import TYPE_CHECKING, Any, Optional, TypeVar
16
15
 
@@ -19,6 +18,8 @@ from synchronicity.async_wrap import asynccontextmanager
19
18
 
20
19
  import modal._runtime.execution_context
21
20
  import modal_proto.api_pb2
21
+ from modal._load_context import LoadContext
22
+ from modal._utils.grpc_utils import Retry
22
23
  from modal_proto import api_pb2
23
24
 
24
25
  from ._functions import _Function
@@ -27,14 +28,14 @@ from ._pty import get_pty_info
27
28
  from ._resolver import Resolver
28
29
  from ._traceback import print_server_warnings, traceback_contains_remote_call
29
30
  from ._utils.async_utils import TaskContext, gather_cancel_on_exc, synchronize_api
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,16 +104,15 @@ async def _init_local_app_new(
103
104
  async def _init_local_app_from_name(
104
105
  client: _Client,
105
106
  name: str,
106
- namespace: Any,
107
+ tags: dict[str, str],
107
108
  environment_name: str = "",
108
109
  ) -> RunningApp:
109
110
  # Look up any existing deployment
110
111
  app_req = api_pb2.AppGetByDeploymentNameRequest(
111
112
  name=name,
112
- namespace=namespace,
113
113
  environment_name=environment_name,
114
114
  )
115
- app_resp = await retry_transient_errors(client.stub.AppGetByDeploymentName, app_req)
115
+ app_resp = await client.stub.AppGetByDeploymentName(app_req)
116
116
  existing_app_id = app_resp.app_id or None
117
117
 
118
118
  # Grab the app
@@ -120,24 +120,19 @@ async def _init_local_app_from_name(
120
120
  return await _init_local_app_existing(client, existing_app_id, environment_name)
121
121
  else:
122
122
  return await _init_local_app_new(
123
- client, name, api_pb2.APP_STATE_INITIALIZING, environment_name=environment_name
123
+ client, name, tags, api_pb2.APP_STATE_INITIALIZING, environment_name=environment_name
124
124
  )
125
125
 
126
126
 
127
127
  async def _create_all_objects(
128
- client: _Client,
129
128
  running_app: RunningApp,
130
- functions: dict[str, _Function],
131
- classes: dict[str, _Cls],
132
- environment_name: str,
129
+ local_app_state: "modal.app._LocalAppState",
130
+ load_context: LoadContext,
133
131
  ) -> None:
134
132
  """Create objects that have been defined but not created on the server."""
135
- indexed_objects: dict[str, _Object] = {**functions, **classes}
136
- resolver = Resolver(
137
- client,
138
- environment_name=environment_name,
139
- app_id=running_app.app_id,
140
- )
133
+ indexed_objects: dict[str, _Object] = {**local_app_state.functions, **local_app_state.classes}
134
+
135
+ resolver = Resolver()
141
136
  with resolver.display():
142
137
  # Get current objects, and reset all objects
143
138
  tag_to_object_id = {**running_app.function_ids, **running_app.class_ids}
@@ -160,7 +155,7 @@ async def _create_all_objects(
160
155
  # Note: preload only currently implemented for Functions, returns None otherwise
161
156
  # this is to ensure that directly referenced functions from the global scope has
162
157
  # ids associated with them when they are serialized into other functions
163
- await resolver.preload(obj, existing_object_id)
158
+ await resolver.preload(obj, load_context, existing_object_id)
164
159
  if obj.is_hydrated:
165
160
  tag_to_object_id[tag] = obj.object_id
166
161
 
@@ -168,7 +163,8 @@ async def _create_all_objects(
168
163
 
169
164
  async def _load(tag, obj):
170
165
  existing_object_id = tag_to_object_id.get(tag)
171
- 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)
172
168
  if _Function._is_id_type(obj.object_id):
173
169
  running_app.function_ids[tag] = obj.object_id
174
170
  elif _Cls._is_id_type(obj.object_id):
@@ -183,29 +179,29 @@ async def _publish_app(
183
179
  client: _Client,
184
180
  running_app: RunningApp,
185
181
  app_state: int, # api_pb2.AppState.value
186
- functions: dict[str, _Function],
187
- classes: dict[str, _Cls],
188
- name: str = "", # Only relevant for deployments
189
- tag: str = "", # Only relevant for deployments
182
+ app_local_state: "modal.app._LocalAppState",
183
+ name: str = "",
184
+ deployment_tag: str = "", # Only relevant for deployments
190
185
  commit_info: Optional[api_pb2.CommitInfo] = None, # Git commit information
191
186
  ) -> tuple[str, list[api_pb2.Warning]]:
192
187
  """Wrapper for AppPublish RPC."""
193
-
188
+ functions = app_local_state.functions
194
189
  definition_ids = {obj.object_id: obj._get_metadata().definition_id for obj in functions.values()} # type: ignore
195
190
 
196
191
  request = api_pb2.AppPublishRequest(
197
192
  app_id=running_app.app_id,
198
193
  name=name,
199
- deployment_tag=tag,
194
+ tags=app_local_state.tags,
195
+ deployment_tag=deployment_tag,
196
+ commit_info=commit_info,
200
197
  app_state=app_state, # type: ignore : should be a api_pb2.AppState.value
201
198
  function_ids=running_app.function_ids,
202
199
  class_ids=running_app.class_ids,
203
200
  definition_ids=definition_ids,
204
- commit_info=commit_info,
205
201
  )
206
202
 
207
203
  try:
208
- response = await retry_transient_errors(client.stub.AppPublish, request)
204
+ response = await client.stub.AppPublish(request)
209
205
  except GRPCError as exc:
210
206
  if exc.status == Status.INVALID_ARGUMENT or exc.status == Status.FAILED_PRECONDITION:
211
207
  raise InvalidError(exc.message)
@@ -229,7 +225,7 @@ async def _disconnect(
229
225
 
230
226
  logger.debug("Sending app disconnect/stop request")
231
227
  req_disconnect = api_pb2.AppClientDisconnectRequest(app_id=app_id, reason=reason, exception=exc_str)
232
- await retry_transient_errors(client.stub.AppClientDisconnect, req_disconnect)
228
+ await client.stub.AppClientDisconnect(req_disconnect)
233
229
  logger.debug("App disconnected")
234
230
 
235
231
 
@@ -259,16 +255,17 @@ async def _status_based_disconnect(client: _Client, app_id: str, exc_info: Optio
259
255
 
260
256
  @asynccontextmanager
261
257
  async def _run_app(
262
- app: _App,
258
+ app: "modal.app._App",
263
259
  *,
264
260
  client: Optional[_Client] = None,
265
261
  detach: bool = False,
266
262
  environment_name: Optional[str] = None,
267
263
  interactive: bool = False,
268
- ) -> AsyncGenerator[_App, None]:
264
+ ) -> AsyncGenerator["modal.app._App", None]:
269
265
  """mdmd:hidden"""
270
- if environment_name is None:
271
- 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
+ )
272
269
 
273
270
  if modal._runtime.execution_context._is_currently_importing:
274
271
  raise InvalidError("Can not run an app in global scope within a container")
@@ -289,35 +286,32 @@ async def _run_app(
289
286
  # https://docs.python.org/3/library/__main__.html#import-main
290
287
  app.set_description(__main__.__name__)
291
288
 
292
- if client is None:
293
- client = await _Client.from_env()
294
-
295
289
  app_state = api_pb2.APP_STATE_DETACHED if detach else api_pb2.APP_STATE_EPHEMERAL
296
290
 
297
291
  output_mgr = _get_output_manager()
298
292
  if interactive and output_mgr is None:
299
- warnings.warn(
300
- "Interactive mode is disabled because no output manager is active. "
301
- "Use 'with modal.enable_output():' to enable interactive mode and see logs.",
302
- stacklevel=2,
303
- )
304
- interactive = False
293
+ msg = "Interactive mode requires output to be enabled. (Use the the `modal.enable_output()` context manager.)"
294
+ raise InvalidError(msg)
295
+
296
+ local_app_state = app._local_state
305
297
 
306
298
  running_app: RunningApp = await _init_local_app_new(
307
- client,
299
+ load_context.client,
308
300
  app.description or "",
309
- environment_name=environment_name or "",
301
+ local_app_state.tags,
302
+ environment_name=load_context.environment_name,
310
303
  app_state=app_state,
311
304
  interactive=interactive,
312
305
  )
306
+ await load_context.in_place_upgrade(app_id=running_app.app_id)
313
307
 
314
308
  logs_timeout = config["logs_timeout"]
315
- 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:
316
310
  # Start heartbeats loop to keep the client alive
317
311
  # we don't log heartbeat exceptions in detached mode
318
312
  # as losing the local connection will not affect the running app
319
313
  def heartbeat():
320
- return _heartbeat(client, running_app.app_id)
314
+ return _heartbeat(load_context.client, running_app.app_id)
321
315
 
322
316
  heartbeat_loop = tc.infinite_loop(heartbeat, sleep=HEARTBEAT_INTERVAL, log_exception=not detach)
323
317
  logs_loop: Optional[asyncio.Task] = None
@@ -338,26 +332,37 @@ async def _run_app(
338
332
  # Start logs loop
339
333
 
340
334
  logs_loop = tc.create_task(
341
- 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
+ )
342
338
  )
343
339
 
344
340
  try:
345
341
  # Create all members
346
- 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)
347
343
 
348
344
  # Publish the app
349
- 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)
350
346
  except asyncio.CancelledError as e:
351
347
  # this typically happens on sigint/ctrl-C during setup (the KeyboardInterrupt happens in the main thread)
352
348
  if output_mgr := _get_output_manager():
353
349
  output_mgr.print("Aborting app initialization...\n")
354
350
 
355
- await _status_based_disconnect(client, running_app.app_id, e)
351
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
356
352
  raise
357
353
  except BaseException as e:
358
- await _status_based_disconnect(client, running_app.app_id, e)
354
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
359
355
  raise
360
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
+
361
366
  try:
362
367
  # Show logs from dynamically created images.
363
368
  # TODO: better way to do this
@@ -366,32 +371,30 @@ async def _run_app(
366
371
 
367
372
  # Yield to context
368
373
  if output_mgr := _get_output_manager():
369
- 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:
370
377
  yield app
371
378
  else:
372
379
  yield app
373
380
  # successful completion!
374
381
  heartbeat_loop.cancel()
375
- 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)
376
383
  except KeyboardInterrupt as e:
377
384
  # this happens only if sigint comes in during the yield block above
378
385
  if detach:
379
386
  if output_mgr := _get_output_manager():
380
387
  output_mgr.print(output_mgr.step_completed("Shutting down Modal client."))
381
- output_mgr.print(
382
- "The detached app keeps running. You can track its progress at: "
383
- f"[magenta]{running_app.app_page_url}[/magenta]"
384
- ""
385
- )
388
+ output_mgr.print(detached_disconnect_msg)
386
389
  if logs_loop:
387
390
  logs_loop.cancel()
388
- await _status_based_disconnect(client, running_app.app_id, e)
391
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
389
392
  else:
390
393
  if output_mgr := _get_output_manager():
391
394
  output_mgr.print(
392
395
  "Disconnecting from Modal - This will terminate your Modal app in a few seconds.\n"
393
396
  )
394
- await _status_based_disconnect(client, running_app.app_id, e)
397
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
395
398
  if logs_loop:
396
399
  try:
397
400
  await asyncio.wait_for(logs_loop, timeout=logs_timeout)
@@ -406,9 +409,17 @@ async def _run_app(
406
409
  )
407
410
  )
408
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
409
420
  except BaseException as e:
410
421
  logger.info("Exception during app run")
411
- await _status_based_disconnect(client, running_app.app_id, e)
422
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
412
423
  raise
413
424
 
414
425
  # wait for logs gracefully, even though the task context would do the same
@@ -429,28 +440,28 @@ async def _run_app(
429
440
 
430
441
 
431
442
  async def _serve_update(
432
- app: _App,
443
+ app: "modal.app._App",
433
444
  existing_app_id: str,
434
445
  is_ready: Event,
435
446
  environment_name: str,
436
447
  ) -> None:
437
448
  """mdmd:hidden"""
438
449
  # Used by child process to reinitialize a served app
439
- client = await _Client.from_env()
450
+ load_context = await app._root_load_context.reset().in_place_upgrade(environment_name=environment_name)
440
451
  try:
441
- running_app: RunningApp = await _init_local_app_existing(client, existing_app_id, environment_name)
442
-
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
443
455
  # Create objects
444
- await _create_all_objects(
445
- client,
446
- running_app,
447
- app._functions,
448
- app._classes,
449
- environment_name,
450
- )
456
+ await _create_all_objects(running_app, local_app_state, load_context)
451
457
 
452
458
  # Publish the updated app
453
- 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
+ )
454
465
 
455
466
  # Communicate to the parent process
456
467
  is_ready.set()
@@ -470,9 +481,9 @@ class DeployResult:
470
481
 
471
482
 
472
483
  async def _deploy_app(
473
- app: _App,
484
+ app: "modal.app._App",
474
485
  name: Optional[str] = None,
475
- namespace: Any = api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
486
+ namespace: Any = None, # mdmd:line-hidden
476
487
  client: Optional[_Client] = None,
477
488
  environment_name: Optional[str] = None,
478
489
  tag: str = "",
@@ -481,8 +492,7 @@ async def _deploy_app(
481
492
 
482
493
  Users should prefer the `modal deploy` CLI or the `App.deploy` method.
483
494
  """
484
- if environment_name is None:
485
- environment_name = typing.cast(str, config.get("environment"))
495
+ warn_if_passing_namespace(namespace, "modal.runner.deploy_app")
486
496
 
487
497
  name = name or app.name or ""
488
498
  if not name:
@@ -497,7 +507,7 @@ async def _deploy_app(
497
507
  else:
498
508
  check_object_name(name, "App")
499
509
 
500
- if tag and not is_valid_tag(tag):
510
+ if tag and not is_valid_tag(tag, max_length=50):
501
511
  raise InvalidError(
502
512
  f"Deployment tag {tag!r} is invalid."
503
513
  "\n\nTags may only contain alphanumeric characters, dashes, periods, and underscores, "
@@ -507,13 +517,24 @@ async def _deploy_app(
507
517
  if client is None:
508
518
  client = await _Client.from_env()
509
519
 
520
+ local_app_state = app._local_state
510
521
  t0 = time.time()
511
522
 
512
523
  # Get git information to track deployment history
513
524
  commit_info_task = asyncio.create_task(get_git_commit_info())
514
525
 
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
+ )
515
532
  running_app: RunningApp = await _init_local_app_from_name(
516
- client, name, namespace, environment_name=environment_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,
517
538
  )
518
539
 
519
540
  async with TaskContext(0) as tc:
@@ -525,13 +546,7 @@ async def _deploy_app(
525
546
 
526
547
  try:
527
548
  # Create all members
528
- await _create_all_objects(
529
- client,
530
- running_app,
531
- app._functions,
532
- app._classes,
533
- environment_name=environment_name,
534
- )
549
+ await _create_all_objects(running_app, local_app_state, root_load_context)
535
550
 
536
551
  commit_info = None
537
552
  try:
@@ -543,11 +558,10 @@ async def _deploy_app(
543
558
  client,
544
559
  running_app,
545
560
  api_pb2.APP_STATE_DEPLOYED,
546
- app._functions,
547
- app._classes,
548
- name,
549
- tag,
550
- commit_info,
561
+ local_app_state,
562
+ name=name,
563
+ deployment_tag=tag,
564
+ commit_info=commit_info,
551
565
  )
552
566
  except Exception as e:
553
567
  # Note that AppClientDisconnect only stops the app if it's still initializing, and is a no-op otherwise.
@@ -567,7 +581,7 @@ async def _deploy_app(
567
581
 
568
582
 
569
583
  async def _interactive_shell(
570
- _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
571
585
  ) -> None:
572
586
  """Run an interactive shell (like `bash`) within the image for this app.
573
587
 
@@ -613,19 +627,19 @@ async def _interactive_shell(
613
627
 
614
628
  try:
615
629
  if pty:
616
- container_process = await sandbox.exec(
630
+ container_process = await sandbox._exec(
617
631
  *sandbox_cmds, pty_info=get_pty_info(shell=True) if pty else None
618
632
  )
619
633
  await container_process.attach()
620
634
  else:
621
- container_process = await sandbox.exec(
635
+ container_process = await sandbox._exec(
622
636
  *sandbox_cmds, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT
623
637
  )
624
638
  await container_process.wait()
625
639
  except InteractiveTimeoutError:
626
640
  # Check on status of Sandbox. It may have crashed, causing connection failure.
627
641
  req = api_pb2.SandboxWaitRequest(sandbox_id=sandbox._object_id, timeout=0)
628
- resp = await retry_transient_errors(sandbox._client.stub.SandboxWait, req)
642
+ resp = await sandbox._client.stub.SandboxWait(req)
629
643
  if resp.result.exception:
630
644
  raise RemoteError(resp.result.exception)
631
645
  else: