33from __future__ import annotations
44
55import asyncio
6+ import json
67import os
78import uuid
8- from collections .abc import Mapping , Sequence
9+ from collections .abc import AsyncIterator , Iterator , Mapping , Sequence
910from contextlib import ExitStack
1011from os import PathLike
1112from pathlib import Path
5455UrlValue = str | URL
5556UrlInput = UrlValue | Sequence [UrlValue ]
5657NormalizedFileTypes : TypeAlias = FileContent | FileTuple2 | FileTuple3 | FileTuple4
58+ DestinationPath = str | PathLike [str ]
5759
5860
5961def _extract_uploaded_file_ids (payload : Any ) -> list [str ]:
@@ -212,6 +214,10 @@ def _normalize_url_inputs(urls: UrlInput) -> list[str]:
212214 return normalized
213215
214216
217+ def _resolve_file_id (file_ref : PdfRestFile | str ) -> str :
218+ return file_ref .id if isinstance (file_ref , PdfRestFile ) else str (file_ref )
219+
220+
215221ClientType = TypeVar ("ClientType" , httpx .Client , httpx .AsyncClient )
216222
217223
@@ -572,6 +578,19 @@ def _send_request(self, request: _RequestModel) -> Any:
572578 def send_request (self , request : _RequestModel ) -> Any :
573579 return self ._send_request (request )
574580
581+ def download_file (self , file_id : str ) -> httpx .Response :
582+ request = self ._client .build_request ("GET" , f"/resource/{ file_id } " )
583+ try :
584+ response = self ._client .send (request , stream = True )
585+ except httpx .HTTPError as exc :
586+ raise translate_httpx_error (exc ) from exc
587+ if not response .is_success :
588+ try :
589+ self ._handle_response (response )
590+ finally :
591+ response .close ()
592+ return response
593+
575594 def fetch_file_info (self , file_id : str ) -> PdfRestFile :
576595 request = self .prepare_request (
577596 "GET" ,
@@ -641,6 +660,19 @@ async def _send_request(self, request: _RequestModel) -> Any:
641660 async def send_request (self , request : _RequestModel ) -> Any :
642661 return await self ._send_request (request )
643662
663+ async def download_file (self , file_id : str ) -> httpx .Response :
664+ request = self ._client .build_request ("GET" , f"/resource/{ file_id } " )
665+ try :
666+ response = await self ._client .send (request , stream = True )
667+ except httpx .HTTPError as exc :
668+ raise translate_httpx_error (exc ) from exc
669+ if not response .is_success :
670+ try :
671+ self ._handle_response (response )
672+ finally :
673+ await response .aclose ()
674+ return response
675+
644676 async def fetch_file_info (self , file_id : str ) -> PdfRestFile :
645677 request = self .prepare_request (
646678 "GET" ,
@@ -651,6 +683,66 @@ async def fetch_file_info(self, file_id: str) -> PdfRestFile:
651683 return PdfRestFile .model_validate (payload )
652684
653685
686+ class PdfRestFileStream :
687+ """Streaming wrapper for synchronously downloading files from pdfRest."""
688+
689+ def __init__ (self , response : httpx .Response ) -> None :
690+ self ._response = response
691+
692+ def iter_bytes (self , chunk_size : int | None = None ) -> Iterator [bytes ]:
693+ yield from self ._response .iter_bytes (chunk_size )
694+
695+ def iter_text (self , chunk_size : int | None = None ) -> Iterator [str ]:
696+ yield from self ._response .iter_text (chunk_size )
697+
698+ def iter_lines (self ) -> Iterator [str ]:
699+ yield from self ._response .iter_lines ()
700+
701+ def iter_raw (self , chunk_size : int | None = None ) -> Iterator [bytes ]:
702+ yield from self ._response .iter_raw (chunk_size )
703+
704+ def close (self ) -> None :
705+ self ._response .close ()
706+
707+ def __enter__ (self ) -> PdfRestFileStream :
708+ return self
709+
710+ def __exit__ (self , exc_type : Any , exc : Any , traceback : Any ) -> None :
711+ self .close ()
712+
713+
714+ class AsyncPdfRestFileStream :
715+ """Streaming wrapper for asynchronously downloading files from pdfRest."""
716+
717+ def __init__ (self , response : httpx .Response ) -> None :
718+ self ._response = response
719+
720+ async def iter_bytes (self , chunk_size : int | None = None ) -> AsyncIterator [bytes ]:
721+ async for chunk in self ._response .aiter_bytes (chunk_size ):
722+ yield chunk
723+
724+ async def iter_text (self , chunk_size : int | None = None ) -> AsyncIterator [str ]:
725+ async for chunk in self ._response .aiter_text (chunk_size ):
726+ yield chunk
727+
728+ async def iter_lines (self ) -> AsyncIterator [str ]:
729+ async for line in self ._response .aiter_lines ():
730+ yield line
731+
732+ async def iter_raw (self , chunk_size : int | None = None ) -> AsyncIterator [bytes ]:
733+ async for chunk in self ._response .aiter_raw (chunk_size ):
734+ yield chunk
735+
736+ async def close (self ) -> None :
737+ await self ._response .aclose ()
738+
739+ async def __aenter__ (self ) -> AsyncPdfRestFileStream :
740+ return self
741+
742+ async def __aexit__ (self , exc_type : Any , exc : Any , traceback : Any ) -> None :
743+ await self .close ()
744+
745+
654746class _FilesClient :
655747 """Expose file-related operations for the synchronous client."""
656748
@@ -709,6 +801,56 @@ def create_from_urls(self, urls: UrlInput) -> list[PdfRestFile]:
709801 file_ids = _extract_uploaded_file_ids (payload )
710802 return [self ._client .fetch_file_info (file_id ) for file_id in file_ids ]
711803
804+ def read_bytes (self , file_ref : PdfRestFile | str ) -> bytes :
805+ response = self ._client .download_file (_resolve_file_id (file_ref ))
806+ try :
807+ return response .read ()
808+ finally :
809+ response .close ()
810+
811+ def read_text (
812+ self ,
813+ file_ref : PdfRestFile | str ,
814+ * ,
815+ encoding : str = "utf-8" ,
816+ ) -> str :
817+ response = self ._client .download_file (_resolve_file_id (file_ref ))
818+ try :
819+ response .encoding = encoding
820+ data = response .read ()
821+ codec = response .encoding or encoding or "utf-8"
822+ return data .decode (codec )
823+ finally :
824+ response .close ()
825+
826+ def read_json (self , file_ref : PdfRestFile | str ) -> Any :
827+ response = self ._client .download_file (_resolve_file_id (file_ref ))
828+ try :
829+ data = response .read ()
830+ codec = response .encoding or "utf-8"
831+ return json .loads (data .decode (codec ))
832+ finally :
833+ response .close ()
834+
835+ def write_bytes (
836+ self ,
837+ file_ref : PdfRestFile | str ,
838+ destination : DestinationPath ,
839+ ) -> Path :
840+ response = self ._client .download_file (_resolve_file_id (file_ref ))
841+ path = Path (destination )
842+ try :
843+ with path .open ("wb" ) as file_handle :
844+ for chunk in response .iter_bytes ():
845+ file_handle .write (chunk )
846+ finally :
847+ response .close ()
848+ return path
849+
850+ def stream (self , file_ref : PdfRestFile | str ) -> PdfRestFileStream :
851+ response = self ._client .download_file (_resolve_file_id (file_ref ))
852+ return PdfRestFileStream (response )
853+
712854
713855class _AsyncFilesClient :
714856 """Expose file-related operations for the asynchronous client."""
@@ -786,6 +928,56 @@ async def fetch(file_id: str) -> PdfRestFile:
786928
787929 return await asyncio .gather (* (fetch (file_id ) for file_id in file_ids ))
788930
931+ async def read_bytes (self , file_ref : PdfRestFile | str ) -> bytes :
932+ response = await self ._client .download_file (_resolve_file_id (file_ref ))
933+ try :
934+ return await response .aread ()
935+ finally :
936+ await response .aclose ()
937+
938+ async def read_text (
939+ self ,
940+ file_ref : PdfRestFile | str ,
941+ * ,
942+ encoding : str = "utf-8" ,
943+ ) -> str :
944+ response = await self ._client .download_file (_resolve_file_id (file_ref ))
945+ try :
946+ response .encoding = encoding
947+ data = await response .aread ()
948+ codec = response .encoding or encoding or "utf-8"
949+ return data .decode (codec )
950+ finally :
951+ await response .aclose ()
952+
953+ async def read_json (self , file_ref : PdfRestFile | str ) -> Any :
954+ response = await self ._client .download_file (_resolve_file_id (file_ref ))
955+ try :
956+ data = await response .aread ()
957+ codec = response .encoding or "utf-8"
958+ return json .loads (data .decode (codec ))
959+ finally :
960+ await response .aclose ()
961+
962+ async def write_bytes (
963+ self ,
964+ file_ref : PdfRestFile | str ,
965+ destination : DestinationPath ,
966+ ) -> Path :
967+ response = await self ._client .download_file (_resolve_file_id (file_ref ))
968+ path = Path (destination )
969+ try :
970+ with path .open ("wb" ) as file_handle :
971+ async for chunk in response .aiter_bytes ():
972+ file_handle .write (chunk )
973+ finally :
974+ await response .aclose ()
975+ return path
976+
977+ async def stream (self , file_ref : PdfRestFile | str ) -> AsyncPdfRestFileStream :
978+ response = await self ._client .download_file (_resolve_file_id (file_ref ))
979+ return AsyncPdfRestFileStream (response )
980+
789981
790982class PdfRestClient (_SyncApiClient ):
791983 """Synchronous client for interacting with the pdfrest API."""
0 commit comments