modal 0.71.8__py3-none-any.whl → 0.71.10__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.71.8"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.10"
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.71.8"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.10"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
modal/file_io.py CHANGED
@@ -12,8 +12,9 @@ import json
12
12
 
13
13
  from grpclib.exceptions import GRPCError, StreamTerminatedError
14
14
 
15
+ from modal._utils.async_utils import TaskContext
15
16
  from modal._utils.grpc_utils import retry_transient_errors
16
- from modal.io_streams_helper import consume_stream_with_retries
17
+ from modal.exception import ClientClosed
17
18
  from modal_proto import api_pb2
18
19
 
19
20
  from ._utils.async_utils import synchronize_api
@@ -56,7 +57,8 @@ async def _delete_bytes(file: "_FileIO", start: Optional[int] = None, end: Optio
56
57
  if start is not None and end is not None:
57
58
  if start >= end:
58
59
  raise ValueError("start must be less than end")
59
- resp = await file._make_request(
60
+ resp = await retry_transient_errors(
61
+ file._client.stub.ContainerFilesystemExec,
60
62
  api_pb2.ContainerFilesystemExecRequest(
61
63
  file_delete_bytes_request=api_pb2.ContainerFileDeleteBytesRequest(
62
64
  file_descriptor=file._file_descriptor,
@@ -64,7 +66,7 @@ async def _delete_bytes(file: "_FileIO", start: Optional[int] = None, end: Optio
64
66
  end_exclusive=end,
65
67
  ),
66
68
  task_id=file._task_id,
67
- )
69
+ ),
68
70
  )
69
71
  await file._wait(resp.exec_id)
70
72
 
@@ -83,7 +85,8 @@ async def _replace_bytes(file: "_FileIO", data: bytes, start: Optional[int] = No
83
85
  raise InvalidError("start must be less than end")
84
86
  if len(data) > WRITE_CHUNK_SIZE:
85
87
  raise InvalidError("Write request payload exceeds 16 MiB limit")
86
- resp = await file._make_request(
88
+ resp = await retry_transient_errors(
89
+ file._client.stub.ContainerFilesystemExec,
87
90
  api_pb2.ContainerFilesystemExecRequest(
88
91
  file_write_replace_bytes_request=api_pb2.ContainerFileWriteReplaceBytesRequest(
89
92
  file_descriptor=file._file_descriptor,
@@ -92,7 +95,7 @@ async def _replace_bytes(file: "_FileIO", data: bytes, start: Optional[int] = No
92
95
  end_exclusive=end,
93
96
  ),
94
97
  task_id=file._task_id,
95
- )
98
+ ),
96
99
  )
97
100
  await file._wait(resp.exec_id)
98
101
 
@@ -140,9 +143,13 @@ class _FileIO(Generic[T]):
140
143
 
141
144
  _task_id: str = ""
142
145
  _file_descriptor: str = ""
143
- _client: Optional[_Client] = None
146
+ _client: _Client
144
147
  _watch_output_buffer: list[Optional[bytes]] = []
145
148
 
149
+ def __init__(self, client: _Client, task_id: str) -> None:
150
+ self._client = client
151
+ self._task_id = task_id
152
+
146
153
  def _validate_mode(self, mode: str) -> None:
147
154
  if not any(char in mode for char in "rwax"):
148
155
  raise ValueError(f"Invalid file mode: {mode}")
@@ -175,7 +182,6 @@ class _FileIO(Generic[T]):
175
182
  exec_id=exec_id,
176
183
  timeout=55,
177
184
  )
178
- assert self._client is not None
179
185
  async for batch in self._client.stub.ContainerFilesystemExecGetOutput.unary_stream(req):
180
186
  if batch.eof:
181
187
  yield None
@@ -186,17 +192,30 @@ class _FileIO(Generic[T]):
186
192
  yield message
187
193
 
188
194
  async def _consume_watch_output(self, exec_id: str) -> None:
189
- def item_handler(item: Optional[bytes]):
190
- self._watch_output_buffer.append(item)
191
-
192
- def completion_check(item: Optional[bytes]):
193
- return item is None
195
+ completed = False
196
+ retries_remaining = 10
197
+ while not completed:
198
+ try:
199
+ iterator = self._consume_output(exec_id)
200
+ async for message in iterator:
201
+ self._watch_output_buffer.append(message)
202
+ if message is None:
203
+ completed = True
204
+ break
194
205
 
195
- await consume_stream_with_retries(
196
- self._consume_output(exec_id),
197
- item_handler,
198
- completion_check,
199
- )
206
+ except (GRPCError, StreamTerminatedError, ClientClosed) as exc:
207
+ if retries_remaining > 0:
208
+ retries_remaining -= 1
209
+ if isinstance(exc, GRPCError):
210
+ if exc.status in RETRYABLE_GRPC_STATUS_CODES:
211
+ await asyncio.sleep(1.0)
212
+ continue
213
+ elif isinstance(exc, StreamTerminatedError):
214
+ continue
215
+ elif isinstance(exc, ClientClosed):
216
+ # If the client was closed, the user has triggered a cleanup.
217
+ break
218
+ raise exc
200
219
 
201
220
  async def _parse_watch_output(self, event: bytes) -> Optional[FileWatchEvent]:
202
221
  try:
@@ -206,23 +225,6 @@ class _FileIO(Generic[T]):
206
225
  # skip invalid events
207
226
  return None
208
227
 
209
- async def _stream_watch_output(self) -> AsyncIterator[FileWatchEvent]:
210
- buffer = b""
211
- while True:
212
- if len(self._watch_output_buffer) > 0:
213
- item = self._watch_output_buffer.pop(0)
214
- if item is None:
215
- break
216
- buffer += item
217
- # a single event may be split across multiple messages, the end of an event is marked by two newlines
218
- if buffer.endswith(b"\n\n"):
219
- event = await self._parse_watch_output(buffer.strip())
220
- if event is not None:
221
- yield event
222
- buffer = b""
223
- else:
224
- await asyncio.sleep(0.1)
225
-
226
228
  async def _wait(self, exec_id: str) -> bytes:
227
229
  # The logic here is similar to how output is read from `exec`
228
230
  output = b""
@@ -254,11 +256,12 @@ class _FileIO(Generic[T]):
254
256
  raise TypeError("Expected str when in text mode")
255
257
 
256
258
  async def _open_file(self, path: str, mode: str) -> None:
257
- resp = await self._make_request(
259
+ resp = await retry_transient_errors(
260
+ self._client.stub.ContainerFilesystemExec,
258
261
  api_pb2.ContainerFilesystemExecRequest(
259
262
  file_open_request=api_pb2.ContainerFileOpenRequest(path=path, mode=mode),
260
263
  task_id=self._task_id,
261
- )
264
+ ),
262
265
  )
263
266
  if not resp.HasField("file_descriptor"):
264
267
  raise FilesystemExecutionError("Failed to open file")
@@ -270,26 +273,19 @@ class _FileIO(Generic[T]):
270
273
  cls, path: str, mode: Union["_typeshed.OpenTextMode", "_typeshed.OpenBinaryMode"], client: _Client, task_id: str
271
274
  ) -> "_FileIO":
272
275
  """Create a new FileIO handle."""
273
- self = cls.__new__(cls)
274
- self._client = client
275
- self._task_id = task_id
276
+ self = _FileIO(client, task_id)
276
277
  self._validate_mode(mode)
277
278
  await self._open_file(path, mode)
278
279
  self._closed = False
279
280
  return self
280
281
 
281
- async def _make_request(
282
- self, request: api_pb2.ContainerFilesystemExecRequest
283
- ) -> api_pb2.ContainerFilesystemExecResponse:
284
- assert self._client is not None
285
- return await retry_transient_errors(self._client.stub.ContainerFilesystemExec, request)
286
-
287
282
  async def _make_read_request(self, n: Optional[int]) -> bytes:
288
- resp = await self._make_request(
283
+ resp = await retry_transient_errors(
284
+ self._client.stub.ContainerFilesystemExec,
289
285
  api_pb2.ContainerFilesystemExecRequest(
290
286
  file_read_request=api_pb2.ContainerFileReadRequest(file_descriptor=self._file_descriptor, n=n),
291
287
  task_id=self._task_id,
292
- )
288
+ ),
293
289
  )
294
290
  return await self._wait(resp.exec_id)
295
291
 
@@ -308,11 +304,12 @@ class _FileIO(Generic[T]):
308
304
  """Read a single line from the current position."""
309
305
  self._check_closed()
310
306
  self._check_readable()
311
- resp = await self._make_request(
307
+ resp = await retry_transient_errors(
308
+ self._client.stub.ContainerFilesystemExec,
312
309
  api_pb2.ContainerFilesystemExecRequest(
313
310
  file_read_line_request=api_pb2.ContainerFileReadLineRequest(file_descriptor=self._file_descriptor),
314
311
  task_id=self._task_id,
315
- )
312
+ ),
316
313
  )
317
314
  output = await self._wait(resp.exec_id)
318
315
  if self._binary:
@@ -349,14 +346,15 @@ class _FileIO(Generic[T]):
349
346
  raise ValueError("Write request payload exceeds 1 GiB limit")
350
347
  for i in range(0, len(data), WRITE_CHUNK_SIZE):
351
348
  chunk = data[i : i + WRITE_CHUNK_SIZE]
352
- resp = await self._make_request(
349
+ resp = await retry_transient_errors(
350
+ self._client.stub.ContainerFilesystemExec,
353
351
  api_pb2.ContainerFilesystemExecRequest(
354
352
  file_write_request=api_pb2.ContainerFileWriteRequest(
355
353
  file_descriptor=self._file_descriptor,
356
354
  data=chunk,
357
355
  ),
358
356
  task_id=self._task_id,
359
- )
357
+ ),
360
358
  )
361
359
  await self._wait(resp.exec_id)
362
360
 
@@ -364,11 +362,12 @@ class _FileIO(Generic[T]):
364
362
  """Flush the buffer to disk."""
365
363
  self._check_closed()
366
364
  self._check_writable()
367
- resp = await self._make_request(
365
+ resp = await retry_transient_errors(
366
+ self._client.stub.ContainerFilesystemExec,
368
367
  api_pb2.ContainerFilesystemExecRequest(
369
368
  file_flush_request=api_pb2.ContainerFileFlushRequest(file_descriptor=self._file_descriptor),
370
369
  task_id=self._task_id,
371
- )
370
+ ),
372
371
  )
373
372
  await self._wait(resp.exec_id)
374
373
 
@@ -389,7 +388,8 @@ class _FileIO(Generic[T]):
389
388
  (relative to the current position) and 2 (relative to the file's end).
390
389
  """
391
390
  self._check_closed()
392
- resp = await self._make_request(
391
+ resp = await retry_transient_errors(
392
+ self._client.stub.ContainerFilesystemExec,
393
393
  api_pb2.ContainerFilesystemExecRequest(
394
394
  file_seek_request=api_pb2.ContainerFileSeekRequest(
395
395
  file_descriptor=self._file_descriptor,
@@ -397,21 +397,20 @@ class _FileIO(Generic[T]):
397
397
  whence=self._get_whence(whence),
398
398
  ),
399
399
  task_id=self._task_id,
400
- )
400
+ ),
401
401
  )
402
402
  await self._wait(resp.exec_id)
403
403
 
404
404
  @classmethod
405
405
  async def ls(cls, path: str, client: _Client, task_id: str) -> list[str]:
406
406
  """List the contents of the provided directory."""
407
- self = cls.__new__(cls)
408
- self._client = client
409
- self._task_id = task_id
410
- resp = await self._make_request(
407
+ self = _FileIO(client, task_id)
408
+ resp = await retry_transient_errors(
409
+ self._client.stub.ContainerFilesystemExec,
411
410
  api_pb2.ContainerFilesystemExecRequest(
412
411
  file_ls_request=api_pb2.ContainerFileLsRequest(path=path),
413
412
  task_id=task_id,
414
- )
413
+ ),
415
414
  )
416
415
  output = await self._wait(resp.exec_id)
417
416
  try:
@@ -422,28 +421,26 @@ class _FileIO(Generic[T]):
422
421
  @classmethod
423
422
  async def mkdir(cls, path: str, client: _Client, task_id: str, parents: bool = False) -> None:
424
423
  """Create a new directory."""
425
- self = cls.__new__(cls)
426
- self._client = client
427
- self._task_id = task_id
428
- resp = await self._make_request(
424
+ self = _FileIO(client, task_id)
425
+ resp = await retry_transient_errors(
426
+ self._client.stub.ContainerFilesystemExec,
429
427
  api_pb2.ContainerFilesystemExecRequest(
430
428
  file_mkdir_request=api_pb2.ContainerFileMkdirRequest(path=path, make_parents=parents),
431
429
  task_id=self._task_id,
432
- )
430
+ ),
433
431
  )
434
432
  await self._wait(resp.exec_id)
435
433
 
436
434
  @classmethod
437
435
  async def rm(cls, path: str, client: _Client, task_id: str, recursive: bool = False) -> None:
438
436
  """Remove a file or directory in the Sandbox."""
439
- self = cls.__new__(cls)
440
- self._client = client
441
- self._task_id = task_id
442
- resp = await self._make_request(
437
+ self = _FileIO(client, task_id)
438
+ resp = await retry_transient_errors(
439
+ self._client.stub.ContainerFilesystemExec,
443
440
  api_pb2.ContainerFilesystemExecRequest(
444
441
  file_rm_request=api_pb2.ContainerFileRmRequest(path=path, recursive=recursive),
445
442
  task_id=self._task_id,
446
- )
443
+ ),
447
444
  )
448
445
  await self._wait(resp.exec_id)
449
446
 
@@ -457,10 +454,9 @@ class _FileIO(Generic[T]):
457
454
  recursive: bool = False,
458
455
  timeout: Optional[int] = None,
459
456
  ) -> AsyncIterator[FileWatchEvent]:
460
- self = cls.__new__(cls)
461
- self._client = client
462
- self._task_id = task_id
463
- resp = await self._make_request(
457
+ self = _FileIO(client, task_id)
458
+ resp = await retry_transient_errors(
459
+ self._client.stub.ContainerFilesystemExec,
464
460
  api_pb2.ContainerFilesystemExecRequest(
465
461
  file_watch_request=api_pb2.ContainerFileWatchRequest(
466
462
  path=path,
@@ -468,22 +464,44 @@ class _FileIO(Generic[T]):
468
464
  timeout_secs=timeout,
469
465
  ),
470
466
  task_id=self._task_id,
471
- )
467
+ ),
472
468
  )
473
- task = asyncio.create_task(self._consume_watch_output(resp.exec_id))
474
- async for event in self._stream_watch_output():
475
- if filter and event.type not in filter:
476
- continue
477
- yield event
478
- task.cancel()
469
+ async with TaskContext() as tc:
470
+ tc.create_task(self._consume_watch_output(resp.exec_id))
471
+
472
+ buffer = b""
473
+ while True:
474
+ if len(self._watch_output_buffer) > 0:
475
+ item = self._watch_output_buffer.pop(0)
476
+ if item is None:
477
+ break
478
+ buffer += item
479
+ # a single event may be split across multiple messages
480
+ # the end of an event is marked by two newlines
481
+ if buffer.endswith(b"\n\n"):
482
+ try:
483
+ event_json = json.loads(buffer.strip().decode())
484
+ event = FileWatchEvent(
485
+ type=FileWatchEventType(event_json["event_type"]),
486
+ paths=event_json["paths"],
487
+ )
488
+ if not filter or event.type in filter:
489
+ yield event
490
+ except (json.JSONDecodeError, KeyError, ValueError):
491
+ # skip invalid events
492
+ pass
493
+ buffer = b""
494
+ else:
495
+ await asyncio.sleep(0.1)
479
496
 
480
497
  async def _close(self) -> None:
481
498
  # Buffer is flushed by the runner on close
482
- resp = await self._make_request(
499
+ resp = await retry_transient_errors(
500
+ self._client.stub.ContainerFilesystemExec,
483
501
  api_pb2.ContainerFilesystemExecRequest(
484
502
  file_close_request=api_pb2.ContainerFileCloseRequest(file_descriptor=self._file_descriptor),
485
503
  task_id=self._task_id,
486
- )
504
+ ),
487
505
  )
488
506
  self._closed = True
489
507
  await self._wait(resp.exec_id)
modal/file_io.pyi CHANGED
@@ -32,15 +32,15 @@ class FileWatchEvent:
32
32
  class _FileIO(typing.Generic[T]):
33
33
  _task_id: str
34
34
  _file_descriptor: str
35
- _client: typing.Optional[modal.client._Client]
35
+ _client: modal.client._Client
36
36
  _watch_output_buffer: list[typing.Optional[bytes]]
37
37
 
38
+ def __init__(self, client: modal.client._Client, task_id: str) -> None: ...
38
39
  def _validate_mode(self, mode: str) -> None: ...
39
40
  def _handle_error(self, error: modal_proto.api_pb2.SystemErrorMessage) -> None: ...
40
41
  def _consume_output(self, exec_id: str) -> typing.AsyncIterator[typing.Optional[bytes]]: ...
41
42
  async def _consume_watch_output(self, exec_id: str) -> None: ...
42
43
  async def _parse_watch_output(self, event: bytes) -> typing.Optional[FileWatchEvent]: ...
43
- def _stream_watch_output(self) -> typing.AsyncIterator[FileWatchEvent]: ...
44
44
  async def _wait(self, exec_id: str) -> bytes: ...
45
45
  def _validate_type(self, data: typing.Union[bytes, str]) -> None: ...
46
46
  async def _open_file(self, path: str, mode: str) -> None: ...
@@ -52,9 +52,6 @@ class _FileIO(typing.Generic[T]):
52
52
  client: modal.client._Client,
53
53
  task_id: str,
54
54
  ) -> _FileIO: ...
55
- async def _make_request(
56
- self, request: modal_proto.api_pb2.ContainerFilesystemExecRequest
57
- ) -> modal_proto.api_pb2.ContainerFilesystemExecResponse: ...
58
55
  async def _make_read_request(self, n: typing.Optional[int]) -> bytes: ...
59
56
  async def read(self, n: typing.Optional[int] = None) -> T: ...
60
57
  async def readline(self) -> T: ...
@@ -108,10 +105,10 @@ T_INNER = typing.TypeVar("T_INNER", covariant=True)
108
105
  class FileIO(typing.Generic[T]):
109
106
  _task_id: str
110
107
  _file_descriptor: str
111
- _client: typing.Optional[modal.client.Client]
108
+ _client: modal.client.Client
112
109
  _watch_output_buffer: list[typing.Optional[bytes]]
113
110
 
114
- def __init__(self, /, *args, **kwargs): ...
111
+ def __init__(self, client: modal.client.Client, task_id: str) -> None: ...
115
112
  def _validate_mode(self, mode: str) -> None: ...
116
113
  def _handle_error(self, error: modal_proto.api_pb2.SystemErrorMessage) -> None: ...
117
114
 
@@ -133,12 +130,6 @@ class FileIO(typing.Generic[T]):
133
130
 
134
131
  _parse_watch_output: ___parse_watch_output_spec
135
132
 
136
- class ___stream_watch_output_spec(typing_extensions.Protocol):
137
- def __call__(self) -> typing.Iterator[FileWatchEvent]: ...
138
- def aio(self) -> typing.AsyncIterator[FileWatchEvent]: ...
139
-
140
- _stream_watch_output: ___stream_watch_output_spec
141
-
142
133
  class ___wait_spec(typing_extensions.Protocol):
143
134
  def __call__(self, exec_id: str) -> bytes: ...
144
135
  async def aio(self, exec_id: str) -> bytes: ...
@@ -162,16 +153,6 @@ class FileIO(typing.Generic[T]):
162
153
  task_id: str,
163
154
  ) -> FileIO: ...
164
155
 
165
- class ___make_request_spec(typing_extensions.Protocol):
166
- def __call__(
167
- self, request: modal_proto.api_pb2.ContainerFilesystemExecRequest
168
- ) -> modal_proto.api_pb2.ContainerFilesystemExecResponse: ...
169
- async def aio(
170
- self, request: modal_proto.api_pb2.ContainerFilesystemExecRequest
171
- ) -> modal_proto.api_pb2.ContainerFilesystemExecResponse: ...
172
-
173
- _make_request: ___make_request_spec
174
-
175
156
  class ___make_read_request_spec(typing_extensions.Protocol):
176
157
  def __call__(self, n: typing.Optional[int]) -> bytes: ...
177
158
  async def aio(self, n: typing.Optional[int]) -> bytes: ...
modal/functions.pyi CHANGED
@@ -462,11 +462,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
462
462
 
463
463
  _call_generator_nowait: ___call_generator_nowait_spec
464
464
 
465
- class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
465
+ class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
466
466
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
467
467
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
468
468
 
469
- remote: __remote_spec[P, ReturnType]
469
+ remote: __remote_spec[ReturnType, P]
470
470
 
471
471
  class __remote_gen_spec(typing_extensions.Protocol):
472
472
  def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
@@ -479,17 +479,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
479
479
  def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
480
480
  def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
481
481
 
482
- class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
482
+ class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
483
483
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
484
484
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
485
485
 
486
- _experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
486
+ _experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
487
487
 
488
- class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
488
+ class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
489
489
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
490
490
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
491
491
 
492
- spawn: __spawn_spec[P, ReturnType]
492
+ spawn: __spawn_spec[ReturnType, P]
493
493
 
494
494
  def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
495
495
 
modal/io_streams.py CHANGED
@@ -14,8 +14,7 @@ from typing import (
14
14
  from grpclib import Status
15
15
  from grpclib.exceptions import GRPCError, StreamTerminatedError
16
16
 
17
- from modal.exception import InvalidError
18
- from modal.io_streams_helper import consume_stream_with_retries
17
+ from modal.exception import ClientClosed, InvalidError
19
18
  from modal_proto import api_pb2
20
19
 
21
20
  from ._utils.async_utils import synchronize_api
@@ -177,21 +176,34 @@ class _StreamReader(Generic[T]):
177
176
  if self._stream_type == StreamType.DEVNULL:
178
177
  return
179
178
 
180
- def item_handler(item: Optional[bytes]):
181
- if self._stream_type == StreamType.STDOUT and item is not None:
182
- print(item.decode("utf-8"), end="")
183
- elif self._stream_type == StreamType.PIPE:
184
- self._container_process_buffer.append(item)
179
+ completed = False
180
+ retries_remaining = 10
181
+ while not completed:
182
+ try:
183
+ iterator = _container_process_logs_iterator(self._object_id, self._file_descriptor, self._client)
185
184
 
186
- def completion_check(item: Optional[bytes]):
187
- return item is None
185
+ async for message in iterator:
186
+ if self._stream_type == StreamType.STDOUT and message:
187
+ print(message.decode("utf-8"), end="")
188
+ elif self._stream_type == StreamType.PIPE:
189
+ self._container_process_buffer.append(message)
190
+ if message is None:
191
+ completed = True
192
+ break
188
193
 
189
- iterator = _container_process_logs_iterator(self._object_id, self._file_descriptor, self._client)
190
- await consume_stream_with_retries(
191
- iterator,
192
- item_handler,
193
- completion_check,
194
- )
194
+ except (GRPCError, StreamTerminatedError, ClientClosed) as exc:
195
+ if retries_remaining > 0:
196
+ retries_remaining -= 1
197
+ if isinstance(exc, GRPCError):
198
+ if exc.status in RETRYABLE_GRPC_STATUS_CODES:
199
+ await asyncio.sleep(1.0)
200
+ continue
201
+ elif isinstance(exc, StreamTerminatedError):
202
+ continue
203
+ elif isinstance(exc, ClientClosed):
204
+ # If the client was closed, the user has triggered a cleanup.
205
+ break
206
+ raise exc
195
207
 
196
208
  async def _stream_container_process(self) -> AsyncGenerator[tuple[Optional[bytes], str], None]:
197
209
  """Streams the container process buffer to the reader."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modal
3
- Version: 0.71.8
3
+ Version: 0.71.10
4
4
  Summary: Python client library for Modal
5
5
  Author: Modal Labs
6
6
  Author-email: support@modal.com
@@ -19,7 +19,7 @@ modal/app.py,sha256=vEE0cK5QPF6_cdW5AJvcuWxz5KmeprHwBEtlDkVRHgE,45582
19
19
  modal/app.pyi,sha256=Gx7gxjfQ70sxhbwfpx1VjvzEON-ZEMTJ_Vy8qt0oQvo,25302
20
20
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
21
21
  modal/client.py,sha256=JAnd4-GCN093BwkvOFAK5a6iy5ycxofjpUncMxlrIMw,15253
22
- modal/client.pyi,sha256=TrQn13Pm1t7N2XRd5-jvxe5zzSL69I5EcZndQUyFmcM,7278
22
+ modal/client.pyi,sha256=3aJYJhiUvXueGEy38n1ScXHG9s4C1UlDRT6yXNwUons,7280
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=3hjb0JcoPjxKZNeK22f5rR43bZRBjoRI7_EMZXY7YrE,31172
@@ -33,17 +33,16 @@ modal/environments.py,sha256=wbv9ttFCbzATGfwcmvYiG608PfHovx0AQmawsg-jmic,6660
33
33
  modal/environments.pyi,sha256=rF7oaaELoSNuoD6qImGnIbuGPtgWwR5SlcExyYJ61hQ,3515
34
34
  modal/exception.py,sha256=4JyO-SACaLNDe2QC48EjsK8GMkZ8AgEurZ8j1YdRu8E,5263
35
35
  modal/experimental.py,sha256=npfKbyMpI41uZZs9HW_QiB3E4ykWfDXZbACXXbw6qeA,2385
36
- modal/file_io.py,sha256=ZR8VBCDsDt5uB9TNN9XbEh7sniJzM_5YL47m8WP0m5c,19617
37
- modal/file_io.pyi,sha256=79Fg75BjmMEeCX0Lx-Z8C4XSNPCotwNdK6ZLIDFm2f4,9770
36
+ modal/file_io.py,sha256=lcMs_E9Xfm0YX1t9U2wNIBPnqHRxmImqjLW1GHqVmyg,20945
37
+ modal/file_io.pyi,sha256=NrIoB0YjIqZ8MDMe826xAnybT0ww_kxQM3iPLo82REU,8898
38
38
  modal/file_pattern_matcher.py,sha256=LaI7Paxg0xR9D-D7Tgc60xR0w1KZee22LjGbFie1Vms,5571
39
39
  modal/functions.py,sha256=3uJPbrEAWhpFfLfUnoRjGmvEUC-_wVh-8yNJBx8eVeM,68249
40
- modal/functions.pyi,sha256=LiSDgH-X7jcZ56pAoLMwo3x9Dzdp_3Sd7W5MVAJPoCg,25407
40
+ modal/functions.pyi,sha256=3ESJ61f8oEDycDmrpnuNB2vjFKuLBG_aqyliXPTdY7M,25407
41
41
  modal/gpu.py,sha256=MTxj6ql8EpgfBg8YmZ5a1cLznyuZFssX1qXbEX4LKVM,7503
42
42
  modal/image.py,sha256=oKqqLhc3Ap2XMG5MKVlERKkMTwJPkNMNcSzxoZh4zuw,85259
43
43
  modal/image.pyi,sha256=Pa1_LVr3FyNsnu_MhBO08fBgCeLazTEe25phYdu0bzE,25365
44
- modal/io_streams.py,sha256=Xxc5grJiO94nBA48FFWH3S3g6SPR0xFVgZ_DZ1oFmvI,14428
44
+ modal/io_streams.py,sha256=QkQiizKRzd5bnbKQsap31LJgBYlAnj4-XkV_50xPYX0,15079
45
45
  modal/io_streams.pyi,sha256=bCCVSxkMcosYd8O3PQDDwJw7TQ8JEcnYonLJ5t27TQs,4804
46
- modal/io_streams_helper.py,sha256=B5Ui56ph7LkRpZX0tAF80Q-gOMsvPPLx5bpIPX0kgDc,1772
47
46
  modal/mount.py,sha256=wOr-2vmKImsE3lHBII8hL2gYy5ng46R58QwId4JultQ,29313
48
47
  modal/mount.pyi,sha256=FiNV1wIKFvd0ZMZ0tm1mz6ZSA5Hjsge-kFSA5tPWfcI,10503
49
48
  modal/network_file_system.py,sha256=INj1TfN_Fsmabmlte7anvey1epodjbMmjBW_TIJSST4,14406
@@ -149,13 +148,13 @@ modal_global_objects/mounts/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0
149
148
  modal_global_objects/mounts/modal_client_package.py,sha256=W0E_yShsRojPzWm6LtIQqNVolapdnrZkm2hVEQuZK_4,767
150
149
  modal_global_objects/mounts/python_standalone.py,sha256=SL_riIxpd8mP4i4CLDCWiFFNj0Ltknm9c_UIGfX5d60,1836
151
150
  modal_proto/__init__.py,sha256=MIEP8jhXUeGq_eCjYFcqN5b1bxBM4fdk0VESpjWR0fc,28
152
- modal_proto/api.proto,sha256=ERfCFygQvpd09LyVtpZ-eInPfJ4p6UIDh_z3za_rbsw,80174
153
- modal_proto/api_grpc.py,sha256=MLsxlZXikcv36aSs0iVZywIOANMWstVZMWJGQTDjdM0,102823
154
- modal_proto/api_pb2.py,sha256=dJJ88RIIPlwyTBQk9rKfSfbAxeZXDZn7FpszS6mF0Qs,294984
155
- modal_proto/api_pb2.pyi,sha256=I1cHME11tDMerj_8DzHF2SEwN1QorEfdjziW3_v-omA,393699
156
- modal_proto/api_pb2_grpc.py,sha256=4d5SwJPLldCqqHz_BGqmJLlX-BMG592WFKUQ6IBg5rg,222415
157
- modal_proto/api_pb2_grpc.pyi,sha256=C4c3jndI6TKEgVnx3vAmT86In4T9JKmB1CImiye_aQk,51822
158
- modal_proto/modal_api_grpc.py,sha256=eeH1vHXgwO768tM7DXqvj-P37u15SI00gZtm8_EK15I,13732
151
+ modal_proto/api.proto,sha256=1hO6_dn7DwgFra9TQSAXBt1NV4ETiiURPHe09bodinc,80368
152
+ modal_proto/api_grpc.py,sha256=VakjV_Ge3fgZDRJN6EeG2yY_LMkZvn6yVXr5SnFKIDA,103542
153
+ modal_proto/api_pb2.py,sha256=77arFb-gAsuI-7Rbr65FxAB13i-mPoJEHYvs7z5Uo1s,295839
154
+ modal_proto/api_pb2.pyi,sha256=iGrfDlLXPg8oC9QdWOFlCrNoJqB0FompkpVa-EskYOg,394251
155
+ modal_proto/api_pb2_grpc.py,sha256=OAvrG7OCPKmF-6eawqc4X5iLN0WFBQ_AvufuWnHm3Es,224030
156
+ modal_proto/api_pb2_grpc.pyi,sha256=-bE8AO-IJsg0WMwSzEEe_jnmeirta3tqj5IwTVIrF6c,52169
157
+ modal_proto/modal_api_grpc.py,sha256=kOABHGzB81bAhxqKwj79DKqdK2HlcC4U0MkFETkQwfQ,13826
159
158
  modal_proto/modal_options_grpc.py,sha256=qJ1cuwA54oRqrdTyPTbvfhFZYd9HhJKK5UCwt523r3Y,120
160
159
  modal_proto/options.proto,sha256=a-siq4swVbZPfaFRXAipRZzGP2bq8OsdUvjlyzAeodQ,488
161
160
  modal_proto/options_grpc.py,sha256=M18X3d-8F_cNYSVM3I25dUTO5rZ0rd-vCCfynfh13Nc,125
@@ -166,10 +165,10 @@ modal_proto/options_pb2_grpc.pyi,sha256=CImmhxHsYnF09iENPoe8S4J-n93jtgUYD2JPAc0y
166
165
  modal_proto/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
167
166
  modal_version/__init__.py,sha256=BEBWj9tcbFUwzEjUrqly601rauw5cYsHdcmJHs3iu0s,470
168
167
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
169
- modal_version/_version_generated.py,sha256=Wsequ_fpOFVKI_31Vx8o7dmn9xo9m9A_dDR6wZzyDIU,148
170
- modal-0.71.8.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
171
- modal-0.71.8.dist-info/METADATA,sha256=KRAySrnrs0NscfurQ57XIYvXEEJ9FKumg47sf3ufFi8,2328
172
- modal-0.71.8.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
173
- modal-0.71.8.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
174
- modal-0.71.8.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
175
- modal-0.71.8.dist-info/RECORD,,
168
+ modal_version/_version_generated.py,sha256=UflrBs-PkGVliapv6qAxiCSr0k-pq7eJQFsV4Q_x40g,149
169
+ modal-0.71.10.dist-info/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
170
+ modal-0.71.10.dist-info/METADATA,sha256=QOEXynkfqz3tQcZs7zhfsXbS3sgF5E3ExlEac6Q9yAI,2329
171
+ modal-0.71.10.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
172
+ modal-0.71.10.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
173
+ modal-0.71.10.dist-info/top_level.txt,sha256=1nvYbOSIKcmU50fNrpnQnrrOpj269ei3LzgB6j9xGqg,64
174
+ modal-0.71.10.dist-info/RECORD,,
modal_proto/api.proto CHANGED
@@ -2680,6 +2680,11 @@ message VolumeRemoveFileRequest {
2680
2680
  bool recursive = 3;
2681
2681
  }
2682
2682
 
2683
+ message VolumeRenameRequest {
2684
+ string volume_id = 1 [ (modal.options.audit_target_attr) = true ];
2685
+ string name = 2;
2686
+ }
2687
+
2683
2688
  message Warning {
2684
2689
  enum WarningType {
2685
2690
  WARNING_TYPE_UNSPECIFIED = 0;
@@ -2887,6 +2892,7 @@ service ModalClient {
2887
2892
  rpc VolumePutFiles(VolumePutFilesRequest) returns (google.protobuf.Empty);
2888
2893
  rpc VolumeReload(VolumeReloadRequest) returns (google.protobuf.Empty);
2889
2894
  rpc VolumeRemoveFile(VolumeRemoveFileRequest) returns (google.protobuf.Empty);
2895
+ rpc VolumeRename(VolumeRenameRequest) returns (google.protobuf.Empty);
2890
2896
 
2891
2897
  // Workspaces
2892
2898
  rpc WorkspaceNameLookup(google.protobuf.Empty) returns (WorkspaceNameLookupResponse);