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.
- modal/__init__.py +0 -2
- modal/__main__.py +3 -4
- modal/_billing.py +80 -0
- modal/_clustered_functions.py +7 -3
- modal/_clustered_functions.pyi +15 -3
- modal/_container_entrypoint.py +51 -69
- modal/_functions.py +508 -240
- modal/_grpc_client.py +171 -0
- modal/_load_context.py +105 -0
- modal/_object.py +81 -21
- modal/_output.py +58 -45
- modal/_partial_function.py +48 -73
- modal/_pty.py +7 -3
- modal/_resolver.py +26 -46
- modal/_runtime/asgi.py +4 -3
- modal/_runtime/container_io_manager.py +358 -220
- modal/_runtime/container_io_manager.pyi +296 -101
- modal/_runtime/execution_context.py +18 -2
- modal/_runtime/execution_context.pyi +64 -7
- modal/_runtime/gpu_memory_snapshot.py +262 -57
- modal/_runtime/user_code_imports.py +28 -58
- modal/_serialization.py +90 -6
- modal/_traceback.py +42 -1
- modal/_tunnel.pyi +380 -12
- modal/_utils/async_utils.py +84 -29
- modal/_utils/auth_token_manager.py +111 -0
- modal/_utils/blob_utils.py +181 -58
- modal/_utils/deprecation.py +19 -0
- modal/_utils/function_utils.py +91 -47
- modal/_utils/grpc_utils.py +89 -66
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/name_utils.py +17 -3
- modal/_utils/task_command_router_client.py +536 -0
- modal/_utils/time_utils.py +34 -6
- modal/app.py +256 -88
- modal/app.pyi +909 -92
- modal/billing.py +5 -0
- modal/builder/2025.06.txt +18 -0
- modal/builder/PREVIEW.txt +18 -0
- modal/builder/base-images.json +58 -0
- modal/cli/_download.py +19 -3
- modal/cli/_traceback.py +3 -2
- modal/cli/app.py +4 -4
- modal/cli/cluster.py +15 -7
- modal/cli/config.py +5 -3
- modal/cli/container.py +7 -6
- modal/cli/dict.py +22 -16
- modal/cli/entry_point.py +12 -5
- modal/cli/environment.py +5 -4
- modal/cli/import_refs.py +3 -3
- modal/cli/launch.py +102 -5
- modal/cli/network_file_system.py +11 -12
- modal/cli/profile.py +3 -2
- modal/cli/programs/launch_instance_ssh.py +94 -0
- modal/cli/programs/run_jupyter.py +1 -1
- modal/cli/programs/run_marimo.py +95 -0
- modal/cli/programs/vscode.py +1 -1
- modal/cli/queues.py +57 -26
- modal/cli/run.py +91 -23
- modal/cli/secret.py +48 -22
- modal/cli/token.py +7 -8
- modal/cli/utils.py +4 -7
- modal/cli/volume.py +31 -25
- modal/client.py +15 -85
- modal/client.pyi +183 -62
- modal/cloud_bucket_mount.py +5 -3
- modal/cloud_bucket_mount.pyi +197 -5
- modal/cls.py +200 -126
- modal/cls.pyi +446 -68
- modal/config.py +29 -11
- modal/container_process.py +319 -19
- modal/container_process.pyi +190 -20
- modal/dict.py +290 -71
- modal/dict.pyi +835 -83
- modal/environments.py +15 -27
- modal/environments.pyi +46 -24
- modal/exception.py +14 -2
- modal/experimental/__init__.py +194 -40
- modal/experimental/flash.py +618 -0
- modal/experimental/flash.pyi +380 -0
- modal/experimental/ipython.py +11 -7
- modal/file_io.py +29 -36
- modal/file_io.pyi +251 -53
- modal/file_pattern_matcher.py +56 -16
- modal/functions.pyi +673 -92
- modal/gpu.py +1 -1
- modal/image.py +528 -176
- modal/image.pyi +1572 -145
- modal/io_streams.py +458 -128
- modal/io_streams.pyi +433 -52
- modal/mount.py +216 -151
- modal/mount.pyi +225 -78
- modal/network_file_system.py +45 -62
- modal/network_file_system.pyi +277 -56
- modal/object.pyi +93 -17
- modal/parallel_map.py +942 -129
- modal/parallel_map.pyi +294 -15
- modal/partial_function.py +0 -2
- modal/partial_function.pyi +234 -19
- modal/proxy.py +17 -8
- modal/proxy.pyi +36 -3
- modal/queue.py +270 -65
- modal/queue.pyi +817 -57
- modal/runner.py +115 -101
- modal/runner.pyi +205 -49
- modal/sandbox.py +512 -136
- modal/sandbox.pyi +845 -111
- modal/schedule.py +1 -1
- modal/secret.py +300 -70
- modal/secret.pyi +589 -34
- modal/serving.py +7 -11
- modal/serving.pyi +7 -8
- modal/snapshot.py +11 -8
- modal/snapshot.pyi +25 -4
- modal/token_flow.py +4 -4
- modal/token_flow.pyi +28 -8
- modal/volume.py +416 -158
- modal/volume.pyi +1117 -121
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/METADATA +10 -9
- modal-1.2.3.dev7.dist-info/RECORD +195 -0
- modal_docs/mdmd/mdmd.py +17 -4
- modal_proto/api.proto +534 -79
- modal_proto/api_grpc.py +337 -1
- modal_proto/api_pb2.py +1522 -968
- modal_proto/api_pb2.pyi +1619 -134
- modal_proto/api_pb2_grpc.py +699 -4
- modal_proto/api_pb2_grpc.pyi +226 -14
- modal_proto/modal_api_grpc.py +175 -154
- modal_proto/sandbox_router.proto +145 -0
- modal_proto/sandbox_router_grpc.py +105 -0
- modal_proto/sandbox_router_pb2.py +149 -0
- modal_proto/sandbox_router_pb2.pyi +333 -0
- modal_proto/sandbox_router_pb2_grpc.py +203 -0
- modal_proto/sandbox_router_pb2_grpc.pyi +75 -0
- modal_proto/task_command_router.proto +144 -0
- modal_proto/task_command_router_grpc.py +105 -0
- modal_proto/task_command_router_pb2.py +149 -0
- modal_proto/task_command_router_pb2.pyi +333 -0
- modal_proto/task_command_router_pb2_grpc.py +203 -0
- modal_proto/task_command_router_pb2_grpc.pyi +75 -0
- modal_version/__init__.py +1 -1
- modal/requirements/PREVIEW.txt +0 -16
- modal/requirements/base-images.json +0 -26
- modal-1.0.3.dev10.dist-info/RECORD +0 -179
- modal_proto/modal_options_grpc.py +0 -3
- modal_proto/options.proto +0 -19
- modal_proto/options_grpc.py +0 -3
- modal_proto/options_pb2.py +0 -35
- modal_proto/options_pb2.pyi +0 -20
- modal_proto/options_pb2_grpc.py +0 -4
- modal_proto/options_pb2_grpc.pyi +0 -7
- /modal/{requirements → builder}/2023.12.312.txt +0 -0
- /modal/{requirements → builder}/2023.12.txt +0 -0
- /modal/{requirements → builder}/2024.04.txt +0 -0
- /modal/{requirements → builder}/2024.10.txt +0 -0
- /modal/{requirements → builder}/README.md +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/WHEEL +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/entry_points.txt +0 -0
- {modal-1.0.3.dev10.dist-info → modal-1.2.3.dev7.dist-info}/licenses/LICENSE +0 -0
- {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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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.
|
|
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.
|
|
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
|
|
642
|
+
resp = await sandbox._client.stub.SandboxWait(req)
|
|
629
643
|
if resp.result.exception:
|
|
630
644
|
raise RemoteError(resp.result.exception)
|
|
631
645
|
else:
|