4444FileTuple3 = tuple [str | None , FileContent , str | None ]
4545FileTuple4 = tuple [str | None , FileContent , str | None , Mapping [str , str ]]
4646FileTypes = FileContent | FileTuple2 | FileTuple3 | FileTuple4
47- UploadFiles = Mapping [ str , FileTypes ] | Sequence [ tuple [ str , FileTypes ]]
47+ UploadFiles = Sequence [ FileTypes ] | FileTypes
4848
4949FilePath = str | PathLike [str ]
5050FilePathTuple2 = tuple [FilePath , str | None ]
5151FilePathTuple3 = tuple [FilePath , str | None , Mapping [str , str ]]
5252FilePathTypes = FilePath | FilePathTuple2 | FilePathTuple3
53+ FilePathInput = FilePathTypes | Sequence [FilePathTypes ]
5354NormalizedFileTypes : TypeAlias = FileContent | FileTuple2 | FileTuple3 | FileTuple4
5455
5556
@@ -128,26 +129,24 @@ def _normalize_file_type(file_value: FileTypes) -> NormalizedFileTypes:
128129def _normalize_upload_files (
129130 files : UploadFiles ,
130131) -> list [tuple [str , NormalizedFileTypes ]]:
131- is_mapping = isinstance (files , Mapping )
132- if is_mapping :
133- mapping_files = cast (Mapping [str , FileTypes ], files )
134- items : list [tuple [str , FileTypes ]] = list (mapping_files .items ())
132+ if isinstance (files , Mapping ):
133+ msg = "Upload files must be provided as a sequence or a single file specification."
134+ raise TypeError (msg )
135+
136+ if isinstance (files , Sequence ) and not isinstance (files , (str , bytes , bytearray )):
137+ items = list (files )
135138 else :
136- sequence_files = cast (Sequence [tuple [str , FileTypes ]], files )
137- items = list (sequence_files )
139+ # Treat single file specification as a one-element sequence.
140+ items = [cast (FileTypes , files )]
141+
138142 if not items :
139143 msg = "At least one file must be provided."
140144 raise ValueError (msg )
141145 normalized_items : list [tuple [str , NormalizedFileTypes ]] = []
142- for entry in items :
143- if is_mapping :
144- field_name , file_value = entry
145- else :
146- if not isinstance (entry , tuple ) or len (entry ) != 2 :
147- msg = "Files sequence entries must be (field_name, file_value) tuples."
148- raise TypeError (msg )
149- field_name , file_value = entry
150- normalized_items .append ((str (field_name ), _normalize_file_type (file_value )))
146+ for file_value in items :
147+ normalized_items .append (
148+ (FILE_UPLOAD_FIELD_NAME , _normalize_file_type (cast (FileTypes , file_value )))
149+ )
151150 return normalized_items
152151
153152
@@ -175,6 +174,22 @@ def _parse_path_spec(spec: FilePathTypes) -> tuple[Path, str | None, Mapping[str
175174 return path , None , {}
176175
177176
177+ def _normalize_path_inputs (
178+ file_paths : FilePathInput ,
179+ ) -> list [FilePathTypes ]:
180+ if isinstance (file_paths , Sequence ) and not isinstance (
181+ file_paths , (str , bytes , bytearray )
182+ ):
183+ sequence_paths = cast (Sequence [FilePathTypes ], file_paths )
184+ items : list [FilePathTypes ] = list (sequence_paths )
185+ else :
186+ items = [cast (FilePathTypes , file_paths )]
187+ if not items :
188+ msg = "At least one file path must be provided."
189+ raise ValueError (msg )
190+ return items
191+
192+
178193ClientType = TypeVar ("ClientType" , httpx .Client , httpx .AsyncClient )
179194
180195
@@ -621,13 +636,11 @@ def __init__(self, client: _SyncApiClient) -> None:
621636 self ._client = client
622637
623638 def create (self , files : UploadFiles ) -> list [PdfRestFile ]:
624- """Upload one or more files by content, in the same style accepted by
625- the `files` parameter of `httpx.Client.post`.
639+ """Upload one or more files by content.
626640
627- Provide either a mapping of field names to file specifications, or a
628- sequence of `(field_name, file_spec)` tuples. File specifications may be
629- raw file-like objects, bytes, str, or the tuple forms documented by
630- httpx.
641+ Provide either a single file specification or a sequence of file
642+ specifications (each matching the shapes accepted by httpx). Every
643+ uploaded part is sent using the field name ``file``.
631644 """
632645 normalized_files = _normalize_upload_files (files )
633646 request = self ._client .prepare_request (
@@ -637,42 +650,29 @@ def create(self, files: UploadFiles) -> list[PdfRestFile]:
637650 file_ids = _extract_uploaded_file_ids (payload )
638651 return [self ._client .fetch_file_info (file_id ) for file_id in file_ids ]
639652
640- def create_from_paths (
641- self , file_paths : Sequence [FilePathTypes ]
642- ) -> list [PdfRestFile ]:
653+ def create_from_paths (self , file_paths : FilePathInput ) -> list [PdfRestFile ]:
643654 """Upload one or more files by their path.
644655
645656 Each entry may be a bare path-like object or a tuple of
646657 `(path, content_type)` / `(path, content_type, headers)` where headers
647658 mirrors the httpx multipart header mapping. All opened file handles are
648659 closed once the request completes.
649660 """
650- if not file_paths :
651- msg = "At least one file path must be provided."
652- raise ValueError (msg )
661+ normalized_paths = _normalize_path_inputs (file_paths )
653662
654663 with ExitStack () as stack :
655- upload_entries : list [tuple [ str , FileTypes ] ] = []
656- for spec in file_paths :
664+ upload_specs : list [FileTypes ] = []
665+ for spec in normalized_paths :
657666 path , content_type , headers = _parse_path_spec (spec )
658667 file_obj = stack .enter_context (path .open ("rb" ))
659668 filename = path .name
660669 if headers :
661- upload_entries .append (
662- (
663- FILE_UPLOAD_FIELD_NAME ,
664- (filename , file_obj , content_type , headers ),
665- )
666- )
670+ upload_specs .append ((filename , file_obj , content_type , headers ))
667671 elif content_type is not None :
668- upload_entries .append (
669- (FILE_UPLOAD_FIELD_NAME , (filename , file_obj , content_type ))
670- )
672+ upload_specs .append ((filename , file_obj , content_type ))
671673 else :
672- upload_entries .append (
673- (FILE_UPLOAD_FIELD_NAME , (filename , file_obj ))
674- )
675- return self .create (upload_entries )
674+ upload_specs .append ((filename , file_obj ))
675+ return self .create (upload_specs )
676676
677677
678678class _AsyncFilesClient :
@@ -688,13 +688,12 @@ def __init__(
688688 self ._concurrency_limit = concurrency_limit
689689
690690 async def create (self , files : UploadFiles ) -> list [PdfRestFile ]:
691- """Upload one or more files by content, in the same style accepted by
692- the `files` parameter of `httpx.AsyncClient.post`.
691+ """Upload one or more files by content.
693692
694- Provide either a mapping of field names to file specifications, or a
695- sequence of `(field_name, file_spec)` tuples. File specifications may be
696- raw file-like objects, bytes, str, or the tuple forms documented by
697- httpx. """
693+ Provide either a single file specification or a sequence of file
694+ specifications (each matching the shapes accepted by httpx). Every
695+ uploaded part is sent using the field name ``file``.
696+ """
698697 normalized_files = _normalize_upload_files (files )
699698 request = self ._client .prepare_request (
700699 "POST" , "/upload" , files = normalized_files
@@ -709,42 +708,29 @@ async def fetch(file_id: str) -> PdfRestFile:
709708
710709 return await asyncio .gather (* (fetch (file_id ) for file_id in file_ids ))
711710
712- async def create_from_paths (
713- self , file_paths : Sequence [FilePathTypes ]
714- ) -> list [PdfRestFile ]:
711+ async def create_from_paths (self , file_paths : FilePathInput ) -> list [PdfRestFile ]:
715712 """Upload one or more files by their path.
716713
717714 Each entry may be a bare path-like object or a tuple of
718715 `(path, content_type)` / `(path, content_type, headers)` where headers
719716 mirrors the httpx multipart header mapping. All opened file handles are
720717 closed once the request completes.
721718 """
722- if not file_paths :
723- msg = "At least one file path must be provided."
724- raise ValueError (msg )
719+ normalized_paths = _normalize_path_inputs (file_paths )
725720
726721 with ExitStack () as stack :
727- upload_entries : list [tuple [ str , FileTypes ] ] = []
728- for spec in file_paths :
722+ upload_specs : list [FileTypes ] = []
723+ for spec in normalized_paths :
729724 path , content_type , headers = _parse_path_spec (spec )
730725 file_obj = stack .enter_context (path .open ("rb" ))
731726 filename = path .name
732727 if headers :
733- upload_entries .append (
734- (
735- FILE_UPLOAD_FIELD_NAME ,
736- (filename , file_obj , content_type , headers ),
737- )
738- )
728+ upload_specs .append ((filename , file_obj , content_type , headers ))
739729 elif content_type is not None :
740- upload_entries .append (
741- (FILE_UPLOAD_FIELD_NAME , (filename , file_obj , content_type ))
742- )
730+ upload_specs .append ((filename , file_obj , content_type ))
743731 else :
744- upload_entries .append (
745- (FILE_UPLOAD_FIELD_NAME , (filename , file_obj ))
746- )
747- return await self .create (upload_entries )
732+ upload_specs .append ((filename , file_obj ))
733+ return await self .create (upload_specs )
748734
749735
750736class PdfRestClient (_SyncApiClient ):
0 commit comments