Skip to content

Commit c517c85

Browse files
authored
refactor: do not recurse for request retries (#412)
Preparation work to implement a more advanced retry policy when doing requests.
1 parent fe7ddf6 commit c517c85

2 files changed

Lines changed: 53 additions & 46 deletions

File tree

hcloud/_client.py

Lines changed: 50 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,11 @@ class Client:
8181
"""Base Client for accessing the Hetzner Cloud API"""
8282

8383
_version = __version__
84-
_retry_wait_time = 0.5
8584
__user_agent_prefix = "hcloud-python"
8685

86+
_retry_interval = 0.5
87+
_retry_max_retries = 5
88+
8789
def __init__(
8890
self,
8991
token: str,
@@ -236,8 +238,6 @@ def request( # type: ignore[no-untyped-def]
236238
self,
237239
method: str,
238240
url: str,
239-
*,
240-
_tries: int = 1,
241241
**kwargs,
242242
) -> dict:
243243
"""Perform a request to the Hetzner Cloud API, wrapper around requests.request
@@ -247,50 +247,57 @@ def request( # type: ignore[no-untyped-def]
247247
:param timeout: Requests timeout in seconds
248248
:return: Response
249249
"""
250-
timeout = kwargs.pop("timeout", self._requests_timeout)
251-
252-
response = self._requests_session.request(
253-
method=method,
254-
url=self._api_endpoint + url,
255-
headers=self._get_headers(),
256-
timeout=timeout,
257-
**kwargs,
258-
)
259-
260-
correlation_id = response.headers.get("X-Correlation-Id")
261-
payload = {}
262-
try:
263-
if len(response.content) > 0:
264-
payload = response.json()
265-
except (TypeError, ValueError) as exc:
266-
raise APIException(
267-
code=response.status_code,
268-
message=response.reason,
269-
details={"content": response.content},
270-
correlation_id=correlation_id,
271-
) from exc
272-
273-
if not response.ok:
274-
if not payload or "error" not in payload:
250+
kwargs.setdefault("timeout", self._requests_timeout)
251+
252+
url = self._api_endpoint + url
253+
headers = self._get_headers()
254+
255+
retries = 0
256+
while True:
257+
response = self._requests_session.request(
258+
method=method,
259+
url=url,
260+
headers=headers,
261+
**kwargs,
262+
)
263+
264+
correlation_id = response.headers.get("X-Correlation-Id")
265+
payload = {}
266+
try:
267+
if len(response.content) > 0:
268+
payload = response.json()
269+
except (TypeError, ValueError) as exc:
275270
raise APIException(
276271
code=response.status_code,
277272
message=response.reason,
278273
details={"content": response.content},
279274
correlation_id=correlation_id,
280-
)
281-
282-
error: dict = payload["error"]
275+
) from exc
276+
277+
if not response.ok:
278+
if not payload or "error" not in payload:
279+
raise APIException(
280+
code=response.status_code,
281+
message=response.reason,
282+
details={"content": response.content},
283+
correlation_id=correlation_id,
284+
)
285+
286+
error: dict = payload["error"]
287+
288+
if (
289+
error["code"] == "rate_limit_exceeded"
290+
and retries < self._retry_max_retries
291+
):
292+
time.sleep(retries * self._retry_interval)
293+
retries += 1
294+
continue
283295

284-
if error["code"] == "rate_limit_exceeded" and _tries < 5:
285-
time.sleep(_tries * self._retry_wait_time)
286-
_tries = _tries + 1
287-
return self.request(method, url, _tries=_tries, **kwargs)
288-
289-
raise APIException(
290-
code=error["code"],
291-
message=error["message"],
292-
details=error.get("details"),
293-
correlation_id=correlation_id,
294-
)
296+
raise APIException(
297+
code=error["code"],
298+
message=error["message"],
299+
details=error.get("details"),
300+
correlation_id=correlation_id,
301+
)
295302

296-
return payload
303+
return payload

tests/unit/test_client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,19 +185,19 @@ def test_request_500_empty_content(self, client, fail_response):
185185
assert str(error) == "Internal Server Error (500)"
186186

187187
def test_request_limit(self, client, rate_limit_response):
188-
client._retry_wait_time = 0
188+
client._retry_interval = 0
189189
client._requests_session.request.return_value = rate_limit_response
190190
with pytest.raises(APIException) as exception_info:
191191
client.request(
192192
"POST", "http://url.com", params={"argument": "value"}, timeout=2
193193
)
194194
error = exception_info.value
195-
assert client._requests_session.request.call_count == 5
195+
assert client._requests_session.request.call_count == 6
196196
assert error.code == "rate_limit_exceeded"
197197
assert error.message == "limit of 10 requests per hour reached"
198198

199199
def test_request_limit_then_success(self, client, rate_limit_response):
200-
client._retry_wait_time = 0
200+
client._retry_interval = 0
201201
response = requests.Response()
202202
response.status_code = 200
203203
response._content = json.dumps({"result": "data"}).encode("utf-8")

0 commit comments

Comments
 (0)