3434import sys
3535import threading
3636import time
37- from typing import Any , AsyncIterator , Iterator , Optional , TYPE_CHECKING , Tuple , Union
37+ from typing import Any , AsyncIterator , Iterator , Optional , TYPE_CHECKING , Tuple , Union , cast , overload
3838from urllib .parse import urlparse
3939from urllib .parse import urlunparse
4040import warnings
@@ -638,6 +638,9 @@ def __del__(self) -> None:
638638class BaseApiClient :
639639 """Client for calling HTTP APIs sending and receiving JSON."""
640640
641+ vertexai : Optional [bool ] = None
642+ custom_base_url : Optional [str ] = None
643+
641644 def __init__ (
642645 self ,
643646 vertexai : Optional [bool ] = None ,
@@ -1962,21 +1965,55 @@ def _upload_fd(
19621965 raise ValueError ('Failed to upload file: Upload status is not finalized.' )
19631966 return HttpResponse (response .headers , response_stream = [response .text ])
19641967
1968+ @overload
19651969 def download_file (
19661970 self ,
19671971 path : str ,
19681972 * ,
19691973 http_options : Optional [HttpOptionsOrDict ] = None ,
1970- ) -> Union [Any , bytes ]:
1974+ destination : None = None ,
1975+ chunk_size : int = 1024 * 1024 ,
1976+ ) -> bytes :
1977+ ...
1978+
1979+ @overload
1980+ def download_file (
1981+ self ,
1982+ path : str ,
1983+ * ,
1984+ http_options : Optional [HttpOptionsOrDict ] = None ,
1985+ destination : Union [str , os .PathLike [str ], io .IOBase ],
1986+ chunk_size : int = 1024 * 1024 ,
1987+ ) -> None :
1988+ ...
1989+
1990+ def download_file (
1991+ self ,
1992+ path : str ,
1993+ * ,
1994+ http_options : Optional [HttpOptionsOrDict ] = None ,
1995+ destination : Optional [Union [str , os .PathLike [str ], io .IOBase ]] = None ,
1996+ chunk_size : int = 1024 * 1024 ,
1997+ ) -> Optional [bytes ]:
19711998 """Downloads the file data.
19721999
19732000 Args:
19742001 path: The request path with query params.
19752002 http_options: The http options to use for the request.
2003+ destination: Optional local file path or writable stream.
2004+ chunk_size: The chunk size in bytes to stream.
19762005
1977- returns :
1978- The file bytes
2006+ Returns :
2007+ The file bytes if destination is None, otherwise None.
19792008 """
2009+ if destination is not None and not isinstance (
2010+ destination , (str , os .PathLike )
2011+ ) and not hasattr (destination , 'write' ):
2012+ raise ValueError (
2013+ f'Unsupported destination type: { type (destination )} . '
2014+ 'Expected str, os.PathLike, or a writable file-like object.'
2015+ )
2016+
19802017 http_request = self ._build_request (
19812018 'get' , path = path , request_dict = {}, http_options = http_options
19822019 )
@@ -1988,18 +2025,86 @@ def download_file(
19882025 else :
19892026 data = http_request .data
19902027
1991- response = self ._httpx_client .request ( # type: ignore[union-attr]
1992- method = http_request .method ,
1993- url = http_request .url ,
1994- headers = http_request .headers ,
1995- content = data ,
1996- timeout = http_request .timeout ,
1997- )
2028+ def _write_chunks (chunks : Iterator [bytes ]) -> None :
2029+ if isinstance (destination , (str , os .PathLike )):
2030+ with open (destination , 'wb' ) as f :
2031+ for chunk in chunks :
2032+ f .write (chunk )
2033+ elif destination is not None and hasattr (destination , 'write' ):
2034+ for chunk in chunks :
2035+ destination .write (chunk )
19982036
1999- errors .APIError .raise_for_response (response )
2000- return HttpResponse (
2001- response .headers , byte_stream = [response .read ()]
2002- ).byte_stream [0 ]
2037+ if self ._use_google_auth_sync ():
2038+ url = str (http_request .url )
2039+ if self ._authorized_session is None :
2040+ from google .auth .transport .requests import AuthorizedSession # pylint: disable=g-import-not-at-top
2041+
2042+ self ._authorized_session = AuthorizedSession ( # type: ignore[no-untyped-call]
2043+ self ._credentials ,
2044+ max_refresh_attempts = 1 ,
2045+ )
2046+ client_cert_source = mtls .default_client_cert_source () # type: ignore[no-untyped-call]
2047+ self ._authorized_session .configure_mtls_channel (
2048+ client_cert_source
2049+ ) # type: ignore[no-untyped-call]
2050+ if self ._authorized_session ._is_mtls and 'googleapis.com' in url :
2051+ if 'sandbox' in url :
2052+ url = url .replace (
2053+ 'sandbox.googleapis.com' , 'mtls.sandbox.googleapis.com'
2054+ )
2055+ else :
2056+ url = url .replace ('googleapis.com' , 'mtls.googleapis.com' )
2057+ if destination is not None :
2058+ response = self ._authorized_session .request ( # type: ignore[no-untyped-call]
2059+ method = http_request .method .upper (),
2060+ url = url ,
2061+ data = data ,
2062+ headers = http_request .headers ,
2063+ timeout = http_request .timeout ,
2064+ stream = True ,
2065+ )
2066+ try :
2067+ errors .APIError .raise_for_response (response )
2068+ _write_chunks (response .iter_content (chunk_size = chunk_size ))
2069+ finally :
2070+ response .close ()
2071+ return None
2072+ else :
2073+ response = self ._authorized_session .request ( # type: ignore[no-untyped-call]
2074+ method = http_request .method .upper (),
2075+ url = url ,
2076+ data = data ,
2077+ headers = http_request .headers ,
2078+ timeout = http_request .timeout ,
2079+ )
2080+ errors .APIError .raise_for_response (response )
2081+ return cast (bytes , response .content )
2082+ else :
2083+ if destination is not None :
2084+ httpx_request = self ._httpx_client .build_request ( # type: ignore[union-attr]
2085+ method = http_request .method ,
2086+ url = http_request .url ,
2087+ content = data ,
2088+ headers = http_request .headers ,
2089+ timeout = http_request .timeout ,
2090+ )
2091+ response = self ._httpx_client .send (httpx_request , stream = True ) # type: ignore[union-attr, arg-type]
2092+ try :
2093+ errors .APIError .raise_for_response (response )
2094+ _write_chunks (response .iter_bytes (chunk_size = chunk_size ))
2095+ finally :
2096+ response .close ()
2097+ return None
2098+ else :
2099+ response = self ._httpx_client .request ( # type: ignore[union-attr]
2100+ method = http_request .method ,
2101+ url = http_request .url ,
2102+ content = data ,
2103+ headers = http_request .headers ,
2104+ timeout = http_request .timeout ,
2105+ )
2106+ errors .APIError .raise_for_response (response )
2107+ return cast (bytes , response .read ())
20032108
20042109 async def async_upload_file (
20052110 self ,
@@ -2237,21 +2342,55 @@ async def _async_upload_fd(
22372342 client_response .headers , response_stream = [client_response .text ]
22382343 )
22392344
2345+ @overload
22402346 async def async_download_file (
22412347 self ,
22422348 path : str ,
22432349 * ,
22442350 http_options : Optional [HttpOptionsOrDict ] = None ,
2245- ) -> Union [Any , bytes ]:
2246- """Downloads the file data.
2351+ destination : None = None ,
2352+ chunk_size : int = 1024 * 1024 ,
2353+ ) -> bytes :
2354+ ...
2355+
2356+ @overload
2357+ async def async_download_file (
2358+ self ,
2359+ path : str ,
2360+ * ,
2361+ http_options : Optional [HttpOptionsOrDict ] = None ,
2362+ destination : Union [str , os .PathLike [str ], io .IOBase ],
2363+ chunk_size : int = 1024 * 1024 ,
2364+ ) -> None :
2365+ ...
2366+
2367+ async def async_download_file (
2368+ self ,
2369+ path : str ,
2370+ * ,
2371+ http_options : Optional [HttpOptionsOrDict ] = None ,
2372+ destination : Optional [Union [str , os .PathLike [str ], io .IOBase ]] = None ,
2373+ chunk_size : int = 1024 * 1024 ,
2374+ ) -> Optional [bytes ]:
2375+ """Downloads the file data asynchronously.
22472376
22482377 Args:
22492378 path: The request path with query params.
22502379 http_options: The http options to use for the request.
2380+ destination: Optional local file path or writable stream.
2381+ chunk_size: The chunk size in bytes to stream.
22512382
2252- returns :
2253- The file bytes
2383+ Returns :
2384+ The file bytes if destination is None, otherwise None.
22542385 """
2386+ if destination is not None and not isinstance (
2387+ destination , (str , os .PathLike )
2388+ ) and not hasattr (destination , 'write' ):
2389+ raise ValueError (
2390+ f'Unsupported destination type: { type (destination )} . '
2391+ 'Expected str, os.PathLike, or a writable file-like object.'
2392+ )
2393+
22552394 http_request = self ._build_request (
22562395 'get' , path = path , request_dict = {}, http_options = http_options
22572396 )
@@ -2263,34 +2402,89 @@ async def async_download_file(
22632402 else :
22642403 data = http_request .data
22652404
2405+ async def _write_chunks (chunks : AsyncIterator [bytes ]) -> None :
2406+ if isinstance (destination , (str , os .PathLike )):
2407+ with open (destination , 'wb' ) as f :
2408+ async for chunk in chunks :
2409+ f .write (chunk )
2410+ elif destination is not None and hasattr (destination , 'write' ):
2411+ async for chunk in chunks :
2412+ res = destination .write (chunk )
2413+ if inspect .isawaitable (res ):
2414+ await res
2415+
22662416 if self ._use_aiohttp ():
22672417 session = await self ._get_aiohttp_session () # type: ignore[assignment]
2418+ url = http_request .url
2419+ if self ._use_google_auth_async ():
2420+ client_cert_source = mtls .default_client_cert_source () # type: ignore[no-untyped-call]
2421+ await session .configure_mtls_channel ( # type: ignore[union-attr]
2422+ client_cert_source
2423+ )
2424+ if session ._is_mtls and 'googleapis.com' in url : # type: ignore[union-attr]
2425+ if 'sandbox' in url :
2426+ url = url .replace (
2427+ 'sandbox.googleapis.com' , 'mtls.sandbox.googleapis.com'
2428+ )
2429+ else :
2430+ url = url .replace ('googleapis.com' , 'mtls.googleapis.com' )
22682431 response = await session .request ( # type: ignore[union-attr]
22692432 method = http_request .method ,
2270- url = http_request . url ,
2433+ url = url ,
22712434 headers = http_request .headers ,
22722435 data = data ,
22732436 timeout = aiohttp .ClientTimeout (total = http_request .timeout ),
2437+ ** self ._async_client_session_request_args ,
22742438 )
2275- await errors .APIError .raise_for_async_response (response )
2276-
2277- return HttpResponse (
2278- response .headers , byte_stream = [await response .read ()]
2279- ).byte_stream [0 ]
2439+ if destination is not None :
2440+ try :
2441+ await errors .APIError .raise_for_async_response (response )
2442+ if hasattr (response , '_response' ):
2443+ raw_response = response ._response
2444+ else :
2445+ raw_response = response
2446+ await _write_chunks (raw_response .content .iter_chunked (chunk_size ))
2447+ finally :
2448+ response .close ()
2449+ return None
2450+ else :
2451+ try :
2452+ await errors .APIError .raise_for_async_response (response )
2453+ return cast (bytes , await response .read ())
2454+ finally :
2455+ response .close ()
22802456 else :
22812457 # aiohttp is not available. Fall back to httpx.
2282- client_response = await self ._async_httpx_client .request ( # type: ignore[union-attr]
2283- method = http_request .method ,
2284- url = http_request .url ,
2285- headers = http_request .headers ,
2286- content = data ,
2287- timeout = http_request .timeout ,
2288- )
2289- await errors .APIError .raise_for_async_response (client_response )
2290-
2291- return HttpResponse (
2292- client_response .headers , byte_stream = [client_response .read ()]
2293- ).byte_stream [0 ]
2458+ if destination is not None :
2459+ httpx_request = self ._async_httpx_client .build_request ( # type: ignore[union-attr]
2460+ method = http_request .method ,
2461+ url = http_request .url ,
2462+ content = data ,
2463+ headers = http_request .headers ,
2464+ timeout = http_request .timeout ,
2465+ )
2466+ client_response = await self ._async_httpx_client .send ( # type: ignore[union-attr]
2467+ httpx_request , # type: ignore[arg-type]
2468+ stream = True ,
2469+ )
2470+ try :
2471+ await errors .APIError .raise_for_async_response (client_response )
2472+ await _write_chunks (
2473+ client_response .aiter_bytes (chunk_size = chunk_size )
2474+ )
2475+ finally :
2476+ await client_response .aclose ()
2477+ return None
2478+ else :
2479+ client_response = await self ._async_httpx_client .request ( # type: ignore[union-attr]
2480+ method = http_request .method ,
2481+ url = http_request .url ,
2482+ headers = http_request .headers ,
2483+ content = data ,
2484+ timeout = http_request .timeout ,
2485+ )
2486+ await errors .APIError .raise_for_async_response (client_response )
2487+ return cast (bytes , client_response .read ())
22942488
22952489 # This method does nothing in the real api client. It is used in the
22962490 # replay_api_client to verify the response from the SDK method matches the
0 commit comments