modal 1.1.5.dev83__py3-none-any.whl → 1.3.1.dev8__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 (139) hide show
  1. modal/__init__.py +4 -4
  2. modal/__main__.py +4 -29
  3. modal/_billing.py +84 -0
  4. modal/_clustered_functions.py +1 -3
  5. modal/_container_entrypoint.py +33 -208
  6. modal/_functions.py +146 -121
  7. modal/_grpc_client.py +191 -0
  8. modal/_ipython.py +16 -6
  9. modal/_load_context.py +106 -0
  10. modal/_object.py +72 -21
  11. modal/_output.py +12 -14
  12. modal/_partial_function.py +31 -4
  13. modal/_resolver.py +44 -57
  14. modal/_runtime/container_io_manager.py +26 -28
  15. modal/_runtime/container_io_manager.pyi +42 -44
  16. modal/_runtime/gpu_memory_snapshot.py +9 -7
  17. modal/_runtime/user_code_event_loop.py +80 -0
  18. modal/_runtime/user_code_imports.py +236 -10
  19. modal/_serialization.py +2 -1
  20. modal/_traceback.py +4 -13
  21. modal/_tunnel.py +16 -11
  22. modal/_tunnel.pyi +25 -3
  23. modal/_utils/async_utils.py +337 -10
  24. modal/_utils/auth_token_manager.py +1 -4
  25. modal/_utils/blob_utils.py +29 -22
  26. modal/_utils/function_utils.py +20 -21
  27. modal/_utils/grpc_testing.py +6 -3
  28. modal/_utils/grpc_utils.py +223 -64
  29. modal/_utils/mount_utils.py +26 -1
  30. modal/_utils/package_utils.py +0 -1
  31. modal/_utils/rand_pb_testing.py +8 -1
  32. modal/_utils/task_command_router_client.py +524 -0
  33. modal/_vendor/cloudpickle.py +144 -48
  34. modal/app.py +215 -96
  35. modal/app.pyi +78 -37
  36. modal/billing.py +5 -0
  37. modal/builder/2025.06.txt +6 -3
  38. modal/builder/PREVIEW.txt +2 -1
  39. modal/builder/base-images.json +4 -2
  40. modal/cli/_download.py +19 -3
  41. modal/cli/cluster.py +4 -2
  42. modal/cli/config.py +3 -1
  43. modal/cli/container.py +5 -4
  44. modal/cli/dict.py +5 -2
  45. modal/cli/entry_point.py +26 -2
  46. modal/cli/environment.py +2 -16
  47. modal/cli/launch.py +1 -76
  48. modal/cli/network_file_system.py +5 -20
  49. modal/cli/queues.py +5 -4
  50. modal/cli/run.py +24 -204
  51. modal/cli/secret.py +1 -2
  52. modal/cli/shell.py +375 -0
  53. modal/cli/utils.py +1 -13
  54. modal/cli/volume.py +11 -17
  55. modal/client.py +16 -125
  56. modal/client.pyi +94 -144
  57. modal/cloud_bucket_mount.py +3 -1
  58. modal/cloud_bucket_mount.pyi +4 -0
  59. modal/cls.py +101 -64
  60. modal/cls.pyi +9 -8
  61. modal/config.py +21 -1
  62. modal/container_process.py +288 -12
  63. modal/container_process.pyi +99 -38
  64. modal/dict.py +72 -33
  65. modal/dict.pyi +88 -57
  66. modal/environments.py +16 -8
  67. modal/environments.pyi +6 -2
  68. modal/exception.py +154 -16
  69. modal/experimental/__init__.py +23 -5
  70. modal/experimental/flash.py +161 -74
  71. modal/experimental/flash.pyi +97 -49
  72. modal/file_io.py +50 -92
  73. modal/file_io.pyi +117 -89
  74. modal/functions.pyi +70 -87
  75. modal/image.py +73 -47
  76. modal/image.pyi +33 -30
  77. modal/io_streams.py +500 -149
  78. modal/io_streams.pyi +279 -189
  79. modal/mount.py +60 -45
  80. modal/mount.pyi +41 -17
  81. modal/network_file_system.py +19 -11
  82. modal/network_file_system.pyi +72 -39
  83. modal/object.pyi +114 -22
  84. modal/parallel_map.py +42 -44
  85. modal/parallel_map.pyi +9 -17
  86. modal/partial_function.pyi +4 -2
  87. modal/proxy.py +14 -6
  88. modal/proxy.pyi +10 -2
  89. modal/queue.py +45 -38
  90. modal/queue.pyi +88 -52
  91. modal/runner.py +96 -96
  92. modal/runner.pyi +44 -27
  93. modal/sandbox.py +225 -108
  94. modal/sandbox.pyi +226 -63
  95. modal/secret.py +58 -56
  96. modal/secret.pyi +28 -13
  97. modal/serving.py +7 -11
  98. modal/serving.pyi +7 -8
  99. modal/snapshot.py +29 -15
  100. modal/snapshot.pyi +18 -10
  101. modal/token_flow.py +1 -1
  102. modal/token_flow.pyi +4 -6
  103. modal/volume.py +102 -55
  104. modal/volume.pyi +125 -66
  105. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/METADATA +10 -9
  106. modal-1.3.1.dev8.dist-info/RECORD +189 -0
  107. modal_proto/api.proto +86 -30
  108. modal_proto/api_grpc.py +10 -25
  109. modal_proto/api_pb2.py +1080 -1047
  110. modal_proto/api_pb2.pyi +253 -79
  111. modal_proto/api_pb2_grpc.py +14 -48
  112. modal_proto/api_pb2_grpc.pyi +6 -18
  113. modal_proto/modal_api_grpc.py +175 -176
  114. modal_proto/{sandbox_router.proto → task_command_router.proto} +62 -45
  115. modal_proto/task_command_router_grpc.py +138 -0
  116. modal_proto/task_command_router_pb2.py +180 -0
  117. modal_proto/{sandbox_router_pb2.pyi → task_command_router_pb2.pyi} +110 -63
  118. modal_proto/task_command_router_pb2_grpc.py +272 -0
  119. modal_proto/task_command_router_pb2_grpc.pyi +100 -0
  120. modal_version/__init__.py +1 -1
  121. modal_version/__main__.py +1 -1
  122. modal/cli/programs/launch_instance_ssh.py +0 -94
  123. modal/cli/programs/run_marimo.py +0 -95
  124. modal-1.1.5.dev83.dist-info/RECORD +0 -191
  125. modal_proto/modal_options_grpc.py +0 -3
  126. modal_proto/options.proto +0 -19
  127. modal_proto/options_grpc.py +0 -3
  128. modal_proto/options_pb2.py +0 -35
  129. modal_proto/options_pb2.pyi +0 -20
  130. modal_proto/options_pb2_grpc.py +0 -4
  131. modal_proto/options_pb2_grpc.pyi +0 -7
  132. modal_proto/sandbox_router_grpc.py +0 -105
  133. modal_proto/sandbox_router_pb2.py +0 -148
  134. modal_proto/sandbox_router_pb2_grpc.py +0 -203
  135. modal_proto/sandbox_router_pb2_grpc.pyi +0 -75
  136. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/WHEEL +0 -0
  137. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/entry_points.txt +0 -0
  138. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/licenses/LICENSE +0 -0
  139. {modal-1.1.5.dev83.dist-info → modal-1.3.1.dev8.dist-info}/top_level.txt +0 -0
