55import asyncio
66import importlib .metadata
77import json
8+ import logging
89import os
910import random
1011import time
9899Query = Mapping [str , QueryParamValue ]
99100Body = Mapping [str , Any ]
100101
102+ PDFREST_LOGGER = logging .getLogger ("pdfrest" )
103+ PDFREST_LOGGER .addHandler (logging .NullHandler ())
104+ LOGGER = logging .getLogger ("pdfrest.client" )
105+ _PDFREST_HANDLER_IDS : set [int ] = set ()
106+
107+
108+ def _ensure_stream_handler (
109+ logger : logging .Logger , formatter : logging .Formatter
110+ ) -> None :
111+ for handler in logger .handlers :
112+ if id (handler ) in _PDFREST_HANDLER_IDS :
113+ return
114+ handler = logging .StreamHandler ()
115+ handler .setFormatter (formatter )
116+ logger .addHandler (handler )
117+ _PDFREST_HANDLER_IDS .add (id (handler ))
118+
119+
120+ def _configure_logging () -> None :
121+ level_name = os .getenv ("PDFREST_LOG" )
122+ if not level_name :
123+ return
124+ normalized = level_name .strip ().lower ()
125+ level_map = {"debug" : logging .DEBUG , "info" : logging .INFO }
126+ level = level_map .get (normalized )
127+ if level is None :
128+ return
129+ formatter = logging .Formatter ("%(asctime)s %(levelname)s [%(name)s] %(message)s" )
130+ pdfrest_logger = PDFREST_LOGGER
131+ pdfrest_logger .setLevel (level )
132+ _ensure_stream_handler (pdfrest_logger , formatter )
133+ pdfrest_logger .propagate = False
134+
135+ httpx_logger = logging .getLogger ("httpx" )
136+ httpx_logger .setLevel (level )
137+ _ensure_stream_handler (httpx_logger , formatter )
138+ httpx_logger .propagate = False
139+
140+
141+ _configure_logging ()
142+
143+
101144FileContent = IO [bytes ] | bytes | str
102145FileTuple2 = tuple [str | None , FileContent ]
103146FileTuple3 = tuple [str | None , FileContent , str | None ]
@@ -369,6 +412,7 @@ def __init__(
369412 headers : AnyMapping | None = None ,
370413 max_retries : int = DEFAULT_MAX_RETRIES ,
371414 ) -> None :
415+ self ._logger = LOGGER
372416 if not isinstance (max_retries , int ) or max_retries < 0 :
373417 msg = "max_retries must be a non-negative integer."
374418 raise PdfRestConfigurationError (msg )
@@ -439,6 +483,42 @@ def _compute_backoff_delay(self, retry_number: int) -> float:
439483 delay = base_delay + jitter
440484 return delay if delay > 0 else 0.0
441485
486+ @staticmethod
487+ def _sanitize_headers (headers : Mapping [str , Any ] | None ) -> dict [str , Any ]:
488+ if not headers :
489+ return {}
490+ sanitized : dict [str , Any ] = {}
491+ for key , value in headers .items ():
492+ if key .lower () == API_KEY_HEADER_NAME .lower ():
493+ sanitized [key ] = "******"
494+ else :
495+ sanitized [key ] = value
496+ return sanitized
497+
498+ def _log_request (self , request : _RequestModel ) -> None :
499+ if not self ._logger .isEnabledFor (logging .DEBUG ):
500+ return
501+ sanitized_headers = self ._sanitize_headers (request .headers )
502+ self ._logger .debug (
503+ "Request %s %s params=%s timeout=%s headers=%s" ,
504+ request .method ,
505+ request .endpoint ,
506+ request .params ,
507+ request .timeout ,
508+ sanitized_headers ,
509+ )
510+ if request .method in {"POST" , "PUT" , "PATCH" } and request .json_body is not None :
511+ self ._logger .debug (
512+ "Request payload %s %s: %s" ,
513+ request .method ,
514+ request .endpoint ,
515+ request .json_body ,
516+ )
517+
518+ @staticmethod
519+ def _describe_request (request : _RequestModel ) -> str :
520+ return f"{ request .method } { request .endpoint } "
521+
442522 @staticmethod
443523 def _base_url_requires_api_key (url : URL ) -> bool :
444524 host = url .host or ""
@@ -567,19 +647,46 @@ def _compose_json_body(
567647 return payload
568648
569649 def _handle_response (self , response : httpx .Response ) -> Any :
650+ request = response .request
651+ request_label = (
652+ f"{ getattr (request , 'method' , 'UNKNOWN' )} { getattr (request , 'url' , '' )} "
653+ if request is not None
654+ else "UNKNOWN"
655+ )
570656 if response .is_success :
657+ if self ._logger .isEnabledFor (logging .DEBUG ):
658+ self ._logger .debug (
659+ "Response %s status=%s" , request_label , response .status_code
660+ )
571661 return self ._decode_json (response )
572662
573663 message , error_payload = self ._extract_error_details (response )
574664
575665 if response .status_code == 401 :
576666 auth_message = message or "Authentication with pdfRest failed."
667+ if self ._logger .isEnabledFor (logging .DEBUG ):
668+ self ._logger .debug (
669+ "Authentication error response %s status=%s message=%s payload=%s" ,
670+ request_label ,
671+ response .status_code ,
672+ auth_message ,
673+ error_payload ,
674+ )
577675 raise PdfRestAuthenticationError (
578676 response .status_code ,
579677 message = auth_message ,
580678 response_content = error_payload ,
581679 )
582680
681+ if self ._logger .isEnabledFor (logging .DEBUG ):
682+ self ._logger .debug (
683+ "Error response %s status=%s message=%s payload=%s" ,
684+ request_label ,
685+ response .status_code ,
686+ message ,
687+ error_payload ,
688+ )
689+
583690 raise PdfRestApiError (
584691 response .status_code , message = message , response_content = error_payload
585692 )
@@ -646,16 +753,29 @@ def __enter__(self) -> _SyncApiClient:
646753 def __exit__ (self , exc_type : Any , exc : Any , traceback : Any ) -> None :
647754 self .close ()
648755
649- def _execute_with_retry (self , func : Callable [[], ReturnType ]) -> ReturnType :
650- for attempt in range (self ._max_retries + 1 ):
756+ def _execute_with_retry (
757+ self , func : Callable [[], ReturnType ], * , operation : str
758+ ) -> ReturnType :
759+ total_attempts = self ._max_retries + 1
760+ for attempt in range (total_attempts ):
651761 try :
652762 return func ()
653763 except PdfRestError as exc :
654- if attempt == self ._max_retries or not self ._should_retry_exception (
655- exc
656- ):
764+ self ._logger .debug (
765+ "Exception during %s attempt %d/%d: %s" ,
766+ operation ,
767+ attempt + 1 ,
768+ total_attempts ,
769+ exc ,
770+ )
771+ should_retry = (
772+ attempt < self ._max_retries and self ._should_retry_exception (exc )
773+ )
774+ if not should_retry :
775+ self ._logger .debug ("No retry for %s; raising exception." , operation )
657776 raise
658777 delay = self ._compute_backoff_delay (attempt )
778+ self ._logger .debug ("Retrying %s after %.2f seconds." , operation , delay )
659779 if delay > 0 :
660780 time .sleep (delay )
661781 msg = "Retry loop exited unexpectedly."
@@ -664,12 +784,14 @@ def _execute_with_retry(self, func: Callable[[], ReturnType]) -> ReturnType:
664784 def _send_request (self , request : _RequestModel ) -> Any :
665785 http_client = self ._client
666786 return self ._execute_with_retry (
667- lambda : self ._perform_request (http_client , request )
787+ lambda : self ._perform_request (http_client , request ),
788+ operation = self ._describe_request (request ),
668789 )
669790
670791 def _perform_request (
671792 self , http_client : httpx .Client , request : _RequestModel
672793 ) -> Any :
794+ self ._log_request (request )
673795 try :
674796 response = http_client .request (
675797 method = request .method ,
@@ -682,7 +804,13 @@ def _perform_request(
682804 data = request .data ,
683805 )
684806 except httpx .HTTPError as exc :
685- raise translate_httpx_error (exc ) from exc
807+ translated = translate_httpx_error (exc )
808+ self ._logger .debug (
809+ "HTTPX exception for %s: %s" ,
810+ self ._describe_request (request ),
811+ translated ,
812+ )
813+ raise translated from exc
686814 try :
687815 payload = self ._handle_response (response )
688816 except PdfRestApiError :
@@ -758,10 +886,12 @@ def download_file(
758886 timeout = timeout ,
759887 )
760888 return self ._execute_with_retry (
761- lambda : self ._download_with_retry (request_model )
889+ lambda : self ._download_with_retry (request_model ),
890+ operation = f"{ request_model .method } { request_model .endpoint } (download)" ,
762891 )
763892
764893 def _download_with_retry (self , request : _RequestModel ) -> httpx .Response :
894+ self ._log_request (request )
765895 http_request = self ._client .build_request (
766896 request .method ,
767897 request .endpoint ,
@@ -778,7 +908,13 @@ def _download_with_retry(self, request: _RequestModel) -> httpx.Response:
778908 try :
779909 response = self ._client .send (http_request , stream = True )
780910 except httpx .HTTPError as exc :
781- raise translate_httpx_error (exc ) from exc
911+ translated = translate_httpx_error (exc )
912+ self ._logger .debug (
913+ "HTTPX exception for %s: %s" ,
914+ self ._describe_request (request ),
915+ translated ,
916+ )
917+ raise translated from exc
782918 if response .is_success :
783919 return response
784920 try :
@@ -852,17 +988,28 @@ async def __aexit__(self, exc_type: Any, exc: Any, traceback: Any) -> None:
852988 await self .aclose ()
853989
854990 async def _execute_with_retry (
855- self , func : Callable [[], Awaitable [ReturnType ]]
991+ self , func : Callable [[], Awaitable [ReturnType ]], * , operation : str
856992 ) -> ReturnType :
857- for attempt in range (self ._max_retries + 1 ):
993+ total_attempts = self ._max_retries + 1
994+ for attempt in range (total_attempts ):
858995 try :
859996 return await func ()
860997 except PdfRestError as exc :
861- if attempt == self ._max_retries or not self ._should_retry_exception (
862- exc
863- ):
998+ self ._logger .debug (
999+ "Exception during %s attempt %d/%d: %s" ,
1000+ operation ,
1001+ attempt + 1 ,
1002+ total_attempts ,
1003+ exc ,
1004+ )
1005+ should_retry = (
1006+ attempt < self ._max_retries and self ._should_retry_exception (exc )
1007+ )
1008+ if not should_retry :
1009+ self ._logger .debug ("No retry for %s; raising exception." , operation )
8641010 raise
8651011 delay = self ._compute_backoff_delay (attempt )
1012+ self ._logger .debug ("Retrying %s after %.2f seconds." , operation , delay )
8661013 if delay > 0 :
8671014 await asyncio .sleep (delay )
8681015 msg = "Retry loop exited unexpectedly."
@@ -871,12 +1018,14 @@ async def _execute_with_retry(
8711018 async def _send_request (self , request : _RequestModel ) -> Any :
8721019 http_client = self ._client
8731020 return await self ._execute_with_retry (
874- lambda : self ._perform_request (http_client , request )
1021+ lambda : self ._perform_request (http_client , request ),
1022+ operation = self ._describe_request (request ),
8751023 )
8761024
8771025 async def _perform_request (
8781026 self , http_client : httpx .AsyncClient , request : _RequestModel
8791027 ) -> Any :
1028+ self ._log_request (request )
8801029 try :
8811030 response = await http_client .request (
8821031 method = request .method ,
@@ -889,7 +1038,13 @@ async def _perform_request(
8891038 data = request .data ,
8901039 )
8911040 except httpx .HTTPError as exc :
892- raise translate_httpx_error (exc ) from exc
1041+ translated = translate_httpx_error (exc )
1042+ self ._logger .debug (
1043+ "HTTPX exception for %s: %s" ,
1044+ self ._describe_request (request ),
1045+ translated ,
1046+ )
1047+ raise translated from exc
8931048 try :
8941049 payload = self ._handle_response (response )
8951050 except PdfRestApiError :
@@ -973,10 +1128,12 @@ async def download_file(
9731128 timeout = timeout ,
9741129 )
9751130 return await self ._execute_with_retry (
976- lambda : self ._download_with_retry (request_model )
1131+ lambda : self ._download_with_retry (request_model ),
1132+ operation = f"{ request_model .method } { request_model .endpoint } (download)" ,
9771133 )
9781134
9791135 async def _download_with_retry (self , request : _RequestModel ) -> httpx .Response :
1136+ self ._log_request (request )
9801137 http_request = self ._client .build_request (
9811138 request .method ,
9821139 request .endpoint ,
@@ -993,7 +1150,13 @@ async def _download_with_retry(self, request: _RequestModel) -> httpx.Response:
9931150 try :
9941151 response = await self ._client .send (http_request , stream = True )
9951152 except httpx .HTTPError as exc :
996- raise translate_httpx_error (exc ) from exc
1153+ translated = translate_httpx_error (exc )
1154+ self ._logger .debug (
1155+ "HTTPX exception for %s: %s" ,
1156+ self ._describe_request (request ),
1157+ translated ,
1158+ )
1159+ raise translated from exc
9971160 if response .is_success :
9981161 return response
9991162 try :
0 commit comments