diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d2b7f71..2accca11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +feat: `Client` can be used as a context manager and closed explicitly with `close()` to release pooled HTTP connections (#87) ## [2.0.1][2.0.1] - 2026-03-09 feat: Added Support for cancel token diff --git a/README.md b/README.md index f4613507..e0d11a30 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,18 @@ client = razorpay.Client(auth=("", "")) client.enable_retry(True) # Enable retry mechanism for failed API calls ``` +The client keeps a pooled HTTP session open. Close it when you are done, or use the client as a context manager: + +```py +with razorpay.Client(auth=("", "")) as client: + client.payment.fetch("pay_XXXXXXXXXXXXXX") +# the session is closed here + +client = razorpay.Client(auth=("", "")) +... +client.close() +``` + ## App Details After setting up client, you can set your app details before making any request diff --git a/razorpay/client.py b/razorpay/client.py index 6d80e526..ae79e2e8 100644 --- a/razorpay/client.py +++ b/razorpay/client.py @@ -145,6 +145,19 @@ def set_app_details(self, app_details): def get_app_details(self): return self.app_details + def close(self): + """ + Close the underlying HTTP session and release its pooled connections. + The client should not be used for further requests after this. + """ + self.session.close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + def enable_retry(self, retry_enabled=False): self.retry_enabled = retry_enabled diff --git a/tests/test_client_session.py b/tests/test_client_session.py new file mode 100644 index 00000000..9fb27171 --- /dev/null +++ b/tests/test_client_session.py @@ -0,0 +1,34 @@ +import unittest +from unittest import mock + +import razorpay + + +class TestClientSession(unittest.TestCase): + + def test_close_closes_session(self): + session = mock.Mock() + client = razorpay.Client(session=session, auth=('key_id', 'key_secret')) + client.close() + session.close.assert_called_once_with() + + def test_context_manager_returns_client_and_closes_on_exit(self): + session = mock.Mock() + with razorpay.Client(session=session, auth=('key_id', 'key_secret')) as client: + self.assertIsInstance(client, razorpay.Client) + session.close.assert_not_called() + session.close.assert_called_once_with() + + def test_context_manager_closes_on_exception(self): + session = mock.Mock() + with self.assertRaises(RuntimeError): + with razorpay.Client(session=session, auth=('key_id', 'key_secret')): + raise RuntimeError('boom') + session.close.assert_called_once_with() + + def test_default_session_is_closed(self): + client = razorpay.Client(auth=('key_id', 'key_secret')) + with mock.patch.object(client.session, 'close') as close: + with client: + pass + close.assert_called_once_with()