modal 1.2.0__py3-none-any.whl → 1.2.1__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/_container_entrypoint.py +4 -1
- modal/_partial_function.py +28 -3
- modal/_utils/function_utils.py +4 -0
- modal/_utils/task_command_router_client.py +537 -0
- modal/app.py +93 -54
- modal/app.pyi +48 -18
- modal/cli/_download.py +19 -3
- modal/cli/cluster.py +4 -2
- modal/cli/container.py +4 -2
- modal/cli/entry_point.py +1 -0
- modal/cli/launch.py +1 -2
- modal/cli/run.py +6 -0
- modal/cli/volume.py +7 -1
- modal/client.pyi +2 -2
- modal/cls.py +5 -12
- modal/config.py +14 -0
- modal/container_process.py +283 -3
- modal/container_process.pyi +95 -32
- modal/exception.py +4 -0
- modal/experimental/flash.py +21 -47
- modal/experimental/flash.pyi +6 -20
- modal/functions.pyi +6 -6
- modal/io_streams.py +455 -122
- modal/io_streams.pyi +220 -95
- modal/partial_function.pyi +4 -1
- modal/runner.py +39 -36
- modal/runner.pyi +40 -24
- modal/sandbox.py +130 -11
- modal/sandbox.pyi +145 -9
- modal/volume.py +23 -3
- modal/volume.pyi +30 -0
- {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/METADATA +5 -5
- {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/RECORD +49 -48
- modal_proto/api.proto +2 -26
- modal_proto/api_grpc.py +0 -32
- modal_proto/api_pb2.py +327 -367
- modal_proto/api_pb2.pyi +6 -69
- modal_proto/api_pb2_grpc.py +0 -67
- modal_proto/api_pb2_grpc.pyi +0 -22
- modal_proto/modal_api_grpc.py +0 -2
- modal_proto/sandbox_router.proto +0 -4
- modal_proto/sandbox_router_pb2.pyi +0 -4
- modal_proto/task_command_router.proto +1 -1
- modal_proto/task_command_router_pb2.py +2 -2
- modal_version/__init__.py +1 -1
- {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/WHEEL +0 -0
- {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/entry_points.txt +0 -0
- {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {modal-1.2.0.dist-info → modal-1.2.1.dist-info}/top_level.txt +0 -0
modal/runner.pyi
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import modal.
|
|
1
|
+
import modal.app
|
|
2
2
|
import modal.client
|
|
3
|
-
import modal.cls
|
|
4
3
|
import modal.running_app
|
|
5
4
|
import modal_proto.api_pb2
|
|
6
5
|
import multiprocessing.synchronize
|
|
@@ -8,8 +7,6 @@ import synchronicity.combined_types
|
|
|
8
7
|
import typing
|
|
9
8
|
import typing_extensions
|
|
10
9
|
|
|
11
|
-
_App = typing.TypeVar("_App")
|
|
12
|
-
|
|
13
10
|
V = typing.TypeVar("V")
|
|
14
11
|
|
|
15
12
|
async def _heartbeat(client: modal.client._Client, app_id: str) -> None: ...
|
|
@@ -29,8 +26,7 @@ async def _init_local_app_from_name(
|
|
|
29
26
|
async def _create_all_objects(
|
|
30
27
|
client: modal.client._Client,
|
|
31
28
|
running_app: modal.running_app.RunningApp,
|
|
32
|
-
|
|
33
|
-
classes: dict[str, modal.cls._Cls],
|
|
29
|
+
local_app_state: modal.app._LocalAppState,
|
|
34
30
|
environment_name: str,
|
|
35
31
|
) -> None:
|
|
36
32
|
"""Create objects that have been defined but not created on the server."""
|
|
@@ -40,10 +36,8 @@ async def _publish_app(
|
|
|
40
36
|
client: modal.client._Client,
|
|
41
37
|
running_app: modal.running_app.RunningApp,
|
|
42
38
|
app_state: int,
|
|
43
|
-
|
|
44
|
-
classes: dict[str, modal.cls._Cls],
|
|
39
|
+
app_local_state: modal.app._LocalAppState,
|
|
45
40
|
name: str = "",
|
|
46
|
-
tags: dict[str, str] = {},
|
|
47
41
|
deployment_tag: str = "",
|
|
48
42
|
commit_info: typing.Optional[modal_proto.api_pb2.CommitInfo] = None,
|
|
49
43
|
) -> tuple[str, list[modal_proto.api_pb2.Warning]]:
|
|
@@ -66,18 +60,18 @@ async def _status_based_disconnect(
|
|
|
66
60
|
...
|
|
67
61
|
|
|
68
62
|
def _run_app(
|
|
69
|
-
app: _App,
|
|
63
|
+
app: modal.app._App,
|
|
70
64
|
*,
|
|
71
65
|
client: typing.Optional[modal.client._Client] = None,
|
|
72
66
|
detach: bool = False,
|
|
73
67
|
environment_name: typing.Optional[str] = None,
|
|
74
68
|
interactive: bool = False,
|
|
75
|
-
) -> typing.AsyncContextManager[_App]:
|
|
69
|
+
) -> typing.AsyncContextManager[modal.app._App]:
|
|
76
70
|
"""mdmd:hidden"""
|
|
77
71
|
...
|
|
78
72
|
|
|
79
73
|
async def _serve_update(
|
|
80
|
-
app: _App, existing_app_id: str, is_ready: multiprocessing.synchronize.Event, environment_name: str
|
|
74
|
+
app: modal.app._App, existing_app_id: str, is_ready: multiprocessing.synchronize.Event, environment_name: str
|
|
81
75
|
) -> None:
|
|
82
76
|
"""mdmd:hidden"""
|
|
83
77
|
...
|
|
@@ -115,7 +109,7 @@ class DeployResult:
|
|
|
115
109
|
...
|
|
116
110
|
|
|
117
111
|
async def _deploy_app(
|
|
118
|
-
app: _App,
|
|
112
|
+
app: modal.app._App,
|
|
119
113
|
name: typing.Optional[str] = None,
|
|
120
114
|
namespace: typing.Any = None,
|
|
121
115
|
client: typing.Optional[modal.client._Client] = None,
|
|
@@ -129,7 +123,7 @@ async def _deploy_app(
|
|
|
129
123
|
...
|
|
130
124
|
|
|
131
125
|
async def _interactive_shell(
|
|
132
|
-
_app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: typing.Any
|
|
126
|
+
_app: modal.app._App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: typing.Any
|
|
133
127
|
) -> None:
|
|
134
128
|
"""Run an interactive shell (like `bash`) within the image for this app.
|
|
135
129
|
|
|
@@ -159,26 +153,26 @@ class __run_app_spec(typing_extensions.Protocol):
|
|
|
159
153
|
def __call__(
|
|
160
154
|
self,
|
|
161
155
|
/,
|
|
162
|
-
app:
|
|
156
|
+
app: modal.app.App,
|
|
163
157
|
*,
|
|
164
158
|
client: typing.Optional[modal.client.Client] = None,
|
|
165
159
|
detach: bool = False,
|
|
166
160
|
environment_name: typing.Optional[str] = None,
|
|
167
161
|
interactive: bool = False,
|
|
168
|
-
) -> synchronicity.combined_types.AsyncAndBlockingContextManager[
|
|
162
|
+
) -> synchronicity.combined_types.AsyncAndBlockingContextManager[modal.app.App]:
|
|
169
163
|
"""mdmd:hidden"""
|
|
170
164
|
...
|
|
171
165
|
|
|
172
166
|
def aio(
|
|
173
167
|
self,
|
|
174
168
|
/,
|
|
175
|
-
app:
|
|
169
|
+
app: modal.app.App,
|
|
176
170
|
*,
|
|
177
171
|
client: typing.Optional[modal.client.Client] = None,
|
|
178
172
|
detach: bool = False,
|
|
179
173
|
environment_name: typing.Optional[str] = None,
|
|
180
174
|
interactive: bool = False,
|
|
181
|
-
) -> typing.AsyncContextManager[
|
|
175
|
+
) -> typing.AsyncContextManager[modal.app.App]:
|
|
182
176
|
"""mdmd:hidden"""
|
|
183
177
|
...
|
|
184
178
|
|
|
@@ -186,13 +180,23 @@ run_app: __run_app_spec
|
|
|
186
180
|
|
|
187
181
|
class __serve_update_spec(typing_extensions.Protocol):
|
|
188
182
|
def __call__(
|
|
189
|
-
self,
|
|
183
|
+
self,
|
|
184
|
+
/,
|
|
185
|
+
app: modal.app.App,
|
|
186
|
+
existing_app_id: str,
|
|
187
|
+
is_ready: multiprocessing.synchronize.Event,
|
|
188
|
+
environment_name: str,
|
|
190
189
|
) -> None:
|
|
191
190
|
"""mdmd:hidden"""
|
|
192
191
|
...
|
|
193
192
|
|
|
194
193
|
async def aio(
|
|
195
|
-
self,
|
|
194
|
+
self,
|
|
195
|
+
/,
|
|
196
|
+
app: modal.app.App,
|
|
197
|
+
existing_app_id: str,
|
|
198
|
+
is_ready: multiprocessing.synchronize.Event,
|
|
199
|
+
environment_name: str,
|
|
196
200
|
) -> None:
|
|
197
201
|
"""mdmd:hidden"""
|
|
198
202
|
...
|
|
@@ -203,7 +207,7 @@ class __deploy_app_spec(typing_extensions.Protocol):
|
|
|
203
207
|
def __call__(
|
|
204
208
|
self,
|
|
205
209
|
/,
|
|
206
|
-
app:
|
|
210
|
+
app: modal.app.App,
|
|
207
211
|
name: typing.Optional[str] = None,
|
|
208
212
|
namespace: typing.Any = None,
|
|
209
213
|
client: typing.Optional[modal.client.Client] = None,
|
|
@@ -219,7 +223,7 @@ class __deploy_app_spec(typing_extensions.Protocol):
|
|
|
219
223
|
async def aio(
|
|
220
224
|
self,
|
|
221
225
|
/,
|
|
222
|
-
app:
|
|
226
|
+
app: modal.app.App,
|
|
223
227
|
name: typing.Optional[str] = None,
|
|
224
228
|
namespace: typing.Any = None,
|
|
225
229
|
client: typing.Optional[modal.client.Client] = None,
|
|
@@ -236,7 +240,13 @@ deploy_app: __deploy_app_spec
|
|
|
236
240
|
|
|
237
241
|
class __interactive_shell_spec(typing_extensions.Protocol):
|
|
238
242
|
def __call__(
|
|
239
|
-
self,
|
|
243
|
+
self,
|
|
244
|
+
/,
|
|
245
|
+
_app: modal.app.App,
|
|
246
|
+
cmds: list[str],
|
|
247
|
+
environment_name: str = "",
|
|
248
|
+
pty: bool = True,
|
|
249
|
+
**kwargs: typing.Any,
|
|
240
250
|
) -> None:
|
|
241
251
|
"""Run an interactive shell (like `bash`) within the image for this app.
|
|
242
252
|
|
|
@@ -263,7 +273,13 @@ class __interactive_shell_spec(typing_extensions.Protocol):
|
|
|
263
273
|
...
|
|
264
274
|
|
|
265
275
|
async def aio(
|
|
266
|
-
self,
|
|
276
|
+
self,
|
|
277
|
+
/,
|
|
278
|
+
_app: modal.app.App,
|
|
279
|
+
cmds: list[str],
|
|
280
|
+
environment_name: str = "",
|
|
281
|
+
pty: bool = True,
|
|
282
|
+
**kwargs: typing.Any,
|
|
267
283
|
) -> None:
|
|
268
284
|
"""Run an interactive shell (like `bash`) within the image for this app.
|
|
269
285
|
|
modal/sandbox.py
CHANGED
|
@@ -3,6 +3,7 @@ import asyncio
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import time
|
|
6
|
+
import uuid
|
|
6
7
|
from collections.abc import AsyncGenerator, Collection, Sequence
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from typing import TYPE_CHECKING, Any, AsyncIterator, Literal, Optional, Union, overload
|
|
@@ -20,7 +21,7 @@ from modal._tunnel import Tunnel
|
|
|
20
21
|
from modal.cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
|
|
21
22
|
from modal.mount import _Mount
|
|
22
23
|
from modal.volume import _Volume
|
|
23
|
-
from modal_proto import api_pb2
|
|
24
|
+
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
24
25
|
|
|
25
26
|
from ._object import _get_environment_name, _Object
|
|
26
27
|
from ._resolver import Resolver
|
|
@@ -30,6 +31,7 @@ from ._utils.deprecation import deprecation_warning
|
|
|
30
31
|
from ._utils.grpc_utils import retry_transient_errors
|
|
31
32
|
from ._utils.mount_utils import validate_network_file_systems, validate_volumes
|
|
32
33
|
from ._utils.name_utils import is_valid_object_name
|
|
34
|
+
from ._utils.task_command_router_client import TaskCommandRouterClient
|
|
33
35
|
from .client import _Client
|
|
34
36
|
from .container_process import _ContainerProcess
|
|
35
37
|
from .exception import AlreadyExistsError, ExecutionError, InvalidError, SandboxTerminatedError, SandboxTimeoutError
|
|
@@ -121,9 +123,10 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
121
123
|
_stdout: _StreamReader[str]
|
|
122
124
|
_stderr: _StreamReader[str]
|
|
123
125
|
_stdin: _StreamWriter
|
|
124
|
-
_task_id: Optional[str]
|
|
125
|
-
_tunnels: Optional[dict[int, Tunnel]]
|
|
126
|
-
_enable_snapshot: bool
|
|
126
|
+
_task_id: Optional[str]
|
|
127
|
+
_tunnels: Optional[dict[int, Tunnel]]
|
|
128
|
+
_enable_snapshot: bool
|
|
129
|
+
_command_router_client: Optional[TaskCommandRouterClient]
|
|
127
130
|
|
|
128
131
|
@staticmethod
|
|
129
132
|
def _default_pty_info() -> api_pb2.PTYInfo:
|
|
@@ -513,14 +516,18 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
513
516
|
return obj
|
|
514
517
|
|
|
515
518
|
def _hydrate_metadata(self, handle_metadata: Optional[Message]):
|
|
516
|
-
self._stdout
|
|
519
|
+
self._stdout = StreamReader(
|
|
517
520
|
api_pb2.FILE_DESCRIPTOR_STDOUT, self.object_id, "sandbox", self._client, by_line=True
|
|
518
521
|
)
|
|
519
|
-
self._stderr
|
|
522
|
+
self._stderr = StreamReader(
|
|
520
523
|
api_pb2.FILE_DESCRIPTOR_STDERR, self.object_id, "sandbox", self._client, by_line=True
|
|
521
524
|
)
|
|
522
525
|
self._stdin = StreamWriter(self.object_id, "sandbox", self._client)
|
|
523
526
|
self._result = None
|
|
527
|
+
self._task_id = None
|
|
528
|
+
self._tunnels = None
|
|
529
|
+
self._enable_snapshot = False
|
|
530
|
+
self._command_router_client = None
|
|
524
531
|
|
|
525
532
|
@staticmethod
|
|
526
533
|
async def from_name(
|
|
@@ -669,11 +676,11 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
669
676
|
async def create_connect_token(
|
|
670
677
|
self, user_metadata: Optional[Union[str, dict[str, Any]]] = None
|
|
671
678
|
) -> SandboxConnectCredentials:
|
|
672
|
-
"""
|
|
673
|
-
[Alpha] Create a token for making HTTP connections to the
|
|
679
|
+
"""
|
|
680
|
+
[Alpha] Create a token for making HTTP connections to the Sandbox.
|
|
674
681
|
|
|
675
682
|
Also accepts an optional user_metadata string or dict to associate with the token. This metadata
|
|
676
|
-
will be added to the headers by the proxy when forwarding requests to the
|
|
683
|
+
will be added to the headers by the proxy when forwarding requests to the Sandbox."""
|
|
677
684
|
if user_metadata is not None and isinstance(user_metadata, dict):
|
|
678
685
|
try:
|
|
679
686
|
user_metadata = json.dumps(user_metadata)
|
|
@@ -730,6 +737,13 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
730
737
|
await asyncio.sleep(0.5)
|
|
731
738
|
return self._task_id
|
|
732
739
|
|
|
740
|
+
async def _get_command_router_client(self, task_id: str) -> Optional[TaskCommandRouterClient]:
|
|
741
|
+
if self._command_router_client is None:
|
|
742
|
+
# Attempt to initialize a router client. Returns None if the new exec path not enabled
|
|
743
|
+
# for this sandbox.
|
|
744
|
+
self._command_router_client = await TaskCommandRouterClient.try_init(self._client, task_id)
|
|
745
|
+
return self._command_router_client
|
|
746
|
+
|
|
733
747
|
@overload
|
|
734
748
|
async def exec(
|
|
735
749
|
self,
|
|
@@ -855,14 +869,49 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
855
869
|
await TaskContext.gather(*secret_coros)
|
|
856
870
|
|
|
857
871
|
task_id = await self._get_task_id()
|
|
872
|
+
kwargs = {
|
|
873
|
+
"task_id": task_id,
|
|
874
|
+
"pty_info": pty_info,
|
|
875
|
+
"stdout": stdout,
|
|
876
|
+
"stderr": stderr,
|
|
877
|
+
"timeout": timeout,
|
|
878
|
+
"workdir": workdir,
|
|
879
|
+
"secret_ids": [secret.object_id for secret in secrets],
|
|
880
|
+
"text": text,
|
|
881
|
+
"bufsize": bufsize,
|
|
882
|
+
"runtime_debug": config.get("function_runtime_debug"),
|
|
883
|
+
}
|
|
884
|
+
# NB: This must come after the task ID is set, since the sandbox must be
|
|
885
|
+
# scheduled before we can create a router client.
|
|
886
|
+
if (command_router_client := await self._get_command_router_client(task_id)) is not None:
|
|
887
|
+
kwargs["command_router_client"] = command_router_client
|
|
888
|
+
return await self._exec_through_command_router(*args, **kwargs)
|
|
889
|
+
else:
|
|
890
|
+
return await self._exec_through_server(*args, **kwargs)
|
|
891
|
+
|
|
892
|
+
async def _exec_through_server(
|
|
893
|
+
self,
|
|
894
|
+
*args: str,
|
|
895
|
+
task_id: str,
|
|
896
|
+
pty_info: Optional[api_pb2.PTYInfo] = None,
|
|
897
|
+
stdout: StreamType = StreamType.PIPE,
|
|
898
|
+
stderr: StreamType = StreamType.PIPE,
|
|
899
|
+
timeout: Optional[int] = None,
|
|
900
|
+
workdir: Optional[str] = None,
|
|
901
|
+
secret_ids: Optional[Collection[str]] = None,
|
|
902
|
+
text: bool = True,
|
|
903
|
+
bufsize: Literal[-1, 1] = -1,
|
|
904
|
+
runtime_debug: bool = False,
|
|
905
|
+
) -> Union[_ContainerProcess[bytes], _ContainerProcess[str]]:
|
|
906
|
+
"""Execute a command through the Modal server."""
|
|
858
907
|
req = api_pb2.ContainerExecRequest(
|
|
859
908
|
task_id=task_id,
|
|
860
909
|
command=args,
|
|
861
910
|
pty_info=pty_info,
|
|
862
|
-
runtime_debug=
|
|
911
|
+
runtime_debug=runtime_debug,
|
|
863
912
|
timeout_secs=timeout or 0,
|
|
864
913
|
workdir=workdir,
|
|
865
|
-
secret_ids=
|
|
914
|
+
secret_ids=secret_ids,
|
|
866
915
|
)
|
|
867
916
|
resp = await retry_transient_errors(self._client.stub.ContainerExec, req)
|
|
868
917
|
by_line = bufsize == 1
|
|
@@ -870,6 +919,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
870
919
|
logger.debug(f"Created ContainerProcess for exec_id {resp.exec_id} on Sandbox {self.object_id}")
|
|
871
920
|
return _ContainerProcess(
|
|
872
921
|
resp.exec_id,
|
|
922
|
+
task_id,
|
|
873
923
|
self._client,
|
|
874
924
|
stdout=stdout,
|
|
875
925
|
stderr=stderr,
|
|
@@ -878,6 +928,75 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
878
928
|
by_line=by_line,
|
|
879
929
|
)
|
|
880
930
|
|
|
931
|
+
async def _exec_through_command_router(
|
|
932
|
+
self,
|
|
933
|
+
*args: str,
|
|
934
|
+
task_id: str,
|
|
935
|
+
command_router_client: TaskCommandRouterClient,
|
|
936
|
+
pty_info: Optional[api_pb2.PTYInfo] = None,
|
|
937
|
+
stdout: StreamType = StreamType.PIPE,
|
|
938
|
+
stderr: StreamType = StreamType.PIPE,
|
|
939
|
+
timeout: Optional[int] = None,
|
|
940
|
+
workdir: Optional[str] = None,
|
|
941
|
+
secret_ids: Optional[Collection[str]] = None,
|
|
942
|
+
text: bool = True,
|
|
943
|
+
bufsize: Literal[-1, 1] = -1,
|
|
944
|
+
runtime_debug: bool = False,
|
|
945
|
+
) -> Union[_ContainerProcess[bytes], _ContainerProcess[str]]:
|
|
946
|
+
"""Execute a command through a task command router running on the Modal worker."""
|
|
947
|
+
|
|
948
|
+
# Generate a random process ID to use as a combination of idempotency key/process identifier.
|
|
949
|
+
process_id = str(uuid.uuid4())
|
|
950
|
+
if stdout == StreamType.PIPE:
|
|
951
|
+
stdout_config = sr_pb2.TaskExecStdoutConfig.TASK_EXEC_STDOUT_CONFIG_PIPE
|
|
952
|
+
elif stdout == StreamType.DEVNULL:
|
|
953
|
+
stdout_config = sr_pb2.TaskExecStdoutConfig.TASK_EXEC_STDOUT_CONFIG_DEVNULL
|
|
954
|
+
elif stdout == StreamType.STDOUT:
|
|
955
|
+
# TODO(saltzm): This is a behavior change from the old implementation. We should
|
|
956
|
+
# probably implement the old behavior of printing to stdout before moving out of beta.
|
|
957
|
+
raise NotImplementedError(
|
|
958
|
+
"Currently the STDOUT stream type is not supported when using exec "
|
|
959
|
+
"through a task command router, which is currently in beta."
|
|
960
|
+
)
|
|
961
|
+
else:
|
|
962
|
+
raise ValueError("Unsupported StreamType for stdout")
|
|
963
|
+
|
|
964
|
+
if stderr == StreamType.PIPE:
|
|
965
|
+
stderr_config = sr_pb2.TaskExecStderrConfig.TASK_EXEC_STDERR_CONFIG_PIPE
|
|
966
|
+
elif stderr == StreamType.DEVNULL:
|
|
967
|
+
stderr_config = sr_pb2.TaskExecStderrConfig.TASK_EXEC_STDERR_CONFIG_DEVNULL
|
|
968
|
+
elif stderr == StreamType.STDOUT:
|
|
969
|
+
stderr_config = sr_pb2.TaskExecStderrConfig.TASK_EXEC_STDERR_CONFIG_STDOUT
|
|
970
|
+
else:
|
|
971
|
+
raise ValueError("Unsupported StreamType for stderr")
|
|
972
|
+
|
|
973
|
+
# Start the process.
|
|
974
|
+
start_req = sr_pb2.TaskExecStartRequest(
|
|
975
|
+
task_id=task_id,
|
|
976
|
+
exec_id=process_id,
|
|
977
|
+
command_args=args,
|
|
978
|
+
stdout_config=stdout_config,
|
|
979
|
+
stderr_config=stderr_config,
|
|
980
|
+
timeout_secs=timeout,
|
|
981
|
+
workdir=workdir,
|
|
982
|
+
secret_ids=secret_ids,
|
|
983
|
+
pty_info=pty_info,
|
|
984
|
+
runtime_debug=runtime_debug,
|
|
985
|
+
)
|
|
986
|
+
_ = await command_router_client.exec_start(start_req)
|
|
987
|
+
|
|
988
|
+
return _ContainerProcess(
|
|
989
|
+
process_id,
|
|
990
|
+
task_id,
|
|
991
|
+
self._client,
|
|
992
|
+
command_router_client=command_router_client,
|
|
993
|
+
stdout=stdout,
|
|
994
|
+
stderr=stderr,
|
|
995
|
+
text=text,
|
|
996
|
+
by_line=bufsize == 1,
|
|
997
|
+
exec_deadline=time.monotonic() + int(timeout) if timeout else None,
|
|
998
|
+
)
|
|
999
|
+
|
|
881
1000
|
async def _experimental_snapshot(self) -> _SandboxSnapshot:
|
|
882
1001
|
await self._get_task_id()
|
|
883
1002
|
snap_req = api_pb2.SandboxSnapshotRequest(sandbox_id=self.object_id)
|
modal/sandbox.pyi
CHANGED
|
@@ -3,6 +3,7 @@ import collections.abc
|
|
|
3
3
|
import google.protobuf.message
|
|
4
4
|
import modal._object
|
|
5
5
|
import modal._tunnel
|
|
6
|
+
import modal._utils.task_command_router_client
|
|
6
7
|
import modal.app
|
|
7
8
|
import modal.client
|
|
8
9
|
import modal.cloud_bucket_mount
|
|
@@ -83,6 +84,7 @@ class _Sandbox(modal._object._Object):
|
|
|
83
84
|
_task_id: typing.Optional[str]
|
|
84
85
|
_tunnels: typing.Optional[dict[int, modal._tunnel.Tunnel]]
|
|
85
86
|
_enable_snapshot: bool
|
|
87
|
+
_command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]
|
|
86
88
|
|
|
87
89
|
@staticmethod
|
|
88
90
|
def _default_pty_info() -> modal_proto.api_pb2.PTYInfo: ...
|
|
@@ -276,11 +278,10 @@ class _Sandbox(modal._object._Object):
|
|
|
276
278
|
async def create_connect_token(
|
|
277
279
|
self, user_metadata: typing.Union[str, dict[str, typing.Any], None] = None
|
|
278
280
|
) -> SandboxConnectCredentials:
|
|
279
|
-
"""
|
|
280
|
-
[Alpha] Create a token for making HTTP connections to the sandbox.
|
|
281
|
+
"""[Alpha] Create a token for making HTTP connections to the Sandbox.
|
|
281
282
|
|
|
282
283
|
Also accepts an optional user_metadata string or dict to associate with the token. This metadata
|
|
283
|
-
will be added to the headers by the proxy when forwarding requests to the
|
|
284
|
+
will be added to the headers by the proxy when forwarding requests to the Sandbox.
|
|
284
285
|
"""
|
|
285
286
|
...
|
|
286
287
|
|
|
@@ -306,6 +307,9 @@ class _Sandbox(modal._object._Object):
|
|
|
306
307
|
...
|
|
307
308
|
|
|
308
309
|
async def _get_task_id(self) -> str: ...
|
|
310
|
+
async def _get_command_router_client(
|
|
311
|
+
self, task_id: str
|
|
312
|
+
) -> typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]: ...
|
|
309
313
|
@typing.overload
|
|
310
314
|
async def exec(
|
|
311
315
|
self,
|
|
@@ -357,6 +361,41 @@ class _Sandbox(modal._object._Object):
|
|
|
357
361
|
"""
|
|
358
362
|
...
|
|
359
363
|
|
|
364
|
+
async def _exec_through_server(
|
|
365
|
+
self,
|
|
366
|
+
*args: str,
|
|
367
|
+
task_id: str,
|
|
368
|
+
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
369
|
+
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
370
|
+
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
371
|
+
timeout: typing.Optional[int] = None,
|
|
372
|
+
workdir: typing.Optional[str] = None,
|
|
373
|
+
secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
|
|
374
|
+
text: bool = True,
|
|
375
|
+
bufsize: typing.Literal[-1, 1] = -1,
|
|
376
|
+
runtime_debug: bool = False,
|
|
377
|
+
) -> typing.Union[modal.container_process._ContainerProcess[bytes], modal.container_process._ContainerProcess[str]]:
|
|
378
|
+
"""Execute a command through the Modal server."""
|
|
379
|
+
...
|
|
380
|
+
|
|
381
|
+
async def _exec_through_command_router(
|
|
382
|
+
self,
|
|
383
|
+
*args: str,
|
|
384
|
+
task_id: str,
|
|
385
|
+
command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient,
|
|
386
|
+
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
387
|
+
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
388
|
+
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
389
|
+
timeout: typing.Optional[int] = None,
|
|
390
|
+
workdir: typing.Optional[str] = None,
|
|
391
|
+
secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
|
|
392
|
+
text: bool = True,
|
|
393
|
+
bufsize: typing.Literal[-1, 1] = -1,
|
|
394
|
+
runtime_debug: bool = False,
|
|
395
|
+
) -> typing.Union[modal.container_process._ContainerProcess[bytes], modal.container_process._ContainerProcess[str]]:
|
|
396
|
+
"""Execute a command through a task command router running on the Modal worker."""
|
|
397
|
+
...
|
|
398
|
+
|
|
360
399
|
async def _experimental_snapshot(self) -> modal.snapshot._SandboxSnapshot: ...
|
|
361
400
|
@staticmethod
|
|
362
401
|
async def _experimental_from_snapshot(
|
|
@@ -445,6 +484,7 @@ class Sandbox(modal.object.Object):
|
|
|
445
484
|
_task_id: typing.Optional[str]
|
|
446
485
|
_tunnels: typing.Optional[dict[int, modal._tunnel.Tunnel]]
|
|
447
486
|
_enable_snapshot: bool
|
|
487
|
+
_command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]
|
|
448
488
|
|
|
449
489
|
def __init__(self, *args, **kwargs):
|
|
450
490
|
"""mdmd:hidden"""
|
|
@@ -832,22 +872,20 @@ class Sandbox(modal.object.Object):
|
|
|
832
872
|
def __call__(
|
|
833
873
|
self, /, user_metadata: typing.Union[str, dict[str, typing.Any], None] = None
|
|
834
874
|
) -> SandboxConnectCredentials:
|
|
835
|
-
"""
|
|
836
|
-
[Alpha] Create a token for making HTTP connections to the sandbox.
|
|
875
|
+
"""[Alpha] Create a token for making HTTP connections to the Sandbox.
|
|
837
876
|
|
|
838
877
|
Also accepts an optional user_metadata string or dict to associate with the token. This metadata
|
|
839
|
-
will be added to the headers by the proxy when forwarding requests to the
|
|
878
|
+
will be added to the headers by the proxy when forwarding requests to the Sandbox.
|
|
840
879
|
"""
|
|
841
880
|
...
|
|
842
881
|
|
|
843
882
|
async def aio(
|
|
844
883
|
self, /, user_metadata: typing.Union[str, dict[str, typing.Any], None] = None
|
|
845
884
|
) -> SandboxConnectCredentials:
|
|
846
|
-
"""
|
|
847
|
-
[Alpha] Create a token for making HTTP connections to the sandbox.
|
|
885
|
+
"""[Alpha] Create a token for making HTTP connections to the Sandbox.
|
|
848
886
|
|
|
849
887
|
Also accepts an optional user_metadata string or dict to associate with the token. This metadata
|
|
850
|
-
will be added to the headers by the proxy when forwarding requests to the
|
|
888
|
+
will be added to the headers by the proxy when forwarding requests to the Sandbox.
|
|
851
889
|
"""
|
|
852
890
|
...
|
|
853
891
|
|
|
@@ -910,6 +948,16 @@ class Sandbox(modal.object.Object):
|
|
|
910
948
|
|
|
911
949
|
_get_task_id: ___get_task_id_spec[typing_extensions.Self]
|
|
912
950
|
|
|
951
|
+
class ___get_command_router_client_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
952
|
+
def __call__(
|
|
953
|
+
self, /, task_id: str
|
|
954
|
+
) -> typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]: ...
|
|
955
|
+
async def aio(
|
|
956
|
+
self, /, task_id: str
|
|
957
|
+
) -> typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]: ...
|
|
958
|
+
|
|
959
|
+
_get_command_router_client: ___get_command_router_client_spec[typing_extensions.Self]
|
|
960
|
+
|
|
913
961
|
class __exec_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
914
962
|
@typing.overload
|
|
915
963
|
def __call__(
|
|
@@ -1029,6 +1077,94 @@ class Sandbox(modal.object.Object):
|
|
|
1029
1077
|
|
|
1030
1078
|
_exec: ___exec_spec[typing_extensions.Self]
|
|
1031
1079
|
|
|
1080
|
+
class ___exec_through_server_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
1081
|
+
def __call__(
|
|
1082
|
+
self,
|
|
1083
|
+
/,
|
|
1084
|
+
*args: str,
|
|
1085
|
+
task_id: str,
|
|
1086
|
+
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
1087
|
+
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1088
|
+
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1089
|
+
timeout: typing.Optional[int] = None,
|
|
1090
|
+
workdir: typing.Optional[str] = None,
|
|
1091
|
+
secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
|
|
1092
|
+
text: bool = True,
|
|
1093
|
+
bufsize: typing.Literal[-1, 1] = -1,
|
|
1094
|
+
runtime_debug: bool = False,
|
|
1095
|
+
) -> typing.Union[
|
|
1096
|
+
modal.container_process.ContainerProcess[bytes], modal.container_process.ContainerProcess[str]
|
|
1097
|
+
]:
|
|
1098
|
+
"""Execute a command through the Modal server."""
|
|
1099
|
+
...
|
|
1100
|
+
|
|
1101
|
+
async def aio(
|
|
1102
|
+
self,
|
|
1103
|
+
/,
|
|
1104
|
+
*args: str,
|
|
1105
|
+
task_id: str,
|
|
1106
|
+
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
1107
|
+
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1108
|
+
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1109
|
+
timeout: typing.Optional[int] = None,
|
|
1110
|
+
workdir: typing.Optional[str] = None,
|
|
1111
|
+
secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
|
|
1112
|
+
text: bool = True,
|
|
1113
|
+
bufsize: typing.Literal[-1, 1] = -1,
|
|
1114
|
+
runtime_debug: bool = False,
|
|
1115
|
+
) -> typing.Union[
|
|
1116
|
+
modal.container_process.ContainerProcess[bytes], modal.container_process.ContainerProcess[str]
|
|
1117
|
+
]:
|
|
1118
|
+
"""Execute a command through the Modal server."""
|
|
1119
|
+
...
|
|
1120
|
+
|
|
1121
|
+
_exec_through_server: ___exec_through_server_spec[typing_extensions.Self]
|
|
1122
|
+
|
|
1123
|
+
class ___exec_through_command_router_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
1124
|
+
def __call__(
|
|
1125
|
+
self,
|
|
1126
|
+
/,
|
|
1127
|
+
*args: str,
|
|
1128
|
+
task_id: str,
|
|
1129
|
+
command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient,
|
|
1130
|
+
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
1131
|
+
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1132
|
+
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1133
|
+
timeout: typing.Optional[int] = None,
|
|
1134
|
+
workdir: typing.Optional[str] = None,
|
|
1135
|
+
secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
|
|
1136
|
+
text: bool = True,
|
|
1137
|
+
bufsize: typing.Literal[-1, 1] = -1,
|
|
1138
|
+
runtime_debug: bool = False,
|
|
1139
|
+
) -> typing.Union[
|
|
1140
|
+
modal.container_process.ContainerProcess[bytes], modal.container_process.ContainerProcess[str]
|
|
1141
|
+
]:
|
|
1142
|
+
"""Execute a command through a task command router running on the Modal worker."""
|
|
1143
|
+
...
|
|
1144
|
+
|
|
1145
|
+
async def aio(
|
|
1146
|
+
self,
|
|
1147
|
+
/,
|
|
1148
|
+
*args: str,
|
|
1149
|
+
task_id: str,
|
|
1150
|
+
command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient,
|
|
1151
|
+
pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
|
|
1152
|
+
stdout: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1153
|
+
stderr: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
1154
|
+
timeout: typing.Optional[int] = None,
|
|
1155
|
+
workdir: typing.Optional[str] = None,
|
|
1156
|
+
secret_ids: typing.Optional[collections.abc.Collection[str]] = None,
|
|
1157
|
+
text: bool = True,
|
|
1158
|
+
bufsize: typing.Literal[-1, 1] = -1,
|
|
1159
|
+
runtime_debug: bool = False,
|
|
1160
|
+
) -> typing.Union[
|
|
1161
|
+
modal.container_process.ContainerProcess[bytes], modal.container_process.ContainerProcess[str]
|
|
1162
|
+
]:
|
|
1163
|
+
"""Execute a command through a task command router running on the Modal worker."""
|
|
1164
|
+
...
|
|
1165
|
+
|
|
1166
|
+
_exec_through_command_router: ___exec_through_command_router_spec[typing_extensions.Self]
|
|
1167
|
+
|
|
1032
1168
|
class ___experimental_snapshot_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
1033
1169
|
def __call__(self, /) -> modal.snapshot.SandboxSnapshot: ...
|
|
1034
1170
|
async def aio(self, /) -> modal.snapshot.SandboxSnapshot: ...
|
modal/volume.py
CHANGED
|
@@ -655,6 +655,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
655
655
|
@retry(n_attempts=5, base_delay=0.1, timeout=None)
|
|
656
656
|
async def read_block(block_url: str) -> bytes:
|
|
657
657
|
async with ClientSessionRegistry.get_session().get(block_url) as get_response:
|
|
658
|
+
get_response.raise_for_status()
|
|
658
659
|
return await get_response.content.read()
|
|
659
660
|
|
|
660
661
|
async def iter_urls() -> AsyncGenerator[str]:
|
|
@@ -670,16 +671,33 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
670
671
|
|
|
671
672
|
@live_method
|
|
672
673
|
async def read_file_into_fileobj(
|
|
673
|
-
self,
|
|
674
|
+
self,
|
|
675
|
+
path: str,
|
|
676
|
+
fileobj: typing.IO[bytes],
|
|
677
|
+
progress_cb: Optional[Callable[..., Any]] = None,
|
|
674
678
|
) -> int:
|
|
675
679
|
"""mdmd:hidden
|
|
676
680
|
Read volume file into file-like IO object.
|
|
677
681
|
"""
|
|
682
|
+
return await self._read_file_into_fileobj(path, fileobj, progress_cb=progress_cb)
|
|
683
|
+
|
|
684
|
+
@live_method
|
|
685
|
+
async def _read_file_into_fileobj(
|
|
686
|
+
self,
|
|
687
|
+
path: str,
|
|
688
|
+
fileobj: typing.IO[bytes],
|
|
689
|
+
concurrency: Optional[int] = None,
|
|
690
|
+
download_semaphore: Optional[asyncio.Semaphore] = None,
|
|
691
|
+
progress_cb: Optional[Callable[..., Any]] = None,
|
|
692
|
+
) -> int:
|
|
678
693
|
if progress_cb is None:
|
|
679
694
|
|
|
680
695
|
def progress_cb(*_, **__):
|
|
681
696
|
pass
|
|
682
697
|
|
|
698
|
+
if concurrency is None:
|
|
699
|
+
concurrency = multiprocessing.cpu_count()
|
|
700
|
+
|
|
683
701
|
req = api_pb2.VolumeGetFile2Request(volume_id=self.object_id, path=path)
|
|
684
702
|
|
|
685
703
|
try:
|
|
@@ -687,8 +705,9 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
687
705
|
except modal.exception.NotFoundError as exc:
|
|
688
706
|
raise FileNotFoundError(exc.args[0])
|
|
689
707
|
|
|
690
|
-
|
|
691
|
-
|
|
708
|
+
if download_semaphore is None:
|
|
709
|
+
download_semaphore = asyncio.Semaphore(concurrency)
|
|
710
|
+
|
|
692
711
|
write_lock = asyncio.Lock()
|
|
693
712
|
start_pos = fileobj.tell()
|
|
694
713
|
|
|
@@ -698,6 +717,7 @@ class _Volume(_Object, type_prefix="vo"):
|
|
|
698
717
|
num_bytes_written = 0
|
|
699
718
|
|
|
700
719
|
async with download_semaphore, ClientSessionRegistry.get_session().get(url) as get_response:
|
|
720
|
+
get_response.raise_for_status()
|
|
701
721
|
async for chunk in get_response.content.iter_any():
|
|
702
722
|
num_chunk_bytes_written = 0
|
|
703
723
|
|