modAL 1.5.3.dev5__py3-none-any.whl → 1.5.3.dev6__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/_functions.py +11 -15
- modal/_utils/task_command_router_client.py +127 -3
- modal/client.pyi +2 -2
- modal/container_process.py +13 -1
- modal/container_process.pyi +27 -0
- modal/functions.pyi +20 -20
- modal/sandbox.py +29 -10
- modal/sandbox.pyi +6 -2
- modal/sandbox_fs.py +32 -80
- modal/sandbox_fs.pyi +24 -0
- {modal-1.5.3.dev5.dist-info → modal-1.5.3.dev6.dist-info}/METADATA +1 -1
- {modal-1.5.3.dev5.dist-info → modal-1.5.3.dev6.dist-info}/RECORD +20 -20
- modal_docs/gen_reference_docs.py +0 -1
- modal_docs/mdmd/mdmd.py +9 -2
- modal_docs/mdmd/types.py +1 -0
- modal_version/__init__.py +1 -1
- {modal-1.5.3.dev5.dist-info → modal-1.5.3.dev6.dist-info}/WHEEL +0 -0
- {modal-1.5.3.dev5.dist-info → modal-1.5.3.dev6.dist-info}/entry_points.txt +0 -0
- {modal-1.5.3.dev5.dist-info → modal-1.5.3.dev6.dist-info}/licenses/LICENSE +0 -0
- {modal-1.5.3.dev5.dist-info → modal-1.5.3.dev6.dist-info}/top_level.txt +0 -0
modal/_functions.py
CHANGED
|
@@ -2073,11 +2073,13 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2073
2073
|
|
|
2074
2074
|
@live_method
|
|
2075
2075
|
async def get_call_graph(self) -> list[InputInfo]:
|
|
2076
|
-
"""
|
|
2077
|
-
call ID, along with the status of execution for each node.
|
|
2076
|
+
"""Fetch information about the graph of Inputs this FunctionCall is part of.
|
|
2078
2077
|
|
|
2079
|
-
|
|
2080
|
-
|
|
2078
|
+
Note: the call graph data is not populated in real-time, and its capture is best-effort.
|
|
2079
|
+
We do not recommend relying on this method for critical use cases.
|
|
2080
|
+
|
|
2081
|
+
See the [`modal.types`](/docs/sdk/py/latest/modal.types) reference for information
|
|
2082
|
+
on the return values.
|
|
2081
2083
|
|
|
2082
2084
|
Returns:
|
|
2083
2085
|
A list of `InputInfo` nodes describing the call graph.
|
|
@@ -2088,19 +2090,13 @@ class _FunctionCall(typing.Generic[ReturnType], _Object, type_prefix="fc"):
|
|
|
2088
2090
|
return _reconstruct_call_graph(response)
|
|
2089
2091
|
|
|
2090
2092
|
@live_method
|
|
2091
|
-
async def cancel(
|
|
2092
|
-
|
|
2093
|
-
# if true, containers running the inputs are forcibly terminated
|
|
2094
|
-
terminate_containers: bool = False,
|
|
2095
|
-
):
|
|
2096
|
-
"""Cancels the function call, which will stop its execution and mark its inputs as
|
|
2097
|
-
[`TERMINATED`](https://modal.com/docs/sdk/py/latest/modal.call_graph#modalcall_graphinputstatus).
|
|
2098
|
-
|
|
2099
|
-
If `terminate_containers=True` - the containers running the cancelled inputs are all terminated
|
|
2100
|
-
causing any non-cancelled inputs on those containers to be rescheduled in new containers.
|
|
2093
|
+
async def cancel(self, terminate_containers: bool = False):
|
|
2094
|
+
"""Cancel the FunctionCall and terminate its inputs without retrying.
|
|
2101
2095
|
|
|
2102
2096
|
Args:
|
|
2103
|
-
terminate_containers: If True,
|
|
2097
|
+
terminate_containers: If True, terminate the containers running the cancelled
|
|
2098
|
+
inputs. Any other inputs running concurrently on those containers will be
|
|
2099
|
+
rescheduled.
|
|
2104
2100
|
"""
|
|
2105
2101
|
request = api_pb2.FunctionCallCancelRequest(
|
|
2106
2102
|
function_call_id=self.object_id, terminate_containers=terminate_containers
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright Modal Labs 2025
|
|
2
2
|
import asyncio
|
|
3
3
|
import base64
|
|
4
|
+
import io
|
|
4
5
|
import json
|
|
5
6
|
import socket
|
|
6
7
|
import ssl
|
|
@@ -8,16 +9,16 @@ import time
|
|
|
8
9
|
import typing
|
|
9
10
|
import urllib.parse
|
|
10
11
|
import weakref
|
|
11
|
-
from collections.abc import AsyncGenerator, Callable
|
|
12
|
+
from collections.abc import AsyncGenerator, AsyncIterable, Callable
|
|
12
13
|
from contextlib import suppress
|
|
13
|
-
from typing import TypeVar
|
|
14
|
+
from typing import BinaryIO, TypeVar
|
|
14
15
|
|
|
15
16
|
import grpclib.client
|
|
16
17
|
from grpclib import GRPCError, Status
|
|
17
18
|
from grpclib.exceptions import StreamTerminatedError
|
|
18
19
|
|
|
19
20
|
from modal.config import logger
|
|
20
|
-
from modal.exception import ExecTimeoutError, TimeoutError as ModalTimeoutError
|
|
21
|
+
from modal.exception import AuthError, ExecTimeoutError, InternalError, ServiceError, TimeoutError as ModalTimeoutError
|
|
21
22
|
from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
|
|
22
23
|
from modal_proto.task_command_router_grpc import TaskCommandRouterStub
|
|
23
24
|
|
|
@@ -26,6 +27,17 @@ from .._utils.grpc_utils import ModalChannel, create_channel_config
|
|
|
26
27
|
from .async_utils import aclosing, retry
|
|
27
28
|
from .grpc_utils import RETRYABLE_GRPC_STATUS_CODES
|
|
28
29
|
|
|
30
|
+
STREAMING_STDIN_CHUNK_SIZE = 256 * 1024
|
|
31
|
+
|
|
32
|
+
_STREAMING_STDIN_RESUMABLE_EXCEPTIONS = (
|
|
33
|
+
AuthError,
|
|
34
|
+
InternalError,
|
|
35
|
+
ServiceError,
|
|
36
|
+
StreamTerminatedError,
|
|
37
|
+
OSError,
|
|
38
|
+
asyncio.TimeoutError,
|
|
39
|
+
)
|
|
40
|
+
|
|
29
41
|
|
|
30
42
|
@retry(n_attempts=34, base_delay=1, max_delay=10, attempt_timeout=10, total_timeout=310)
|
|
31
43
|
async def _connect_channel(channel: grpclib.client.Channel):
|
|
@@ -468,6 +480,118 @@ class TaskCommandRouterClient:
|
|
|
468
480
|
lambda: self._call_with_auth_retry(self._stub.TaskExecStdinWrite, request)
|
|
469
481
|
)
|
|
470
482
|
|
|
483
|
+
async def exec_stdin_write_stream(
|
|
484
|
+
self,
|
|
485
|
+
task_id: str,
|
|
486
|
+
exec_id: str,
|
|
487
|
+
source: BinaryIO,
|
|
488
|
+
chunk_size: int = STREAMING_STDIN_CHUNK_SIZE,
|
|
489
|
+
max_resume_attempts: int = 9,
|
|
490
|
+
) -> int:
|
|
491
|
+
"""Stream `source` into the exec's stdin, with bounded resume on transient failures.
|
|
492
|
+
|
|
493
|
+
Streams the full contents of `source` in one client-streaming RPC and
|
|
494
|
+
closes stdin (EOF) on success. On a resumable error, queries
|
|
495
|
+
`exec_stdin_status` for the server's canonical offset, seeks `source`
|
|
496
|
+
to that point, and reopens the stream.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
task_id: The task ID of the task running the exec'd command.
|
|
500
|
+
exec_id: The execution ID of the command to write to.
|
|
501
|
+
source: A seekable byte source (file, BytesIO, etc.).
|
|
502
|
+
chunk_size: Bytes per outbound message.
|
|
503
|
+
max_resume_attempts: Upper bound on resume retries before giving up.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
The total number of bytes streamed into the exec's stdin.
|
|
507
|
+
"""
|
|
508
|
+
offset = 0
|
|
509
|
+
attempt = 0
|
|
510
|
+
while True:
|
|
511
|
+
|
|
512
|
+
async def _chunks():
|
|
513
|
+
while True:
|
|
514
|
+
chunk = source.read(chunk_size)
|
|
515
|
+
if not chunk:
|
|
516
|
+
return
|
|
517
|
+
yield chunk
|
|
518
|
+
|
|
519
|
+
try:
|
|
520
|
+
source.seek(offset)
|
|
521
|
+
await self._exec_stdin_write_stream(task_id, exec_id, offset, _chunks())
|
|
522
|
+
return source.tell()
|
|
523
|
+
except (*_STREAMING_STDIN_RESUMABLE_EXCEPTIONS, AttributeError) as e:
|
|
524
|
+
# StreamTerminatedError surfaces as AttributeError in grpclib<=0.4.7;
|
|
525
|
+
# see call_with_retries_on_transient_errors.
|
|
526
|
+
if isinstance(e, AttributeError) and "_write_appdata" not in str(e):
|
|
527
|
+
raise
|
|
528
|
+
attempt += 1
|
|
529
|
+
if attempt > max_resume_attempts:
|
|
530
|
+
raise
|
|
531
|
+
status = await self.exec_stdin_status(task_id=task_id, exec_id=exec_id)
|
|
532
|
+
if status.closed:
|
|
533
|
+
pos = source.tell()
|
|
534
|
+
if status.num_bytes_written == pos and source.seek(0, io.SEEK_END) == pos:
|
|
535
|
+
logger.debug(f"exec_stdin_write_stream completed but response was lost: {e}")
|
|
536
|
+
return pos
|
|
537
|
+
raise
|
|
538
|
+
offset = status.num_bytes_written
|
|
539
|
+
logger.debug(f"exec_stdin_write_stream resuming from offset {offset} after error: {e}")
|
|
540
|
+
|
|
541
|
+
async def _exec_stdin_write_stream(
|
|
542
|
+
self,
|
|
543
|
+
task_id: str,
|
|
544
|
+
exec_id: str,
|
|
545
|
+
start_offset: int,
|
|
546
|
+
chunks: AsyncIterable[bytes],
|
|
547
|
+
) -> sr_pb2.TaskExecStdinWriteStreamResponse | None:
|
|
548
|
+
"""Single client-streaming attempt: Start, Data chunks, then End (EOF).
|
|
549
|
+
|
|
550
|
+
Does not retry; `exec_stdin_write_stream` owns resume.
|
|
551
|
+
"""
|
|
552
|
+
with grpc_error_converter():
|
|
553
|
+
stream = self._stub.TaskExecStdinWriteStream.open(metadata=self._get_metadata())
|
|
554
|
+
async with stream as s:
|
|
555
|
+
start = sr_pb2.TaskExecStdinWriteStreamRequest(
|
|
556
|
+
start=sr_pb2.TaskExecStdinWriteStreamStart(
|
|
557
|
+
task_id=task_id,
|
|
558
|
+
exec_id=exec_id,
|
|
559
|
+
offset=start_offset,
|
|
560
|
+
),
|
|
561
|
+
)
|
|
562
|
+
await s.send_message(start)
|
|
563
|
+
async for data in chunks:
|
|
564
|
+
if not data:
|
|
565
|
+
continue
|
|
566
|
+
await s.send_message(sr_pb2.TaskExecStdinWriteStreamRequest(data=data))
|
|
567
|
+
# The server closes stdin only on this explicit End message. A
|
|
568
|
+
# stream that breaks before it leaves stdin open for resume.
|
|
569
|
+
await s.send_message(
|
|
570
|
+
sr_pb2.TaskExecStdinWriteStreamRequest(end=sr_pb2.TaskExecStdinWriteStreamEnd()),
|
|
571
|
+
end=True,
|
|
572
|
+
)
|
|
573
|
+
return await s.recv_message()
|
|
574
|
+
|
|
575
|
+
async def exec_stdin_status(self, task_id: str, exec_id: str) -> sr_pb2.TaskExecStdinStatusResponse:
|
|
576
|
+
"""Read the current stdin write status for an exec'd command, to support retries from the right offset.
|
|
577
|
+
|
|
578
|
+
Used by streaming clients to find the resume offset after a stream
|
|
579
|
+
failure. Evicts any in-flight stdin stream for the exec, so it is not
|
|
580
|
+
safe to call for read-only observability.
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
task_id: The task ID of the task running the exec'd command.
|
|
584
|
+
exec_id: The execution ID of the command to query.
|
|
585
|
+
Raises:
|
|
586
|
+
Other errors: If retries are exhausted on transient errors or if there's an error
|
|
587
|
+
from the RPC itself.
|
|
588
|
+
"""
|
|
589
|
+
request = sr_pb2.TaskExecStdinStatusRequest(task_id=task_id, exec_id=exec_id)
|
|
590
|
+
with grpc_error_converter():
|
|
591
|
+
return await call_with_retries_on_transient_errors(
|
|
592
|
+
lambda: self._call_with_auth_retry(self._stub.TaskExecStdinStatus, request)
|
|
593
|
+
)
|
|
594
|
+
|
|
471
595
|
async def sandbox_stdin_write_v2(
|
|
472
596
|
self, task_id: str, offset: int, data: bytes, eof: bool
|
|
473
597
|
) -> sr_pb2.SandboxStdinWriteV2Response:
|
modal/client.pyi
CHANGED
|
@@ -37,7 +37,7 @@ class _Client:
|
|
|
37
37
|
server_url: str,
|
|
38
38
|
client_type: int,
|
|
39
39
|
credentials: typing.Optional[tuple[str, str]],
|
|
40
|
-
version: str = "1.5.3.
|
|
40
|
+
version: str = "1.5.3.dev6",
|
|
41
41
|
):
|
|
42
42
|
"""mdmd:hidden
|
|
43
43
|
The Modal client object is not intended to be instantiated directly by users.
|
|
@@ -207,7 +207,7 @@ class Client:
|
|
|
207
207
|
server_url: str,
|
|
208
208
|
client_type: int,
|
|
209
209
|
credentials: typing.Optional[tuple[str, str]],
|
|
210
|
-
version: str = "1.5.3.
|
|
210
|
+
version: str = "1.5.3.dev6",
|
|
211
211
|
):
|
|
212
212
|
"""mdmd:hidden
|
|
213
213
|
The Modal client object is not intended to be instantiated directly by users.
|
modal/container_process.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Copyright Modal Labs 2024
|
|
2
2
|
import asyncio
|
|
3
3
|
import platform
|
|
4
|
-
from typing import Generic, TypeVar
|
|
4
|
+
from typing import BinaryIO, Generic, TypeVar
|
|
5
5
|
|
|
6
6
|
from modal_proto import api_pb2
|
|
7
7
|
|
|
@@ -117,6 +117,18 @@ class _ContainerProcess(Generic[T]):
|
|
|
117
117
|
)
|
|
118
118
|
return self._returncode
|
|
119
119
|
|
|
120
|
+
async def _stdin_write_stream(self, source: BinaryIO) -> int:
|
|
121
|
+
"""mdmd:hidden
|
|
122
|
+
Stream `source` into the process's stdin and close it (EOF) on success.
|
|
123
|
+
|
|
124
|
+
Returns the total bytes streamed.
|
|
125
|
+
"""
|
|
126
|
+
return await self._command_router_client.exec_stdin_write_stream(
|
|
127
|
+
task_id=self._task_id,
|
|
128
|
+
exec_id=self._process_id,
|
|
129
|
+
source=source,
|
|
130
|
+
)
|
|
131
|
+
|
|
120
132
|
async def poll(self) -> int | None:
|
|
121
133
|
"""Check if the container process has finished running.
|
|
122
134
|
|
modal/container_process.pyi
CHANGED
|
@@ -53,6 +53,14 @@ class _ContainerProcess(typing.Generic[T]):
|
|
|
53
53
|
|
|
54
54
|
@property
|
|
55
55
|
def returncode(self) -> int: ...
|
|
56
|
+
async def _stdin_write_stream(self, source: typing.BinaryIO) -> int:
|
|
57
|
+
"""mdmd:hidden
|
|
58
|
+
Stream `source` into the process's stdin and close it (EOF) on success.
|
|
59
|
+
|
|
60
|
+
Returns the total bytes streamed.
|
|
61
|
+
"""
|
|
62
|
+
...
|
|
63
|
+
|
|
56
64
|
async def poll(self) -> typing.Optional[int]:
|
|
57
65
|
"""Check if the container process has finished running.
|
|
58
66
|
|
|
@@ -105,6 +113,25 @@ class ContainerProcess(typing.Generic[T]):
|
|
|
105
113
|
@property
|
|
106
114
|
def returncode(self) -> int: ...
|
|
107
115
|
|
|
116
|
+
class ___stdin_write_stream_spec(typing_extensions.Protocol):
|
|
117
|
+
def __call__(self, /, source: typing.BinaryIO) -> int:
|
|
118
|
+
"""mdmd:hidden
|
|
119
|
+
Stream `source` into the process's stdin and close it (EOF) on success.
|
|
120
|
+
|
|
121
|
+
Returns the total bytes streamed.
|
|
122
|
+
"""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
async def aio(self, /, source: typing.BinaryIO) -> int:
|
|
126
|
+
"""mdmd:hidden
|
|
127
|
+
Stream `source` into the process's stdin and close it (EOF) on success.
|
|
128
|
+
|
|
129
|
+
Returns the total bytes streamed.
|
|
130
|
+
"""
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
_stdin_write_stream: ___stdin_write_stream_spec
|
|
134
|
+
|
|
108
135
|
class __poll_spec(typing_extensions.Protocol):
|
|
109
136
|
def __call__(self, /) -> typing.Optional[int]:
|
|
110
137
|
"""Check if the container process has finished running.
|
modal/functions.pyi
CHANGED
|
@@ -1044,11 +1044,13 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
|
|
|
1044
1044
|
|
|
1045
1045
|
class __get_call_graph_spec(typing_extensions.Protocol):
|
|
1046
1046
|
def __call__(self, /) -> list[modal.types.InputInfo]:
|
|
1047
|
-
"""
|
|
1048
|
-
call ID, along with the status of execution for each node.
|
|
1047
|
+
"""Fetch information about the graph of Inputs this FunctionCall is part of.
|
|
1049
1048
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1049
|
+
Note: the call graph data is not populated in real-time, and its capture is best-effort.
|
|
1050
|
+
We do not recommend relying on this method for critical use cases.
|
|
1051
|
+
|
|
1052
|
+
See the [`modal.types`](/docs/sdk/py/latest/modal.types) reference for information
|
|
1053
|
+
on the return values.
|
|
1052
1054
|
|
|
1053
1055
|
Returns:
|
|
1054
1056
|
A list of `InputInfo` nodes describing the call graph.
|
|
@@ -1056,11 +1058,13 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
|
|
|
1056
1058
|
...
|
|
1057
1059
|
|
|
1058
1060
|
async def aio(self, /) -> list[modal.types.InputInfo]:
|
|
1059
|
-
"""
|
|
1060
|
-
|
|
1061
|
+
"""Fetch information about the graph of Inputs this FunctionCall is part of.
|
|
1062
|
+
|
|
1063
|
+
Note: the call graph data is not populated in real-time, and its capture is best-effort.
|
|
1064
|
+
We do not recommend relying on this method for critical use cases.
|
|
1061
1065
|
|
|
1062
|
-
See [`modal.
|
|
1063
|
-
|
|
1066
|
+
See the [`modal.types`](/docs/sdk/py/latest/modal.types) reference for information
|
|
1067
|
+
on the return values.
|
|
1064
1068
|
|
|
1065
1069
|
Returns:
|
|
1066
1070
|
A list of `InputInfo` nodes describing the call graph.
|
|
@@ -1071,26 +1075,22 @@ class FunctionCall(typing.Generic[modal._functions.ReturnType], modal.object.Obj
|
|
|
1071
1075
|
|
|
1072
1076
|
class __cancel_spec(typing_extensions.Protocol):
|
|
1073
1077
|
def __call__(self, /, terminate_containers: bool = False):
|
|
1074
|
-
"""
|
|
1075
|
-
[`TERMINATED`](https://modal.com/docs/sdk/py/latest/modal.call_graph#modalcall_graphinputstatus).
|
|
1076
|
-
|
|
1077
|
-
If `terminate_containers=True` - the containers running the cancelled inputs are all terminated
|
|
1078
|
-
causing any non-cancelled inputs on those containers to be rescheduled in new containers.
|
|
1078
|
+
"""Cancel the FunctionCall and terminate its inputs without retrying.
|
|
1079
1079
|
|
|
1080
1080
|
Args:
|
|
1081
|
-
terminate_containers: If True,
|
|
1081
|
+
terminate_containers: If True, terminate the containers running the cancelled
|
|
1082
|
+
inputs. Any other inputs running concurrently on those containers will be
|
|
1083
|
+
rescheduled.
|
|
1082
1084
|
"""
|
|
1083
1085
|
...
|
|
1084
1086
|
|
|
1085
1087
|
async def aio(self, /, terminate_containers: bool = False):
|
|
1086
|
-
"""
|
|
1087
|
-
[`TERMINATED`](https://modal.com/docs/sdk/py/latest/modal.call_graph#modalcall_graphinputstatus).
|
|
1088
|
-
|
|
1089
|
-
If `terminate_containers=True` - the containers running the cancelled inputs are all terminated
|
|
1090
|
-
causing any non-cancelled inputs on those containers to be rescheduled in new containers.
|
|
1088
|
+
"""Cancel the FunctionCall and terminate its inputs without retrying.
|
|
1091
1089
|
|
|
1092
1090
|
Args:
|
|
1093
|
-
terminate_containers: If True,
|
|
1091
|
+
terminate_containers: If True, terminate the containers running the cancelled
|
|
1092
|
+
inputs. Any other inputs running concurrently on those containers will be
|
|
1093
|
+
rescheduled.
|
|
1094
1094
|
"""
|
|
1095
1095
|
...
|
|
1096
1096
|
|
modal/sandbox.py
CHANGED
|
@@ -332,6 +332,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
332
332
|
_attached: bool
|
|
333
333
|
_filesystem: _SandboxFilesystem | None
|
|
334
334
|
_is_v2: bool = False
|
|
335
|
+
_app_id: str | None
|
|
335
336
|
|
|
336
337
|
@staticmethod
|
|
337
338
|
def _default_pty_info() -> api_pb2.PTYInfo:
|
|
@@ -500,7 +501,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
500
501
|
create_resp = await load_context.client.stub.SandboxCreate(create_req)
|
|
501
502
|
rpc_elapsed = time.monotonic() - rpc_start
|
|
502
503
|
sandbox_id = create_resp.sandbox_id
|
|
503
|
-
self._hydrate(sandbox_id, load_context.client,
|
|
504
|
+
self._hydrate(sandbox_id, load_context.client, create_resp.metadata)
|
|
504
505
|
|
|
505
506
|
if logger.isEnabledFor(logging.DEBUG):
|
|
506
507
|
total_elapsed = time.monotonic() - load_start
|
|
@@ -1017,7 +1018,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1017
1018
|
)
|
|
1018
1019
|
rpc_elapsed = time.monotonic() - rpc_start
|
|
1019
1020
|
sandbox_id = create_resp.sandbox_id
|
|
1020
|
-
self._hydrate(sandbox_id, load_context.client,
|
|
1021
|
+
self._hydrate(sandbox_id, load_context.client, create_resp.metadata)
|
|
1021
1022
|
self._is_v2 = True
|
|
1022
1023
|
self._task_id = create_resp.task_id
|
|
1023
1024
|
self._hydrate_metadata_v2()
|
|
@@ -1070,7 +1071,22 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1070
1071
|
await resolver.load(obj, load_context)
|
|
1071
1072
|
return obj
|
|
1072
1073
|
|
|
1073
|
-
def
|
|
1074
|
+
def _get_metadata(self) -> api_pb2.SandboxHandleMetadata:
|
|
1075
|
+
metadata = api_pb2.SandboxHandleMetadata(app_id=self._app_id or "")
|
|
1076
|
+
if hasattr(self, "_result") and self._result is not None:
|
|
1077
|
+
metadata.result.CopyFrom(self._result)
|
|
1078
|
+
return metadata
|
|
1079
|
+
|
|
1080
|
+
def _hydrate_metadata(self, handle_metadata: Message | None) -> None:
|
|
1081
|
+
self._app_id = None
|
|
1082
|
+
self._result = None
|
|
1083
|
+
if handle_metadata is not None:
|
|
1084
|
+
assert isinstance(handle_metadata, api_pb2.SandboxHandleMetadata), (
|
|
1085
|
+
f"{type(handle_metadata)} is not SandboxHandleMetadata"
|
|
1086
|
+
)
|
|
1087
|
+
self._app_id = handle_metadata.app_id
|
|
1088
|
+
if handle_metadata.HasField("result"):
|
|
1089
|
+
self._result = handle_metadata.result
|
|
1074
1090
|
self._stdout = StreamReader(
|
|
1075
1091
|
_StreamReaderThroughServerParams(
|
|
1076
1092
|
file_descriptor=api_pb2.FILE_DESCRIPTOR_STDOUT,
|
|
@@ -1088,13 +1104,14 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1088
1104
|
by_line=True,
|
|
1089
1105
|
)
|
|
1090
1106
|
self._stdin = StreamWriter(_StreamWriterThroughServerParams(object_id=self.object_id, client=self._client))
|
|
1091
|
-
self._result = None
|
|
1092
1107
|
self._task_id = None
|
|
1093
1108
|
self._tunnels = None
|
|
1094
1109
|
self._enable_snapshot = False
|
|
1095
1110
|
self._command_router_client = None
|
|
1096
1111
|
self._filesystem = None
|
|
1097
|
-
self._is_v2 =
|
|
1112
|
+
self._is_v2 = _is_v2_sandbox_id(self.object_id)
|
|
1113
|
+
if self._is_v2:
|
|
1114
|
+
self._hydrate_metadata_v2()
|
|
1098
1115
|
|
|
1099
1116
|
def _hydrate_metadata_v2(self) -> None:
|
|
1100
1117
|
"""Wire up V2 stdio readers that read directly from the worker. Cheap
|
|
@@ -1133,11 +1150,13 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1133
1150
|
super()._initialize_from_other(other)
|
|
1134
1151
|
self._attached = other._attached
|
|
1135
1152
|
self._is_v2 = other._is_v2
|
|
1153
|
+
self._app_id = other._app_id
|
|
1136
1154
|
|
|
1137
1155
|
def _initialize_from_empty(self):
|
|
1138
1156
|
super()._initialize_from_empty()
|
|
1139
1157
|
self._attached = True
|
|
1140
1158
|
self._is_v2 = False
|
|
1159
|
+
self._app_id = None
|
|
1141
1160
|
|
|
1142
1161
|
async def detach(self):
|
|
1143
1162
|
"""Disconnects your client from the sandbox and cleans up resources assoicated with the connection.
|
|
@@ -1200,7 +1219,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1200
1219
|
|
|
1201
1220
|
req = api_pb2.SandboxGetFromNameRequest(sandbox_name=name, app_name=app_name, environment_name=env_name)
|
|
1202
1221
|
resp = await client.stub.SandboxGetFromName(req)
|
|
1203
|
-
return _Sandbox._new_hydrated(resp.sandbox_id, client,
|
|
1222
|
+
return _Sandbox._new_hydrated(resp.sandbox_id, client, resp.metadata)
|
|
1204
1223
|
|
|
1205
1224
|
@staticmethod
|
|
1206
1225
|
async def _experimental_from_name(
|
|
@@ -1235,7 +1254,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1235
1254
|
auth_token = await client._auth_token_manager.get_token()
|
|
1236
1255
|
resp = await client.stub.SandboxGetFromNameV2(req, metadata=[("x-modal-auth-token", auth_token)])
|
|
1237
1256
|
|
|
1238
|
-
obj = _Sandbox._new_hydrated(resp.sandbox_id, client,
|
|
1257
|
+
obj = _Sandbox._new_hydrated(resp.sandbox_id, client, resp.metadata)
|
|
1239
1258
|
obj._is_v2 = True
|
|
1240
1259
|
obj._hydrate_metadata_v2()
|
|
1241
1260
|
return obj
|
|
@@ -1266,7 +1285,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
1266
1285
|
else:
|
|
1267
1286
|
resp = await client.stub.SandboxWait(req)
|
|
1268
1287
|
|
|
1269
|
-
obj = _Sandbox._new_hydrated(sandbox_id, client,
|
|
1288
|
+
obj = _Sandbox._new_hydrated(sandbox_id, client, resp.metadata)
|
|
1270
1289
|
obj._is_v2 = is_v2
|
|
1271
1290
|
if is_v2:
|
|
1272
1291
|
obj._hydrate_metadata_v2()
|
|
@@ -2360,7 +2379,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
2360
2379
|
|
|
2361
2380
|
for sandbox_info in resp.sandboxes:
|
|
2362
2381
|
sandbox_info: api_pb2.SandboxInfo
|
|
2363
|
-
obj = _Sandbox._new_hydrated(sandbox_info.id, client,
|
|
2382
|
+
obj = _Sandbox._new_hydrated(sandbox_info.id, client, sandbox_info.metadata)
|
|
2364
2383
|
obj._result = sandbox_info.task_info.result # TODO: send SandboxInfo as metadata to _new_hydrated?
|
|
2365
2384
|
yield obj
|
|
2366
2385
|
|
|
@@ -2415,7 +2434,7 @@ class _Sandbox(_Object, type_prefix="sb"):
|
|
|
2415
2434
|
|
|
2416
2435
|
for sandbox_info in resp.sandboxes:
|
|
2417
2436
|
sandbox_info: api_pb2.SandboxInfo
|
|
2418
|
-
obj = _Sandbox._new_hydrated(sandbox_info.id, client,
|
|
2437
|
+
obj = _Sandbox._new_hydrated(sandbox_info.id, client, sandbox_info.metadata)
|
|
2419
2438
|
# SandboxListV2 only returns V2 sandboxes; mark them as such so
|
|
2420
2439
|
# operations like wait/terminate/exec use the V2 RPCs and stdio.
|
|
2421
2440
|
obj._is_v2 = True
|
modal/sandbox.pyi
CHANGED
|
@@ -150,6 +150,7 @@ class _Sandbox(modal._object._Object):
|
|
|
150
150
|
_attached: bool
|
|
151
151
|
_filesystem: typing.Optional[modal.sandbox_fs._SandboxFilesystem]
|
|
152
152
|
_is_v2: bool
|
|
153
|
+
_app_id: typing.Optional[str]
|
|
153
154
|
|
|
154
155
|
@staticmethod
|
|
155
156
|
def _default_pty_info() -> modal_proto.api_pb2.PTYInfo: ...
|
|
@@ -424,7 +425,8 @@ class _Sandbox(modal._object._Object):
|
|
|
424
425
|
"""
|
|
425
426
|
...
|
|
426
427
|
|
|
427
|
-
def
|
|
428
|
+
def _get_metadata(self) -> modal_proto.api_pb2.SandboxHandleMetadata: ...
|
|
429
|
+
def _hydrate_metadata(self, handle_metadata: typing.Optional[google.protobuf.message.Message]) -> None: ...
|
|
428
430
|
def _hydrate_metadata_v2(self) -> None:
|
|
429
431
|
"""Wire up V2 stdio readers that read directly from the worker. Cheap
|
|
430
432
|
to call eagerly: the router connection is opened lazily on first read.
|
|
@@ -1342,6 +1344,7 @@ class Sandbox(modal.object.Object):
|
|
|
1342
1344
|
_attached: bool
|
|
1343
1345
|
_filesystem: typing.Optional[modal.sandbox_fs.SandboxFilesystem]
|
|
1344
1346
|
_is_v2: bool
|
|
1347
|
+
_app_id: typing.Optional[str]
|
|
1345
1348
|
|
|
1346
1349
|
def __init__(self, *args, **kwargs):
|
|
1347
1350
|
"""mdmd:hidden"""
|
|
@@ -1872,7 +1875,8 @@ class Sandbox(modal.object.Object):
|
|
|
1872
1875
|
|
|
1873
1876
|
_experimental_create: typing.ClassVar[___experimental_create_spec]
|
|
1874
1877
|
|
|
1875
|
-
def
|
|
1878
|
+
def _get_metadata(self) -> modal_proto.api_pb2.SandboxHandleMetadata: ...
|
|
1879
|
+
def _hydrate_metadata(self, handle_metadata: typing.Optional[google.protobuf.message.Message]) -> None: ...
|
|
1876
1880
|
def _hydrate_metadata_v2(self) -> None:
|
|
1877
1881
|
"""Wire up V2 stdio readers that read directly from the worker. Cheap
|
|
1878
1882
|
to call eagerly: the router connection is opened lazily on first read.
|
modal/sandbox_fs.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Copyright Modal Labs 2026
|
|
2
2
|
import asyncio
|
|
3
|
+
import io
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
5
6
|
import random
|
|
@@ -7,7 +8,7 @@ import string
|
|
|
7
8
|
import time
|
|
8
9
|
import weakref
|
|
9
10
|
from pathlib import Path
|
|
10
|
-
from typing import TYPE_CHECKING, AsyncIterator, Optional, Union, cast
|
|
11
|
+
from typing import TYPE_CHECKING, AsyncIterator, BinaryIO, Optional, Union, cast
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
import modal.sandbox
|
|
@@ -33,7 +34,6 @@ from ._utils.sandbox_fs_utils import (
|
|
|
33
34
|
validate_absolute_remote_path,
|
|
34
35
|
)
|
|
35
36
|
from .exception import ConflictError
|
|
36
|
-
from .io_streams import TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE
|
|
37
37
|
from .types import FileInfo, FileType, FileWatchEvent, FileWatchEventType
|
|
38
38
|
|
|
39
39
|
_SANDBOX_FS_TOOLS_PATH = "/__modal/.bin/modal-sandbox-fs-tools"
|
|
@@ -111,69 +111,8 @@ class _SandboxFilesystem:
|
|
|
111
111
|
validate_absolute_remote_path(remote_path, "copy_from_local")
|
|
112
112
|
|
|
113
113
|
t0 = time.monotonic()
|
|
114
|
-
total_bytes = 0
|
|
115
114
|
with open(local_path, "rb") as file_obj:
|
|
116
|
-
|
|
117
|
-
process = await self._container.exec(_SANDBOX_FS_TOOLS_PATH, make_write_file_command(remote_path))
|
|
118
|
-
try:
|
|
119
|
-
while True:
|
|
120
|
-
logger.debug(
|
|
121
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): reading "
|
|
122
|
-
f"{TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE} bytes from {local_path} at offset {total_bytes}"
|
|
123
|
-
)
|
|
124
|
-
# TODO(saltzm): If this fails, the ContainerProcess will remain alive indefinitely since
|
|
125
|
-
# stdin will remain open. Unfortunately we can't just call write_eof either, since that
|
|
126
|
-
# would lead to a partially written file being persisted. We should catch exceptions
|
|
127
|
-
# from this and kill the ContainerProcess when we have a way to do this.
|
|
128
|
-
chunk = file_obj.read(TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE)
|
|
129
|
-
if not chunk:
|
|
130
|
-
logger.debug(
|
|
131
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): read no data from "
|
|
132
|
-
f"{local_path}, finished reading file"
|
|
133
|
-
)
|
|
134
|
-
break
|
|
135
|
-
total_bytes += len(chunk)
|
|
136
|
-
logger.debug(
|
|
137
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): writing {len(chunk)} bytes "
|
|
138
|
-
f"from {local_path} to remote {remote_path}"
|
|
139
|
-
)
|
|
140
|
-
process.stdin.write(chunk)
|
|
141
|
-
await process.stdin.drain()
|
|
142
|
-
logger.debug(
|
|
143
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished writing "
|
|
144
|
-
f"{len(chunk)} bytes from {local_path} to remote {remote_path}"
|
|
145
|
-
)
|
|
146
|
-
logger.debug(
|
|
147
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): writing eof to remote {remote_path}"
|
|
148
|
-
)
|
|
149
|
-
process.stdin.write_eof()
|
|
150
|
-
await process.stdin.drain()
|
|
151
|
-
logger.debug(
|
|
152
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished writing eof to remote "
|
|
153
|
-
f"{remote_path}"
|
|
154
|
-
)
|
|
155
|
-
# When the FS tools binary exits early on an error, the worker
|
|
156
|
-
# reports the dropped stdin write as ConflictError.
|
|
157
|
-
except ConflictError:
|
|
158
|
-
pass
|
|
159
|
-
|
|
160
|
-
async def read_stderr():
|
|
161
|
-
stderr = await process.stderr.read()
|
|
162
|
-
logger.debug(f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished reading stderr")
|
|
163
|
-
return stderr
|
|
164
|
-
|
|
165
|
-
async def wait_for_process_completion():
|
|
166
|
-
returncode = await process.wait()
|
|
167
|
-
logger.debug(
|
|
168
|
-
f"sandbox copy_from_local('{local_path}', '{remote_path}'): finished waiting for process "
|
|
169
|
-
"completion"
|
|
170
|
-
)
|
|
171
|
-
return returncode
|
|
172
|
-
|
|
173
|
-
stderr, returncode = await asyncio.gather(read_stderr(), wait_for_process_completion())
|
|
174
|
-
|
|
175
|
-
if returncode != 0:
|
|
176
|
-
raise_write_file_error(returncode, stderr, remote_path)
|
|
115
|
+
total_bytes = await self._exec_fs_tool_write(file_obj, remote_path, "copy_from_local")
|
|
177
116
|
|
|
178
117
|
dur_s = max(time.monotonic() - t0, 0.001)
|
|
179
118
|
_log_throughput(f"copy_from_local('{local_path}', '{remote_path}')", total_bytes, dur_s)
|
|
@@ -638,22 +577,7 @@ class _SandboxFilesystem:
|
|
|
638
577
|
raise TypeError("data must be bytes-like")
|
|
639
578
|
|
|
640
579
|
t0 = time.monotonic()
|
|
641
|
-
|
|
642
|
-
process = await self._container.exec(_SANDBOX_FS_TOOLS_PATH, make_write_file_command(remote_path))
|
|
643
|
-
try:
|
|
644
|
-
for offset in range(0, max(len(data), 1), TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE):
|
|
645
|
-
process.stdin.write(data[offset : offset + TASK_COMMAND_ROUTER_MAX_BUFFER_SIZE])
|
|
646
|
-
await process.stdin.drain()
|
|
647
|
-
process.stdin.write_eof()
|
|
648
|
-
await process.stdin.drain()
|
|
649
|
-
# When the FS tools binary exits early on an error, the worker
|
|
650
|
-
# reports the dropped stdin write as ConflictError.
|
|
651
|
-
except ConflictError:
|
|
652
|
-
pass
|
|
653
|
-
stderr, returncode = await asyncio.gather(process.stderr.read(), process.wait())
|
|
654
|
-
|
|
655
|
-
if returncode != 0:
|
|
656
|
-
raise_write_file_error(returncode, stderr, remote_path)
|
|
580
|
+
await self._exec_fs_tool_write(io.BytesIO(data), remote_path, "write_bytes")
|
|
657
581
|
|
|
658
582
|
dur_s = max(time.monotonic() - t0, 0.001)
|
|
659
583
|
_log_throughput(f"write_bytes {remote_path}", len(data), dur_s)
|
|
@@ -686,5 +610,33 @@ class _SandboxFilesystem:
|
|
|
686
610
|
raise TypeError("data must be str")
|
|
687
611
|
await self.write_bytes(data.encode("utf-8"), remote_path)
|
|
688
612
|
|
|
613
|
+
async def _exec_fs_tool_write(self, source: BinaryIO, remote_path: str, op_name: str) -> int:
|
|
614
|
+
"""Exec the FS-tools write command and stream `source` into its stdin.
|
|
615
|
+
|
|
616
|
+
Returns the number of bytes streamed.
|
|
617
|
+
"""
|
|
618
|
+
with translate_exec_errors(op_name, remote_path):
|
|
619
|
+
process = await self._container.exec(_SANDBOX_FS_TOOLS_PATH, make_write_file_command(remote_path))
|
|
620
|
+
# TODO(saltzm): If streaming fails after resume attempts are exhausted, the
|
|
621
|
+
# ContainerProcess will remain alive indefinitely since stdin will remain open.
|
|
622
|
+
# We should catch exceptions from this and kill the ContainerProcess when we
|
|
623
|
+
# have a way to do this.
|
|
624
|
+
try:
|
|
625
|
+
total_bytes = await process._stdin_write_stream(source)
|
|
626
|
+
# When the FS tools binary exits early on an error, the worker
|
|
627
|
+
# reports the dropped stdin write as ConflictError.
|
|
628
|
+
except ConflictError:
|
|
629
|
+
# ConflictError can come from a failure in fs-tools or server-side.
|
|
630
|
+
# - if server-side, the process won't exit, so the gather below would hang forever -> raise
|
|
631
|
+
# - else if fs-tools, process will be closed, use raise_write_file_error below
|
|
632
|
+
if await process.poll() is None:
|
|
633
|
+
raise
|
|
634
|
+
total_bytes = source.tell()
|
|
635
|
+
stderr, returncode = await asyncio.gather(process.stderr.read(), process.wait())
|
|
636
|
+
|
|
637
|
+
if returncode != 0:
|
|
638
|
+
raise_write_file_error(returncode, stderr, remote_path)
|
|
639
|
+
return total_bytes
|
|
640
|
+
|
|
689
641
|
|
|
690
642
|
SandboxFilesystem = synchronize_api(_SandboxFilesystem)
|
modal/sandbox_fs.pyi
CHANGED
|
@@ -348,6 +348,13 @@ class _SandboxFilesystem:
|
|
|
348
348
|
"""
|
|
349
349
|
...
|
|
350
350
|
|
|
351
|
+
async def _exec_fs_tool_write(self, source: typing.BinaryIO, remote_path: str, op_name: str) -> int:
|
|
352
|
+
"""Exec the FS-tools write command and stream `source` into its stdin.
|
|
353
|
+
|
|
354
|
+
Returns the number of bytes streamed.
|
|
355
|
+
"""
|
|
356
|
+
...
|
|
357
|
+
|
|
351
358
|
class SandboxFilesystem:
|
|
352
359
|
"""mdmd:namespace
|
|
353
360
|
Namespace for Sandbox filesystem APIs.
|
|
@@ -1041,3 +1048,20 @@ class SandboxFilesystem:
|
|
|
1041
1048
|
...
|
|
1042
1049
|
|
|
1043
1050
|
write_text: __write_text_spec
|
|
1051
|
+
|
|
1052
|
+
class ___exec_fs_tool_write_spec(typing_extensions.Protocol):
|
|
1053
|
+
def __call__(self, /, source: typing.BinaryIO, remote_path: str, op_name: str) -> int:
|
|
1054
|
+
"""Exec the FS-tools write command and stream `source` into its stdin.
|
|
1055
|
+
|
|
1056
|
+
Returns the number of bytes streamed.
|
|
1057
|
+
"""
|
|
1058
|
+
...
|
|
1059
|
+
|
|
1060
|
+
async def aio(self, /, source: typing.BinaryIO, remote_path: str, op_name: str) -> int:
|
|
1061
|
+
"""Exec the FS-tools write command and stream `source` into its stdin.
|
|
1062
|
+
|
|
1063
|
+
Returns the number of bytes streamed.
|
|
1064
|
+
"""
|
|
1065
|
+
...
|
|
1066
|
+
|
|
1067
|
+
_exec_fs_tool_write: ___exec_fs_tool_write_spec
|
|
@@ -6,7 +6,7 @@ modal/_clustered_functions.pyi,sha256=akbff8aXkp5aJapn72YMXPnzeYa6OFAfuakcy7Jx6w
|
|
|
6
6
|
modal/_container_entrypoint.py,sha256=AyhMFBiaApVn1mueur2yrVy7IXR9qdXdwY4IePtVv74,22760
|
|
7
7
|
modal/_environments.py,sha256=mD5pmecDSzZNU26jtfhuGRNPAMNXlTk-UX4U4h5TEDM,20253
|
|
8
8
|
modal/_function_variants.py,sha256=ixkwW9naMMBzRB-92BZd5k3iVWtOoYukveT52hLDIbc,12549
|
|
9
|
-
modal/_functions.py,sha256=
|
|
9
|
+
modal/_functions.py,sha256=XE_4HPrYbHaxhSsMoF25Y8z8Snb0ANu-j9yWnEllvwg,95010
|
|
10
10
|
modal/_grpc_client.py,sha256=QL5btzOz6zZzTDcgSwFjI3PBy2gkaycyrlP7Jw-jkbY,6962
|
|
11
11
|
modal/_image.py,sha256=TW-0QiFa3Lo1w9khZcE8mgSHQgj-PJ0eChb8IkS2eqo,128738
|
|
12
12
|
modal/_ipython.py,sha256=Bu9fc3hm3xt2O47qLe_d-Mu2jS81LwMsmXNJ1sawBA0,575
|
|
@@ -31,14 +31,14 @@ modal/billing.py,sha256=E1-P_P8bJFLp-bTaggYd4BaaViImPB5UdtXiY8VeAZQ,390
|
|
|
31
31
|
modal/billing.pyi,sha256=4m7VLYNS8IiJ2I3CJx0awvrOM6BILWRVLfdeqtAYr8M,7189
|
|
32
32
|
modal/call_graph.py,sha256=XGALIDn1bLchWo_goyX23bcPjzCZcfqKUo8INIfDapo,1775
|
|
33
33
|
modal/client.py,sha256=9FGuDQpF0fvg45ylf-WLzxkF5U-CGvjHcH5n_y4jgSQ,16366
|
|
34
|
-
modal/client.pyi,sha256=
|
|
34
|
+
modal/client.pyi,sha256=FXPShD0ZtT9JmV6pfGrV4x9By0Mg4OsPKIJFS_qlolg,17997
|
|
35
35
|
modal/cloud_bucket_mount.py,sha256=03gECcV9l2Lnt9k23YrDOTGFW7ajfXMW5O8b1prhtwQ,6254
|
|
36
36
|
modal/cloud_bucket_mount.pyi,sha256=lMOb44pbNE9kDVEqWfcmvpsnh1sdNNFzuKoVyZZ3d5M,8058
|
|
37
37
|
modal/cls.py,sha256=lNsIz0rm5pbayfrODwb3MVGOqNnhosWJQTFpVLfjJvk,40981
|
|
38
38
|
modal/cls.pyi,sha256=FzYY7OkxuoqTZ-196Nsp6iC4yLud-Rq4qui_x7jxNzc,29421
|
|
39
39
|
modal/config.py,sha256=iklz4iNM9ozr9-ubz-kmVZ6r7gsyyrXwoaG6GDQgsIs,16026
|
|
40
|
-
modal/container_process.py,sha256=
|
|
41
|
-
modal/container_process.pyi,sha256
|
|
40
|
+
modal/container_process.py,sha256=gy7Dwh4kYtKj-9J4RHMowDFf5Gd4bfK1PV8Y23lpBl8,8822
|
|
41
|
+
modal/container_process.pyi,sha256=lzMigs10shDgQ3kt-1ajkMFk-JJMHiD7dBJ3Lec7yNw,5489
|
|
42
42
|
modal/dict.py,sha256=LILESg1hALZHwlW7uslbUyxc2mpgtNisEmk8xl-559Y,23953
|
|
43
43
|
modal/dict.pyi,sha256=SPafrdRoStNJFt3YtO2H6PucXfbP-rDPB9zAWgY91sY,38253
|
|
44
44
|
modal/environments.py,sha256=CMOYbsx3fg9M8-SSPOjOUGqay8r1zewWtbwp9XnEUNs,1486
|
|
@@ -48,7 +48,7 @@ modal/file_io.py,sha256=GYTa67mrmbTTTBsYQnb51VmwGIo0YY-1qQ3YBhagkmA,21795
|
|
|
48
48
|
modal/file_io.pyi,sha256=3181-DS07flFWLf2BKjqCOiWIjieEOolf33wx6fBp5c,15377
|
|
49
49
|
modal/file_pattern_matcher.py,sha256=6g_awxudMy_ontBvkznybyeNnwGMj297YrmIHCbREI0,8132
|
|
50
50
|
modal/functions.py,sha256=EZRcHFfnoYbvawUJnAHF5nIvoo9GX2Djy7mUmOCXmXc,258
|
|
51
|
-
modal/functions.pyi,sha256=
|
|
51
|
+
modal/functions.pyi,sha256=R-vSYjEU_jnu5bVUHtpPHNEDlbM6ccpI2i6ydEogwLU,47942
|
|
52
52
|
modal/image.py,sha256=c0lUSAoU-Ip3g5DGZ0uhdCzoWSQWh1g0i-7Rxo-Sz-I,160
|
|
53
53
|
modal/image.pyi,sha256=Whbfmda2YLAyzctLEF9uF4FQ5b1canwLojSksRJ4gx8,59716
|
|
54
54
|
modal/io_streams.py,sha256=3fhhb7SDJ69pVUu9J3PiMLBePD9bpiIxPN05SCzf8FE,29118
|
|
@@ -73,10 +73,10 @@ modal/retries.py,sha256=tSGB5bGM5xZZUDXMkuelIEoQJY0vOukOssZhwc6tXAo,5014
|
|
|
73
73
|
modal/runner.py,sha256=YdBqWOWkcGgSDKLEz1zrGNHrh0-d2tSNx24XPB6QyoM,30284
|
|
74
74
|
modal/runner.pyi,sha256=0hIfkl-7VTyw1n-1B0AV7f53t3i3hCzRPyBu3DUyHnk,9669
|
|
75
75
|
modal/running_app.py,sha256=xSR1VVlhxEFytmjx2x697_0GpD25Vc9crhp6r117_QY,1145
|
|
76
|
-
modal/sandbox.py,sha256=
|
|
77
|
-
modal/sandbox.pyi,sha256=
|
|
78
|
-
modal/sandbox_fs.py,sha256=
|
|
79
|
-
modal/sandbox_fs.pyi,sha256=
|
|
76
|
+
modal/sandbox.py,sha256=zyBUCRqwQCa34nxxBYZI8OTp1OdJHfba0tmmuohk1vg,116648
|
|
77
|
+
modal/sandbox.pyi,sha256=vMAWDXLRAt-FnAnDdTAexBfxU9qZVG9p2iIuaGPX_1c,138637
|
|
78
|
+
modal/sandbox_fs.py,sha256=vA-L-OCFR3QMqof8db6d_qw0VaEaac3mBtZrfl00Jts,27045
|
|
79
|
+
modal/sandbox_fs.pyi,sha256=5_h1UAKD5UXDVeP4x6_UyLQLD92O9_Mm0d1dH6JXU4A,45381
|
|
80
80
|
modal/schedule.py,sha256=3F15Vh4g8C34jGcO-vgFkBHZl7XvP7qieXayQ9gKsyw,3248
|
|
81
81
|
modal/scheduler_placement.py,sha256=S2-MZQcWCt6NVwIWXUP11KsKZeRDQViJLlOngGUvjkM,1173
|
|
82
82
|
modal/secret.py,sha256=tHBzXOFALjIshuUfz6htAB6PKoTMsZeml4Y_X1ED2ic,20215
|
|
@@ -138,7 +138,7 @@ modal/_utils/proxy_support.py,sha256=hLA_XojYxzPysOiLqyUbB-SgMLmn_X8WtXTk92xh7g4
|
|
|
138
138
|
modal/_utils/rand_pb_testing.py,sha256=kwAKIros_g6XPXluiJAktQ79YPkD9SGopKGl83GQ1l0,4361
|
|
139
139
|
modal/_utils/sandbox_fs_utils.py,sha256=rsfo1uDbVSoiY21XERDzsmp-DALcNmlPfJ6jn_2p5T0,15680
|
|
140
140
|
modal/_utils/shell_utils.py,sha256=_2yHrlx4Crx5JWQXECLPQ51kclu8H-U3N7jOkBN7q5U,3390
|
|
141
|
-
modal/_utils/task_command_router_client.py,sha256=
|
|
141
|
+
modal/_utils/task_command_router_client.py,sha256=BElu8TCO6tTYJqT1pwqY4TXOerj3T-TsRv8Z60Kn3jI,43983
|
|
142
142
|
modal/_utils/time_utils.py,sha256=3T2RkXRpOFwBiQ5fTZCPkObaeemLXNWW9UyDhG8qsv4,9042
|
|
143
143
|
modal/_vendor/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
|
|
144
144
|
modal/_vendor/a2wsgi_wsgi.py,sha256=RVLIYVJA8a8o96QxEXYT5tuog2zev2S24XOuAVTXZAs,21872
|
|
@@ -194,16 +194,16 @@ modal/experimental/flash.py,sha256=GAJ1c-EGL8M3_loAkqr5gdUfySMHY3U_oc6RywfZ8-0,3
|
|
|
194
194
|
modal/experimental/flash.pyi,sha256=lpTqJFZRMGWscw0xpYZVdBMnAYjj16asXE4_64A6itQ,17774
|
|
195
195
|
modal/experimental/ipython.py,sha256=kJ6o9wkAY_MiSb7paewI7f1MJAeqdi8TX5pBdhr_mUI,2925
|
|
196
196
|
modal/skills/modal/SKILL.md,sha256=WzLjYJ1utjc_ZOf6qZTPwB6wxxwRYB5PibdvIf_FLy0,5316
|
|
197
|
-
modal-1.5.3.
|
|
197
|
+
modal-1.5.3.dev6.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
|
|
198
198
|
modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
|
|
199
199
|
modal_docs/gen_cli_docs.py,sha256=9-b8vl2EsLWD9zJqpCK8IUi5h44Bjn3i3rSdedZV4bA,6415
|
|
200
200
|
modal_docs/gen_cli_docs_main.py,sha256=UPGr7O1DYTIRLwfINe2rbdh89J3cPVTU1OWL-SjxnOQ,198
|
|
201
|
-
modal_docs/gen_reference_docs.py,sha256=
|
|
201
|
+
modal_docs/gen_reference_docs.py,sha256=HRTjnzPzmGkYjL8tumqbiBlEXqclTMHD11t8LjpgF-Q,13684
|
|
202
202
|
modal_docs/gen_reference_docs_main.py,sha256=56Cr3YKgq4qgb01bEcumTo1Px87boGGOFeHQ0Wcjg74,210
|
|
203
203
|
modal_docs/mdmd/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
|
|
204
|
-
modal_docs/mdmd/mdmd.py,sha256=
|
|
204
|
+
modal_docs/mdmd/mdmd.py,sha256=yo0-5zYSy4PEiK6X-otdpgvaE_qvSKQ_zVRT2FSKf5E,24989
|
|
205
205
|
modal_docs/mdmd/signatures.py,sha256=OASroKle763Kfumn4Ek4PABJXEGP56JMCTug44QoBx4,11327
|
|
206
|
-
modal_docs/mdmd/types.py,sha256=
|
|
206
|
+
modal_docs/mdmd/types.py,sha256=tv7fvziPr-MeDzOp4e2pK5h3d1DXTUOO74rPqDoBn6g,736
|
|
207
207
|
modal_proto/__init__.py,sha256=tFdSv9eaHBvYcwhgq-XqIlYDNDw57clrvO91Fb-oUV4,130
|
|
208
208
|
modal_proto/api_grpc.py,sha256=q_AsVyWC73bpEsWJEISHhrukmb1WgDAy_euZcekIpsk,180684
|
|
209
209
|
modal_proto/api_pb2.py,sha256=UxZIkyXj4UB3x96KBFamvhoKDmeC-c6xTXA_UlPeQvA,491803
|
|
@@ -217,10 +217,10 @@ modal_proto/task_command_router_pb2.py,sha256=gWtT_LkHjPpt5WoG0WYP5jBLgJOtkJ3bfU
|
|
|
217
217
|
modal_proto/task_command_router_pb2.pyi,sha256=q9_C58oT0j9gJor3Tkm7SW8dbZEmgZvMBUh5QNAq-u4,45132
|
|
218
218
|
modal_proto/task_command_router_pb2_grpc.py,sha256=zuFkJgKcswB2ftjnyEgs70dp61VMA7gK0h0uzR3gYFw,42563
|
|
219
219
|
modal_proto/task_command_router_pb2_grpc.pyi,sha256=UjAJFqz9DpOFXP04fVm5lB2eAyTtjOg3oKjfWei8yLU,14262
|
|
220
|
-
modal_version/__init__.py,sha256=
|
|
220
|
+
modal_version/__init__.py,sha256=JL9s9bbDS3RB-1ET4OdzoflhuqWVHtjNRR0z-v8jSBU,120
|
|
221
221
|
modal_version/__main__.py,sha256=nLUbubxKx7GXNzJIkTB7M7_u8080LyLM0IS0rzFkC_c,119
|
|
222
|
-
modal-1.5.3.
|
|
223
|
-
modal-1.5.3.
|
|
224
|
-
modal-1.5.3.
|
|
225
|
-
modal-1.5.3.
|
|
226
|
-
modal-1.5.3.
|
|
222
|
+
modal-1.5.3.dev6.dist-info/METADATA,sha256=tkxpaVyoGJQNIAoF7Nofg1KPuBgmOLOIB2unkyPyf2w,2512
|
|
223
|
+
modal-1.5.3.dev6.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
|
|
224
|
+
modal-1.5.3.dev6.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
|
|
225
|
+
modal-1.5.3.dev6.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
|
|
226
|
+
modal-1.5.3.dev6.dist-info/RECORD,,
|
modal_docs/gen_reference_docs.py
CHANGED
|
@@ -254,7 +254,6 @@ def run(output_dir: str | None = None):
|
|
|
254
254
|
# Standalone module reference pages, sorted into the sidebar with top-level `modal.X` entries.
|
|
255
255
|
module_docs = [
|
|
256
256
|
("modal.billing", "modal.billing"),
|
|
257
|
-
("modal.call_graph", "modal.call_graph"),
|
|
258
257
|
("modal.config", "modal.config"),
|
|
259
258
|
("modal.container_process", "modal.container_process"),
|
|
260
259
|
("modal.exception", "modal.exception"),
|
modal_docs/mdmd/mdmd.py
CHANGED
|
@@ -109,14 +109,15 @@ def parse_docstring(name: str, signature: str, docstring: str) -> ParsedDoc:
|
|
|
109
109
|
return "\n".join(normalized_lines).strip()
|
|
110
110
|
|
|
111
111
|
args_section, args_range = extract_section(["Args:"])
|
|
112
|
-
returns_section, returns_range = extract_section(["Returns:"
|
|
112
|
+
returns_section, returns_range = extract_section(["Returns:"])
|
|
113
|
+
yields_section, yields_range = extract_section(["Yields:"])
|
|
113
114
|
raises_section, raises_range = extract_section(["Raises:"])
|
|
114
115
|
examples_section, examples_range = extract_section(["Examples:", "Example:"])
|
|
115
116
|
see_also_section, see_also_range = extract_section(["See Also:", "See also:"])
|
|
116
117
|
|
|
117
118
|
section_ranges = [
|
|
118
119
|
section_range
|
|
119
|
-
for section_range in [args_range, returns_range, raises_range, examples_range, see_also_range]
|
|
120
|
+
for section_range in [args_range, returns_range, yields_range, raises_range, examples_range, see_also_range]
|
|
120
121
|
if section_range
|
|
121
122
|
]
|
|
122
123
|
|
|
@@ -240,6 +241,7 @@ def parse_docstring(name: str, signature: str, docstring: str) -> ParsedDoc:
|
|
|
240
241
|
description=description,
|
|
241
242
|
params=params,
|
|
242
243
|
returns=section_body_without_header(returns_section),
|
|
244
|
+
yields=section_body_without_header(yields_section),
|
|
243
245
|
raises=parse_raises(raises_section),
|
|
244
246
|
examples=section_body_without_header(examples_section),
|
|
245
247
|
see_also=section_body_without_header(see_also_section),
|
|
@@ -266,6 +268,11 @@ def _markdown_body_from_parsed_doc(parsed: ParsedDoc) -> str:
|
|
|
266
268
|
output.append(_parameter_tag(param))
|
|
267
269
|
output.append("")
|
|
268
270
|
|
|
271
|
+
if parsed.yields:
|
|
272
|
+
output.append("**Yields**\n")
|
|
273
|
+
output.append(parsed.yields)
|
|
274
|
+
output.append("")
|
|
275
|
+
|
|
269
276
|
if parsed.returns:
|
|
270
277
|
output.append("**Returns**\n")
|
|
271
278
|
output.append(parsed.returns)
|
modal_docs/mdmd/types.py
CHANGED
modal_version/__init__.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|