modal 1.2.1.dev8__py3-none-any.whl → 1.2.2.dev19__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.
- modal/_clustered_functions.py +1 -3
- modal/_container_entrypoint.py +4 -1
- modal/_functions.py +33 -49
- modal/_grpc_client.py +148 -0
- modal/_output.py +3 -4
- modal/_partial_function.py +22 -2
- modal/_runtime/container_io_manager.py +21 -22
- modal/_utils/async_utils.py +12 -3
- modal/_utils/auth_token_manager.py +1 -4
- modal/_utils/blob_utils.py +3 -4
- modal/_utils/function_utils.py +4 -0
- modal/_utils/grpc_utils.py +80 -51
- modal/_utils/mount_utils.py +26 -1
- modal/_utils/task_command_router_client.py +536 -0
- modal/app.py +7 -5
- modal/cli/cluster.py +4 -2
- modal/cli/config.py +3 -1
- modal/cli/container.py +5 -4
- modal/cli/entry_point.py +1 -0
- modal/cli/launch.py +1 -2
- modal/cli/network_file_system.py +1 -4
- modal/cli/queues.py +1 -2
- modal/cli/secret.py +1 -2
- modal/client.py +5 -115
- modal/client.pyi +2 -91
- modal/cls.py +1 -2
- modal/config.py +3 -1
- modal/container_process.py +287 -11
- modal/container_process.pyi +95 -32
- modal/dict.py +12 -12
- modal/environments.py +1 -2
- modal/exception.py +4 -0
- modal/experimental/__init__.py +2 -3
- modal/experimental/flash.py +27 -57
- modal/experimental/flash.pyi +6 -20
- modal/file_io.py +13 -27
- modal/functions.pyi +6 -6
- modal/image.py +24 -3
- modal/image.pyi +4 -0
- modal/io_streams.py +433 -127
- modal/io_streams.pyi +236 -171
- modal/mount.py +4 -4
- modal/network_file_system.py +5 -6
- modal/parallel_map.py +29 -31
- modal/parallel_map.pyi +3 -9
- modal/partial_function.pyi +4 -1
- modal/queue.py +17 -18
- modal/runner.py +12 -11
- modal/sandbox.py +148 -42
- modal/sandbox.pyi +139 -0
- modal/secret.py +4 -5
- modal/snapshot.py +1 -4
- modal/token_flow.py +1 -1
- modal/volume.py +22 -22
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/METADATA +1 -1
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/RECORD +70 -68
- modal_proto/api.proto +2 -24
- modal_proto/api_grpc.py +0 -32
- modal_proto/api_pb2.py +838 -878
- modal_proto/api_pb2.pyi +8 -70
- modal_proto/api_pb2_grpc.py +0 -67
- modal_proto/api_pb2_grpc.pyi +0 -22
- modal_proto/modal_api_grpc.py +175 -177
- modal_proto/sandbox_router.proto +0 -4
- modal_proto/sandbox_router_pb2.pyi +0 -4
- modal_version/__init__.py +1 -1
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/WHEEL +0 -0
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/entry_points.txt +0 -0
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/licenses/LICENSE +0 -0
- {modal-1.2.1.dev8.dist-info → modal-1.2.2.dev19.dist-info}/top_level.txt +0 -0
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,16 +21,16 @@ 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
|
|
27
28
|
from ._resources import convert_fn_config_to_resources_config
|
|
28
29
|
from ._utils.async_utils import TaskContext, synchronize_api
|
|
29
30
|
from ._utils.deprecation import deprecation_warning
|
|
30
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
31
31
|
from ._utils.mount_utils import validate_network_file_systems, validate_volumes
|
|
32
32
|
from ._utils.name_utils import is_valid_object_name
|
|
33
|
+
from ._utils.task_command_router_client import TaskCommandRouterClient
|
|
33
34
|
from .client import _Client
|
|
34
35
|
from .container_process import _ContainerProcess
|
|
35
36
|
from .exception import AlreadyExistsError, ExecutionError, InvalidError, SandboxTerminatedError, SandboxTimeoutError
|
|
@@ -121,9 +122,10 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
121
122
|
_stdout: _StreamReader[str]
|
|
122
123
|
_stderr: _StreamReader[str]
|
|
123
124
|
_stdin: _StreamWriter
|
|
124
|
-
_task_id: Optional[str]
|
|
125
|
-
_tunnels: Optional[dict[int, Tunnel]]
|
|
126
|
-
_enable_snapshot: bool
|
|
125
|
+
_task_id: Optional[str]
|
|
126
|
+
_tunnels: Optional[dict[int, Tunnel]]
|
|
127
|
+
_enable_snapshot: bool
|
|
128
|
+
_command_router_client: Optional[TaskCommandRouterClient]
|
|
127
129
|
|
|
128
130
|
@staticmethod
|
|
129
131
|
def _default_pty_info() -> api_pb2.PTYInfo:
|
|
@@ -270,7 +272,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
270
272
|
|
|
271
273
|
create_req = api_pb2.SandboxCreateRequest(app_id=resolver.app_id, definition=definition)
|
|
272
274
|
try:
|
|
273
|
-
create_resp = await
|
|
275
|
+
create_resp = await resolver.client.stub.SandboxCreate(create_req)
|
|
274
276
|
except GRPCError as exc:
|
|
275
277
|
if exc.status == Status.ALREADY_EXISTS:
|
|
276
278
|
raise AlreadyExistsError(exc.message)
|
|
@@ -513,14 +515,18 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
513
515
|
return obj
|
|
514
516
|
|
|
515
517
|
def _hydrate_metadata(self, handle_metadata: Optional[Message]):
|
|
516
|
-
self._stdout
|
|
518
|
+
self._stdout = StreamReader(
|
|
517
519
|
api_pb2.FILE_DESCRIPTOR_STDOUT, self.object_id, "sandbox", self._client, by_line=True
|
|
518
520
|
)
|
|
519
|
-
self._stderr
|
|
521
|
+
self._stderr = StreamReader(
|
|
520
522
|
api_pb2.FILE_DESCRIPTOR_STDERR, self.object_id, "sandbox", self._client, by_line=True
|
|
521
523
|
)
|
|
522
524
|
self._stdin = StreamWriter(self.object_id, "sandbox", self._client)
|
|
523
525
|
self._result = None
|
|
526
|
+
self._task_id = None
|
|
527
|
+
self._tunnels = None
|
|
528
|
+
self._enable_snapshot = False
|
|
529
|
+
self._command_router_client = None
|
|
524
530
|
|
|
525
531
|
@staticmethod
|
|
526
532
|
async def from_name(
|
|
@@ -540,7 +546,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
540
546
|
env_name = _get_environment_name(environment_name)
|
|
541
547
|
|
|
542
548
|
req = api_pb2.SandboxGetFromNameRequest(sandbox_name=name, app_name=app_name, environment_name=env_name)
|
|
543
|
-
resp = await
|
|
549
|
+
resp = await client.stub.SandboxGetFromName(req)
|
|
544
550
|
return _Sandbox._new_hydrated(resp.sandbox_id, client, None)
|
|
545
551
|
|
|
546
552
|
@staticmethod
|
|
@@ -553,7 +559,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
553
559
|
client = await _Client.from_env()
|
|
554
560
|
|
|
555
561
|
req = api_pb2.SandboxWaitRequest(sandbox_id=sandbox_id, timeout=0)
|
|
556
|
-
resp = await
|
|
562
|
+
resp = await client.stub.SandboxWait(req)
|
|
557
563
|
|
|
558
564
|
obj = _Sandbox._new_hydrated(sandbox_id, client, None)
|
|
559
565
|
|
|
@@ -566,7 +572,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
566
572
|
"""Fetches any tags (key-value pairs) currently attached to this Sandbox from the server."""
|
|
567
573
|
req = api_pb2.SandboxTagsGetRequest(sandbox_id=self.object_id)
|
|
568
574
|
try:
|
|
569
|
-
resp = await
|
|
575
|
+
resp = await self._client.stub.SandboxTagsGet(req)
|
|
570
576
|
except GRPCError as exc:
|
|
571
577
|
raise InvalidError(exc.message) if exc.status == Status.INVALID_ARGUMENT else exc
|
|
572
578
|
|
|
@@ -590,7 +596,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
590
596
|
tags=tags_list,
|
|
591
597
|
)
|
|
592
598
|
try:
|
|
593
|
-
await
|
|
599
|
+
await self._client.stub.SandboxTagsSet(req)
|
|
594
600
|
except GRPCError as exc:
|
|
595
601
|
raise InvalidError(exc.message) if exc.status == Status.INVALID_ARGUMENT else exc
|
|
596
602
|
|
|
@@ -602,7 +608,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
602
608
|
"""
|
|
603
609
|
await self._get_task_id() # Ensure the sandbox has started
|
|
604
610
|
req = api_pb2.SandboxSnapshotFsRequest(sandbox_id=self.object_id, timeout=timeout)
|
|
605
|
-
resp = await
|
|
611
|
+
resp = await self._client.stub.SandboxSnapshotFs(req)
|
|
606
612
|
|
|
607
613
|
if resp.result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
|
|
608
614
|
raise ExecutionError(resp.result.exception)
|
|
@@ -627,7 +633,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
627
633
|
|
|
628
634
|
while True:
|
|
629
635
|
req = api_pb2.SandboxWaitRequest(sandbox_id=self.object_id, timeout=10)
|
|
630
|
-
resp = await
|
|
636
|
+
resp = await self._client.stub.SandboxWait(req)
|
|
631
637
|
if resp.result.status:
|
|
632
638
|
logger.debug(f"Sandbox {self.object_id} wait completed with status {resp.result.status}")
|
|
633
639
|
self._result = resp.result
|
|
@@ -653,7 +659,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
653
659
|
return self._tunnels
|
|
654
660
|
|
|
655
661
|
req = api_pb2.SandboxGetTunnelsRequest(sandbox_id=self.object_id, timeout=timeout)
|
|
656
|
-
resp = await
|
|
662
|
+
resp = await self._client.stub.SandboxGetTunnels(req)
|
|
657
663
|
|
|
658
664
|
# If we couldn't get the tunnels in time, report the timeout.
|
|
659
665
|
if resp.result.status == api_pb2.GenericResult.GENERIC_STATUS_TIMEOUT:
|
|
@@ -681,7 +687,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
681
687
|
raise InvalidError(f"Failed to serialize user_metadata: {e}")
|
|
682
688
|
|
|
683
689
|
req = api_pb2.SandboxCreateConnectTokenRequest(sandbox_id=self.object_id, user_metadata=user_metadata)
|
|
684
|
-
resp = await
|
|
690
|
+
resp = await self._client.stub.SandboxCreateConnectToken(req)
|
|
685
691
|
return SandboxConnectCredentials(resp.url, resp.token)
|
|
686
692
|
|
|
687
693
|
async def reload_volumes(self) -> None:
|
|
@@ -690,8 +696,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
690
696
|
Added in v1.1.0.
|
|
691
697
|
"""
|
|
692
698
|
task_id = await self._get_task_id()
|
|
693
|
-
await
|
|
694
|
-
self._client.stub.ContainerReloadVolumes,
|
|
699
|
+
await self._client.stub.ContainerReloadVolumes(
|
|
695
700
|
api_pb2.ContainerReloadVolumesRequest(
|
|
696
701
|
task_id=task_id,
|
|
697
702
|
),
|
|
@@ -702,9 +707,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
702
707
|
|
|
703
708
|
This is a no-op if the Sandbox has already finished running."""
|
|
704
709
|
|
|
705
|
-
await
|
|
706
|
-
self._client.stub.SandboxTerminate, api_pb2.SandboxTerminateRequest(sandbox_id=self.object_id)
|
|
707
|
-
)
|
|
710
|
+
await self._client.stub.SandboxTerminate(api_pb2.SandboxTerminateRequest(sandbox_id=self.object_id))
|
|
708
711
|
|
|
709
712
|
async def poll(self) -> Optional[int]:
|
|
710
713
|
"""Check if the Sandbox has finished running.
|
|
@@ -713,7 +716,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
713
716
|
"""
|
|
714
717
|
|
|
715
718
|
req = api_pb2.SandboxWaitRequest(sandbox_id=self.object_id, timeout=0)
|
|
716
|
-
resp = await
|
|
719
|
+
resp = await self._client.stub.SandboxWait(req)
|
|
717
720
|
|
|
718
721
|
if resp.result.status:
|
|
719
722
|
self._result = resp.result
|
|
@@ -722,14 +725,19 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
722
725
|
|
|
723
726
|
async def _get_task_id(self) -> str:
|
|
724
727
|
while not self._task_id:
|
|
725
|
-
resp = await
|
|
726
|
-
self._client.stub.SandboxGetTaskId, api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id)
|
|
727
|
-
)
|
|
728
|
+
resp = await self._client.stub.SandboxGetTaskId(api_pb2.SandboxGetTaskIdRequest(sandbox_id=self.object_id))
|
|
728
729
|
self._task_id = resp.task_id
|
|
729
730
|
if not self._task_id:
|
|
730
731
|
await asyncio.sleep(0.5)
|
|
731
732
|
return self._task_id
|
|
732
733
|
|
|
734
|
+
async def _get_command_router_client(self, task_id: str) -> Optional[TaskCommandRouterClient]:
|
|
735
|
+
if self._command_router_client is None:
|
|
736
|
+
# Attempt to initialize a router client. Returns None if the new exec path not enabled
|
|
737
|
+
# for this sandbox.
|
|
738
|
+
self._command_router_client = await TaskCommandRouterClient.try_init(self._client, task_id)
|
|
739
|
+
return self._command_router_client
|
|
740
|
+
|
|
733
741
|
@overload
|
|
734
742
|
async def exec(
|
|
735
743
|
self,
|
|
@@ -791,13 +799,8 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
791
799
|
|
|
792
800
|
**Usage**
|
|
793
801
|
|
|
794
|
-
```python
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
sandbox = modal.Sandbox.create("sleep", "infinity", app=app)
|
|
798
|
-
|
|
799
|
-
process = sandbox.exec("bash", "-c", "for i in $(seq 1 10); do echo foo $i; sleep 0.5; done")
|
|
800
|
-
|
|
802
|
+
```python fixture:sandbox
|
|
803
|
+
process = sandbox.exec("bash", "-c", "for i in $(seq 1 3); do echo foo $i; sleep 0.1; done")
|
|
801
804
|
for line in process.stdout:
|
|
802
805
|
print(line)
|
|
803
806
|
```
|
|
@@ -855,21 +858,57 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
855
858
|
await TaskContext.gather(*secret_coros)
|
|
856
859
|
|
|
857
860
|
task_id = await self._get_task_id()
|
|
861
|
+
kwargs = {
|
|
862
|
+
"task_id": task_id,
|
|
863
|
+
"pty_info": pty_info,
|
|
864
|
+
"stdout": stdout,
|
|
865
|
+
"stderr": stderr,
|
|
866
|
+
"timeout": timeout,
|
|
867
|
+
"workdir": workdir,
|
|
868
|
+
"secret_ids": [secret.object_id for secret in secrets],
|
|
869
|
+
"text": text,
|
|
870
|
+
"bufsize": bufsize,
|
|
871
|
+
"runtime_debug": config.get("function_runtime_debug"),
|
|
872
|
+
}
|
|
873
|
+
# NB: This must come after the task ID is set, since the sandbox must be
|
|
874
|
+
# scheduled before we can create a router client.
|
|
875
|
+
if (command_router_client := await self._get_command_router_client(task_id)) is not None:
|
|
876
|
+
kwargs["command_router_client"] = command_router_client
|
|
877
|
+
return await self._exec_through_command_router(*args, **kwargs)
|
|
878
|
+
else:
|
|
879
|
+
return await self._exec_through_server(*args, **kwargs)
|
|
880
|
+
|
|
881
|
+
async def _exec_through_server(
|
|
882
|
+
self,
|
|
883
|
+
*args: str,
|
|
884
|
+
task_id: str,
|
|
885
|
+
pty_info: Optional[api_pb2.PTYInfo] = None,
|
|
886
|
+
stdout: StreamType = StreamType.PIPE,
|
|
887
|
+
stderr: StreamType = StreamType.PIPE,
|
|
888
|
+
timeout: Optional[int] = None,
|
|
889
|
+
workdir: Optional[str] = None,
|
|
890
|
+
secret_ids: Optional[Collection[str]] = None,
|
|
891
|
+
text: bool = True,
|
|
892
|
+
bufsize: Literal[-1, 1] = -1,
|
|
893
|
+
runtime_debug: bool = False,
|
|
894
|
+
) -> Union[_ContainerProcess[bytes], _ContainerProcess[str]]:
|
|
895
|
+
"""Execute a command through the Modal server."""
|
|
858
896
|
req = api_pb2.ContainerExecRequest(
|
|
859
897
|
task_id=task_id,
|
|
860
898
|
command=args,
|
|
861
899
|
pty_info=pty_info,
|
|
862
|
-
runtime_debug=
|
|
900
|
+
runtime_debug=runtime_debug,
|
|
863
901
|
timeout_secs=timeout or 0,
|
|
864
902
|
workdir=workdir,
|
|
865
|
-
secret_ids=
|
|
903
|
+
secret_ids=secret_ids,
|
|
866
904
|
)
|
|
867
|
-
resp = await
|
|
905
|
+
resp = await self._client.stub.ContainerExec(req)
|
|
868
906
|
by_line = bufsize == 1
|
|
869
907
|
exec_deadline = time.monotonic() + int(timeout) + CONTAINER_EXEC_TIMEOUT_BUFFER if timeout else None
|
|
870
908
|
logger.debug(f"Created ContainerProcess for exec_id {resp.exec_id} on Sandbox {self.object_id}")
|
|
871
909
|
return _ContainerProcess(
|
|
872
910
|
resp.exec_id,
|
|
911
|
+
task_id,
|
|
873
912
|
self._client,
|
|
874
913
|
stdout=stdout,
|
|
875
914
|
stderr=stderr,
|
|
@@ -878,17 +917,86 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
878
917
|
by_line=by_line,
|
|
879
918
|
)
|
|
880
919
|
|
|
920
|
+
async def _exec_through_command_router(
|
|
921
|
+
self,
|
|
922
|
+
*args: str,
|
|
923
|
+
task_id: str,
|
|
924
|
+
command_router_client: TaskCommandRouterClient,
|
|
925
|
+
pty_info: Optional[api_pb2.PTYInfo] = None,
|
|
926
|
+
stdout: StreamType = StreamType.PIPE,
|
|
927
|
+
stderr: StreamType = StreamType.PIPE,
|
|
928
|
+
timeout: Optional[int] = None,
|
|
929
|
+
workdir: Optional[str] = None,
|
|
930
|
+
secret_ids: Optional[Collection[str]] = None,
|
|
931
|
+
text: bool = True,
|
|
932
|
+
bufsize: Literal[-1, 1] = -1,
|
|
933
|
+
runtime_debug: bool = False,
|
|
934
|
+
) -> Union[_ContainerProcess[bytes], _ContainerProcess[str]]:
|
|
935
|
+
"""Execute a command through a task command router running on the Modal worker."""
|
|
936
|
+
|
|
937
|
+
# Generate a random process ID to use as a combination of idempotency key/process identifier.
|
|
938
|
+
process_id = str(uuid.uuid4())
|
|
939
|
+
if stdout == StreamType.PIPE:
|
|
940
|
+
stdout_config = sr_pb2.TaskExecStdoutConfig.TASK_EXEC_STDOUT_CONFIG_PIPE
|
|
941
|
+
elif stdout == StreamType.DEVNULL:
|
|
942
|
+
stdout_config = sr_pb2.TaskExecStdoutConfig.TASK_EXEC_STDOUT_CONFIG_DEVNULL
|
|
943
|
+
elif stdout == StreamType.STDOUT:
|
|
944
|
+
# TODO(saltzm): This is a behavior change from the old implementation. We should
|
|
945
|
+
# probably implement the old behavior of printing to stdout before moving out of beta.
|
|
946
|
+
raise NotImplementedError(
|
|
947
|
+
"Currently the STDOUT stream type is not supported when using exec "
|
|
948
|
+
"through a task command router, which is currently in beta."
|
|
949
|
+
)
|
|
950
|
+
else:
|
|
951
|
+
raise ValueError("Unsupported StreamType for stdout")
|
|
952
|
+
|
|
953
|
+
if stderr == StreamType.PIPE:
|
|
954
|
+
stderr_config = sr_pb2.TaskExecStderrConfig.TASK_EXEC_STDERR_CONFIG_PIPE
|
|
955
|
+
elif stderr == StreamType.DEVNULL:
|
|
956
|
+
stderr_config = sr_pb2.TaskExecStderrConfig.TASK_EXEC_STDERR_CONFIG_DEVNULL
|
|
957
|
+
elif stderr == StreamType.STDOUT:
|
|
958
|
+
stderr_config = sr_pb2.TaskExecStderrConfig.TASK_EXEC_STDERR_CONFIG_STDOUT
|
|
959
|
+
else:
|
|
960
|
+
raise ValueError("Unsupported StreamType for stderr")
|
|
961
|
+
|
|
962
|
+
# Start the process.
|
|
963
|
+
start_req = sr_pb2.TaskExecStartRequest(
|
|
964
|
+
task_id=task_id,
|
|
965
|
+
exec_id=process_id,
|
|
966
|
+
command_args=args,
|
|
967
|
+
stdout_config=stdout_config,
|
|
968
|
+
stderr_config=stderr_config,
|
|
969
|
+
timeout_secs=timeout,
|
|
970
|
+
workdir=workdir,
|
|
971
|
+
secret_ids=secret_ids,
|
|
972
|
+
pty_info=pty_info,
|
|
973
|
+
runtime_debug=runtime_debug,
|
|
974
|
+
)
|
|
975
|
+
_ = await command_router_client.exec_start(start_req)
|
|
976
|
+
|
|
977
|
+
return _ContainerProcess(
|
|
978
|
+
process_id,
|
|
979
|
+
task_id,
|
|
980
|
+
self._client,
|
|
981
|
+
command_router_client=command_router_client,
|
|
982
|
+
stdout=stdout,
|
|
983
|
+
stderr=stderr,
|
|
984
|
+
text=text,
|
|
985
|
+
by_line=bufsize == 1,
|
|
986
|
+
exec_deadline=time.monotonic() + int(timeout) if timeout else None,
|
|
987
|
+
)
|
|
988
|
+
|
|
881
989
|
async def _experimental_snapshot(self) -> _SandboxSnapshot:
|
|
882
990
|
await self._get_task_id()
|
|
883
991
|
snap_req = api_pb2.SandboxSnapshotRequest(sandbox_id=self.object_id)
|
|
884
|
-
snap_resp = await
|
|
992
|
+
snap_resp = await self._client.stub.SandboxSnapshot(snap_req)
|
|
885
993
|
|
|
886
994
|
snapshot_id = snap_resp.snapshot_id
|
|
887
995
|
|
|
888
996
|
# wait for the snapshot to succeed. this is implemented as a second idempotent rpc
|
|
889
997
|
# because the snapshot itself may take a while to complete.
|
|
890
998
|
wait_req = api_pb2.SandboxSnapshotWaitRequest(snapshot_id=snapshot_id, timeout=55.0)
|
|
891
|
-
wait_resp = await
|
|
999
|
+
wait_resp = await self._client.stub.SandboxSnapshotWait(wait_req)
|
|
892
1000
|
if wait_resp.result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
|
|
893
1001
|
raise ExecutionError(wait_resp.result.exception)
|
|
894
1002
|
|
|
@@ -931,9 +1039,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
931
1039
|
sandbox_name_override_type=api_pb2.SandboxRestoreRequest.SANDBOX_NAME_OVERRIDE_TYPE_STRING,
|
|
932
1040
|
)
|
|
933
1041
|
try:
|
|
934
|
-
restore_resp: api_pb2.SandboxRestoreResponse = await
|
|
935
|
-
client.stub.SandboxRestore, restore_req
|
|
936
|
-
)
|
|
1042
|
+
restore_resp: api_pb2.SandboxRestoreResponse = await client.stub.SandboxRestore(restore_req)
|
|
937
1043
|
except GRPCError as exc:
|
|
938
1044
|
if exc.status == Status.ALREADY_EXISTS:
|
|
939
1045
|
raise AlreadyExistsError(exc.message)
|
|
@@ -944,7 +1050,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
944
1050
|
task_id_req = api_pb2.SandboxGetTaskIdRequest(
|
|
945
1051
|
sandbox_id=restore_resp.sandbox_id, wait_until_ready=True, timeout=55.0
|
|
946
1052
|
)
|
|
947
|
-
resp = await
|
|
1053
|
+
resp = await client.stub.SandboxGetTaskId(task_id_req)
|
|
948
1054
|
if resp.task_result.status not in [
|
|
949
1055
|
api_pb2.GenericResult.GENERIC_STATUS_UNSPECIFIED,
|
|
950
1056
|
api_pb2.GenericResult.GENERIC_STATUS_SUCCESS,
|
|
@@ -1079,7 +1185,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1079
1185
|
|
|
1080
1186
|
# Fetches a batch of sandboxes.
|
|
1081
1187
|
try:
|
|
1082
|
-
resp = await
|
|
1188
|
+
resp = await client.stub.SandboxList(req)
|
|
1083
1189
|
except GRPCError as exc:
|
|
1084
1190
|
raise InvalidError(exc.message) if exc.status == Status.INVALID_ARGUMENT else exc
|
|
1085
1191
|
|
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: ...
|
|
@@ -305,6 +307,9 @@ class _Sandbox(modal._object._Object):
|
|
|
305
307
|
...
|
|
306
308
|
|
|
307
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]: ...
|
|
308
313
|
@typing.overload
|
|
309
314
|
async def exec(
|
|
310
315
|
self,
|
|
@@ -356,6 +361,41 @@ class _Sandbox(modal._object._Object):
|
|
|
356
361
|
"""
|
|
357
362
|
...
|
|
358
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
|
+
|
|
359
399
|
async def _experimental_snapshot(self) -> modal.snapshot._SandboxSnapshot: ...
|
|
360
400
|
@staticmethod
|
|
361
401
|
async def _experimental_from_snapshot(
|
|
@@ -444,6 +484,7 @@ class Sandbox(modal.object.Object):
|
|
|
444
484
|
_task_id: typing.Optional[str]
|
|
445
485
|
_tunnels: typing.Optional[dict[int, modal._tunnel.Tunnel]]
|
|
446
486
|
_enable_snapshot: bool
|
|
487
|
+
_command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient]
|
|
447
488
|
|
|
448
489
|
def __init__(self, *args, **kwargs):
|
|
449
490
|
"""mdmd:hidden"""
|
|
@@ -907,6 +948,16 @@ class Sandbox(modal.object.Object):
|
|
|
907
948
|
|
|
908
949
|
_get_task_id: ___get_task_id_spec[typing_extensions.Self]
|
|
909
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
|
+
|
|
910
961
|
class __exec_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
911
962
|
@typing.overload
|
|
912
963
|
def __call__(
|
|
@@ -1026,6 +1077,94 @@ class Sandbox(modal.object.Object):
|
|
|
1026
1077
|
|
|
1027
1078
|
_exec: ___exec_spec[typing_extensions.Self]
|
|
1028
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
|
+
|
|
1029
1168
|
class ___experimental_snapshot_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
1030
1169
|
def __call__(self, /) -> modal.snapshot.SandboxSnapshot: ...
|
|
1031
1170
|
async def aio(self, /) -> modal.snapshot.SandboxSnapshot: ...
|
modal/secret.py
CHANGED
|
@@ -15,7 +15,6 @@ from ._resolver import Resolver
|
|
|
15
15
|
from ._runtime.execution_context import is_local
|
|
16
16
|
from ._utils.async_utils import synchronize_api
|
|
17
17
|
from ._utils.deprecation import deprecation_warning, warn_if_passing_namespace
|
|
18
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
19
18
|
from ._utils.name_utils import check_object_name
|
|
20
19
|
from ._utils.time_utils import as_timestamp, timestamp_to_localized_dt
|
|
21
20
|
from .client import _Client
|
|
@@ -91,7 +90,7 @@ class _SecretManager:
|
|
|
91
90
|
env_dict=env_dict,
|
|
92
91
|
)
|
|
93
92
|
try:
|
|
94
|
-
await
|
|
93
|
+
await client.stub.SecretGetOrCreate(req)
|
|
95
94
|
except GRPCError as exc:
|
|
96
95
|
if exc.status == Status.ALREADY_EXISTS and not allow_existing:
|
|
97
96
|
raise AlreadyExistsError(exc.message)
|
|
@@ -143,7 +142,7 @@ class _SecretManager:
|
|
|
143
142
|
req = api_pb2.SecretListRequest(
|
|
144
143
|
environment_name=_get_environment_name(environment_name), pagination=pagination
|
|
145
144
|
)
|
|
146
|
-
resp = await
|
|
145
|
+
resp = await client.stub.SecretList(req)
|
|
147
146
|
items.extend(resp.items)
|
|
148
147
|
finished = (len(resp.items) < max_page_size) or (max_objects is not None and len(items) >= max_objects)
|
|
149
148
|
return finished
|
|
@@ -200,7 +199,7 @@ class _SecretManager:
|
|
|
200
199
|
raise
|
|
201
200
|
else:
|
|
202
201
|
req = api_pb2.SecretDeleteRequest(secret_id=obj.object_id)
|
|
203
|
-
await
|
|
202
|
+
await obj._client.stub.SecretDelete(req)
|
|
204
203
|
|
|
205
204
|
|
|
206
205
|
SecretManager = synchronize_api(_SecretManager)
|
|
@@ -454,7 +453,7 @@ class _Secret(_Object, type_prefix="st"):
|
|
|
454
453
|
object_creation_type=object_creation_type,
|
|
455
454
|
env_dict=env_dict,
|
|
456
455
|
)
|
|
457
|
-
resp = await
|
|
456
|
+
resp = await client.stub.SecretGetOrCreate(request)
|
|
458
457
|
return resp.secret_id
|
|
459
458
|
|
|
460
459
|
@live_method
|
modal/snapshot.py
CHANGED
|
@@ -6,7 +6,6 @@ from modal_proto import api_pb2
|
|
|
6
6
|
from ._object import _Object
|
|
7
7
|
from ._resolver import Resolver
|
|
8
8
|
from ._utils.async_utils import synchronize_api
|
|
9
|
-
from ._utils.grpc_utils import retry_transient_errors
|
|
10
9
|
from .client import _Client
|
|
11
10
|
|
|
12
11
|
|
|
@@ -28,9 +27,7 @@ class _SandboxSnapshot(_Object, type_prefix="sn"):
|
|
|
28
27
|
client = await _Client.from_env()
|
|
29
28
|
|
|
30
29
|
async def _load(self: _SandboxSnapshot, resolver: Resolver, existing_object_id: Optional[str]):
|
|
31
|
-
await
|
|
32
|
-
client.stub.SandboxSnapshotGet, api_pb2.SandboxSnapshotGetRequest(snapshot_id=sandbox_snapshot_id)
|
|
33
|
-
)
|
|
30
|
+
await client.stub.SandboxSnapshotGet(api_pb2.SandboxSnapshotGetRequest(snapshot_id=sandbox_snapshot_id))
|
|
34
31
|
|
|
35
32
|
rep = "SandboxSnapshot()"
|
|
36
33
|
obj = _SandboxSnapshot._from_loader(_load, rep)
|
modal/token_flow.py
CHANGED
|
@@ -56,7 +56,7 @@ class _TokenFlow:
|
|
|
56
56
|
req = api_pb2.TokenFlowWaitRequest(
|
|
57
57
|
token_flow_id=self.token_flow_id, timeout=timeout, wait_secret=self.wait_secret
|
|
58
58
|
)
|
|
59
|
-
resp = await self.stub.TokenFlowWait(req, timeout=
|
|
59
|
+
resp = await self.stub.TokenFlowWait(req, retry=None, timeout=timeout + grpc_extra_timeout)
|
|
60
60
|
if not resp.timeout:
|
|
61
61
|
return resp
|
|
62
62
|
else:
|