4848 PdfRestApiError ,
4949 PdfRestAuthenticationError ,
5050 PdfRestConfigurationError ,
51+ PdfRestConnectTimeoutError ,
5152 PdfRestError ,
53+ PdfRestPoolTimeoutError ,
5254 PdfRestRequestError ,
5355 PdfRestTimeoutError ,
5456 PdfRestTransportError ,
101103MAX_BACKOFF_SECONDS = 8.0
102104BACKOFF_JITTER_SECONDS = 0.1
103105RETRYABLE_STATUS_CODES = {408 , 425 , 429 , 499 }
104- FileStreamSnapshot = tuple [IO [Any ], int ]
105-
106-
107- def _empty_snapshot_list () -> list [FileStreamSnapshot ]:
108- return []
109106
110107
111108HttpMethod = Literal ["GET" , "POST" , "PUT" , "PATCH" , "DELETE" , "OPTIONS" , "HEAD" ]
@@ -423,11 +420,7 @@ class _RequestModel(BaseModel):
423420
424421 model_config = ConfigDict (arbitrary_types_allowed = True )
425422
426- _stream_snapshots : list [FileStreamSnapshot ] = PrivateAttr (
427- default_factory = _empty_snapshot_list
428- )
429- _stream_tracking_ready : bool = PrivateAttr (default = False )
430- _has_non_rewindable_streams : bool = PrivateAttr (default = False )
423+ _has_stream_uploads : bool = PrivateAttr (default = False )
431424
432425 @field_validator ("endpoint" )
433426 @classmethod
@@ -437,25 +430,11 @@ def _validate_endpoint(cls, value: str) -> str:
437430 raise PdfRestConfigurationError (msg )
438431 return value
439432
440- def stream_tracking_ready (self ) -> bool :
441- return self ._stream_tracking_ready
442-
443- def mark_stream_tracking_ready (self ) -> None :
444- self ._stream_tracking_ready = True
445-
446- def has_non_rewindable_streams (self ) -> bool :
447- return self ._has_non_rewindable_streams
433+ def mark_has_stream_uploads (self ) -> None :
434+ self ._has_stream_uploads = True
448435
449- def mark_non_rewindable_streams (self ) -> None :
450- self ._has_non_rewindable_streams = True
451- self ._stream_snapshots .clear ()
452- self ._stream_tracking_ready = True
453-
454- def record_stream_snapshot (self , stream : IO [Any ], position : int ) -> None :
455- self ._stream_snapshots .append ((stream , position ))
456-
457- def stream_snapshots (self ) -> tuple [FileStreamSnapshot , ...]:
458- return tuple (self ._stream_snapshots )
436+ def has_stream_uploads (self ) -> bool :
437+ return self ._has_stream_uploads
459438
460439
461440class _BaseApiClient (Generic [ClientType ]):
@@ -658,6 +637,8 @@ def _prepare_request(
658637 raise
659638 except ValidationError as exc : # pragma: no cover - defensive
660639 raise PdfRestConfigurationError (str (exc )) from exc
640+ if self ._contains_open_stream (files_payload ):
641+ request .mark_has_stream_uploads ()
661642 return request
662643
663644 def prepare_request (
@@ -725,109 +706,35 @@ def _compose_json_body(
725706 return payload
726707
727708 @staticmethod
728- def _iterate_file_like_objects (value : Any ) -> Iterator [ IO [ Any ]] :
709+ def _contains_open_stream (value : Any ) -> bool :
729710 if value is None :
730- return
731- if hasattr (value , "read" ):
732- yield cast (IO [Any ], value )
733- return
711+ return False
734712 if isinstance (value , (bytes , bytearray , str )):
735- return
713+ return False
714+ if hasattr (value , "read" ):
715+ return True
736716 if isinstance (value , Mapping ):
737- for item in value .values ():
738- yield from _BaseApiClient ._iterate_file_like_objects (item )
739- return
740- if isinstance (value , Sequence ):
741- for item in value :
742- yield from _BaseApiClient ._iterate_file_like_objects (item )
717+ return any (
718+ _BaseApiClient ._contains_open_stream (item ) for item in value .values ()
719+ )
720+ if isinstance (value , Sequence ) and not isinstance (
721+ value , (str , bytes , bytearray )
722+ ):
723+ return any (_BaseApiClient ._contains_open_stream (item ) for item in value )
724+ return False
743725
744- def _log_non_rewindable_stream (self , request : _RequestModel , reason : str ) -> None :
745- self ._logger .error (
746- "Cannot retry %s because %s" ,
747- self ._describe_request (request ),
748- reason ,
749- )
726+ def _build_stream_retry_checker (
727+ self , request : _RequestModel
728+ ) -> Callable [[PdfRestError ], bool ] | None :
729+ if not request .has_stream_uploads ():
730+ return None
750731
751- def _capture_file_stream_positions (self , request : _RequestModel ) -> None :
752- if request .stream_tracking_ready ():
753- return
754- if request .files is None or self ._max_retries == 0 :
755- request .mark_stream_tracking_ready ()
756- return
757- snapshots = request .stream_snapshots ()
758- seen_ids : set [int ] = {id (stream ) for stream , _ in snapshots }
759- for stream in self ._iterate_file_like_objects (request .files ):
760- stream_id = id (stream )
761- if stream_id in seen_ids :
762- continue
763- seek_fn = getattr (stream , "seek" , None )
764- tell_fn = getattr (stream , "tell" , None )
765- if not callable (seek_fn ) or not callable (tell_fn ):
766- request .mark_non_rewindable_streams ()
767- self ._log_non_rewindable_stream (
768- request ,
769- "one or more upload streams do not provide seek/tell" ,
770- )
771- return
772- try :
773- position_value = tell_fn ()
774- except (OSError , ValueError ):
775- request .mark_non_rewindable_streams ()
776- self ._log_non_rewindable_stream (
777- request ,
778- "reading the current position failed for an upload stream" ,
779- )
780- return
781- position = cast (int , position_value )
782- request .record_stream_snapshot (stream , position )
783- seen_ids .add (stream_id )
784- request .mark_stream_tracking_ready ()
785-
786- def _rewind_stream_snapshots (self , request : _RequestModel ) -> bool :
787- for stream , position in request .stream_snapshots ():
788- seek_fn = getattr (stream , "seek" , None )
789- if not callable (seek_fn ):
790- request .mark_non_rewindable_streams ()
791- self ._log_non_rewindable_stream (
792- request ,
793- "one or more upload streams do not support seek" ,
794- )
795- return False
796- try :
797- seek_fn (position )
798- except (OSError , ValueError ) as exc : # pragma: no cover - defensive
799- request .mark_non_rewindable_streams ()
800- self ._log_non_rewindable_stream (
801- request ,
802- "resetting an upload stream failed" ,
803- )
804- if self ._logger .isEnabledFor (logging .DEBUG ):
805- self ._logger .debug (
806- "Failed to reset upload stream for %s: %s" ,
807- self ._describe_request (request ),
808- exc ,
809- )
810- return False
811- return True
812-
813- def _prepare_request_files_for_attempt (
814- self ,
815- request : _RequestModel ,
816- * ,
817- is_retry : bool ,
818- ) -> bool :
819- if request .files is None or self ._max_retries == 0 :
820- return True
821- if not request .stream_tracking_ready ():
822- self ._capture_file_stream_positions (request )
823- if not is_retry :
824- return True
825- if request .has_non_rewindable_streams ():
826- return False
827- snapshots = request .stream_snapshots ()
828- if not snapshots :
829- return True
830- return self ._rewind_stream_snapshots (request )
732+ def checker (exc : PdfRestError ) -> bool :
733+ return bool (
734+ isinstance (exc , (PdfRestConnectTimeoutError , PdfRestPoolTimeoutError ))
735+ )
736+
737+ return checker
831738
832739 def _handle_response (self , response : httpx .Response ) -> Any :
833740 request = response .request
@@ -946,24 +853,13 @@ def _execute_with_retry(
946853 func : Callable [[], ReturnType ],
947854 * ,
948855 operation : str ,
949- before_attempt : Callable [[bool ], bool ] | None = None ,
950856 should_continue : Callable [[PdfRestError ], bool ] | None = None ,
951857 ) -> ReturnType :
952858 total_attempts = self ._max_retries + 1
953- last_exception : PdfRestError | None = None
954859 for attempt in range (total_attempts ):
955- is_retry = attempt > 0
956- if before_attempt is not None :
957- can_continue = before_attempt (is_retry )
958- if not can_continue :
959- if last_exception is not None :
960- raise last_exception
961- msg = "Retry aborted before the initial attempt."
962- raise RuntimeError (msg )
963860 try :
964861 return func ()
965862 except PdfRestError as exc :
966- last_exception = exc
967863 self ._logger .debug (
968864 "Exception during %s attempt %d/%d: %s" ,
969865 operation ,
@@ -992,13 +888,12 @@ def _execute_with_retry(
992888 def _send_request (self , request : _RequestModel ) -> Any :
993889 http_client = self ._client
994890
995- def before_attempt (is_retry : bool ) -> bool :
996- return self ._prepare_request_files_for_attempt (request , is_retry = is_retry )
891+ stream_retry_checker = self ._build_stream_retry_checker (request )
997892
998893 return self ._execute_with_retry (
999894 lambda : self ._perform_request (http_client , request ),
1000895 operation = self ._describe_request (request ),
1001- before_attempt = before_attempt ,
896+ should_continue = stream_retry_checker ,
1002897 )
1003898
1004899 def _perform_request (
@@ -1205,24 +1100,13 @@ async def _execute_with_retry(
12051100 func : Callable [[], Awaitable [ReturnType ]],
12061101 * ,
12071102 operation : str ,
1208- before_attempt : Callable [[bool ], bool ] | None = None ,
12091103 should_continue : Callable [[PdfRestError ], bool ] | None = None ,
12101104 ) -> ReturnType :
12111105 total_attempts = self ._max_retries + 1
1212- last_exception : PdfRestError | None = None
12131106 for attempt in range (total_attempts ):
1214- is_retry = attempt > 0
1215- if before_attempt is not None :
1216- can_continue = before_attempt (is_retry )
1217- if not can_continue :
1218- if last_exception is not None :
1219- raise last_exception
1220- msg = "Retry aborted before the initial attempt."
1221- raise RuntimeError (msg )
12221107 try :
12231108 return await func ()
12241109 except PdfRestError as exc :
1225- last_exception = exc
12261110 self ._logger .debug (
12271111 "Exception during %s attempt %d/%d: %s" ,
12281112 operation ,
@@ -1251,13 +1135,12 @@ async def _execute_with_retry(
12511135 async def _send_request (self , request : _RequestModel ) -> Any :
12521136 http_client = self ._client
12531137
1254- def before_attempt (is_retry : bool ) -> bool :
1255- return self ._prepare_request_files_for_attempt (request , is_retry = is_retry )
1138+ stream_retry_checker = self ._build_stream_retry_checker (request )
12561139
12571140 return await self ._execute_with_retry (
12581141 lambda : self ._perform_request (http_client , request ),
12591142 operation = self ._describe_request (request ),
1260- before_attempt = before_attempt ,
1143+ should_continue = stream_retry_checker ,
12611144 )
12621145
12631146 async def _perform_request (
0 commit comments