From c37b1965eb8dd3a1268ac7980cbb8aa92d8bcf45 Mon Sep 17 00:00:00 2001 From: ankitdas13 Date: Fri, 22 Aug 2025 15:17:41 +0530 Subject: [PATCH 1/4] added request retry mechanism --- razorpay/client.py | 110 ++++++++++++++++++++++++++++++++------------- 1 file changed, 78 insertions(+), 32 deletions(-) diff --git a/razorpay/client.py b/razorpay/client.py index ea6cf08d..4f7f326e 100644 --- a/razorpay/client.py +++ b/razorpay/client.py @@ -1,9 +1,10 @@ import os import json import requests -import pkg_resources +import importlib.metadata +import time -from pkg_resources import DistributionNotFound +from importlib.metadata import PackageNotFoundError from types import ModuleType @@ -36,7 +37,9 @@ class Client: """Razorpay client class""" DEFAULTS = { - 'base_url': URL.BASE_URL + 'base_url': URL.BASE_URL, + 'max_retries': 5, + 'initial_delay': 1 } def __init__(self, session=None, auth=None, **options): @@ -50,6 +53,8 @@ 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.app_details = [] @@ -68,6 +73,10 @@ 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) + return base_url def _update_user_agent_header(self, options): @@ -84,8 +93,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 return version @@ -111,36 +132,61 @@ def get_app_details(self): 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) - url = "{}{}".format(self.base_url, path) - - response = getattr(self.session, method)(url, auth=self.auth, - 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 + + for attempt in range(self.max_retries): + try: + response = getattr(self.session, method)(url, auth=self.auth, + 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 attempt < self.max_retries - 1: # Don't sleep on the last attempt + print(f"ConnectionError: {e}. Retrying in {delay_seconds} seconds... (Attempt {attempt + 1}/{self.max_retries})") + time.sleep(delay_seconds) + delay_seconds *= 2 # Exponential backoff + else: + print(f"Max retries ({self.max_retries}) exceeded. Connection failed.") + raise + except requests.exceptions.Timeout as e: + if attempt < self.max_retries - 1: # Don't sleep on the last attempt + print(f"Timeout: {e}. Retrying in {delay_seconds} seconds... (Attempt {attempt + 1}/{self.max_retries})") + time.sleep(delay_seconds) + delay_seconds *= 2 # Exponential backoff + else: + print(f"Max retries ({self.max_retries}) exceeded. Request timed out.") + 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): """ From 3d4cd0ec6a6441bcc58d8914b871c6dcd196d584 Mon Sep 17 00:00:00 2001 From: ankitdas13 Date: Tue, 2 Sep 2025 11:47:30 +0530 Subject: [PATCH 2/4] added enable retry --- razorpay/client.py | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/razorpay/client.py b/razorpay/client.py index af394629..da4956d6 100644 --- a/razorpay/client.py +++ b/razorpay/client.py @@ -55,6 +55,7 @@ def __init__(self, session=None, auth=None, **options): 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.retry_enabled = False self.app_details = [] @@ -130,19 +131,41 @@ 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 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: + auth_to_use = (self.auth[0], '') # Use key_id only, empty key_secret + + # Inject device mode header if provided + device_mode = options.pop('device_mode', None) + if device_mode is not None: + if 'headers' not in options: + options['headers'] = {} + options['headers']['X-Razorpay-Device-Mode'] = device_mode + url = "{}{}".format(self.base_url, path) delay_seconds = self.initial_delay - for attempt in range(self.max_retries): + # 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=self.auth, + response = getattr(self.session, method)(url, auth=auth_to_use, verify=self.cert_path, **options) @@ -169,20 +192,20 @@ def request(self, method, path, **options): raise ServerError(msg) except requests.exceptions.ConnectionError as e: - if attempt < self.max_retries - 1: # Don't sleep on the last attempt - print(f"ConnectionError: {e}. Retrying in {delay_seconds} seconds... (Attempt {attempt + 1}/{self.max_retries})") + if self.retry_enabled and attempt < max_attempts - 1: # Don't sleep on the last attempt + print(f"ConnectionError: {e}. Retrying in {delay_seconds} seconds... (Attempt {attempt + 1}/{max_attempts})") time.sleep(delay_seconds) delay_seconds *= 2 # Exponential backoff else: - print(f"Max retries ({self.max_retries}) exceeded. Connection failed.") + print(f"Connection failed." + (f" Max retries ({max_attempts}) exceeded." if self.retry_enabled else "")) raise except requests.exceptions.Timeout as e: - if attempt < self.max_retries - 1: # Don't sleep on the last attempt - print(f"Timeout: {e}. Retrying in {delay_seconds} seconds... (Attempt {attempt + 1}/{self.max_retries})") + if self.retry_enabled and attempt < max_attempts - 1: # Don't sleep on the last attempt + print(f"Timeout: {e}. Retrying in {delay_seconds} seconds... (Attempt {attempt + 1}/{max_attempts})") time.sleep(delay_seconds) delay_seconds *= 2 # Exponential backoff else: - print(f"Max retries ({self.max_retries}) exceeded. Request timed out.") + 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 From 6124bbd2884ac8267af51fc1f3120525f87189c4 Mon Sep 17 00:00:00 2001 From: ankitdas13 Date: Thu, 4 Sep 2025 08:49:09 +0530 Subject: [PATCH 3/4] added exponential backoff --- razorpay/client.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/razorpay/client.py b/razorpay/client.py index da4956d6..07841856 100644 --- a/razorpay/client.py +++ b/razorpay/client.py @@ -3,6 +3,7 @@ import requests import importlib.metadata import time +import random from importlib.metadata import PackageNotFoundError @@ -39,7 +40,9 @@ class Client: DEFAULTS = { 'base_url': URL.BASE_URL, 'max_retries': 5, - 'initial_delay': 1 + 'initial_delay': 1, + 'max_delay': 60, + 'jitter': 0.25 } def __init__(self, session=None, auth=None, **options): @@ -55,6 +58,8 @@ def __init__(self, session=None, auth=None, **options): 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 = [] @@ -77,6 +82,8 @@ def _set_base_url(self, **options): # 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 @@ -193,17 +200,29 @@ def request(self, method, path, **options): except requests.exceptions.ConnectionError as e: if self.retry_enabled and attempt < max_attempts - 1: # Don't sleep on the last attempt - print(f"ConnectionError: {e}. Retrying in {delay_seconds} seconds... (Attempt {attempt + 1}/{max_attempts})") - time.sleep(delay_seconds) - delay_seconds *= 2 # Exponential backoff + # 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 - print(f"Timeout: {e}. Retrying in {delay_seconds} seconds... (Attempt {attempt + 1}/{max_attempts})") - time.sleep(delay_seconds) - delay_seconds *= 2 # Exponential backoff + # 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 From 5891545cac1b00db0f54d99d9f081cf78d3e662d Mon Sep 17 00:00:00 2001 From: ankitdas13 Date: Wed, 17 Sep 2025 16:04:01 +0530 Subject: [PATCH 4/4] resolved conflict changes --- razorpay/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/razorpay/client.py b/razorpay/client.py index 8b5eba3a..6d80e526 100644 --- a/razorpay/client.py +++ b/razorpay/client.py @@ -2,6 +2,8 @@ import json import requests import warnings +import random +import time from types import ModuleType