11import os
22import json
33import requests
4- import pkg_resources
5-
6- from pkg_resources import DistributionNotFound
4+ import warnings
5+ import random
6+ import time
77
88from types import ModuleType
99
@@ -36,7 +36,11 @@ class Client:
3636 """Razorpay client class"""
3737
3838 DEFAULTS = {
39- '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
4044 }
4145
4246 def __init__ (self , session = None , auth = None , ** options ):
@@ -50,6 +54,11 @@ def __init__(self, session=None, auth=None, **options):
5054 self .cert_path = file_dir + '/ca-bundle.crt'
5155
5256 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
5362
5463 self .app_details = []
5564
@@ -68,6 +77,12 @@ def _set_base_url(self, **options):
6877 base_url = options ['base_url' ]
6978 del (options ['base_url' ])
7079
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+
7186 return base_url
7287
7388 def _update_user_agent_header (self , options ):
@@ -84,9 +99,30 @@ def _update_user_agent_header(self, options):
8499 def _get_version (self ):
85100 version = ""
86101 try : # nosemgrep : gitlab.bandit.B110
87- version = pkg_resources .require ("razorpay" )[0 ].version
88- except DistributionNotFound : # pragma: no cover
89- pass
102+ # Try importlib.metadata first (modern approach)
103+ try :
104+ import importlib .metadata
105+ from importlib .metadata import PackageNotFoundError
106+ version = importlib .metadata .version ("razorpay" )
107+ except ImportError :
108+ # Fall back to pkg_resources
109+ import pkg_resources
110+ from pkg_resources import DistributionNotFound
111+ version = pkg_resources .require ("razorpay" )[0 ].version
112+ except (PackageNotFoundError , DistributionNotFound , NameError ): # pragma: no cover
113+ # PackageNotFoundError: importlib.metadata couldn't find the package
114+ # DistributionNotFound: pkg_resources couldn't find the package
115+ # NameError: in case the exception classes aren't defined due to import issues
116+
117+ # If all else fails, use the hardcoded version from the package
118+ version = "1.4.3"
119+
120+ warnings .warn (
121+ "Could not detect razorpay package version. Using fallback version."
122+ "This may indicate an installation issue." ,
123+ UserWarning ,
124+ stacklevel = 4
125+ )
90126 return version
91127
92128 def _get_app_details_ua (self ):
@@ -109,16 +145,19 @@ def set_app_details(self, app_details):
109145 def get_app_details (self ):
110146 return self .app_details
111147
148+ def enable_retry (self , retry_enabled = False ):
149+ self .retry_enabled = retry_enabled
150+
112151 def request (self , method , path , ** options ):
113152 """
114- Dispatches a request to the Razorpay HTTP API
153+ Dispatches a request to the Razorpay HTTP API with retry mechanism
115154 """
116155 options = self ._update_user_agent_header (options )
117156
118157 # Determine authentication type
119158 use_public_auth = options .pop ('use_public_auth' , False )
120159 auth_to_use = self .auth
121-
160+
122161 if use_public_auth :
123162 # For public auth, use key_id only
124163 if self .auth and isinstance (self .auth , tuple ) and len (self .auth ) >= 1 :
@@ -132,31 +171,72 @@ def request(self, method, path, **options):
132171 options ['headers' ]['X-Razorpay-Device-Mode' ] = device_mode
133172
134173 url = "{}{}" .format (self .base_url , path )
135-
136- response = getattr (self .session , method )(url , auth = auth_to_use ,
137- verify = self .cert_path ,
138- ** options )
139- if ((response .status_code >= HTTP_STATUS_CODE .OK ) and
140- (response .status_code < HTTP_STATUS_CODE .REDIRECT )):
141- return json .dumps ({}) if (response .status_code == 204 ) else response .json ()
142- else :
143- msg = ""
144- code = ""
145- json_response = response .json ()
146- if 'error' in json_response :
147- if 'description' in json_response ['error' ]:
148- msg = json_response ['error' ]['description' ]
149- if 'code' in json_response ['error' ]:
150- code = str (json_response ['error' ]['code' ])
151-
152- if str .upper (code ) == ERROR_CODE .BAD_REQUEST_ERROR :
153- raise BadRequestError (msg )
154- elif str .upper (code ) == ERROR_CODE .GATEWAY_ERROR :
155- raise GatewayError (msg )
156- elif str .upper (code ) == ERROR_CODE .SERVER_ERROR : # nosemgrep : python.lang.maintainability.useless-ifelse.useless-if-body
157- raise ServerError (msg )
158- else :
159- 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
160240
161241 def get (self , path , params , ** options ):
162242 """
0 commit comments