Skip to content

Commit 131711d

Browse files
authored
Merge branch 'master' into feature/add-pos-documentation
2 parents fd9a649 + 9edfbac commit 131711d

File tree

4 files changed

+128
-36
lines changed

4 files changed

+128
-36
lines changed

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format
44

55
## Unreleased
66

7+
## [2.0.0][2.0.0] - 2025-08-17
8+
fix: pkg_resources deprecation warning on runtime
9+
feat: Added retry mechanism for failed API calls with `enable_retry(True)` method
10+
feat: Enhanced error handling for network connectivity issues
11+
712
## [1.5.0][1.5.0] - 2024-12-19
813

914
feat: Add DeviceActivity support for POS Gateway integration
@@ -149,7 +154,12 @@ Added Documents API (uploadAccountDoc(, fetchAccountDoc, uploadStakeholderDoc, f
149154
- Payments: List, fetch and capture payments.
150155
- Refunds: List, fetch and initiate refunds.
151156

152-
[unreleased]: https://github.com/razorpay/razorpay-python/compare/1.2.0...HEAD
157+
[unreleased]: https://github.com/razorpay/razorpay-python/compare/2.0.0...HEAD
158+
[2.0.0]: https://github.com/razorpay/razorpay-python/compare/1.5.0...2.0.0
159+
[1.5.0]: https://github.com/razorpay/razorpay-python/compare/1.4.2...1.5.0
160+
[1.4.2]: https://github.com/razorpay/razorpay-python/compare/1.4.1...1.4.2
161+
[1.4.1]: https://github.com/razorpay/razorpay-python/compare/1.3.1...1.4.1
162+
[1.3.1]: https://github.com/razorpay/razorpay-python/compare/1.3.0...1.3.1
153163
[1.2.0]: https://github.com/razorpay/razorpay-python/compare/1.1.1...1.2.0
154164
[1.1.1]: https://github.com/razorpay/razorpay-python/compare/1.1.0...1.1.1
155165
[1.1.0]: https://github.com/razorpay/razorpay-python/compare/1.0.2...1.1.0

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ You can find your API keys at <https://dashboard.razorpay.com/#/app/keys>.
2020
```py
2121
import razorpay
2222
client = razorpay.Client(auth=("<YOUR_API_KEY>", "<YOUR_API_SECRET>"))
23+
24+
client.enable_retry(True) # Enable retry mechanism for failed API calls
2325
```
2426

2527
## App Details

razorpay/client.py

Lines changed: 114 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import os
22
import json
33
import requests
4-
import pkg_resources
5-
6-
from pkg_resources import DistributionNotFound
4+
import warnings
5+
import random
6+
import time
77

88
from 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
"""

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name="razorpay",
8-
version="1.5.0",
8+
version="2.0.0",
99
description="Razorpay Python Client",
1010
long_description=readme_content,
1111
long_description_content_type='text/markdown',

0 commit comments

Comments
 (0)