modal 0.71.7__py3-none-any.whl → 0.71.12__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.
@@ -116,7 +116,7 @@ class UserCodeEventLoop:
116
116
 
117
117
  def __enter__(self):
118
118
  self.loop = asyncio.new_event_loop()
119
- self.tasks = []
119
+ self.tasks = set()
120
120
  return self
121
121
 
122
122
  def __exit__(self, exc_type, exc_value, traceback):
@@ -130,7 +130,10 @@ class UserCodeEventLoop:
130
130
  self.loop.close()
131
131
 
132
132
  def create_task(self, coro):
133
- self.tasks.append(self.loop.create_task(coro))
133
+ task = self.loop.create_task(coro)
134
+ self.tasks.add(task)
135
+ task.add_done_callback(self.tasks.discard)
136
+ return task
134
137
 
135
138
  def run(self, coro):
136
139
  task = asyncio.ensure_future(coro, loop=self.loop)
@@ -531,10 +534,13 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
531
534
  with container_io_manager.handle_user_exception():
532
535
  finalized_functions = service.get_finalized_functions(function_def, container_io_manager)
533
536
  # Execute the function.
537
+ lifespan_background_tasks = []
534
538
  try:
535
539
  for finalized_function in finalized_functions.values():
536
540
  if finalized_function.lifespan_manager:
537
- event_loop.create_task(finalized_function.lifespan_manager.background_task())
541
+ lifespan_background_tasks.append(
542
+ event_loop.create_task(finalized_function.lifespan_manager.background_task())
543
+ )
538
544
  with container_io_manager.handle_user_exception():
539
545
  event_loop.run(finalized_function.lifespan_manager.lifespan_startup())