modal/runner.py CHANGED
@@ -8,17 +8,17 @@ import asyncio
8
8
  import dataclasses
9
9
  import os
10
10
  import time
11
- import typing
12
11
  from collections.abc import AsyncGenerator
13
12
  from contextlib import nullcontext
14
13
  from multiprocessing.synchronize import Event
15
14
  from typing import TYPE_CHECKING, Any, Optional, TypeVar
16
15
 
17
- from grpclib import GRPCError, Status
18
16
  from synchronicity.async_wrap import asynccontextmanager
19
17
 
20
18
  import modal._runtime.execution_context
21
19
  import modal_proto.api_pb2
20
+ from modal._load_context import LoadContext
21
+ from modal._utils.grpc_utils import Retry
22
22
  from modal_proto import api_pb2
23
23
 
24
24
  from ._functions import _Function
@@ -29,13 +29,12 @@ from ._traceback import print_server_warnings, traceback_contains_remote_call
29
29
  from ._utils.async_utils import TaskContext, gather_cancel_on_exc, synchronize_api
30
30
  from ._utils.deprecation import warn_if_passing_namespace
31
31
  from ._utils.git_utils import get_git_commit_info
32
- from ._utils.grpc_utils import retry_transient_errors
33
32
  from ._utils.name_utils import check_object_name, is_valid_tag
34
33
  from .client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
35
34
  from .cls import _Cls
36
35
  from .config import config, logger
37
36
  from .environments import _get_environment_cached
38
- from .exception import InteractiveTimeoutError, InvalidError, RemoteError, _CliUserExecutionError
37
+ from .exception import ConnectionError, InteractiveTimeoutError, InvalidError, RemoteError, _CliUserExecutionError
39
38
  from .output import _get_output_manager, enable_output
40
39
  from .running_app import RunningApp, running_app_from_layout
41
40
  from .sandbox import _Sandbox
@@ -43,9 +42,7 @@ from .secret import _Secret
43
42
  from .stream_type import StreamType
44
43
 
45
44
  if TYPE_CHECKING:
46
- from .app import _App
47
- else:
48
- _App = TypeVar("_App")
45
+ import modal.app
49
46
 
50
47
 
51
48
  V = TypeVar("V")
