1+ import asyncio
2+
13from typing import AsyncIterator , IO , List , Literal , Optional , Union , cast , overload
24from http import HTTPStatus
35
@@ -464,11 +466,9 @@ async def read_file(
464466
465467 :param path: Path to the file
466468 :param format: Format of the file content—`text` by default
467- :param stream_idle_timeout: Idle timeout in **seconds** for a streamed
468- read (`format="stream"`)—abort if no chunk arrives within this
469- window while reading. Resets on every chunk, so it bounds a stalled
470- stream without limiting total transfer time. Defaults to the request
471- timeout; pass `0` to disable.
469+ :param stream_idle_timeout: Deprecated and ignored. A stalled streamed
470+ read is bounded by a transport-wide idle read timeout instead,
471+ which resets on every chunk.
472472 :param opts: Connection options
473473
474474 :return: File content as string, bytes, or async iterator of bytes
@@ -482,38 +482,45 @@ async def read_file(
482482 )
483483
484484 if format == "stream" :
485- # The request timeout bounds connection setup, not total transfer;
486- # consuming the body must not be killed by it. httpx's per-chunk
487- # `read` timeout becomes the idle-read timeout for the body
488- # (defaults to the request timeout), bounding a stalled stream
489- # without limiting total transfer time. Pass `0` to disable.
490- # Mirrors the sandbox files stream path.
491- idle_timeout = (
492- timeout if stream_idle_timeout is None else stream_idle_timeout
485+ # Through the pyqwest adapter a per-request timeout is a
486+ # whole-request deadline that would kill long downloads, so a
487+ # streamed read is sent with one only when the caller set
488+ # `request_timeout` explicitly (making it the total-transfer
489+ # deadline). A stalled stream is instead bounded by the
490+ # transport-wide idle read timeout (see `get_transport`), which
491+ # resets on every chunk without limiting total transfer time.
492+ stream_timeout = VolumeConnectionConfig ._get_request_timeout (
493+ None , opts .get ("request_timeout" )
493494 )
494- stream_timeout = httpx .Timeout (timeout , read = idle_timeout or None )
495495
496496 async def stream_file () -> AsyncIterator [bytes ]:
497- async with api_client .get_async_httpx_client ().stream (
498- method = "GET" ,
499- url = f"/volumecontent/{ self ._volume_id } /file" ,
500- params = params ,
501- timeout = stream_timeout ,
502- ) as response :
503- if response .status_code == 404 :
504- raise NotFoundException (f"Path { path } not found" )
505-
506- if response .status_code >= 300 :
507- api_response = Response (
508- status_code = HTTPStatus (response .status_code ),
509- content = await response .aread (),
510- headers = response .headers ,
511- parsed = None ,
512- )
513- raise handle_api_exception (api_response , VolumeException )
514-
515- async for chunk in response .aiter_bytes ():
516- yield chunk
497+ # pyqwest raises the builtin TimeoutError; keep the httpx
498+ # exception the streamed-read contract established.
499+ try :
500+ async with api_client .get_async_httpx_client ().stream (
501+ method = "GET" ,
502+ url = f"/volumecontent/{ self ._volume_id } /file" ,
503+ params = params ,
504+ timeout = stream_timeout ,
505+ ) as response :
506+ if response .status_code == 404 :
507+ raise NotFoundException (f"Path { path } not found" )
508+
509+ if response .status_code >= 300 :
510+ api_response = Response (
511+ status_code = HTTPStatus (response .status_code ),
512+ content = await response .aread (),
513+ headers = response .headers ,
514+ parsed = None ,
515+ )
516+ raise handle_api_exception (api_response , VolumeException )
517+
518+ async for chunk in response .aiter_bytes ():
519+ yield chunk
520+ # asyncio.TimeoutError is distinct from the builtin until 3.11,
521+ # and the adapter's whole-request deadline raises it.
522+ except (TimeoutError , asyncio .TimeoutError ) as e :
523+ raise httpx .ReadTimeout (str (e )) from e
517524
518525 return stream_file ()
519526
0 commit comments