Skip to content
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 105 additions & 32 deletions razorpay/client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import os
import json
import requests
import pkg_resources
import importlib.metadata
import time
import random

from pkg_resources import DistributionNotFound
from importlib.metadata import PackageNotFoundError

from types import ModuleType

Expand Down Expand Up @@ -36,7 +38,11 @@ class Client:
"""Razorpay client class"""

DEFAULTS = {
'base_url': URL.BASE_URL
'base_url': URL.BASE_URL,
'max_retries': 5,
'initial_delay': 1,
'max_delay': 60,
'jitter': 0.25
}

def __init__(self, session=None, auth=None, **options):
Expand All @@ -50,6 +56,11 @@ def __init__(self, session=None, auth=None, **options):
self.cert_path = file_dir + '/ca-bundle.crt'

self.base_url = self._set_base_url(**options)
self.max_retries = options.get('max_retries', self.DEFAULTS['max_retries'])
self.initial_delay = options.get('initial_delay', self.DEFAULTS['initial_delay'])
self.max_delay = options.get('max_delay', self.DEFAULTS['max_delay'])
self.jitter = options.get('jitter', self.DEFAULTS['jitter'])
self.retry_enabled = False

self.app_details = []

Expand All @@ -68,6 +79,12 @@ def _set_base_url(self, **options):
base_url = options['base_url']
del(options['base_url'])

# Remove retry options from options if they exist
options.pop('max_retries', None)
options.pop('initial_delay', None)
options.pop('max_delay', None)
options.pop('jitter', None)

return base_url

def _update_user_agent_header(self, options):
Expand All @@ -84,8 +101,20 @@ def _update_user_agent_header(self, options):
def _get_version(self):
version = ""
try: # nosemgrep : gitlab.bandit.B110
version = pkg_resources.require("razorpay")[0].version
except DistributionNotFound: # pragma: no cover
# Try importlib.metadata first (modern approach)
try:
import importlib.metadata
from importlib.metadata import PackageNotFoundError
version = importlib.metadata.version("razorpay")
except ImportError:
# Fall back to pkg_resources
import pkg_resources
from pkg_resources import DistributionNotFound
version = pkg_resources.require("razorpay")[0].version
except (PackageNotFoundError, DistributionNotFound, NameError): # pragma: no cover
# PackageNotFoundError: importlib.metadata couldn't find the package
# DistributionNotFound: pkg_resources couldn't find the package
# NameError: in case the exception classes aren't defined due to import issues
pass
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore these changes, i already created seperate PR for this , which is approved.

return version

Expand All @@ -109,16 +138,19 @@ def set_app_details(self, app_details):
def get_app_details(self):
return self.app_details

def enable_retry(self, retry_enabled=False):
self.retry_enabled = retry_enabled

def request(self, method, path, **options):
"""
Dispatches a request to the Razorpay HTTP API
Dispatches a request to the Razorpay HTTP API with retry mechanism
"""
options = self._update_user_agent_header(options)

# Determine authentication type
use_public_auth = options.pop('use_public_auth', False)
auth_to_use = self.auth

if use_public_auth:
# For public auth, use key_id only
if self.auth and isinstance(self.auth, tuple) and len(self.auth) >= 1:
Expand All @@ -132,31 +164,72 @@ def request(self, method, path, **options):
options['headers']['X-Razorpay-Device-Mode'] = device_mode

url = "{}{}".format(self.base_url, path)

response = getattr(self.session, method)(url, auth=auth_to_use,
verify=self.cert_path,
**options)
if ((response.status_code >= HTTP_STATUS_CODE.OK) and
(response.status_code < HTTP_STATUS_CODE.REDIRECT)):
return json.dumps({}) if(response.status_code==204) else response.json()
else:
msg = ""
code = ""
json_response = response.json()
if 'error' in json_response:
if 'description' in json_response['error']:
msg = json_response['error']['description']
if 'code' in json_response['error']:
code = str(json_response['error']['code'])

if str.upper(code) == ERROR_CODE.BAD_REQUEST_ERROR:
raise BadRequestError(msg)
elif str.upper(code) == ERROR_CODE.GATEWAY_ERROR:
raise GatewayError(msg)
elif str.upper(code) == ERROR_CODE.SERVER_ERROR: # nosemgrep : python.lang.maintainability.useless-ifelse.useless-if-body
raise ServerError(msg)
else:
raise ServerError(msg)

delay_seconds = self.initial_delay

# If retry is not enabled, set max attempts to 1
max_attempts = self.max_retries if self.retry_enabled else 1

for attempt in range(max_attempts):
try:
response = getattr(self.session, method)(url, auth=auth_to_use,
verify=self.cert_path,
**options)

if ((response.status_code >= HTTP_STATUS_CODE.OK) and
(response.status_code < HTTP_STATUS_CODE.REDIRECT)):
return json.dumps({}) if(response.status_code==204) else response.json()
else:
msg = ""
code = ""
json_response = response.json()
if 'error' in json_response:
if 'description' in json_response['error']:
msg = json_response['error']['description']
if 'code' in json_response['error']:
code = str(json_response['error']['code'])

if str.upper(code) == ERROR_CODE.BAD_REQUEST_ERROR:
raise BadRequestError(msg)
elif str.upper(code) == ERROR_CODE.GATEWAY_ERROR:
raise GatewayError(msg)
elif str.upper(code) == ERROR_CODE.SERVER_ERROR: # nosemgrep : python.lang.maintainability.useless-ifelse.useless-if-body
raise ServerError(msg)
else:
raise ServerError(msg)

except requests.exceptions.ConnectionError as e:
if self.retry_enabled and attempt < max_attempts - 1: # Don't sleep on the last attempt
# Apply exponential backoff with jitter
jitter_value = random.uniform(-self.jitter, self.jitter)
jittered_delay = delay_seconds * (1 + jitter_value)
# Cap the delay at max_delay
actual_delay = min(jittered_delay, self.max_delay)

print(f"ConnectionError: {e}. Retrying in {actual_delay:.2f} seconds... (Attempt {attempt + 1}/{max_attempts})")
time.sleep(actual_delay)
delay_seconds *= 2 # Exponential backoff for next attempt
else:
print(f"Connection failed." + (f" Max retries ({max_attempts}) exceeded." if self.retry_enabled else ""))
raise
except requests.exceptions.Timeout as e:
if self.retry_enabled and attempt < max_attempts - 1: # Don't sleep on the last attempt
# Apply exponential backoff with jitter
jitter_value = random.uniform(-self.jitter, self.jitter)
jittered_delay = delay_seconds * (1 + jitter_value)
# Cap the delay at max_delay
actual_delay = min(jittered_delay, self.max_delay)

print(f"Timeout: {e}. Retrying in {actual_delay:.2f} seconds... (Attempt {attempt + 1}/{max_attempts})")
time.sleep(actual_delay)
delay_seconds *= 2 # Exponential backoff for next attempt
else:
print(f"Request timed out." + (f" Max retries ({max_attempts}) exceeded." if self.retry_enabled else ""))
raise
except requests.exceptions.RequestException as e:
# For other request exceptions, don't retry
print(f"Request error occurred: {e}")
raise

def get(self, path, params, **options):
"""
Expand Down
Loading