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/io_streams.pyi
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import collections.abc
|
|
2
|
+
import modal._utils.task_command_router_client
|
|
2
3
|
import modal.client
|
|
3
4
|
import modal.stream_type
|
|
4
5
|
import typing
|
|
@@ -17,6 +18,165 @@ def _container_process_logs_iterator(
|
|
|
17
18
|
|
|
18
19
|
T = typing.TypeVar("T")
|
|
19
20
|
|
|
21
|
+
class _StreamReaderThroughServer(typing.Generic[T]):
|
|
22
|
+
"""A StreamReader implementation that reads from the server."""
|
|
23
|
+
|
|
24
|
+
_stream: typing.Optional[collections.abc.AsyncGenerator[T, None]]
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
file_descriptor: int,
|
|
29
|
+
object_id: str,
|
|
30
|
+
object_type: typing.Literal["sandbox", "container_process"],
|
|
31
|
+
client: modal.client._Client,
|
|
32
|
+
stream_type: modal.stream_type.StreamType = modal.stream_type.StreamType.PIPE,
|
|
33
|
+
text: bool = True,
|
|
34
|
+
by_line: bool = False,
|
|
35
|
+
deadline: typing.Optional[float] = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""mdmd:hidden"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def file_descriptor(self) -> int:
|
|
42
|
+
"""Possible values are `1` for stdout and `2` for stderr."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
async def read(self) -> T:
|
|
46
|
+
"""Fetch the entire contents of the stream until EOF."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
async def _consume_container_process_stream(self):
|
|
50
|
+
"""Consume the container process stream and store messages in the buffer."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def _stream_container_process(self) -> collections.abc.AsyncGenerator[tuple[typing.Optional[bytes], str], None]:
|
|
54
|
+
"""Streams the container process buffer to the reader."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def _get_logs(self, skip_empty_messages: bool = True) -> collections.abc.AsyncGenerator[bytes, None]:
|
|
58
|
+
"""Streams sandbox or process logs from the server to the reader.
|
|
59
|
+
|
|
60
|
+
Logs returned by this method may contain partial or multiple lines at a time.
|
|
61
|
+
|
|
62
|
+
When the stream receives an EOF, it yields None. Once an EOF is received,
|
|
63
|
+
subsequent invocations will not yield logs.
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def _get_logs_by_line(self) -> collections.abc.AsyncGenerator[bytes, None]:
|
|
68
|
+
"""Process logs from the server and yield complete lines only."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
def _ensure_stream(self) -> collections.abc.AsyncGenerator[T, None]: ...
|
|
72
|
+
async def __anext__(self) -> T:
|
|
73
|
+
"""mdmd:hidden"""
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
async def aclose(self):
|
|
77
|
+
"""mdmd:hidden"""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
def _decode_bytes_stream_to_str(
|
|
81
|
+
stream: collections.abc.AsyncGenerator[bytes, None],
|
|
82
|
+
) -> collections.abc.AsyncGenerator[str, None]:
|
|
83
|
+
"""Incrementally decode a bytes async generator as UTF-8 without breaking on chunk boundaries.
|
|
84
|
+
|
|
85
|
+
This function uses a streaming UTF-8 decoder so that multi-byte characters split across
|
|
86
|
+
chunks are handled correctly instead of raising ``UnicodeDecodeError``.
|
|
87
|
+
"""
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
def _stream_by_line(stream: collections.abc.AsyncGenerator[bytes, None]) -> collections.abc.AsyncGenerator[bytes, None]:
|
|
91
|
+
"""Yield complete lines only (ending with
|
|
92
|
+
), buffering partial lines until complete.
|
|
93
|
+
"""
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
class _StreamReaderThroughCommandRouterParams:
|
|
97
|
+
"""_StreamReaderThroughCommandRouterParams(file_descriptor: 'api_pb2.FileDescriptor.ValueType', task_id: str, object_id: str, command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient, deadline: Optional[float])"""
|
|
98
|
+
|
|
99
|
+
file_descriptor: int
|
|
100
|
+
task_id: str
|
|
101
|
+
object_id: str
|
|
102
|
+
command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient
|
|
103
|
+
deadline: typing.Optional[float]
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
file_descriptor: int,
|
|
108
|
+
task_id: str,
|
|
109
|
+
object_id: str,
|
|
110
|
+
command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient,
|
|
111
|
+
deadline: typing.Optional[float],
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
114
|
+
...
|
|
115
|
+
|
|
116
|
+
def __repr__(self):
|
|
117
|
+
"""Return repr(self)."""
|
|
118
|
+
...
|
|
119
|
+
|
|
120
|
+
def __eq__(self, other):
|
|
121
|
+
"""Return self==value."""
|
|
122
|
+
...
|
|
123
|
+
|
|
124
|
+
def _stdio_stream_from_command_router(
|
|
125
|
+
params: _StreamReaderThroughCommandRouterParams,
|
|
126
|
+
) -> collections.abc.AsyncGenerator[bytes, None]:
|
|
127
|
+
"""Stream raw bytes from the router client."""
|
|
128
|
+
...
|
|
129
|
+
|
|
130
|
+
class _BytesStreamReaderThroughCommandRouter(typing.Generic[T]):
|
|
131
|
+
"""StreamReader implementation that will read directly from the worker that
|
|
132
|
+
hosts the sandbox.
|
|
133
|
+
|
|
134
|
+
This implementation is used for non-text streams.
|
|
135
|
+
"""
|
|
136
|
+
def __init__(self, params: _StreamReaderThroughCommandRouterParams) -> None:
|
|
137
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
138
|
+
...
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def file_descriptor(self) -> int: ...
|
|
142
|
+
async def read(self) -> T: ...
|
|
143
|
+
def __aiter__(self) -> collections.abc.AsyncIterator[T]: ...
|
|
144
|
+
async def __anext__(self) -> T: ...
|
|
145
|
+
async def aclose(self): ...
|
|
146
|
+
|
|
147
|
+
class _TextStreamReaderThroughCommandRouter(typing.Generic[T]):
|
|
148
|
+
"""StreamReader implementation that will read directly from the worker
|
|
149
|
+
that hosts the sandbox.
|
|
150
|
+
|
|
151
|
+
This implementation is used for text streams.
|
|
152
|
+
"""
|
|
153
|
+
def __init__(self, params: _StreamReaderThroughCommandRouterParams, by_line: bool) -> None:
|
|
154
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def file_descriptor(self) -> int: ...
|
|
159
|
+
async def read(self) -> T: ...
|
|
160
|
+
def __aiter__(self) -> collections.abc.AsyncIterator[T]: ...
|
|
161
|
+
async def __anext__(self) -> T: ...
|
|
162
|
+
async def aclose(self): ...
|
|
163
|
+
|
|
164
|
+
class _DevnullStreamReader(typing.Generic[T]):
|
|
165
|
+
"""StreamReader implementation for a stream configured with
|
|
166
|
+
StreamType.DEVNULL. Throws an error if read or any other method is
|
|
167
|
+
called.
|
|
168
|
+
"""
|
|
169
|
+
def __init__(self, file_descriptor: int) -> None:
|
|
170
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
171
|
+
...
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def file_descriptor(self) -> int: ...
|
|
175
|
+
async def read(self) -> T: ...
|
|
176
|
+
def __aiter__(self) -> collections.abc.AsyncIterator[T]: ...
|
|
177
|
+
async def __anext__(self) -> T: ...
|
|
178
|
+
async def aclose(self): ...
|
|
179
|
+
|
|
20
180
|
class _StreamReader(typing.Generic[T]):
|
|
21
181
|
"""Retrieve logs from a stream (`stdout` or `stderr`).
|
|
22
182
|
|
|
@@ -38,9 +198,6 @@ class _StreamReader(typing.Generic[T]):
|
|
|
38
198
|
print(f"Message: {message}")
|
|
39
199
|
```
|
|
40
200
|
"""
|
|
41
|
-
|
|
42
|
-
_stream: typing.Optional[collections.abc.AsyncGenerator[typing.Optional[bytes], None]]
|
|
43
|
-
|
|
44
201
|
def __init__(
|
|
45
202
|
self,
|
|
46
203
|
file_descriptor: int,
|
|
@@ -51,6 +208,8 @@ class _StreamReader(typing.Generic[T]):
|
|
|
51
208
|
text: bool = True,
|
|
52
209
|
by_line: bool = False,
|
|
53
210
|
deadline: typing.Optional[float] = None,
|
|
211
|
+
command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient] = None,
|
|
212
|
+
task_id: typing.Optional[str] = None,
|
|
54
213
|
) -> None:
|
|
55
214
|
"""mdmd:hidden"""
|
|
56
215
|
...
|
|
@@ -76,52 +235,79 @@ class _StreamReader(typing.Generic[T]):
|
|
|
76
235
|
"""
|
|
77
236
|
...
|
|
78
237
|
|
|
79
|
-
|
|
80
|
-
"""
|
|
238
|
+
def __aiter__(self) -> collections.abc.AsyncIterator[T]:
|
|
239
|
+
"""mdmd:hidden"""
|
|
81
240
|
...
|
|
82
241
|
|
|
83
|
-
def
|
|
84
|
-
"""
|
|
242
|
+
async def __anext__(self) -> T:
|
|
243
|
+
"""mdmd:hidden"""
|
|
85
244
|
...
|
|
86
245
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"""Streams sandbox or process logs from the server to the reader.
|
|
246
|
+
async def aclose(self):
|
|
247
|
+
"""mdmd:hidden"""
|
|
248
|
+
...
|
|
91
249
|
|
|
92
|
-
|
|
250
|
+
class _StreamWriterThroughServer:
|
|
251
|
+
"""Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
|
|
252
|
+
def __init__(
|
|
253
|
+
self, object_id: str, object_type: typing.Literal["sandbox", "container_process"], client: modal.client._Client
|
|
254
|
+
) -> None:
|
|
255
|
+
"""mdmd:hidden"""
|
|
256
|
+
...
|
|
93
257
|
|
|
94
|
-
|
|
95
|
-
|
|
258
|
+
def _get_next_index(self) -> int: ...
|
|
259
|
+
def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None:
|
|
260
|
+
"""Write data to the stream but does not send it immediately.
|
|
261
|
+
|
|
262
|
+
This is non-blocking and queues the data to an internal buffer. Must be
|
|
263
|
+
used along with the `drain()` method, which flushes the buffer.
|
|
96
264
|
"""
|
|
97
265
|
...
|
|
98
266
|
|
|
99
|
-
def
|
|
100
|
-
"""
|
|
101
|
-
...
|
|
267
|
+
def write_eof(self) -> None:
|
|
268
|
+
"""Close the write end of the stream after the buffered data is drained.
|
|
102
269
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
270
|
+
If the process was blocked on input, it will become unblocked after
|
|
271
|
+
`write_eof()`. This method needs to be used along with the `drain()`
|
|
272
|
+
method, which flushes the EOF to the process.
|
|
273
|
+
"""
|
|
106
274
|
...
|
|
107
275
|
|
|
108
|
-
async def
|
|
109
|
-
"""
|
|
276
|
+
async def drain(self) -> None:
|
|
277
|
+
"""Flush the write buffer and send data to the running process.
|
|
278
|
+
|
|
279
|
+
This is a flow control method that blocks until data is sent. It returns
|
|
280
|
+
when it is appropriate to continue writing data to the stream.
|
|
281
|
+
"""
|
|
110
282
|
...
|
|
111
283
|
|
|
112
|
-
|
|
113
|
-
|
|
284
|
+
class _StreamWriterThroughCommandRouter:
|
|
285
|
+
def __init__(
|
|
286
|
+
self,
|
|
287
|
+
object_id: str,
|
|
288
|
+
command_router_client: modal._utils.task_command_router_client.TaskCommandRouterClient,
|
|
289
|
+
task_id: str,
|
|
290
|
+
) -> None:
|
|
291
|
+
"""Initialize self. See help(type(self)) for accurate signature."""
|
|
114
292
|
...
|
|
115
293
|
|
|
294
|
+
def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None: ...
|
|
295
|
+
def write_eof(self) -> None: ...
|
|
296
|
+
async def drain(self) -> None: ...
|
|
297
|
+
|
|
116
298
|
class _StreamWriter:
|
|
117
299
|
"""Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
|
|
118
300
|
def __init__(
|
|
119
|
-
self,
|
|
301
|
+
self,
|
|
302
|
+
object_id: str,
|
|
303
|
+
object_type: typing.Literal["sandbox", "container_process"],
|
|
304
|
+
client: modal.client._Client,
|
|
305
|
+
command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient] = None,
|
|
306
|
+
task_id: typing.Optional[str] = None,
|
|
120
307
|
) -> None:
|
|
121
308
|
"""mdmd:hidden"""
|
|
122
309
|
...
|
|
123
310
|
|
|
124
|
-
def _get_next_index(self) -> int: ...
|
|
125
311
|
def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None:
|
|
126
312
|
"""Write data to the stream but does not send it immediately.
|
|
127
313
|
|
|
@@ -204,9 +390,6 @@ class StreamReader(typing.Generic[T]):
|
|
|
204
390
|
print(f"Message: {message}")
|
|
205
391
|
```
|
|
206
392
|
"""
|
|
207
|
-
|
|
208
|
-
_stream: typing.Optional[collections.abc.AsyncGenerator[typing.Optional[bytes], None]]
|
|
209
|
-
|
|
210
393
|
def __init__(
|
|
211
394
|
self,
|
|
212
395
|
file_descriptor: int,
|
|
@@ -217,6 +400,8 @@ class StreamReader(typing.Generic[T]):
|
|
|
217
400
|
text: bool = True,
|
|
218
401
|
by_line: bool = False,
|
|
219
402
|
deadline: typing.Optional[float] = None,
|
|
403
|
+
command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient] = None,
|
|
404
|
+
task_id: typing.Optional[str] = None,
|
|
220
405
|
) -> None:
|
|
221
406
|
"""mdmd:hidden"""
|
|
222
407
|
...
|
|
@@ -261,70 +446,6 @@ class StreamReader(typing.Generic[T]):
|
|
|
261
446
|
|
|
262
447
|
read: __read_spec[T, typing_extensions.Self]
|
|
263
448
|
|
|
264
|
-
class ___consume_container_process_stream_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
265
|
-
def __call__(self, /):
|
|
266
|
-
"""Consume the container process stream and store messages in the buffer."""
|
|
267
|
-
...
|
|
268
|
-
|
|
269
|
-
async def aio(self, /):
|
|
270
|
-
"""Consume the container process stream and store messages in the buffer."""
|
|
271
|
-
...
|
|
272
|
-
|
|
273
|
-
_consume_container_process_stream: ___consume_container_process_stream_spec[typing_extensions.Self]
|
|
274
|
-
|
|
275
|
-
class ___stream_container_process_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
276
|
-
def __call__(self, /) -> typing.Generator[tuple[typing.Optional[bytes], str], None, None]:
|
|
277
|
-
"""Streams the container process buffer to the reader."""
|
|
278
|
-
...
|
|
279
|
-
|
|
280
|
-
def aio(self, /) -> collections.abc.AsyncGenerator[tuple[typing.Optional[bytes], str], None]:
|
|
281
|
-
"""Streams the container process buffer to the reader."""
|
|
282
|
-
...
|
|
283
|
-
|
|
284
|
-
_stream_container_process: ___stream_container_process_spec[typing_extensions.Self]
|
|
285
|
-
|
|
286
|
-
class ___get_logs_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
287
|
-
def __call__(self, /, skip_empty_messages: bool = True) -> typing.Generator[typing.Optional[bytes], None, None]:
|
|
288
|
-
"""Streams sandbox or process logs from the server to the reader.
|
|
289
|
-
|
|
290
|
-
Logs returned by this method may contain partial or multiple lines at a time.
|
|
291
|
-
|
|
292
|
-
When the stream receives an EOF, it yields None. Once an EOF is received,
|
|
293
|
-
subsequent invocations will not yield logs.
|
|
294
|
-
"""
|
|
295
|
-
...
|
|
296
|
-
|
|
297
|
-
def aio(
|
|
298
|
-
self, /, skip_empty_messages: bool = True
|
|
299
|
-
) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]:
|
|
300
|
-
"""Streams sandbox or process logs from the server to the reader.
|
|
301
|
-
|
|
302
|
-
Logs returned by this method may contain partial or multiple lines at a time.
|
|
303
|
-
|
|
304
|
-
When the stream receives an EOF, it yields None. Once an EOF is received,
|
|
305
|
-
subsequent invocations will not yield logs.
|
|
306
|
-
"""
|
|
307
|
-
...
|
|
308
|
-
|
|
309
|
-
_get_logs: ___get_logs_spec[typing_extensions.Self]
|
|
310
|
-
|
|
311
|
-
class ___get_logs_by_line_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
312
|
-
def __call__(self, /) -> typing.Generator[typing.Optional[bytes], None, None]:
|
|
313
|
-
"""Process logs from the server and yield complete lines only."""
|
|
314
|
-
...
|
|
315
|
-
|
|
316
|
-
def aio(self, /) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]:
|
|
317
|
-
"""Process logs from the server and yield complete lines only."""
|
|
318
|
-
...
|
|
319
|
-
|
|
320
|
-
_get_logs_by_line: ___get_logs_by_line_spec[typing_extensions.Self]
|
|
321
|
-
|
|
322
|
-
class ___ensure_stream_spec(typing_extensions.Protocol[SUPERSELF]):
|
|
323
|
-
def __call__(self, /) -> typing.Generator[typing.Optional[bytes], None, None]: ...
|
|
324
|
-
def aio(self, /) -> collections.abc.AsyncGenerator[typing.Optional[bytes], None]: ...
|
|
325
|
-
|
|
326
|
-
_ensure_stream: ___ensure_stream_spec[typing_extensions.Self]
|
|
327
|
-
|
|
328
449
|
def __iter__(self) -> typing.Iterator[T]:
|
|
329
450
|
"""mdmd:hidden"""
|
|
330
451
|
...
|
|
@@ -352,12 +473,16 @@ class StreamReader(typing.Generic[T]):
|
|
|
352
473
|
class StreamWriter:
|
|
353
474
|
"""Provides an interface to buffer and write logs to a sandbox or container process stream (`stdin`)."""
|
|
354
475
|
def __init__(
|
|
355
|
-
self,
|
|
476
|
+
self,
|
|
477
|
+
object_id: str,
|
|
478
|
+
object_type: typing.Literal["sandbox", "container_process"],
|
|
479
|
+
client: modal.client.Client,
|
|
480
|
+
command_router_client: typing.Optional[modal._utils.task_command_router_client.TaskCommandRouterClient] = None,
|
|
481
|
+
task_id: typing.Optional[str] = None,
|
|
356
482
|
) -> None:
|
|
357
483
|
"""mdmd:hidden"""
|
|
358
484
|
...
|
|
359
485
|
|
|
360
|
-
def _get_next_index(self) -> int: ...
|
|
361
486
|
def write(self, data: typing.Union[bytes, bytearray, memoryview, str]) -> None:
|
|
362
487
|
"""Write data to the stream but does not send it immediately.
|
|
363
488
|
|
modal/partial_function.pyi
CHANGED
|
@@ -329,7 +329,10 @@ def batched(
|
|
|
329
329
|
...
|
|
330
330
|
|
|
331
331
|
def concurrent(
|
|
332
|
-
_warn_parentheses_missing=None,
|
|
332
|
+
_warn_parentheses_missing=None,
|
|
333
|
+
*,
|
|
334
|
+
max_inputs: typing.Optional[int] = None,
|
|
335
|
+
target_inputs: typing.Optional[int] = None,
|
|
333
336
|
) -> collections.abc.Callable[
|
|
334
337
|
[
|
|
335
338
|
typing.Union[
|
modal/runner.py
CHANGED
|
@@ -35,7 +35,7 @@ from .client import HEARTBEAT_INTERVAL, HEARTBEAT_TIMEOUT, _Client
|
|
|
35
35
|
from .cls import _Cls
|
|
36
36
|
from .config import config, logger
|
|
37
37
|
from .environments import _get_environment_cached
|
|
38
|
-
from .exception import InteractiveTimeoutError, InvalidError, RemoteError, _CliUserExecutionError
|
|
38
|
+
from .exception import ConnectionError, InteractiveTimeoutError, InvalidError, RemoteError, _CliUserExecutionError
|
|
39
39
|
from .output import _get_output_manager, enable_output
|
|
40
40
|
from .running_app import RunningApp, running_app_from_layout
|
|
41
41
|
from .sandbox import _Sandbox
|
|
@@ -43,9 +43,7 @@ from .secret import _Secret
|
|
|
43
43
|
from .stream_type import StreamType
|
|
44
44
|
|
|
45
45
|
if TYPE_CHECKING:
|
|
46
|
-
|
|
47
|
-
else:
|
|
48
|
-
_App = TypeVar("_App")
|
|
46
|
+
import modal.app
|
|
49
47
|
|
|
50
48
|
|
|
51
49
|
V = TypeVar("V")
|
|
@@ -126,12 +124,11 @@ async def _init_local_app_from_name(
|
|
|
126
124
|
async def _create_all_objects(
|
|
127
125
|
client: _Client,
|
|
128
126
|
running_app: RunningApp,
|
|
129
|
-
|
|
130
|
-
classes: dict[str, _Cls],
|
|
127
|
+
local_app_state: "modal.app._LocalAppState",
|
|
131
128
|
environment_name: str,
|
|
132
129
|
) -> None:
|
|
133
130
|
"""Create objects that have been defined but not created on the server."""
|
|
134
|
-
indexed_objects: dict[str, _Object] = {**functions, **classes}
|
|
131
|
+
indexed_objects: dict[str, _Object] = {**local_app_state.functions, **local_app_state.classes}
|
|
135
132
|
resolver = Resolver(
|
|
136
133
|
client,
|
|
137
134
|
environment_name=environment_name,
|
|
@@ -182,21 +179,19 @@ async def _publish_app(
|
|
|
182
179
|
client: _Client,
|
|
183
180
|
running_app: RunningApp,
|
|
184
181
|
app_state: int, # api_pb2.AppState.value
|
|
185
|
-
|
|
186
|
-
classes: dict[str, _Cls],
|
|
182
|
+
app_local_state: "modal.app._LocalAppState",
|
|
187
183
|
name: str = "",
|
|
188
|
-
tags: dict[str, str] = {}, # Additional App metadata
|
|
189
184
|
deployment_tag: str = "", # Only relevant for deployments
|
|
190
185
|
commit_info: Optional[api_pb2.CommitInfo] = None, # Git commit information
|
|
191
186
|
) -> tuple[str, list[api_pb2.Warning]]:
|
|
192
187
|
"""Wrapper for AppPublish RPC."""
|
|
193
|
-
|
|
188
|
+
functions = app_local_state.functions
|
|
194
189
|
definition_ids = {obj.object_id: obj._get_metadata().definition_id for obj in functions.values()} # type: ignore
|
|
195
190
|
|
|
196
191
|
request = api_pb2.AppPublishRequest(
|
|
197
192
|
app_id=running_app.app_id,
|
|
198
193
|
name=name,
|
|
199
|
-
tags=tags,
|
|
194
|
+
tags=app_local_state.tags,
|
|
200
195
|
deployment_tag=deployment_tag,
|
|
201
196
|
commit_info=commit_info,
|
|
202
197
|
app_state=app_state, # type: ignore : should be a api_pb2.AppState.value
|
|
@@ -260,13 +255,13 @@ async def _status_based_disconnect(client: _Client, app_id: str, exc_info: Optio
|
|
|
260
255
|
|
|
261
256
|
@asynccontextmanager
|
|
262
257
|
async def _run_app(
|
|
263
|
-
app: _App,
|
|
258
|
+
app: "modal.app._App",
|
|
264
259
|
*,
|
|
265
260
|
client: Optional[_Client] = None,
|
|
266
261
|
detach: bool = False,
|
|
267
262
|
environment_name: Optional[str] = None,
|
|
268
263
|
interactive: bool = False,
|
|
269
|
-
) -> AsyncGenerator[_App, None]:
|
|
264
|
+
) -> AsyncGenerator["modal.app._App", None]:
|
|
270
265
|
"""mdmd:hidden"""
|
|
271
266
|
if environment_name is None:
|
|
272
267
|
environment_name = typing.cast(str, config.get("environment"))
|
|
@@ -338,12 +333,13 @@ async def _run_app(
|
|
|
338
333
|
get_app_logs_loop(client, output_mgr, app_id=running_app.app_id, app_logs_url=running_app.app_logs_url)
|
|
339
334
|
)
|
|
340
335
|
|
|
336
|
+
local_app_state = app._local_state
|
|
341
337
|
try:
|
|
342
338
|
# Create all members
|
|
343
|
-
await _create_all_objects(client, running_app,
|
|
339
|
+
await _create_all_objects(client, running_app, local_app_state, environment_name)
|
|
344
340
|
|
|
345
341
|
# Publish the app
|
|
346
|
-
await _publish_app(client, running_app, app_state,
|
|
342
|
+
await _publish_app(client, running_app, app_state, local_app_state)
|
|
347
343
|
except asyncio.CancelledError as e:
|
|
348
344
|
# this typically happens on sigint/ctrl-C during setup (the KeyboardInterrupt happens in the main thread)
|
|
349
345
|
if output_mgr := _get_output_manager():
|
|
@@ -355,6 +351,15 @@ async def _run_app(
|
|
|
355
351
|
await _status_based_disconnect(client, running_app.app_id, e)
|
|
356
352
|
raise
|
|
357
353
|
|
|
354
|
+
detached_disconnect_msg = (
|
|
355
|
+
"The detached App will keep running. You can track its progress on the Dashboard: "
|
|
356
|
+
f"[magenta underline]{running_app.app_page_url}[/magenta underline]"
|
|
357
|
+
"\n\nStream App logs:\n"
|
|
358
|
+
f"[green]modal app logs {running_app.app_id}[/green]"
|
|
359
|
+
"\n\nStop the App:\n"
|
|
360
|
+
f"[green]modal app stop {running_app.app_id}[/green]"
|
|
361
|
+
)
|
|
362
|
+
|
|
358
363
|
try:
|
|
359
364
|
# Show logs from dynamically created images.
|
|
360
365
|
# TODO: better way to do this
|
|
@@ -377,11 +382,7 @@ async def _run_app(
|
|
|
377
382
|
if detach:
|
|
378
383
|
if output_mgr := _get_output_manager():
|
|
379
384
|
output_mgr.print(output_mgr.step_completed("Shutting down Modal client."))
|
|
380
|
-
output_mgr.print(
|
|
381
|
-
"The detached app keeps running. You can track its progress at: "
|
|
382
|
-
f"[magenta]{running_app.app_page_url}[/magenta]"
|
|
383
|
-
""
|
|
384
|
-
)
|
|
385
|
+
output_mgr.print(detached_disconnect_msg)
|
|
385
386
|
if logs_loop:
|
|
386
387
|
logs_loop.cancel()
|
|
387
388
|
await _status_based_disconnect(client, running_app.app_id, e)
|
|
@@ -405,6 +406,14 @@ async def _run_app(
|
|
|
405
406
|
)
|
|
406
407
|
)
|
|
407
408
|
return
|
|
409
|
+
except ConnectionError as e:
|
|
410
|
+
# If we lose connection to the server after a detached App has started running, it will continue
|
|
411
|
+
# I think we can only exit "nicely" if we are able to print output though, otherwise we should raise
|
|
412
|
+
if detach and (output_mgr := _get_output_manager()):
|
|
413
|
+
output_mgr.print(":white_exclamation_mark: Connection lost!")
|
|
414
|
+
output_mgr.print(detached_disconnect_msg)
|
|
415
|
+
return
|
|
416
|
+
raise
|
|
408
417
|
except BaseException as e:
|
|
409
418
|
logger.info("Exception during app run")
|
|
410
419
|
await _status_based_disconnect(client, running_app.app_id, e)
|
|
@@ -428,7 +437,7 @@ async def _run_app(
|
|
|
428
437
|
|
|
429
438
|
|
|
430
439
|
async def _serve_update(
|
|
431
|
-
app: _App,
|
|
440
|
+
app: "modal.app._App",
|
|
432
441
|
existing_app_id: str,
|
|
433
442
|
is_ready: Event,
|
|
434
443
|
environment_name: str,
|
|
@@ -438,13 +447,12 @@ async def _serve_update(
|
|
|
438
447
|
client = await _Client.from_env()
|
|
439
448
|
try:
|
|
440
449
|
running_app: RunningApp = await _init_local_app_existing(client, existing_app_id, environment_name)
|
|
441
|
-
|
|
450
|
+
local_app_state = app._local_state
|
|
442
451
|
# Create objects
|
|
443
452
|
await _create_all_objects(
|
|
444
453
|
client,
|
|
445
454
|
running_app,
|
|
446
|
-
|
|
447
|
-
app._classes,
|
|
455
|
+
local_app_state,
|
|
448
456
|
environment_name,
|
|
449
457
|
)
|
|
450
458
|
|
|
@@ -453,9 +461,7 @@ async def _serve_update(
|
|
|
453
461
|
client,
|
|
454
462
|
running_app,
|
|
455
463
|
app_state=api_pb2.APP_STATE_UNSPECIFIED,
|
|
456
|
-
|
|
457
|
-
classes=app._classes,
|
|
458
|
-
tags=app._tags,
|
|
464
|
+
app_local_state=local_app_state,
|
|
459
465
|
)
|
|
460
466
|
|
|
461
467
|
# Communicate to the parent process
|
|
@@ -476,7 +482,7 @@ class DeployResult:
|
|
|
476
482
|
|
|
477
483
|
|
|
478
484
|
async def _deploy_app(
|
|
479
|
-
app: _App,
|
|
485
|
+
app: "modal.app._App",
|
|
480
486
|
name: Optional[str] = None,
|
|
481
487
|
namespace: Any = None, # mdmd:line-hidden
|
|
482
488
|
client: Optional[_Client] = None,
|
|
@@ -534,8 +540,7 @@ async def _deploy_app(
|
|
|
534
540
|
await _create_all_objects(
|
|
535
541
|
client,
|
|
536
542
|
running_app,
|
|
537
|
-
app.
|
|
538
|
-
app._classes,
|
|
543
|
+
app._local_state,
|
|
539
544
|
environment_name=environment_name,
|
|
540
545
|
)
|
|
541
546
|
|
|
@@ -548,11 +553,9 @@ async def _deploy_app(
|
|
|
548
553
|
app_url, warnings = await _publish_app(
|
|
549
554
|
client,
|
|
550
555
|
running_app,
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
classes=app._classes,
|
|
556
|
+
api_pb2.APP_STATE_DEPLOYED,
|
|
557
|
+
app._local_state,
|
|
554
558
|
name=name,
|
|
555
|
-
tags=app._tags,
|
|
556
559
|
deployment_tag=tag,
|
|
557
560
|
commit_info=commit_info,
|
|
558
561
|
)
|
|
@@ -574,7 +577,7 @@ async def _deploy_app(
|
|
|
574
577
|
|
|
575
578
|
|
|
576
579
|
async def _interactive_shell(
|
|
577
|
-
_app: _App, cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: Any
|
|
580
|
+
_app: "modal.app._App", cmds: list[str], environment_name: str = "", pty: bool = True, **kwargs: Any
|
|
578
581
|
) -> None:
|
|
579
582
|
"""Run an interactive shell (like `bash`) within the image for this app.
|
|
580
583
|
|