From fa6377971ba806202fd990fd7c13411de9f5774f Mon Sep 17 00:00:00 2001 From: "gocardless-ci-robot[bot]" <123969075+gocardless-ci-robot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:49:00 +0000 Subject: [PATCH] Changes generated by 811a5c904f03659e41505c41b790f52df39fadd3 This commit was automatically created from gocardless/client-library-templates@811a5c904f03659e41505c41b790f52df39fadd3 by the `push-files` action. Workflow run: https://github.com/gocardless/client-library-templates/actions/runs/40309967068 --- README.rst | 9 +++++++++ gocardless_pro/api_client.py | 19 ++++++++++++++----- gocardless_pro/client.py | 10 +++++++--- tests/api_client_test.py | 24 ++++++++++++++++++++++++ tests/client_test.py | 7 +++++++ 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 14a5b16d..d095d81b 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,15 @@ you want to use: token = os.environ['ACCESS_TOKEN'] client = gocardless_pro.Client(access_token=token, environment='live') +Requests time out after 60 seconds by default. To change this, pass ``timeout`` in +seconds, or a ``(connect, read)`` tuple to set the two limits separately, or ``None`` +to wait indefinitely. A request that times out raises ``requests.Timeout`` once the +library has exhausted its automatic retries: + +.. code:: python + + client = gocardless_pro.Client(access_token=token, environment='live', timeout=10) + Access API endpoints using the corresponding methods on the client object: .. code:: python diff --git a/gocardless_pro/api_client.py b/gocardless_pro/api_client.py index a8bf5b86..5ea3bc10 100644 --- a/gocardless_pro/api_client.py +++ b/gocardless_pro/api_client.py @@ -16,17 +16,22 @@ from . import errors from .rate_limit import RateLimit, update_rate_limit +DEFAULT_TIMEOUT = 60 + class ApiClient(object): """Client for interacting with a JSON HTTP API, using OAuth2-style auth. Args: base_url (string): The prefix that's prepended to all request paths. access_token (string): Token used in the Authorization header. + timeout (float or tuple, optional): Passed to ``requests`` as its + ``timeout`` argument. Defaults to 60 seconds; ``None`` waits indefinitely. """ - def __init__(self, base_url, access_token): + def __init__(self, base_url, access_token, timeout=DEFAULT_TIMEOUT): self.base_url = base_url self.access_token = access_token + self.timeout = timeout self.rate_limit = RateLimit() @update_rate_limit @@ -46,7 +51,8 @@ def get(self, path, params=None, headers=None): response = requests.get( self._url_for(path), params=params, - headers=self._headers(headers) + headers=self._headers(headers), + timeout=self.timeout ) self._handle_errors(response) return response @@ -69,7 +75,8 @@ def post(self, path, body, headers=None): response = requests.post( self._url_for(path), data=json.dumps(body), - headers=self._headers(headers) + headers=self._headers(headers), + timeout=self.timeout ) self._handle_errors(response) return response @@ -91,7 +98,8 @@ def put(self, path, body, headers=None): response = requests.put( self._url_for(path), data=json.dumps(body), - headers=self._headers(headers) + headers=self._headers(headers), + timeout=self.timeout ) self._handle_errors(response) return response @@ -113,7 +121,8 @@ def delete(self, path, body, headers=None): response = requests.delete( self._url_for(path), data=json.dumps(body), - headers=self._headers(headers) + headers=self._headers(headers), + timeout=self.timeout ) self._handle_errors(response) return response diff --git a/gocardless_pro/client.py b/gocardless_pro/client.py index ef5d3ec2..2c0d0ac4 100644 --- a/gocardless_pro/client.py +++ b/gocardless_pro/client.py @@ -4,7 +4,7 @@ # from . import services -from .api_client import ApiClient +from .api_client import ApiClient, DEFAULT_TIMEOUT class Client(object): """Client for interacting with the GoCardless API. @@ -19,6 +19,10 @@ class Client(object): base_url (str): Manually set a base URL. Most people should use `environment` instead. raise_on_idempotency_conflict (bool): Configure refetching of conflicting resource + timeout (float or tuple): Seconds to wait for the API to respond before + raising ``requests.Timeout``. Pass a ``(connect, read)`` tuple to set + the two limits separately. Defaults to 60 seconds; pass ``None`` + to wait indefinitely. Example: client = Client(access_token=ACCESS_TOKEN, environment='sandbox') @@ -26,7 +30,7 @@ class Client(object): print('{} {}'.format(customer.family_name, customer.given_name)) """ - def __init__(self, access_token=None, environment=None, base_url=None, raise_on_idempotency_conflict=False): + def __init__(self, access_token=None, environment=None, base_url=None, raise_on_idempotency_conflict=False, timeout=DEFAULT_TIMEOUT): if access_token is None: raise ValueError('No access_token provided') @@ -34,7 +38,7 @@ def __init__(self, access_token=None, environment=None, base_url=None, raise_on_ raise ValueError('No environment or base_url specified') base_url = base_url or self._environment_url(environment) - self._api_client = ApiClient(base_url, access_token) + self._api_client = ApiClient(base_url, access_token, timeout=timeout) self._raise_on_idempotency_conflict = raise_on_idempotency_conflict @property diff --git a/tests/api_client_test.py b/tests/api_client_test.py index 40e3e82d..42e6d05e 100644 --- a/tests/api_client_test.py +++ b/tests/api_client_test.py @@ -21,6 +21,30 @@ def test_uses_correct_url(): responses.add(responses.GET, 'http://example.com/test', body='{}') client.get('/test') +@responses.activate +def test_sends_a_default_timeout(): + responses.add(responses.GET, 'http://example.com/test', body='{}') + client.get('/test') + assert responses.calls[0].request.req_kwargs['timeout'] == 60 + +@responses.activate +def test_timeout_can_be_disabled_with_none(): + no_timeout_client = api_client.ApiClient('http://example.com', access_token, timeout=None) + responses.add(responses.GET, 'http://example.com/test', body='{}') + no_timeout_client.get('/test') + assert responses.calls[0].request.req_kwargs['timeout'] is None + +@responses.activate +@pytest.mark.parametrize('method', ['get', 'post', 'put', 'delete']) +def test_passes_timeout_to_requests(method): + timeout_client = api_client.ApiClient('http://example.com', access_token, timeout=(3, 10)) + responses.add(getattr(responses, method.upper()), 'http://example.com/test', body='{}') + if method == 'get': + timeout_client.get('/test') + else: + getattr(timeout_client, method)('/test', {}) + assert responses.calls[0].request.req_kwargs['timeout'] == (3, 10) + @responses.activate def test_authorization_header_present(): responses.add(responses.GET, 'http://example.com/test', body='{}') diff --git a/tests/client_test.py b/tests/client_test.py index a51d5af2..01cfc60b 100644 --- a/tests/client_test.py +++ b/tests/client_test.py @@ -16,6 +16,13 @@ def test_requires_valid_environment(): with pytest.raises(ValueError): Client(access_token=access_token, environment='invalid') +def test_has_a_default_timeout(): + assert client._api_client.timeout == 60 + +def test_passes_timeout_to_api_client(): + timeout_client = Client(access_token=access_token, base_url='http://example.com', timeout=5) + assert timeout_client._api_client.timeout == 5 + def test_balances_returns_service(): assert isinstance(client.balances, services.BalancesService)