540
546
  call_function(
@@ -559,6 +565,10 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
559
565
  with container_io_manager.handle_user_exception():
560
566
  event_loop.run(finalized_function.lifespan_manager.lifespan_shutdown())
561
567
  finally:
568
+ # no need to keep the lifespan asgi call around - we send it no more messages
569
+ for lifespan_background_task in lifespan_background_tasks:
570
+ lifespan_background_task.cancel() # prevent dangling tasks
571
+
562
572
  # Identify "exit" methods and run them.
563
573
  # want to make sure this is called even if the lifespan manager fails
564
574
  if service.user_cls_instance is not None and not is_auto_snapshot:
modal/_runtime/asgi.py CHANGED
@@ -22,11 +22,11 @@ FIRST_MESSAGE_TIMEOUT_SECONDS = 5.0
22
22
 
23
23
 
24
24
  class LifespanManager:
25
- startup: asyncio.Future
26
- shutdown: asyncio.Future
27
- queue: asyncio.Queue
28
- has_run_init: bool = False
29
- lifespan_supported: bool = False
25
+ _startup: asyncio.Future
26
+ _shutdown: asyncio.Future
27
+ _queue: asyncio.Queue
28
+ _has_run_init: bool = False
29
+ _lifespan_supported: bool = False
30
30
 
31
31
  def __init__(self, asgi_app, state):
32
32
  self.asgi_app = asgi_app
@@ -37,59 +37,63 @@ class LifespanManager:
37
37
  # no async code since it has to run inside
38
38
  # the event loop to tie the
39
39
  # objects to the correct loop in python 3.9
40
- if not self.has_run_init:
41
- self.queue = asyncio.Queue()
42
- self.startup = asyncio.Future()
43
- self.shutdown = asyncio.Future()
44
- self.has_run_init = True
40
+ if not self._has_run_init:
41
+ self._queue = asyncio.Queue()
42
+ self._startup = asyncio.Future()
43
+ self._shutdown = asyncio.Future()
44
+ self._has_run_init = True
45
45
 
46
46
  async def background_task(self):
47
47
  await self.ensure_init()
48
48
 
49
49
  async def receive():
50
- self.lifespan_supported = True
51
- return await self.queue.get()
50
+ self._lifespan_supported = True
51
+ return await self._queue.get()
52
52
 
53
53
  async def send(message):
54
54
  if message["type"] == "lifespan.startup.complete":
55
- self.startup.set_result(None)
55
+ self._startup.set_result(None)
56
56
  elif message["type"] == "lifespan.startup.failed":
57
- self.startup.set_exception(ExecutionError("ASGI lifespan startup failed"))
57
+ self._startup.set_exception(ExecutionError("ASGI lifespan startup failed"))
58
58
  elif message["type"] == "lifespan.shutdown.complete":
59
- self.shutdown.set_result(None)
59
+ self._shutdown.set_result(None)
60
60
  elif message["type"] == "lifespan.shutdown.failed":
61
- self.shutdown.set_exception(ExecutionError("ASGI lifespan shutdown failed"))
61
+ self._shutdown.set_exception(ExecutionError("ASGI lifespan shutdown failed"))
62
62
  else:
63
63
  raise ExecutionError(f"Unexpected message type: {message['type']}")
64
64
 
65
65
  try:
66
66
  await self.asgi_app({"type": "lifespan", "state": self.state}, receive, send)
67
67
  except Exception as e:
68
- if not self.lifespan_supported:
68
+ if not self._lifespan_supported:
69
69
  logger.info(f"ASGI lifespan task exited before receiving any messages with exception:\n{e}")
70
- self.startup.set_result(None)
71
- self.shutdown.set_result(None)
70
+ if not self._startup.done():
71
+ self._startup.set_result(None)
72
+ if not self._shutdown.done():
73
+ self._shutdown.set_result(None)
72
74
  return
73
75
 
74
76
  logger.error(f"Error in ASGI lifespan task: {e}")
75
- if not self.startup.done():
76
- self.startup.set_exception(ExecutionError("ASGI lifespan task exited startup"))
77
- if not self.shutdown.done():
78
- self.shutdown.set_exception(ExecutionError("ASGI lifespan task exited shutdown"))
77
+ if not self._startup.done():
78
+ self._startup.set_exception(ExecutionError("ASGI lifespan task exited startup"))
79
+ if not self._shutdown.done():
80
+ self._shutdown.set_exception(ExecutionError("ASGI lifespan task exited shutdown"))
79
81
  else:
80
82
  logger.info("ASGI Lifespan protocol is probably not supported by this library")
81
- self.startup.set_result(None)
82
- self.shutdown.set_result(None)
83
+ if not self._startup.done():
84
+ self._startup.set_result(None)
85
+ if not self._shutdown.done():
86
+ self._shutdown.set_result(None)
83
87
 
84
88
  async def lifespan_startup(self):
85
89
  await self.ensure_init()
86
- self.queue.put_nowait({"type": "lifespan.startup"})
87
- await self.startup
90
+ self._queue.put_nowait({"type": "lifespan.startup"})
91
+ await self._startup
88
92
 
89
93
  async def lifespan_shutdown(self):
90
94
  await self.ensure_init()
91
- self.queue.put_nowait({"type": "lifespan.shutdown"})
92
- await self.shutdown
95
+ self._queue.put_nowait({"type": "lifespan.shutdown"})
96
+ await self._shutdown
93
97
 
94
98
 
95
99
  def asgi_app_wrapper(asgi_app, container_io_manager) -> tuple[Callable[..., AsyncGenerator], LifespanManager]:
modal/cli/volume.py CHANGED
@@ -287,3 +287,26 @@ async def delete(
287
287
  )
288
288
 
289
289
  await _Volume.delete(volume_name, environment_name=env)
290
+
291
+
292
+ @volume_cli.command(
293
+ name="rename",
294
+ help="Rename a modal.Volume.",
295
+ rich_help_panel="Management",
296
+ )
297
+ @synchronizer.create_blocking
298
+ async def rename(
299
+ old_name: str,
300
+ new_name: str,
301
+ yes: bool = YES_OPTION,
302
+ env: Optional[str] = ENV_OPTION,
303
+ ):
304
+ if not yes:
305
+ typer.confirm(
306
+ f"Are you sure you want rename the modal.Volume '{old_name}'?"
307
+ " This may break any Apps currently using it.",
308
+ default=False,
309
+ abort=True,
310
+ )
311
+
312
+ await _Volume.rename(old_name, new_name, environment_name=env)
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.7"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.12"
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.7"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.71.12"
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: ...