modal 0.68.1__py3-none-any.whl → 0.68.2__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/client.pyi CHANGED
@@ -26,7 +26,7 @@ class _Client:
26
26
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
27
27
 
28
28
  def __init__(
29
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.1"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.2"
30
30
  ): ...
31
31
  def is_closed(self) -> bool: ...
32
32
  @property
@@ -81,7 +81,7 @@ class Client:
81
81
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
82
82
 
83
83
  def __init__(
84
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.1"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.2"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
modal/exception.py CHANGED
@@ -222,6 +222,10 @@ class ClientClosed(Error):
222
222
  pass
223
223
 
224
224
 
225
+ class FilesystemExecutionError(Error):
226
+ """Raised when an unknown error is thrown during a container filesystem operation."""
227
+
228
+
225
229
  def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
226
230
  # TODO(erikbern): move this to modal._utils.deprecation
227
231
  for warning in server_warnings:
modal/file_io.py ADDED
@@ -0,0 +1,380 @@
1
+ # Copyright Modal Labs 2024
2
+ import asyncio
3
+ import io
4
+ from typing import TYPE_CHECKING, AsyncIterator, Generic, Optional, Sequence, TypeVar, Union, cast
5
+
6
+ if TYPE_CHECKING:
7
+ import _typeshed
8
+
9
+ from grpclib.exceptions import GRPCError, StreamTerminatedError
10
+
11
+ from modal._utils.grpc_utils import retry_transient_errors
12
+ from modal_proto import api_pb2
13
+
14
+ from ._utils.async_utils import synchronize_api
15
+ from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES
16
+ from .client import _Client
17
+ from .exception import FilesystemExecutionError, InvalidError
18
+
19
+ LARGE_FILE_SIZE_LIMIT = 16 * 1024 * 1024 # 16 MiB
20
+ READ_FILE_SIZE_LIMIT = 100 * 1024 * 1024 # 100 MiB
21
+
22
+ ERROR_MAPPING = {
23
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_UNSPECIFIED: FilesystemExecutionError,
24
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_PERM: PermissionError,
25
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_NOENT: FileNotFoundError,
26
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_IO: IOError,
27
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_NXIO: IOError,
28
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_NOMEM: MemoryError,
29
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_ACCES: PermissionError,
30
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_EXIST: FileExistsError,
31
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_NOTDIR: NotADirectoryError,
32
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_ISDIR: IsADirectoryError,
33
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_INVAL: OSError,
34
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_MFILE: OSError,
35
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_FBIG: OSError,
36
+ api_pb2.SystemErrorCode.SYSTEM_ERROR_CODE_NOSPC: OSError,
37
+ }
38
+
39
+ T = TypeVar("T", str, bytes)
40
+
41
+
42
+ async def _delete_bytes(file: "_FileIO", start: Optional[int] = None, end: Optional[int] = None) -> None:
43
+ """Delete a range of bytes from the file.
44
+
45
+ `start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
46
+ If either is None, the start or end of the file is used, respectively.
47
+ """
48
+ assert file._file_descriptor is not None
49
+ file._check_closed()
50
+ if start is not None and end is not None:
51
+ if start >= end:
52
+ raise ValueError("start must be less than end")
53
+ resp = await file._make_request(
54
+ api_pb2.ContainerFilesystemExecRequest(
55
+ file_delete_bytes_request=api_pb2.ContainerFileDeleteBytesRequest(
56
+ file_descriptor=file._file_descriptor,
57
+ start_inclusive=start,
58
+ end_exclusive=end,
59
+ ),
60
+ task_id=file._task_id,
61
+ )
62
+ )
63
+ await file._wait(resp.exec_id)
64
+
65
+
66
+ async def _replace_bytes(file: "_FileIO", data: bytes, start: Optional[int] = None, end: Optional[int] = None) -> None:
67
+ """Replace a range of bytes in the file with new data. The length of the data does not
68
+ have to be the same as the length of the range being replaced.
69
+
70
+ `start` and `end` are byte offsets. `start` is inclusive, `end` is exclusive.
71
+ If either is None, the start or end of the file is used, respectively.
72
+ """
73
+ assert file._file_descriptor is not None
74
+ file._check_closed()
75
+ if start is not None and end is not None:
76
+ if start >= end:
77
+ raise InvalidError("start must be less than end")
78
+ if len(data) > LARGE_FILE_SIZE_LIMIT:
79
+ raise InvalidError("Write request payload exceeds 16 MiB limit")
80
+ resp = await file._make_request(
81
+ api_pb2.ContainerFilesystemExecRequest(
82
+ file_write_replace_bytes_request=api_pb2.ContainerFileWriteReplaceBytesRequest(
83
+ file_descriptor=file._file_descriptor,
84
+ data=data,
85
+ start_inclusive=start,
86
+ end_exclusive=end,
87
+ ),
88
+ task_id=file._task_id,
89
+ )
90
+ )
91
+ await file._wait(resp.exec_id)
92
+
93
+
94
+ # The FileIO class is designed to mimic Python's io.FileIO
95
+ # See https://github.com/python/cpython/blob/main/Lib/_pyio.py#L1459
96
+ class _FileIO(Generic[T]):
97
+ """FileIO handle, used in the Sandbox filesystem API.
98
+
99
+ The API is designed to mimic Python's io.FileIO.
100
+
101
+ **Usage**
102
+
103
+ ```python
104
+ import modal
105
+
106
+ app = modal.App.lookup("my-app", create_if_missing=True)
107
+
108
+ sb = modal.Sandbox.create(app=app)
109
+ f = sb.open("/tmp/foo.txt", "w")
110
+ f.write("hello")
111
+ f.close()
112
+ ```
113
+ """
114
+
115
+ _binary = False
116
+ _readable = False
117
+ _writable = False
118
+ _appended = False
119
+ _closed = True
120
+
121
+ _task_id: str = ""
122
+ _file_descriptor: str = ""
123
+ _client: Optional[_Client] = None
124
+
125
+ def _validate_mode(self, mode: str) -> None:
126
+ if not any(char in mode for char in "rwax"):
127
+ raise ValueError(f"Invalid file mode: {mode}")
128
+
129
+ self._readable = "r" in mode or "+" in mode
130
+ self._writable = "w" in mode or "a" in mode or "x" in mode or "+" in mode
131
+ self._appended = "a" in mode
132
+ self._binary = "b" in mode
133
+
134
+ valid_chars = set("rwaxb+")
135
+ if any(char not in valid_chars for char in mode):
136
+ raise ValueError(f"Invalid file mode: {mode}")
137
+
138
+ mode_count = sum(1 for c in mode if c in "rwax")
139
+ if mode_count > 1:
140
+ raise ValueError("must have exactly one of create/read/write/append mode")
141
+
142
+ seen_chars = set()
143
+ for char in mode:
144
+ if char in seen_chars:
145
+ raise ValueError(f"Invalid file mode: {mode}")
146
+ seen_chars.add(char)
147
+
148
+ def _handle_error(self, error: api_pb2.SystemErrorMessage) -> None:
149
+ error_class = ERROR_MAPPING.get(error.error_code, FilesystemExecutionError)
150
+ raise error_class(error.error_message)
151
+
152
+ async def _consume_output(self, exec_id: str) -> AsyncIterator[Optional[bytes]]:
153
+ req = api_pb2.ContainerFilesystemExecGetOutputRequest(
154
+ exec_id=exec_id,
155
+ timeout=55,
156
+ )
157
+ assert self._client is not None
158
+ async for batch in self._client.stub.ContainerFilesystemExecGetOutput.unary_stream(req):
159
+ if batch.eof:
160
+ yield None
161
+ break
162
+ if batch.HasField("error"):
163
+ self._handle_error(batch.error)
164
+ for message in batch.output:
165
+ yield message
166
+
167
+ async def _wait(self, exec_id: str) -> bytes:
168
+ # The logic here is similar to how output is read from `exec`
169
+ output = b""
170
+ completed = False
171
+ retries_remaining = 10
172
+ while not completed:
173
+ try:
174
+ async for data in self._consume_output(exec_id):
175
+ if data is None:
176
+ completed = True
177
+ break
178
+ output += data
179
+ except (GRPCError, StreamTerminatedError) as exc:
180
+ if retries_remaining > 0:
181
+ retries_remaining -= 1
182
+ if isinstance(exc, GRPCError):
183
+ if exc.status in RETRYABLE_GRPC_STATUS_CODES:
184
+ await asyncio.sleep(1.0)
185
+ continue
186
+ elif isinstance(exc, StreamTerminatedError):
187
+ continue
188
+ raise
189
+ return output
190
+
191
+ def _validate_type(self, data: Union[bytes, str]) -> None:
192
+ if self._binary and isinstance(data, str):
193
+ raise TypeError("Expected bytes when in binary mode")
194
+ if not self._binary and isinstance(data, bytes):
195
+ raise TypeError("Expected str when in text mode")
196
+
197
+ async def _open_file(self, path: str, mode: str) -> None:
198
+ resp = await self._make_request(
199
+ api_pb2.ContainerFilesystemExecRequest(
200
+ file_open_request=api_pb2.ContainerFileOpenRequest(path=path, mode=mode),
201
+ task_id=self._task_id,
202
+ )
203
+ )
204
+ if not resp.HasField("file_descriptor"):
205
+ raise FilesystemExecutionError("Failed to open file")
206
+ self._file_descriptor = resp.file_descriptor
207
+ await self._wait(resp.exec_id)
208
+
209
+ @classmethod
210
+ async def create(
211
+ cls, path: str, mode: Union["_typeshed.OpenTextMode", "_typeshed.OpenBinaryMode"], client: _Client, task_id: str
212
+ ) -> "_FileIO":
213
+ """Create a new FileIO handle."""
214
+ self = cls.__new__(cls)
215
+ self._client = client
216
+ self._task_id = task_id
217
+ self._validate_mode(mode)
218
+ await self._open_file(path, mode)
219
+ self._closed = False
220
+ return self
221
+
222
+ async def _make_request(
223
+ self, request: api_pb2.ContainerFilesystemExecRequest
224
+ ) -> api_pb2.ContainerFilesystemExecResponse:
225
+ assert self._client is not None
226
+ return await retry_transient_errors(self._client.stub.ContainerFilesystemExec, request)
227
+
228
+ async def _make_read_request(self, n: Optional[int]) -> bytes:
229
+ resp = await self._make_request(
230
+ api_pb2.ContainerFilesystemExecRequest(
231
+ file_read_request=api_pb2.ContainerFileReadRequest(file_descriptor=self._file_descriptor, n=n),
232
+ task_id=self._task_id,
233
+ )
234
+ )
235
+ return await self._wait(resp.exec_id)
236
+
237
+ async def read(self, n: Optional[int] = None) -> T:
238
+ """Read n bytes from the current position, or the entire remaining file if n is None."""
239
+ self._check_closed()
240
+ self._check_readable()
241
+ if n is not None and n > READ_FILE_SIZE_LIMIT:
242
+ raise ValueError("Read request payload exceeds 100 MiB limit")
243
+ output = await self._make_read_request(n)
244
+ if self._binary:
245
+ return cast(T, output)
246
+ return cast(T, output.decode("utf-8"))
247
+
248
+ async def readline(self) -> T:
249
+ """Read a single line from the current position."""
250
+ self._check_closed()
251
+ self._check_readable()
252
+ resp = await self._make_request(
253
+ api_pb2.ContainerFilesystemExecRequest(
254
+ file_read_line_request=api_pb2.ContainerFileReadLineRequest(file_descriptor=self._file_descriptor),
255
+ task_id=self._task_id,
256
+ )
257
+ )
258
+ output = await self._wait(resp.exec_id)
259
+ if self._binary:
260
+ return cast(T, output)
261
+ return cast(T, output.decode("utf-8"))
262
+
263
+ async def readlines(self) -> Sequence[T]:
264
+ """Read all lines from the current position."""
265
+ self._check_closed()
266
+ self._check_readable()
267
+ output = await self._make_read_request(None)
268
+ if self._binary:
269
+ lines_bytes = output.split(b"\n")
270
+ output = [line + b"\n" for line in lines_bytes[:-1]] + ([lines_bytes[-1]] if lines_bytes[-1] else [])
271
+ return cast(Sequence[T], output)
272
+ else:
273
+ lines = output.decode("utf-8").split("\n")
274
+ output = [line + "\n" for line in lines[:-1]] + ([lines[-1]] if lines[-1] else [])
275
+ return cast(Sequence[T], output)
276
+
277
+ async def write(self, data: Union[bytes, str]) -> None:
278
+ """Write data to the current position.
279
+
280
+ Writes may not appear until the entire buffer is flushed, which
281
+ can be done manually with `flush()` or automatically when the file is
282
+ closed.
283
+ """
284
+ self._check_closed()
285
+ self._check_writable()
286
+ self._validate_type(data)
287
+ if isinstance(data, str):
288
+ data = data.encode("utf-8")
289
+ if len(data) > LARGE_FILE_SIZE_LIMIT:
290
+ raise ValueError("Write request payload exceeds 16 MiB limit")
291
+ resp = await self._make_request(
292
+ api_pb2.ContainerFilesystemExecRequest(
293
+ file_write_request=api_pb2.ContainerFileWriteRequest(file_descriptor=self._file_descriptor, data=data),
294
+ task_id=self._task_id,
295
+ )
296
+ )
297
+ await self._wait(resp.exec_id)
298
+
299
+ async def flush(self) -> None:
300
+ """Flush the buffer to disk."""
301
+ self._check_closed()
302
+ self._check_writable()
303
+ resp = await self._make_request(
304
+ api_pb2.ContainerFilesystemExecRequest(
305
+ file_flush_request=api_pb2.ContainerFileFlushRequest(file_descriptor=self._file_descriptor),
306
+ task_id=self._task_id,
307
+ )
308
+ )
309
+ await self._wait(resp.exec_id)
310
+
311
+ def _get_whence(self, whence: int):
312
+ if whence == 0:
313
+ return api_pb2.SeekWhence.SEEK_SET
314
+ elif whence == 1:
315
+ return api_pb2.SeekWhence.SEEK_CUR
316
+ elif whence == 2:
317
+ return api_pb2.SeekWhence.SEEK_END
318
+ else:
319
+ raise ValueError(f"Invalid whence value: {whence}")
320
+
321
+ async def seek(self, offset: int, whence: int = 0) -> None:
322
+ """Move to a new position in the file.
323
+
324
+ `whence` defaults to 0 (absolute file positioning); other values are 1
325
+ (relative to the current position) and 2 (relative to the file's end).
326
+ """
327
+ self._check_closed()
328
+ resp = await self._make_request(
329
+ api_pb2.ContainerFilesystemExecRequest(
330
+ file_seek_request=api_pb2.ContainerFileSeekRequest(
331
+ file_descriptor=self._file_descriptor,
332
+ offset=offset,
333
+ whence=self._get_whence(whence),
334
+ ),
335
+ task_id=self._task_id,
336
+ )
337
+ )
338
+ await self._wait(resp.exec_id)
339
+
340
+ async def _close(self) -> None:
341
+ # Buffer is flushed by the runner on close
342
+ resp = await self._make_request(
343
+ api_pb2.ContainerFilesystemExecRequest(
344
+ file_close_request=api_pb2.ContainerFileCloseRequest(file_descriptor=self._file_descriptor),
345
+ task_id=self._task_id,
346
+ )
347
+ )
348
+ self._closed = True
349
+ await self._wait(resp.exec_id)
350
+
351
+ async def close(self) -> None:
352
+ """Flush the buffer and close the file."""
353
+ await self._close()
354
+
355
+ # also validated in the runner, but checked in the client to catch errors early
356
+ def _check_writable(self) -> None:
357
+ if not self._writable:
358
+ raise io.UnsupportedOperation("not writeable")
359
+
360
+ # also validated in the runner, but checked in the client to catch errors early
361
+ def _check_readable(self) -> None:
362
+ if not self._readable:
363
+ raise io.UnsupportedOperation("not readable")
364
+
365
+ # also validated in the runner, but checked in the client to catch errors early
366
+ def _check_closed(self) -> None:
367
+ if self._closed:
368
+ raise ValueError("I/O operation on closed file")
369
+
370
+ def __enter__(self) -> "_FileIO":
371
+ self._check_closed()
372
+ return self
373
+
374
+ async def __exit__(self, exc_type, exc_value, traceback) -> None:
375
+ await self._close()
376
+
377
+
378
+ delete_bytes = synchronize_api(_delete_bytes)
379
+ replace_bytes = synchronize_api(_replace_bytes)
380
+ FileIO = synchronize_api(_FileIO)
modal/file_io.pyi ADDED
@@ -0,0 +1,185 @@
1
+ import _typeshed
2
+ import modal.client
3
+ import modal_proto.api_pb2
4
+ import typing
5
+ import typing_extensions
6
+
7
+ T = typing.TypeVar("T")
8
+
9
+ async def _delete_bytes(
10
+ file: _FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None
11
+ ) -> None: ...
12
+ async def _replace_bytes(
13
+ file: _FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
14
+ ) -> None: ...
15
+
16
+ class _FileIO(typing.Generic[T]):
17
+ _task_id: str
18
+ _file_descriptor: str
19
+ _client: typing.Optional[modal.client._Client]
20
+
21
+ def _validate_mode(self, mode: str) -> None: ...
22
+ def _handle_error(self, error: modal_proto.api_pb2.SystemErrorMessage) -> None: ...
23
+ def _consume_output(self, exec_id: str) -> typing.AsyncIterator[typing.Optional[bytes]]: ...
24
+ async def _wait(self, exec_id: str) -> bytes: ...
25
+ def _validate_type(self, data: typing.Union[bytes, str]) -> None: ...
26
+ async def _open_file(self, path: str, mode: str) -> None: ...
27
+ @classmethod
28
+ async def create(
29
+ cls,
30
+ path: str,
31
+ mode: typing.Union[_typeshed.OpenTextMode, _typeshed.OpenBinaryMode],
32
+ client: modal.client._Client,
33
+ task_id: str,
34
+ ) -> _FileIO: ...
35
+ async def _make_request(
36
+ self, request: modal_proto.api_pb2.ContainerFilesystemExecRequest
37
+ ) -> modal_proto.api_pb2.ContainerFilesystemExecResponse: ...
38
+ async def _make_read_request(self, n: typing.Optional[int]) -> bytes: ...
39
+ async def read(self, n: typing.Optional[int] = None) -> T: ...
40
+ async def readline(self) -> T: ...
41
+ async def readlines(self) -> typing.Sequence[T]: ...
42
+ async def write(self, data: typing.Union[bytes, str]) -> None: ...
43
+ async def flush(self) -> None: ...
44
+ def _get_whence(self, whence: int): ...
45
+ async def seek(self, offset: int, whence: int = 0) -> None: ...
46
+ async def _close(self) -> None: ...
47
+ async def close(self) -> None: ...
48
+ def _check_writable(self) -> None: ...
49
+ def _check_readable(self) -> None: ...
50
+ def _check_closed(self) -> None: ...
51
+ def __enter__(self) -> _FileIO: ...
52
+ async def __exit__(self, exc_type, exc_value, traceback) -> None: ...
53
+
54
+ class __delete_bytes_spec(typing_extensions.Protocol):
55
+ def __call__(self, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None: ...
56
+ async def aio(self, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None: ...
57
+
58
+ delete_bytes: __delete_bytes_spec
59
+
60
+ class __replace_bytes_spec(typing_extensions.Protocol):
61
+ def __call__(
62
+ self, file: FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
63
+ ) -> None: ...
64
+ async def aio(
65
+ self, file: FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
66
+ ) -> None: ...
67
+
68
+ replace_bytes: __replace_bytes_spec
69
+
70
+ T_INNER = typing.TypeVar("T_INNER", covariant=True)
71
+
72
+ class FileIO(typing.Generic[T]):
73
+ _task_id: str
74
+ _file_descriptor: str
75
+ _client: typing.Optional[modal.client.Client]
76
+
77
+ def __init__(self, /, *args, **kwargs): ...
78
+ def _validate_mode(self, mode: str) -> None: ...
79
+ def _handle_error(self, error: modal_proto.api_pb2.SystemErrorMessage) -> None: ...
80
+
81
+ class ___consume_output_spec(typing_extensions.Protocol):
82
+ def __call__(self, exec_id: str) -> typing.Iterator[typing.Optional[bytes]]: ...
83
+ def aio(self, exec_id: str) -> typing.AsyncIterator[typing.Optional[bytes]]: ...
84
+
85
+ _consume_output: ___consume_output_spec
86
+
87
+ class ___wait_spec(typing_extensions.Protocol):
88
+ def __call__(self, exec_id: str) -> bytes: ...
89
+ async def aio(self, exec_id: str) -> bytes: ...
90
+
91
+ _wait: ___wait_spec
92
+
93
+ def _validate_type(self, data: typing.Union[bytes, str]) -> None: ...
94
+
95
+ class ___open_file_spec(typing_extensions.Protocol):
96
+ def __call__(self, path: str, mode: str) -> None: ...
97
+ async def aio(self, path: str, mode: str) -> None: ...
98
+
99
+ _open_file: ___open_file_spec
100
+
101
+ @classmethod
102
+ def create(
103
+ cls,
104
+ path: str,
105
+ mode: typing.Union[_typeshed.OpenTextMode, _typeshed.OpenBinaryMode],
106
+ client: modal.client.Client,
107
+ task_id: str,
108
+ ) -> FileIO: ...
109
+
110
+ class ___make_request_spec(typing_extensions.Protocol):
111
+ def __call__(
112
+ self, request: modal_proto.api_pb2.ContainerFilesystemExecRequest
113
+ ) -> modal_proto.api_pb2.ContainerFilesystemExecResponse: ...
114
+ async def aio(
115
+ self, request: modal_proto.api_pb2.ContainerFilesystemExecRequest
116
+ ) -> modal_proto.api_pb2.ContainerFilesystemExecResponse: ...
117
+
118
+ _make_request: ___make_request_spec
119
+
120
+ class ___make_read_request_spec(typing_extensions.Protocol):
121
+ def __call__(self, n: typing.Optional[int]) -> bytes: ...
122
+ async def aio(self, n: typing.Optional[int]) -> bytes: ...
123
+
124
+ _make_read_request: ___make_read_request_spec
125
+
126
+ class __read_spec(typing_extensions.Protocol[T_INNER]):
127
+ def __call__(self, n: typing.Optional[int] = None) -> T_INNER: ...
128
+ async def aio(self, n: typing.Optional[int] = None) -> T_INNER: ...
129
+
130
+ read: __read_spec[T]
131
+
132
+ class __readline_spec(typing_extensions.Protocol[T_INNER]):
133
+ def __call__(self) -> T_INNER: ...
134
+ async def aio(self) -> T_INNER: ...
135
+
136
+ readline: __readline_spec[T]
137
+
138
+ class __readlines_spec(typing_extensions.Protocol[T_INNER]):
139
+ def __call__(self) -> typing.Sequence[T_INNER]: ...
140
+ async def aio(self) -> typing.Sequence[T_INNER]: ...
141
+
142
+ readlines: __readlines_spec[T]
143
+
144
+ class __write_spec(typing_extensions.Protocol):
145
+ def __call__(self, data: typing.Union[bytes, str]) -> None: ...
146
+ async def aio(self, data: typing.Union[bytes, str]) -> None: ...
147
+
148
+ write: __write_spec
149
+
150
+ class __flush_spec(typing_extensions.Protocol):
151
+ def __call__(self) -> None: ...
152
+ async def aio(self) -> None: ...
153
+
154
+ flush: __flush_spec
155
+
156
+ def _get_whence(self, whence: int): ...
157
+
158
+ class __seek_spec(typing_extensions.Protocol):
159
+ def __call__(self, offset: int, whence: int = 0) -> None: ...
160
+ async def aio(self, offset: int, whence: int = 0) -> None: ...
161
+
162
+ seek: __seek_spec
163
+
164
+ class ___close_spec(typing_extensions.Protocol):
165
+ def __call__(self) -> None: ...
166
+ async def aio(self) -> None: ...
167
+
168
+ _close: ___close_spec
169
+
170
+ class __close_spec(typing_extensions.Protocol):
171
+ def __call__(self) -> None: ...
172
+ async def aio(self) -> None: ...
173
+
174
+ close: __close_spec
175
+
176
+ def _check_writable(self) -> None: ...
177
+ def _check_readable(self) -> None: ...
178
+ def _check_closed(self) -> None: ...
179
+ def __enter__(self) -> FileIO: ...
180
+
181
+ class ____exit___spec(typing_extensions.Protocol):
182
+ def __call__(self, exc_type, exc_value, traceback) -> None: ...
183
+ async def aio(self, exc_type, exc_value, traceback) -> None: ...
184
+
185
+ __exit__: ____exit___spec
modal/functions.pyi CHANGED
@@ -455,11 +455,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
455
455
 
456
456
  _call_generator_nowait: ___call_generator_nowait_spec
457
457
 
458
- class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
458
+ class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
459
459
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
460
460
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
461
461
 
462
- remote: __remote_spec[ReturnType, P]
462
+ remote: __remote_spec[P, ReturnType]
463
463
 
464
464
  class __remote_gen_spec(typing_extensions.Protocol):
465
465
  def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
@@ -471,17 +471,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
471
471
  def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
472
472
  def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
473
473
 
474
- class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
474
+ class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
475
475
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
476
476
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
477
477
 
478
- _experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
478
+ _experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
479
479
 
480
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
480
+ class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
481
481
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
482
482
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
483
483
 
484
- spawn: __spawn_spec[ReturnType, P]
484
+ spawn: __spawn_spec[P, ReturnType]
485
485
 
486
486
  def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
487
487
 
modal/sandbox.py CHANGED
@@ -4,6 +4,9 @@ import os
4
4
  from collections.abc import AsyncGenerator, Sequence
5
5
  from typing import TYPE_CHECKING, Literal, Optional, Union, overload
6
6
 
7
+ if TYPE_CHECKING:
8
+ import _typeshed
9
+
7
10
  from google.protobuf.message import Message
8
11
  from grpclib import GRPCError, Status
9
12
 
@@ -29,6 +32,7 @@ from .exception import (
29
32
  deprecation_error,
30
33
  deprecation_warning,
31
34
  )
35
+ from .file_io import _FileIO
32
36
  from .gpu import GPU_T
33
37
  from .image import _Image
34
38
  from .io_streams import StreamReader, StreamWriter, _StreamReader, _StreamWriter
@@ -527,6 +531,42 @@ class _Sandbox(_Object, type_prefix="sb"):
527
531
  by_line = bufsize == 1
528
532
  return _ContainerProcess(resp.exec_id, self._client, stdout=stdout, stderr=stderr, text=text, by_line=by_line)
529
533
 
534
+ @overload
535
+ async def open(
536
+ self,
537
+ path: str,
538
+ mode: "_typeshed.OpenTextMode",
539
+ ) -> _FileIO[str]:
540
+ ...
541
+
542
+ @overload
543
+ async def open(
544
+ self,
545
+ path: str,
546
+ mode: "_typeshed.OpenBinaryMode",
547
+ ) -> _FileIO[bytes]:
548
+ ...
549
+
550
+ async def open(
551
+ self,
552
+ path: str,
553
+ mode: Union["_typeshed.OpenTextMode", "_typeshed.OpenBinaryMode"] = "r",
554
+ ):
555
+ """Open a file in the Sandbox and return
556
+ a [`FileIO`](/docs/reference/modal.FileIO#modalfile_io) handle.
557
+
558
+ **Usage**
559
+
560
+ ```python notest
561
+ sb = modal.Sandbox.create(app=sb_app)
562
+ f = sb.open("/test.txt", "w")
563
+ f.write("hello")
564
+ f.close()
565
+ ```
566
+ """
567
+ task_id = await self._get_task_id()
568
+ return await _FileIO.create(path, mode, self._client, task_id)
569
+
530
570
  @property
531
571
  def stdout(self) -> _StreamReader[str]:
532
572
  """
modal/sandbox.pyi CHANGED
@@ -1,3 +1,4 @@
1
+ import _typeshed
1
2
  import collections.abc
2
3
  import google.protobuf.message
3
4
  import modal._tunnel
@@ -5,6 +6,7 @@ import modal.app
5
6
  import modal.client
6
7
  import modal.cloud_bucket_mount
7
8
  import modal.container_process
9
+ import modal.file_io
8
10
  import modal.gpu
9
11
  import modal.image
10
12
  import modal.io_streams
@@ -122,6 +124,10 @@ class _Sandbox(modal.object._Object):
122
124
  bufsize: typing.Literal[-1, 1] = -1,
123
125
  _pty_info: typing.Optional[modal_proto.api_pb2.PTYInfo] = None,
124
126
  ) -> modal.container_process._ContainerProcess[bytes]: ...
127
+ @typing.overload
128
+ async def open(self, path: str, mode: _typeshed.OpenTextMode) -> modal.file_io._FileIO[str]: ...
129
+ @typing.overload
130
+ async def open(self, path: str, mode: _typeshed.OpenBinaryMode) -> modal.file_io._FileIO[bytes]: ...
125
131
  @property
126
132
  def stdout(self) -> modal.io_streams._StreamReader[str]: ...
127
133
  @property
@@ -349,6 +355,18 @@ class Sandbox(modal.object.Object):
349
355
 
350
356
  exec: __exec_spec
351
357
 
358
+ class __open_spec(typing_extensions.Protocol):
359
+ @typing.overload
360
+ def __call__(self, path: str, mode: _typeshed.OpenTextMode) -> modal.file_io.FileIO[str]: ...
361
+ @typing.overload
362
+ def __call__(self, path: str, mode: _typeshed.OpenBinaryMode) -> modal.file_io.FileIO[bytes]: ...
363
+ @typing.overload
364
+ async def aio(self, path: str, mode: _typeshed.OpenTextMode) -> modal.file_io.FileIO[str]: ...
365
+ @typing.overload
366
+ async def aio(self, path: str, mode: _typeshed.OpenBinaryMode) -> modal.file_io.FileIO[bytes]: ...
367
+
368
+ open: __open_spec
369
+
352
370
  @property
353
371
  def stdout(self) -> modal.io_streams.StreamReader[str]: ...
354
372
  @property
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.68.1
3
+ Version: 0.68.2
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -21,7 +21,7 @@ Requires-Dist: fastapi
21
21
  Requires-Dist: grpclib (==0.4.7)
22
22
  Requires-Dist: protobuf (!=4.24.0,<6.0,>=3.19)
23
23
  Requires-Dist: rich (>=12.0.0)
24
- Requires-Dist: synchronicity (~=0.9.4)
24
+ Requires-Dist: synchronicity (~=0.9.6)
25
25
  Requires-Dist: toml
26
26
  Requires-Dist: typer (>=0.9)
27
27
  Requires-Dist: types-certifi
@@ -19,7 +19,7 @@ modal/app.py,sha256=EJ7FUN6rWnSwLJoYJh8nmKg_t-8hdN8_rt0OrkP7JvQ,46084
19
19
  modal/app.pyi,sha256=BE5SlR5tRECuc6-e2lUuOknDdov3zxgZ4N0AsLb5ZVQ,25270
20
20
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
21
21
  modal/client.py,sha256=uy1Lw3hipt9ILv19VdmBBGYIW7VHmr4rzXi7pO7Kg7s,15791
22
- modal/client.pyi,sha256=Mpm_iZr5M6GeCM_Dey5sDQtDM0T-2mppR9-n15iBr0Q,7352
22
+ modal/client.pyi,sha256=HARg2kCIdOYmjEulbyyZJAN3F01iJPF8IiBZbqlcEFk,7352
23
23
  modal/cloud_bucket_mount.py,sha256=G7T7jWLD0QkmrfKR75mSTwdUZ2xNfj7pkVqb4ipmxmI,5735
24
24
  modal/cloud_bucket_mount.pyi,sha256=CEi7vrH3kDUF4LAy4qP6tfImy2UJuFRcRbsgRNM1wo8,1403
25
25
  modal/cls.py,sha256=pMirIOmb59YzaIK2rn7Vd756E1QKDDweYT90GYIHiMk,27472
@@ -31,10 +31,12 @@ modal/dict.py,sha256=RmJlEwFJOdSfAYcVa50hbbFccV8e7BvC5tc5g1HXF-c,12622
31
31
  modal/dict.pyi,sha256=2cYgOqBxYZih4BYgMV0c3rNPuxYR6-cB1GBXzFkHA5c,7265
32
32
  modal/environments.py,sha256=5cgA-zbm6ngKLsRA19zSOgtgo9-BarJK3FJK0BiF2Lo,6505
33
33
  modal/environments.pyi,sha256=XalNpiPkAtHWAvOU2Cotq0ozmtl-Jv0FDsR8h9mr27Q,3521
34
- modal/exception.py,sha256=eSQZ0u1z-RPMW9x6AGMySi-LWyqkTtjZM-RnvhyfWWQ,6997
34
+ modal/exception.py,sha256=dRK789TD1HaB63kHhu1yZuvS2vP_Vua3iLMBtA6dgqk,7128
35
35
  modal/experimental.py,sha256=jFuNbwrNHos47viMB9q-cHJSvf2RDxDdoEcss9plaZE,2302
36
+ modal/file_io.py,sha256=q8s872qf6Ntdw7dPogDlpYbixxGkwCA0BlQn2UUoVhY,14637
37
+ modal/file_io.pyi,sha256=pfkmJiaBpMCZReE6-KCjYOzB1dVtyYDYokJoYX8ARK4,6932
36
38
  modal/functions.py,sha256=Z2g1CuDTz_491ReOJKs66qGlodPDpKAiKHVwARL2zYA,66792
37
- modal/functions.pyi,sha256=4k5CaJ9iTuEyHQ2rC5OysNHBLv1CZrD7zBMU1zXIU4w,24988
39
+ modal/functions.pyi,sha256=IyuM9TV79JfrtfTaJ4yq3EcWp3yHuxLavpxTOwSWEDw,24988
38
40
  modal/gpu.py,sha256=r4rL6uH3UJIQthzYvfWauXNyh01WqCPtKZCmmSX1fd4,6881
39
41
  modal/image.py,sha256=cQ6WP1xHXZT_nY8z3aEFiGwKzrTV0yxi3Ab8JzF91eo,79653
40
42
  modal/image.pyi,sha256=PIKH6JBA4L5TfdJrQu3pm2ykyIITmiP920TpP8cdyQA,24585
@@ -60,8 +62,8 @@ modal/retries.py,sha256=HKR2Q9aNPWkMjQ5nwobqYTuZaSuw0a8lI2zrtY5IW98,5230
60
62
  modal/runner.py,sha256=Q02VdfLCO7YKpnOSqqh58XL3hR2XHaDeiJVYW3MKz_8,24580
61
63
  modal/runner.pyi,sha256=BvMS1ZVzWSn8B8q0KnIZOJKPkN5L-i5b-USbV6SWWHQ,5177
62
64
  modal/running_app.py,sha256=CshNvGDJtagOdKW54uYjY8HY73j2TpnsL9jkPFZAsfA,560
63
- modal/sandbox.py,sha256=Yd__KipEINH2euDPOcm5MPBnagRv19Sa5z5tJCvGOQs,26360
64
- modal/sandbox.pyi,sha256=fRl32Pt5F6TbK7aYewjlcL4WQxxmp7m6Ybktmkd2VOk,18108
65
+ modal/sandbox.py,sha256=abmprnPtA-WVsHKu4J1deoltpAUvXtIHQHX-ZpYjYPE,27293
66
+ modal/sandbox.pyi,sha256=QPNuiTLNoKwYf8JK_fmfUBXpdGYlukyaksFV1DpCd2g,18987
65
67
  modal/schedule.py,sha256=0ZFpKs1bOxeo5n3HZjoL7OE2ktsb-_oGtq-WJEPO4tY,2615
66
68
  modal/scheduler_placement.py,sha256=BAREdOY5HzHpzSBqt6jDVR6YC_jYfHMVqOzkyqQfngU,1235
67
69
  modal/secret.py,sha256=Y1WgybQIkfkxdzH9CQ1h-Wd1DJJpzipigMhyyvSxTww,10007
@@ -131,7 +133,7 @@ modal/requirements/README.md,sha256=9tK76KP0Uph7O0M5oUgsSwEZDj5y-dcUPsnpR0Sc-Ik,
131
133
  modal/requirements/base-images.json,sha256=kLNo5Sqmnhp9H6Hr9IcaGJFrRaRg1yfuepUWkm-y8iQ,571
132
134
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
133
135
  modal_docs/gen_cli_docs.py,sha256=c1yfBS_x--gL5bs0N4ihMwqwX8l3IBWSkBAKNNIi6bQ,3801
134
- modal_docs/gen_reference_docs.py,sha256=AI8h-JKfwn7Tohm3P3D5G0SivSpdGgN6gnw-fED-S0s,6613
136
+ modal_docs/gen_reference_docs.py,sha256=aDcUSSDtAAZ4eeFWyroeIg2TOzyRoYcic-d9Zh9TdLY,6656
135
137
  modal_docs/mdmd/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
136
138
  modal_docs/mdmd/mdmd.py,sha256=F6EXKkjwrTbOiG6I7wKtNGVVmmeWLAJ5pnE7DUkDpvM,6231
137
139
  modal_docs/mdmd/signatures.py,sha256=XJaZrK7Mdepk5fdX51A8uENiLFNil85Ud0d4MH8H5f0,3218
@@ -159,10 +161,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
159
161
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
162
  modal_version/__init__.py,sha256=RT6zPoOdFO99u5Wcxxaoir4ZCuPTbQ22cvzFAXl3vUY,470
161
163
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
162
- modal_version/_version_generated.py,sha256=WCQ6ahtgY9jKjpnPehIAR2jb6JCAyY8f-a6ORy1yMZE,148
163
- modal-0.68.1.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
164
- modal-0.68.1.dist-info/METADATA,sha256=ZRM5JqpOFrRmhBWuNm2S4AkEL1IaWjJi8xpbnLfl6hI,2328
165
- modal-0.68.1.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
166
- modal-0.68.1.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
167
- modal-0.68.1.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
168
- modal-0.68.1.dist-info/RECORD,,
164
+ modal_version/_version_generated.py,sha256=6Pvpcc5MvVX_YgJS7zOEdJe-DbGK8uv3kqiGt-OVVXE,148
165
+ modal-0.68.2.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
166
+ modal-0.68.2.dist-info/METADATA,sha256=5nS06eizxvISwpNNTho3610CO0mCCjpisIfo5STyfYY,2328
167
+ modal-0.68.2.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
168
+ modal-0.68.2.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
169
+ modal-0.68.2.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
170
+ modal-0.68.2.dist-info/RECORD,,
@@ -86,6 +86,7 @@ def run(output_dir: str = None):
86
86
  ("modal.Sandbox", "modal.sandbox"),
87
87
  ("modal.ContainerProcess", "modal.container_process"),
88
88
  ("modal.io_streams", "modal.io_streams"),
89
+ ("modal.FileIO", "modal.file_io"),
89
90
  ]
90
91
  # These aren't defined in `modal`, but should still be documented as top-level entries.
91
92
  forced_members = {"web_endpoint", "asgi_app", "method", "wsgi_app", "forward"}
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2024
2
2
 
3
3
  # Note: Reset this value to -1 whenever you make a minor `0.X` release of the client.
4
- build_number = 1 # git: a723471
4
+ build_number = 2 # git: bf9e3ce
File without changes