22import json
33import requests
44import warnings
5+ import random
6+ import time
57
68from types import ModuleType
79
@@ -34,7 +36,11 @@ class Client:
3436 """Razorpay client class"""
3537
3638 DEFAULTS = {
37- 'base_url' : URL .BASE_URL
39+ 'base_url' : URL .BASE_URL ,
40+ 'max_retries' : 5 ,
41+ 'initial_delay' : 1 ,
42+ 'max_delay' : 60 ,
43+ 'jitter' : 0.25
3844 }
3945
4046 def __init__ (self , session = None , auth = None , ** options ):
@@ -48,6 +54,11 @@ def __init__(self, session=None, auth=None, **options):
4854 self .cert_path = file_dir + '/ca-bundle.crt'
4955
5056 self .base_url = self ._set_base_url (** options )
57+ self .max_retries = options .get ('max_retries' , self .DEFAULTS ['max_retries' ])
58+ self .initial_delay = options .get ('initial_delay' , self .DEFAULTS ['initial_delay' ])
59+ self .max_delay = options .get ('max_delay' , self .DEFAULTS ['max_delay' ])
60+ self .jitter = options .get ('jitter' , self .DEFAULTS ['jitter' ])
61+ self .retry_enabled = False
5162
5263 self .app_details = []
5364
@@ -66,6 +77,12 @@ def _set_base_url(self, **options):
6677 base_url = options ['base_url' ]
6778 del (options ['base_url' ])
6879
80+ # Remove retry options from options if they exist
81+ options .pop ('max_retries' , None )
82+ options .pop ('initial_delay' , None )
83+ options .pop ('max_delay' , None )
84+ options .pop ('jitter' , None )
85+
6986 return base_url
7087
7188 def _update_user_agent_header (self , options ):
@@ -128,16 +145,19 @@ def set_app_details(self, app_details):
128145 def get_app_details (self ):
129146 return self .app_details
130147
148+ def enable_retry (self , retry_enabled = False ):
149+ self .retry_enabled = retry_enabled
150+
131151 def request (self , method , path , ** options ):
132152 """
133- Dispatches a request to the Razorpay HTTP API
153+ Dispatches a request to the Razorpay HTTP API with retry mechanism
134154 """
135155 options = self ._update_user_agent_header (options )
136156
137157 # Determine authentication type
138158 use_public_auth = options .pop ('use_public_auth' , False )
139159 auth_to_use = self .auth
140-
160+
141161 if use_public_auth :
142162 # For public auth, use key_id only
143163 if self .auth and isinstance (self .auth , tuple ) and len (self .auth ) >= 1 :
@@ -151,31 +171,72 @@ def request(self, method, path, **options):
151171 options ['headers' ]['X-Razorpay-Device-Mode' ] = device_mode
152172
153173 url = "{}{}" .format (self .base_url , path )
154-
155- response = getattr (self .session , method )(url , auth = auth_to_use ,
156- verify = self .cert_path ,
157- ** options )
158- if ((response .status_code >= HTTP_STATUS_CODE .OK ) and
159- (response .status_code < HTTP_STATUS_CODE .REDIRECT )):
160- return json .dumps ({}) if (response .status_code == 204 ) else response .json ()
161- else :
162- msg = ""
163- code = ""
164- json_response = response .json ()
165- if 'error' in json_response :
166- if 'description' in json_response ['error' ]:
167- msg = json_response ['error' ]['description' ]
168- if 'code' in json_response ['error' ]:
169- code = str (json_response ['error' ]['code' ])
170-
171- if str .upper (code ) == ERROR_CODE .BAD_REQUEST_ERROR :
172- raise BadRequestError (msg )
173- elif str .upper (code ) == ERROR_CODE .GATEWAY_ERROR :
174- raise GatewayError (msg )
175- elif str .upper (code ) == ERROR_CODE .SERVER_ERROR : # nosemgrep : python.lang.maintainability.useless-ifelse.useless-if-body
176- raise ServerError (msg )
177- else :
178- raise ServerError (msg )
174+
175+ delay_seconds = self .initial_delay
176+
177+ # If retry is not enabled, set max attempts to 1
178+ max_attempts = self .max_retries if self .retry_enabled else 1
179+
180+ for attempt in range (max_attempts ):
181+ try :
182+ response = getattr (self .session , method )(url , auth = auth_to_use ,
183+ verify = self .cert_path ,
184+ ** options )
185+
186+ if ((response .status_code >= HTTP_STATUS_CODE .OK ) and
187+ (response .status_code < HTTP_STATUS_CODE .REDIRECT )):
188+ return json .dumps ({}) if (response .status_code == 204 ) else response .json ()
189+ else :
190+ msg = ""
191+ code = ""
192+ json_response = response .json ()
193+ if 'error' in json_response :
194+ if 'description' in json_response ['error' ]:
195+ msg = json_response ['error' ]['description' ]
196+ if 'code' in json_response ['error' ]:
197+ code = str (json_response ['error' ]['code' ])
198+
199+ if str .upper (code ) == ERROR_CODE .BAD_REQUEST_ERROR :
200+ raise BadRequestError (msg )
201+ elif str .upper (code ) == ERROR_CODE .GATEWAY_ERROR :
202+ raise GatewayError (msg )
203+ elif str .upper (code ) == ERROR_CODE .SERVER_ERROR : # nosemgrep : python.lang.maintainability.useless-ifelse.useless-if-body
204+ raise ServerError (msg )
205+ else :
206+ raise ServerError (msg )
207+
208+ except requests .exceptions .ConnectionError as e :
209+ if self .retry_enabled and attempt < max_attempts - 1 : # Don't sleep on the last attempt
210+ # Apply exponential backoff with jitter
211+ jitter_value = random .uniform (- self .jitter , self .jitter )
212+ jittered_delay = delay_seconds * (1 + jitter_value )
213+ # Cap the delay at max_delay
214+ actual_delay = min (jittered_delay , self .max_delay )
215+
216+ print (f"ConnectionError: { e } . Retrying in { actual_delay :.2f} seconds... (Attempt { attempt + 1 } /{ max_attempts } )" )
217+ time .sleep (actual_delay )
218+ delay_seconds *= 2 # Exponential backoff for next attempt
219+ else :
220+ print (f"Connection failed." + (f" Max retries ({ max_attempts } ) exceeded." if self .retry_enabled else "" ))
221+ raise
222+ except requests .exceptions .Timeout as e :
223+ if self .retry_enabled and attempt < max_attempts - 1 : # Don't sleep on the last attempt
224+ # Apply exponential backoff with jitter
225+ jitter_value = random .uniform (- self .jitter , self .jitter )
226+ jittered_delay = delay_seconds * (1 + jitter_value )
227+ # Cap the delay at max_delay
228+ actual_delay = min (jittered_delay , self .max_delay )
229+
230+ print (f"Timeout: { e } . Retrying in { actual_delay :.2f} seconds... (Attempt { attempt + 1 } /{ max_attempts } )" )
231+ time .sleep (actual_delay )
232+ delay_seconds *= 2 # Exponential backoff for next attempt
233+ else :
234+ print (f"Request timed out." + (f" Max retries ({ max_attempts } ) exceeded." if self .retry_enabled else "" ))
235+ raise
236+ except requests .exceptions .RequestException as e :
237+ # For other request exceptions, don't retry
238+ print (f"Request error occurred: { e } " )
239+ raise
179240
180241 def get (self , path , params , ** options ):
181242 """
0 commit comments