Skip to content

Commit

Permalink
refactor: do not use recurse for retries
Browse files Browse the repository at this point in the history
  • Loading branch information
jooola committed Jul 3, 2024
1 parent f1c6594 commit 39b71dc
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 46 deletions.
93 changes: 50 additions & 43 deletions hcloud/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
6 changes: 3 additions & 3 deletions tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 39b71dc

Please sign in to comment.