@@ -56,14 +53,14 @@ async def _heartbeat(client: _Client, app_id: str) -> None:
56
53
  # TODO(erikbern): we should capture exceptions here
57
54
  # * if request fails: destroy the client
58
55
  # * if server says the app is gone: print a helpful warning about detaching
59
- await retry_transient_errors(client.stub.AppHeartbeat, request, attempt_timeout=HEARTBEAT_TIMEOUT)
56
+ await client.stub.AppHeartbeat(request, retry=Retry(attempt_timeout=HEARTBEAT_TIMEOUT))
60
57
 
61
58
 
62
59
  async def _init_local_app_existing(client: _Client, existing_app_id: str, environment_name: str) -> RunningApp:
63
60
  # Get all the objects first
64
61
  obj_req = api_pb2.AppGetLayoutRequest(app_id=existing_app_id)
65
62
  obj_resp, _ = await gather_cancel_on_exc(
66
- retry_transient_errors(client.stub.AppGetLayout, obj_req),
63
+ client.stub.AppGetLayout(obj_req),
67
64
  # Cache the environment associated with the app now as we will use it later
68
65
  _get_environment_cached(environment_name, client),
69
66
  )
@@ -78,6 +75,7 @@ async def _init_local_app_existing(client: _Client, existing_app_id: str, enviro
78
75
  async def _init_local_app_new(
79
76
  client: _Client,
80
77
  description: str,
78
+ tags: dict[str, str],
81
79
  app_state: int, # ValueType
82
80
  environment_name: str = "",
83
81
  interactive: bool = False,
@@ -86,9 +84,10 @@ async def _init_local_app_new(
86
84
  description=description,
87
85
  environment_name=environment_name,
88
86
  app_state=app_state, # type: ignore
87
+ tags=tags,
89
88
  )
90
89
  app_resp, _ = await gather_cancel_on_exc( # TODO: use TaskGroup?
91
- retry_transient_errors(client.stub.AppCreate, app_req),
90
+ client.stub.AppCreate(app_req),
92
91
  # Cache the environment associated with the app now as we will use it later
93
92
  _get_environment_cached(environment_name, client),
94
93
  )
@@ -104,6 +103,7 @@ async def _init_local_app_new(
104
103
  async def _init_local_app_from_name(
105
104
  client: _Client,
106
105
  name: str,
106
+ tags: dict[str, str],
107
107
  environment_name: str = "",
108
108
  ) -> RunningApp:
109
109
  # Look up any existing deployment
@@ -111,7 +111,7 @@ async def _init_local_app_from_name(
111
111
  name=name,
112
112
  environment_name=environment_name,
113
113
  )
114
- app_resp = await retry_transient_errors(client.stub.AppGetByDeploymentName, app_req)
114
+ app_resp = await client.stub.AppGetByDeploymentName(app_req)
115
115
  existing_app_id = app_resp.app_id or None
116
116
 
117
117
  # Grab the app
@@ -119,24 +119,19 @@ async def _init_local_app_from_name(
119
119
  return await _init_local_app_existing(client, existing_app_id, environment_name)
120
120
  else:
121
121
  return await _init_local_app_new(
122
- client, name, api_pb2.APP_STATE_INITIALIZING, environment_name=environment_name
122
+ client, name, tags, api_pb2.APP_STATE_INITIALIZING, environment_name=environment_name
123
123
  )
124
124
 
125
125
 
126
126
  async def _create_all_objects(
127
- client: _Client,
128
127
  running_app: RunningApp,
129
- functions: dict[str, _Function],
130
- classes: dict[str, _Cls],
131
- environment_name: str,
128
+ local_app_state: "modal.app._LocalAppState",
129
+ load_context: LoadContext,
132
130
  ) -> None:
133
131
  """Create objects that have been defined but not created on the server."""
134
- indexed_objects: dict[str, _Object] = {**functions, **classes}
135
- resolver = Resolver(
136
- client,
137
- environment_name=environment_name,
138
- app_id=running_app.app_id,
139
- )
132
+ indexed_objects: dict[str, _Object] = {**local_app_state.functions, **local_app_state.classes}
133
+
134
+ resolver = Resolver()
140
135
  with resolver.display():
141
136
  # Get current objects, and reset all objects
142
137
  tag_to_object_id = {**running_app.function_ids, **running_app.class_ids}
@@ -159,7 +154,7 @@ async def _create_all_objects(
159
154
  # Note: preload only currently implemented for Functions, returns None otherwise
160
155
  # this is to ensure that directly referenced functions from the global scope has
161
156
  # ids associated with them when they are serialized into other functions
162
- await resolver.preload(obj, existing_object_id)
157
+ await resolver.preload(obj, load_context, existing_object_id)
163
158
  if obj.is_hydrated:
164
159
  tag_to_object_id[tag] = obj.object_id
165
160
 
@@ -167,7 +162,8 @@ async def _create_all_objects(
167
162
 
168
163
  async def _load(tag, obj):
169
164
  existing_object_id = tag_to_object_id.get(tag)
170
- await resolver.load(obj, existing_object_id)
165
+ # Pass load_context so dependencies can inherit app_id, client, etc.
166
+ await resolver.load(obj, load_context, existing_object_id=existing_object_id)
171
167
  if _Function._is_id_type(obj.object_id):
172
168
  running_app.function_ids[tag] = obj.object_id
173
169
  elif _Cls._is_id_type(obj.object_id):
@@ -182,21 +178,19 @@ async def _publish_app(
182
178
  client: _Client,
183
179
  running_app: RunningApp,
184
180
  app_state: int, # api_pb2.AppState.value
185
- functions: dict[str, _Function],
186
- classes: dict[str, _Cls],
181
+ app_local_state: "modal.app._LocalAppState",
187
182
  name: str = "",
188
- tags: dict[str, str] = {}, # Additional App metadata
189
183
  deployment_tag: str = "", # Only relevant for deployments
190
184
  commit_info: Optional[api_pb2.CommitInfo] = None, # Git commit information
191
185
  ) -> tuple[str, list[api_pb2.Warning]]:
192
186
  """Wrapper for AppPublish RPC."""
193
-
187
+ functions = app_local_state.functions
194
188
  definition_ids = {obj.object_id: obj._get_metadata().definition_id for obj in functions.values()} # type: ignore
195
189
 
196
190
  request = api_pb2.AppPublishRequest(
197
191
  app_id=running_app.app_id,
198
192
  name=name,
199
- tags=tags,
193
+ tags=app_local_state.tags,
200
194
  deployment_tag=deployment_tag,
201
195
  commit_info=commit_info,
202
196
  app_state=app_state, # type: ignore : should be a api_pb2.AppState.value
@@ -205,13 +199,7 @@ async def _publish_app(
205
199
  definition_ids=definition_ids,
206
200
  )
207
201
 
208
- try:
209
- response = await retry_transient_errors(client.stub.AppPublish, request)
210
- except GRPCError as exc:
211
- if exc.status == Status.INVALID_ARGUMENT or exc.status == Status.FAILED_PRECONDITION:
212
- raise InvalidError(exc.message)
213
- raise
214
-
202
+ response = await client.stub.AppPublish(request)
215
203
  print_server_warnings(response.server_warnings)
216
204
  return response.url, response.server_warnings
217
205
 
@@ -230,7 +218,7 @@ async def _disconnect(
230
218
 
231
219
  logger.debug("Sending app disconnect/stop request")
232
220
  req_disconnect = api_pb2.AppClientDisconnectRequest(app_id=app_id, reason=reason, exception=exc_str)
233
- await retry_transient_errors(client.stub.AppClientDisconnect, req_disconnect)
221
+ await client.stub.AppClientDisconnect(req_disconnect)
234
222
  logger.debug("App disconnected")
235
223
 
236
224
 
@@ -260,16 +248,17 @@ async def _status_based_disconnect(client: _Client, app_id: str, exc_info: Optio
260
248
 
261
249
  @asynccontextmanager
262
250
  async def _run_app(
263
- app: _App,
251
+ app: "modal.app._App",
264
252
  *,
265
253
  client: Optional[_Client] = None,
266
254
  detach: bool = False,
267
255
  environment_name: Optional[str] = None,
268
256
  interactive: bool = False,
269
- ) -> AsyncGenerator[_App, None]:
257
+ ) -> AsyncGenerator["modal.app._App", None]:
270
258
  """mdmd:hidden"""
271
- if environment_name is None:
272
- environment_name = typing.cast(str, config.get("environment"))
259
+ load_context = await app._root_load_context.reset().in_place_upgrade(
260
+ client=client, environment_name=environment_name
261
+ )
273
262
 
274
263
  if modal._runtime.execution_context._is_currently_importing:
275
264
  raise InvalidError("Can not run an app in global scope within a container")
@@ -290,9 +279,6 @@ async def _run_app(
290
279
  # https://docs.python.org/3/library/__main__.html#import-main
291
280
  app.set_description(__main__.__name__)
292
281
 
293
- if client is None:
294
- client = await _Client.from_env()
295
-
296
282
  app_state = api_pb2.APP_STATE_DETACHED if detach else api_pb2.APP_STATE_EPHEMERAL
297
283
 
298
284
  output_mgr = _get_output_manager()
@@ -300,21 +286,25 @@ async def _run_app(
300
286
  msg = "Interactive mode requires output to be enabled. (Use the the `modal.enable_output()` context manager.)"
301
287
  raise InvalidError(msg)
302
288
 
289
+ local_app_state = app._local_state
290
+
303
291
  running_app: RunningApp = await _init_local_app_new(
304
- client,
292
+ load_context.client,
305
293
  app.description or "",
306
- environment_name=environment_name or "",
294
+ local_app_state.tags,
295
+ environment_name=load_context.environment_name,
307
296
  app_state=app_state,
308
297
  interactive=interactive,
309
298
  )
299
+ await load_context.in_place_upgrade(app_id=running_app.app_id)
310
300
 
311
301
  logs_timeout = config["logs_timeout"]
312
- async with app._set_local_app(client, running_app), TaskContext(grace=logs_timeout) as tc:
302
+ async with app._set_local_app(load_context.client, running_app), TaskContext(grace=logs_timeout) as tc:
313
303
  # Start heartbeats loop to keep the client alive
314
304
  # we don't log heartbeat exceptions in detached mode
315
305
  # as losing the local connection will not affect the running app
316
306
  def heartbeat():
317
- return _heartbeat(client, running_app.app_id)
307
+ return _heartbeat(load_context.client, running_app.app_id)
318
308
 
319
309
  heartbeat_loop = tc.infinite_loop(heartbeat, sleep=HEARTBEAT_INTERVAL, log_exception=not detach)
320
310
  logs_loop: Optional[asyncio.Task] = None
@@ -335,26 +325,37 @@ async def _run_app(
335
325
  # Start logs loop
336
326
 
337
327
  logs_loop = tc.create_task(
338
- get_app_logs_loop(client, output_mgr, app_id=running_app.app_id, app_logs_url=running_app.app_logs_url)
328
+ get_app_logs_loop(
329
+ load_context.client, output_mgr, app_id=running_app.app_id, app_logs_url=running_app.app_logs_url
330
+ )
339
331
  )
340
332
 
341
333
  try:
342
334
  # Create all members
343
- await _create_all_objects(client, running_app, app._functions, app._classes, environment_name)
335
+ await _create_all_objects(running_app, local_app_state, load_context)
344
336
 
345
337
  # Publish the app
346
- await _publish_app(client, running_app, app_state, app._functions, app._classes, tags=app._tags)
338
+ await _publish_app(load_context.client, running_app, app_state, local_app_state)
347
339
  except asyncio.CancelledError as e:
348
340
  # this typically happens on sigint/ctrl-C during setup (the KeyboardInterrupt happens in the main thread)
349
341
  if output_mgr := _get_output_manager():
350
342
  output_mgr.print("Aborting app initialization...\n")
351
343
 
352
- await _status_based_disconnect(client, running_app.app_id, e)
344
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
353
345
  raise
354
346
  except BaseException as e:
355
- await _status_based_disconnect(client, running_app.app_id, e)
347
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
356
348
  raise
357
349
 
350
+ detached_disconnect_msg = (
351
+ "The detached App will keep running. You can track its progress on the Dashboard: "
352
+ f"[magenta underline]{running_app.app_page_url}[/magenta underline]"
353
+ "\n\nStream App logs:\n"
354
+ f"[green]modal app logs {running_app.app_id}[/green]"
355
+ "\n\nStop the App:\n"
356
+ f"[green]modal app stop {running_app.app_id}[/green]"
357
+ )
358
+
358
359
  try:
359
360
  # Show logs from dynamically created images.
360
361
  # TODO: better way to do this
@@ -371,26 +372,22 @@ async def _run_app(
371
372
  yield app
372
373
  # successful completion!
373
374
  heartbeat_loop.cancel()
374
- await _status_based_disconnect(client, running_app.app_id, exc_info=None)
375
+ await _status_based_disconnect(load_context.client, running_app.app_id, exc_info=None)
375
376
  except KeyboardInterrupt as e:
376
377
  # this happens only if sigint comes in during the yield block above
377
378
  if detach:
378
379
  if output_mgr := _get_output_manager():
379
380
  output_mgr.print(output_mgr.step_completed("Shutting down Modal client."))
380
- output_mgr.print(
381
- "The detached app keeps running. You can track its progress at: "
382
- f"[magenta]{running_app.app_page_url}[/magenta]"
383
- ""
384
- )
381
+ output_mgr.print(detached_disconnect_msg)
385
382
  if logs_loop:
386
383
  logs_loop.cancel()
387
- await _status_based_disconnect(client, running_app.app_id, e)
384
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
388
385
  else:
389
386
  if output_mgr := _get_output_manager():
390
387
  output_mgr.print(
391
388
  "Disconnecting from Modal - This will terminate your Modal app in a few seconds.\n"
392
389
  )
393
- await _status_based_disconnect(client, running_app.app_id, e)
390
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
394
391
  if logs_loop:
395
392
  try:
396
393
  await asyncio.wait_for(logs_loop, timeout=logs_timeout)
@@ -405,9 +402,17 @@ async def _run_app(
405
402
  )
406
403
  )
407
404
  return
405
+ except ConnectionError as e:
406
+ # If we lose connection to the server after a detached App has started running, it will continue
407
+ # I think we can only exit "nicely" if we are able to print output though, otherwise we should raise
408
+ if detach and (output_mgr := _get_output_manager()):
409
+ output_mgr.print(":white_exclamation_mark: Connection lost!")
410
+ output_mgr.print(detached_disconnect_msg)
411
+ return
412
+ raise
408
413
  except BaseException as e:
409
414
  logger.info("Exception during app run")
410
- await _status_based_disconnect(client, running_app.app_id, e)
415
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
411
416
  raise
412
417
 
413
418
  # wait for logs gracefully, even though the task context would do the same
@@ -428,34 +433,27 @@ async def _run_app(
428
433
 
429
434
 
430
435
  async def _serve_update(
431
- app: _App,
436
+ app: "modal.app._App",
432
437
  existing_app_id: str,
433
438
  is_ready: Event,
434
439
  environment_name: str,
435
440
  ) -> None:
436
441
  """mdmd:hidden"""
437
442
  # Used by child process to reinitialize a served app
438
- client = await _Client.from_env()
443
+ load_context = await app._root_load_context.reset().in_place_upgrade(environment_name=environment_name)
439
444
  try:
440
- running_app: RunningApp = await _init_local_app_existing(client, existing_app_id, environment_name)
441
-
445
+ running_app: RunningApp = await _init_local_app_existing(load_context.client, existing_app_id, environment_name)
446
+ await load_context.in_place_upgrade(app_id=running_app.app_id)
447
+ local_app_state = app._local_state
442
448
  # Create objects
443
- await _create_all_objects(
444
- client,
445
- running_app,
446
- app._functions,
447
- app._classes,
448
- environment_name,
449
- )
449
+ await _create_all_objects(running_app, local_app_state, load_context)
450
450
 
451
451
  # Publish the updated app
452
452
  await _publish_app(
453
- client,
453
+ load_context.client,
454
454
  running_app,
455
455
  app_state=api_pb2.APP_STATE_UNSPECIFIED,
456
- functions=app._functions,
457
- classes=app._classes,
458
- tags=app._tags,
456
+ app_local_state=local_app_state,
459
457
  )
460
458
 
461
459
  # Communicate to the parent process
@@ -476,7 +474,7 @@ class DeployResult:
476
474
 
477
475
 
478
476
  async def _deploy_app(
479
- app: _App,
477
+ app: "modal.app._App",
480
478
  name: Optional[str] = None,
481
479
  namespace: Any = None, # mdmd:line-hidden
482
480
  client: Optional[_Client] = None,
@@ -487,9 +485,6 @@ async def _deploy_app(
487
485
 
488
486
  Users should prefer the `modal deploy` CLI or the `App.deploy` method.
489
487
  """
490
- if environment_name is None:
491
- environment_name = typing.cast(str, config.get("environment"))
492
-
493
488
  warn_if_passing_namespace(namespace, "modal.runner.deploy_app")
494
489
 
495
490
  name = name or app.name or ""
@@ -515,12 +510,25 @@ async def _deploy_app(
515
510
  if client is None:
516
511
  client = await _Client.from_env()
517
512
 
513
+ local_app_state = app._local_state
518
514
  t0 = time.time()
519
515
 
520
516
  # Get git information to track deployment history
521
517
  commit_info_task = asyncio.create_task(get_git_commit_info())
522
518
 
523
- running_app: RunningApp = await _init_local_app_from_name(client, name, environment_name=environment_name)
519
+ # We need to do in-place replacement of fields in self._root_load_context in case it has already "spread"
520
+ # to with_options() instances or similar before load
521
+ root_load_context = await app._root_load_context.reset().in_place_upgrade(
522
+ client=client,
523
+ environment_name=environment_name,
524
+ )
525
+ running_app: RunningApp = await _init_local_app_from_name(
526
+ root_load_context.client, name, local_app_state.tags, environment_name=root_load_context.environment_name
527
+ )
528
+
529
+ await root_load_context.in_place_upgrade(
530
+ app_id=running_app.app_id,
531
+ )
524
532
 
525
533
  async with TaskContext(0) as tc:
526
534
  # Start heartbeats loop to keep the client alive
@@ -531,13 +539,7 @@ async def _deploy_app(
531
539
 
532
540
  try:
533
541
  # Create all members
534
- await _create_all_objects(
535
- client,
536
- running_app,
537
- app._functions,
538
- app._classes,
539
- environment_name=environment_name,
540
- )
542
+ await _create_all_objects(running_app, local_app_state, root_load_context)
541
543
 
542
544
  commit_info = None
543
545
  try:
@@ -548,11 +550,9 @@ async def _deploy_app(
548
550
  app_url, warnings = await _publish_app(
549
551
  client,
550
552
  running_app,
551
- app_state=api_pb2.APP_STATE_DEPLOYED,
552
- functions=app._functions,
553
- classes=app._classes,
553
+ api_pb2.APP_STATE_DEPLOYED,
554
+ local_app_state,
554
555
  name=name,
555
- tags=app._tags,
556
556
  deployment_tag=tag,
557
557
  commit_info=commit_info,
558
558
  )
@@ -574,7 +574,7 @@ async def _deploy_app(
574
574
 
575
575
 
576
576
  async def _interactive_shell(
577
- _app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: Any
577
+ _app: "modal.app._App", cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: Any
578
578
  ) -> None:
579
579
  """Run an interactive shell (like `bash`) within the image for this app.
580
580
 
@@ -621,7 +621,7 @@ async def _interactive_shell(
621
621
  try:
622
622
  if pty:
623
623
  container_process = await sandbox._exec(
624
- *sandbox_cmds, pty_info=get_pty_info(shell=True) if pty else None
624
+ *sandbox_cmds, pty_info=get_pty_info(shell=True) if pty else None, text=False
625
625
  )
626
626
  await container_process.attach()
627
627
  else:
@@ -632,7 +632,7 @@ async def _interactive_shell(
632
632
  except InteractiveTimeoutError:
633
633
  # Check on status of Sandbox. It may have crashed, causing connection failure.
634
634
  req = api_pb2.SandboxWaitRequest(sandbox_id=sandbox._object_id, timeout=0)
635
- resp = await retry_transient_errors(sandbox._client.stub.SandboxWait, req)
635
+ resp = await sandbox._client.stub.SandboxWait(req)
636
636
  if resp.result.exception:
637
637
  raise RemoteError(resp.result.exception)
638
638
  else:
modal/runner.pyi CHANGED
@@ -1,6 +1,6 @@
1
- import modal._functions
1
+ import modal._load_context
2
+ import modal.app
2
3
  import modal.client
3
- import modal.cls
4
4
  import modal.running_app
5
5
  import modal_proto.api_pb2
6
6
  import multiprocessing.synchronize
@@ -8,8 +8,6 @@ import synchronicity.combined_types
8
8
  import typing
9
9
  import typing_extensions
10
10
 
11
- _App = typing.TypeVar("_App")
12
-
13
11
  V = typing.TypeVar("V")
14
12
 
15
13
  async def _heartbeat(client: modal.client._Client, app_id: str) -> None: ...
@@ -19,19 +17,18 @@ async def _init_local_app_existing(
19
17
  async def _init_local_app_new(
20
18
  client: modal.client._Client,
21
19
  description: str,
20
+ tags: dict[str, str],
22
21
  app_state: int,
23
22
  environment_name: str = "",
24
23
  interactive: bool = False,
25
24
  ) -> modal.running_app.RunningApp: ...
26
25
  async def _init_local_app_from_name(
27
- client: modal.client._Client, name: str, environment_name: str = ""
26
+ client: modal.client._Client, name: str, tags: dict[str, str], environment_name: str = ""
28
27
  ) -> modal.running_app.RunningApp: ...
29
28
  async def _create_all_objects(
30
- client: modal.client._Client,
31
29
  running_app: modal.running_app.RunningApp,
32
- functions: dict[str, modal._functions._Function],
33
- classes: dict[str, modal.cls._Cls],
34
- environment_name: str,
30
+ local_app_state: modal.app._LocalAppState,
31
+ load_context: modal._load_context.LoadContext,
35
32
  ) -> None:
36
33
  """Create objects that have been defined but not created on the server."""
37
34
  ...
@@ -40,10 +37,8 @@ async def _publish_app(
40
37
  client: modal.client._Client,
41
38
  running_app: modal.running_app.RunningApp,
42
39
  app_state: int,
43
- functions: dict[str, modal._functions._Function],
44
- classes: dict[str, modal.cls._Cls],
40
+ app_local_state: modal.app._LocalAppState,
45
41
  name: str = "",
46
- tags: dict[str, str] = {},
47
42
  deployment_tag: str = "",
48
43
  commit_info: typing.Optional[modal_proto.api_pb2.CommitInfo] = None,
49
44
  ) -> tuple[str, list[modal_proto.api_pb2.Warning]]:
@@ -66,18 +61,18 @@ async def _status_based_disconnect(
66
61
  ...
67
62
 
68
63
  def _run_app(
69
- app: _App,
64
+ app: modal.app._App,
70
65
  *,
71
66
  client: typing.Optional[modal.client._Client] = None,
72
67
  detach: bool = False,
73
68
  environment_name: typing.Optional[str] = None,
74
69
  interactive: bool = False,
75
- ) -> typing.AsyncContextManager[_App]:
70
+ ) -> typing.AsyncContextManager[modal.app._App]:
76
71
  """mdmd:hidden"""
77
72
  ...
78
73
 
79
74
  async def _serve_update(
80
- app: _App, existing_app_id: str, is_ready: multiprocessing.synchronize.Event, environment_name: str
75
+ app: modal.app._App, existing_app_id: str, is_ready: multiprocessing.synchronize.Event, environment_name: str
81
76
  ) -> None:
82
77
  """mdmd:hidden"""
83
78
  ...
@@ -115,7 +110,7 @@ class DeployResult:
115
110
  ...
116
111
 
117
112
  async def _deploy_app(
118
- app: _App,
113
+ app: modal.app._App,
119
114
  name: typing.Optional[str] = None,
120
115
  namespace: typing.Any = None,
121
116
  client: typing.Optional[modal.client._Client] = None,
@@ -129,7 +124,7 @@ async def _deploy_app(
129
124
  ...
130
125
 
131
126
  async def _interactive_shell(
132
- _app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: typing.Any
127
+ _app: modal.app._App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: typing.Any
133
128
  ) -> None:
134
129
  """Run an interactive shell (like `bash`) within the image for this app.
135
130
 
@@ -159,26 +154,26 @@ class __run_app_spec(typing_extensions.Protocol):
159
154
  def __call__(
160
155
  self,
161
156
  /,
162
- app: _App,
157
+ app: modal.app.App,
163
158
  *,
164
159
  client: typing.Optional[modal.client.Client] = None,
165
160
  detach: bool = False,
166
161
  environment_name: typing.Optional[str] = None,
167
162
  interactive: bool = False,
168
- ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[_App]:
163
+ ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[modal.app.App]:
169
164
  """mdmd:hidden"""
170
165
  ...
171
166
 
172
167
  def aio(
173
168
  self,
174
169
  /,
175
- app: _App,
170
+ app: modal.app.App,
176
171
  *,
177
172
  client: typing.Optional[modal.client.Client] = None,
178
173
  detach: bool = False,
179
174
  environment_name: typing.Optional[str] = None,
180
175
  interactive: bool = False,
181
- ) -> typing.AsyncContextManager[_App]:
176
+ ) -> typing.AsyncContextManager[modal.app.App]:
182
177
  """mdmd:hidden"""
183
178
  ...
184
179
 
@@ -186,13 +181,23 @@ run_app: __run_app_spec
186
181
 
187
182
  class __serve_update_spec(typing_extensions.Protocol):
188
183
  def __call__(
189
- self, /, app: _App, existing_app_id: str, is_ready: multiprocessing.synchronize.Event, environment_name: str
184
+ self,
185
+ /,
186
+ app: modal.app.App,
187
+ existing_app_id: str,
188
+ is_ready: multiprocessing.synchronize.Event,
189
+ environment_name: str,
190
190
  ) -> None:
191
191
  """mdmd:hidden"""
192
192
  ...
193
193
 
194
194
  async def aio(
195
- self, /, app: _App, existing_app_id: str, is_ready: multiprocessing.synchronize.Event, environment_name: str
195
+ self,
196
+ /,
197
+ app: modal.app.App,
198
+ existing_app_id: str,
199
+ is_ready: multiprocessing.synchronize.Event,
200
+ environment_name: str,
196
201
  ) -> None:
197
202
  """mdmd:hidden"""
198
203
  ...
@@ -203,7 +208,7 @@ class __deploy_app_spec(typing_extensions.Protocol):
203
208
  def __call__(
204
209
  self,
205
210
  /,
206
- app: _App,
211
+ app: modal.app.App,
207
212
  name: typing.Optional[str] = None,
208
213
  namespace: typing.Any = None,
209
214
  client: typing.Optional[modal.client.Client] = None,
@@ -219,7 +224,7 @@ class __deploy_app_spec(typing_extensions.Protocol):
219
224
  async def aio(
220
225
  self,
221
226
  /,
222
- app: _App,
227
+ app: modal.app.App,
223
228
  name: typing.Optional[str] = None,
224
229
  namespace: typing.Any = None,
225
230
  client: typing.Optional[modal.client.Client] = None,
@@ -236,7 +241,13 @@ deploy_app: __deploy_app_spec
236
241
 
237
242
  class __interactive_shell_spec(typing_extensions.Protocol):
238
243
  def __call__(
239
- self, /, _app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: typing.Any
244
+ self,
245
+ /,
246
+ _app: modal.app.App,
247
+ cmds: list[str],
248
+ environment_name: str = "",
249
+ pty: bool = True,
250
+ **kwargs: typing.Any,
240
251
  ) -> None:
241
252
  """Run an interactive shell (like `bash`) within the image for this app.
242
253
 
@@ -263,7 +274,13 @@ class __interactive_shell_spec(typing_extensions.Protocol):
263
274
  ...
264
275
 
265
276
  async def aio(
266
- self, /, _app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: typing.Any
277
+ self,
278
+ /,
279
+ _app: modal.app.App,
280
+ cmds: list[str],
281
+ environment_name: str = "",
282
+ pty: bool = True,
283
+ **kwargs: typing.Any,
267
284
  ) -> None:
268
285
  """Run an interactive shell (like `bash`) within the image for this app.
269
286