11import os
22import platform
33import ssl
4- from typing import Callable , ClassVar , Dict , Optional
4+ from typing import TYPE_CHECKING , Callable , ClassVar , Dict , Optional
55
66import requests
77from requests import Response , exceptions
88from requests .adapters import HTTPAdapter
9+ from tenacity import retry , retry_if_exception , stop_after_attempt , wait_random_exponential
910
1011from cycode .cli .exceptions .custom_exceptions import (
1112 HttpUnauthorizedError ,
1920from cycode .cyclient .headers import get_cli_user_agent , get_correlation_id
2021from cycode .cyclient .logger import logger
2122
23+ if TYPE_CHECKING :
24+ from tenacity import RetryCallState
25+
2226
2327class SystemStorageSslContext (HTTPAdapter ):
2428 def init_poolmanager (self , * args , ** kwargs ) -> None :
@@ -45,6 +49,47 @@ def _get_request_function() -> Callable:
4549 return session .request
4650
4751
52+ _REQUEST_ERRORS_TO_RETRY = (
53+ RequestTimeout ,
54+ RequestConnectionError ,
55+ exceptions .ChunkedEncodingError ,
56+ exceptions .ContentDecodingError ,
57+ )
58+ _RETRY_MAX_ATTEMPTS = 3
59+ _RETRY_STOP_STRATEGY = stop_after_attempt (_RETRY_MAX_ATTEMPTS )
60+ _RETRY_WAIT_STRATEGY = wait_random_exponential (multiplier = 1 , min = 2 , max = 10 )
61+
62+
63+ def _retry_before_sleep (retry_state : 'RetryCallState' ) -> None :
64+ exception_name = 'None'
65+ if retry_state .outcome .failed :
66+ exception = retry_state .outcome .exception ()
67+ exception_name = f'{ exception .__class__ .__name__ } '
68+
69+ logger .debug (
70+ 'Retrying request after error: %s. Attempt %s of %s. Upcoming sleep: %s' ,
71+ exception_name ,
72+ retry_state .attempt_number ,
73+ _RETRY_MAX_ATTEMPTS ,
74+ retry_state .upcoming_sleep ,
75+ )
76+
77+
78+ def _should_retry_exception (exception : BaseException ) -> bool :
79+ if 'PYTEST_CURRENT_TEST' in os .environ :
80+ # We are running under pytest, don't retry
81+ return False
82+
83+ # Don't retry client errors (400, 401, etc.)
84+ if isinstance (exception , RequestHttpError ):
85+ return not exception .status_code < 500
86+
87+ is_request_error = isinstance (exception , _REQUEST_ERRORS_TO_RETRY )
88+ is_server_error = isinstance (exception , RequestHttpError ) and exception .status_code >= 500
89+
90+ return is_request_error or is_server_error
91+
92+
4893class CycodeClientBase :
4994 MANDATORY_HEADERS : ClassVar [Dict [str , str ]] = {
5095 'User-Agent' : get_cli_user_agent (),
@@ -72,6 +117,13 @@ def put(self, url_path: str, body: Optional[dict] = None, headers: Optional[dict
72117 def get (self , url_path : str , headers : Optional [dict ] = None , ** kwargs ) -> Response :
73118 return self ._execute (method = 'get' , endpoint = url_path , headers = headers , ** kwargs )
74119
120+ @retry (
121+ retry = retry_if_exception (_should_retry_exception ),
122+ stop = _RETRY_STOP_STRATEGY ,
123+ wait = _RETRY_WAIT_STRATEGY ,
124+ reraise = True ,
125+ before_sleep = _retry_before_sleep ,
126+ )
75127 def _execute (
76128 self ,
77129 method : str ,
0 commit comments