From 39b71dce27708f489df006b6e412e10e7fc85f4b Mon Sep 17 00:00:00 2001 From: jo Date: Wed, 3 Jul 2024 16:51:03 +0200 Subject: [PATCH] refactor: do not use recurse for retries --- hcloud/_client.py | 93 +++++++++++++++++++++------------------ tests/unit/test_client.py | 6 +-- 2 files changed, 53 insertions(+), 46 deletions(-) diff --git a/hcloud/_client.py b/hcloud/_client.py index a0de13ae..d3a01f95 100644 --- a/hcloud/_client.py +++ b/hcloud/_client.py @@ -39,9 +39,11 @@ class Client: """Base Client for accessing the Hetzner Cloud API""" _version = __version__ - _retry_wait_time = 0.5 __user_agent_prefix = "hcloud-python" + _retry_interval = 1 + _retry_max_retries = 5 + def __init__( self, token: str, @@ -194,8 +196,6 @@ def request( # type: ignore[no-untyped-def] self, method: str, url: str, - *, - _tries: int = 1, **kwargs, ) -> dict: """Perform a request to the Hetzner Cloud API, wrapper around requests.request @@ -205,50 +205,57 @@ def request( # type: ignore[no-untyped-def] :param timeout: Requests timeout in seconds :return: Response """ - timeout = kwargs.pop("timeout", self._requests_timeout) - - response = self._requests_session.request( - method=method, - url=self._api_endpoint + url, - headers=self._get_headers(), - timeout=timeout, - **kwargs, - ) - - correlation_id = response.headers.get("X-Correlation-Id") - payload = {} - try: - if len(response.content) > 0: - payload = response.json() - except (TypeError, ValueError) as exc: - raise APIException( - code=response.status_code, - message=response.reason, - details={"content": response.content}, - correlation_id=correlation_id, - ) from exc - - if not response.ok: - if not payload or "error" not in payload: + kwargs.setdefault("timeout", self._requests_timeout) + + url = self._api_endpoint + url + headers = self._get_headers() + + retries = 0 + while True: + response = self._requests_session.request( + method=method, + url=url, + headers=headers, + **kwargs, + ) + + correlation_id = response.headers.get("X-Correlation-Id") + payload = {} + try: + if len(response.content) > 0: + payload = response.json() + except (TypeError, ValueError) as exc: raise APIException( code=response.status_code, message=response.reason, details={"content": response.content}, correlation_id=correlation_id, - ) - - error: dict = payload["error"] + ) from exc + + if not response.ok: + if not payload or "error" not in payload: + raise APIException( + code=response.status_code, + message=response.reason, + details={"content": response.content}, + correlation_id=correlation_id, + ) + + error: dict = payload["error"] + + if ( + error["code"] == "rate_limit_exceeded" + and retries < self._retry_max_retries + ): + time.sleep(retries * self._retry_interval) + retries += 1 + continue - if error["code"] == "rate_limit_exceeded" and _tries < 5: - time.sleep(_tries * self._retry_wait_time) - _tries = _tries + 1 - return self.request(method, url, _tries=_tries, **kwargs) - - raise APIException( - code=error["code"], - message=error["message"], - details=error.get("details"), - correlation_id=correlation_id, - ) + raise APIException( + code=error["code"], + message=error["message"], + details=error.get("details"), + correlation_id=correlation_id, + ) - return payload + return payload diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index e258a518..4b7e7af3 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -180,19 +180,19 @@ def test_request_500_empty_content(self, client, fail_response): assert str(error) == "Internal Server Error (500)" def test_request_limit(self, client, rate_limit_response): - client._retry_wait_time = 0 + client._retry_interval = 0 client._requests_session.request.return_value = rate_limit_response with pytest.raises(APIException) as exception_info: client.request( "POST", "http://url.com", params={"argument": "value"}, timeout=2 ) error = exception_info.value - assert client._requests_session.request.call_count == 5 + assert client._requests_session.request.call_count == 6 assert error.code == "rate_limit_exceeded" assert error.message == "limit of 10 requests per hour reached" def test_request_limit_then_success(self, client, rate_limit_response): - client._retry_wait_time = 0 + client._retry_interval = 0 response = requests.Response() response.status_code = 200 response._content = json.dumps({"result": "data"}).encode("utf-8")