From 15fc69dbd17fa3c3fb26654ca4c8e4548fd4d0d3 Mon Sep 17 00:00:00 2001 From: Wolfgang Popp Date: Mon, 27 Oct 2025 17:26:00 +0100 Subject: [PATCH 1/3] Add retry logic for rate limit errors Signed-off-by: Wolfgang Popp --- vilocify/http.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/vilocify/http.py b/vilocify/http.py index 6375c83..8cbac53 100644 --- a/vilocify/http.py +++ b/vilocify/http.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import logging +import time from dataclasses import dataclass import requests @@ -17,6 +18,11 @@ def __init__(self, error_code: int, message: str): self.message = message +class RateLimitError(Exception): + def __init__(self, retry_after: int): + self.retry_after = retry_after + + @dataclass class JSONAPIError: title: str = "" @@ -46,7 +52,18 @@ def from_response(code: int, response_json: JSON) -> "JSONAPIRequestError": return JSONAPIRequestError(code, message, errors) -def _request(verb: str, url: str, json: JSON = None, params: dict[str, str] | None = None) -> JSON: +def _request(verb: str, url: str, json: JSON = None, params: dict[str, str] | None = None): + for i in range(1, 4): + try: + return _rate_limited_request(verb, url, json, params) + except RateLimitError as e: + logger.warning("Throttling due to rate limit. Attempt %d", i) + time.sleep(e.retry_after) + + raise RequestError(429, "Ratelimit exceeded and retry failed") + + +def _rate_limited_request(verb: str, url: str, json: JSON = None, params: dict[str, str] | None = None) -> JSON: logger.debug("%s: url=%s, params=%s, json=%s", verb.upper(), url, params, json) response = api_config.client.request( verb, url, timeout=api_config.request_timeout_seconds, json=json, params=params @@ -63,6 +80,10 @@ def _request(verb: str, url: str, json: JSON = None, params: dict[str, str] | No if "Server-Timing" in response.headers: logger.debug("server-timing: %s", response.headers["Server-Timing"]) + if response.status_code == 429: + # TODO get retry_after from RateLimit-Reset or Retry-After headers + raise RateLimitError(retry_after=10) + if not response.ok: raise JSONAPIRequestError.from_response(response.status_code, response_json) From 17aa431149df03d87fbe74c535e6945b86555084 Mon Sep 17 00:00:00 2001 From: Wolfgang Popp Date: Thu, 30 Oct 2025 17:24:49 +0100 Subject: [PATCH 2/3] Quicker retry on rate limit errors Signed-off-by: Wolfgang Popp --- vilocify/http.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/vilocify/http.py b/vilocify/http.py index 8cbac53..01e1a60 100644 --- a/vilocify/http.py +++ b/vilocify/http.py @@ -1,6 +1,5 @@ # SPDX-FileCopyrightText: 2025 Siemens AG # SPDX-License-Identifier: MIT - import logging import time from dataclasses import dataclass @@ -19,8 +18,7 @@ def __init__(self, error_code: int, message: str): class RateLimitError(Exception): - def __init__(self, retry_after: int): - self.retry_after = retry_after + """Raised for rate limiting.""" @dataclass @@ -53,12 +51,12 @@ def from_response(code: int, response_json: JSON) -> "JSONAPIRequestError": def _request(verb: str, url: str, json: JSON = None, params: dict[str, str] | None = None): - for i in range(1, 4): + for i in range(10): try: return _rate_limited_request(verb, url, json, params) - except RateLimitError as e: - logger.warning("Throttling due to rate limit. Attempt %d", i) - time.sleep(e.retry_after) + except RateLimitError: + logger.warning("Pausing due to rate limit") + time.sleep(1) raise RequestError(429, "Ratelimit exceeded and retry failed") @@ -81,8 +79,7 @@ def _rate_limited_request(verb: str, url: str, json: JSON = None, params: dict[s logger.debug("server-timing: %s", response.headers["Server-Timing"]) if response.status_code == 429: - # TODO get retry_after from RateLimit-Reset or Retry-After headers - raise RateLimitError(retry_after=10) + raise RateLimitError() if not response.ok: raise JSONAPIRequestError.from_response(response.status_code, response_json) From c4dc7473ed24cb80e7c7a65ec56f4ec8f74fa8e7 Mon Sep 17 00:00:00 2001 From: Wolfgang Popp Date: Fri, 31 Oct 2025 13:11:32 +0100 Subject: [PATCH 3/3] Fix linter Signed-off-by: Wolfgang Popp --- vilocify/http.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vilocify/http.py b/vilocify/http.py index 01e1a60..6e214ad 100644 --- a/vilocify/http.py +++ b/vilocify/http.py @@ -50,15 +50,15 @@ def from_response(code: int, response_json: JSON) -> "JSONAPIRequestError": return JSONAPIRequestError(code, message, errors) -def _request(verb: str, url: str, json: JSON = None, params: dict[str, str] | None = None): +def _request(verb: str, url: str, json: JSON = None, params: dict[str, str] | None = None) -> JSON: for i in range(10): try: return _rate_limited_request(verb, url, json, params) except RateLimitError: - logger.warning("Pausing due to rate limit") + logger.debug("Pausing due to rate limit") time.sleep(1) - raise RequestError(429, "Ratelimit exceeded and retry failed") + raise RequestError(requests.codes.too_many_requests, "Ratelimit exceeded and retry failed") def _rate_limited_request(verb: str, url: str, json: JSON = None, params: dict[str, str] | None = None) -> JSON: @@ -78,7 +78,7 @@ def _rate_limited_request(verb: str, url: str, json: JSON = None, params: dict[s if "Server-Timing" in response.headers: logger.debug("server-timing: %s", response.headers["Server-Timing"]) - if response.status_code == 429: + if response.status_code == requests.codes.too_many_requests: raise RateLimitError() if not response.ok: