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