From dbe4daee5bae54768c5953f1c6e72b41ee5181f2 Mon Sep 17 00:00:00 2001 From: himanshu Date: Tue, 1 Sep 2026 12:32:39 +0530 Subject: [PATCH] bug fix in req retry logic if user api limit is exhausted --- src/newsdataapi/__init__.py | 2 +- src/newsdataapi/client.py | 18 +++++++++++++++++- tests/test_unit.py | 16 ++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/newsdataapi/__init__.py b/src/newsdataapi/__init__.py index 752248e..047e27a 100644 --- a/src/newsdataapi/__init__.py +++ b/src/newsdataapi/__init__.py @@ -15,7 +15,7 @@ ) from .websocket import NewsDataApiWebSocket -__version__ = "0.3.0" +__version__ = "0.3.1" __all__ = [ "NewsDataApiClient", diff --git a/src/newsdataapi/client.py b/src/newsdataapi/client.py index ed1864d..677ba16 100644 --- a/src/newsdataapi/client.py +++ b/src/newsdataapi/client.py @@ -84,6 +84,8 @@ ("domain", "domainurl", "excludedomain"), ) +_QUOTA_EXHAUSTED_CODES = frozenset({"ApiKeyLimitExceeded", "ApiLimitExceeded"}) + def _validate_params(user_params: Mapping[str, Any]) -> dict[str, Any]: """Validate and normalize user-provided endpoint parameters. @@ -298,6 +300,17 @@ def _parse_retry_after(value: str | None) -> int | None: return None +def _extract_error_code(body: Any) -> str | None: + """Return the API error code from an error response body, if present.""" + if isinstance(body, dict): + results = body.get("results") + if isinstance(results, dict): + code = results.get("code") + if isinstance(code, str): + return code + return None + + def _redact_url(url: str, param: str = "apikey") -> str: """Return ``url`` with the value of ``param`` replaced by ``REDACTED``. @@ -1201,7 +1214,10 @@ def _request( # Rate limit. if response.status_code == 429: retry_after = _parse_retry_after(response.headers.get("Retry-After")) - if attempt >= self.max_retries: + if ( + _extract_error_code(body) in _QUOTA_EXHAUSTED_CODES + or attempt >= self.max_retries + ): raise NewsdataRateLimitError( body, status_code=429, diff --git a/tests/test_unit.py b/tests/test_unit.py index d71ae31..418a497 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -823,6 +823,22 @@ def test_429_exhausted_raises_rate_limit_with_retry_after( assert exc_info.value.retry_after == 5 +@pytest.mark.parametrize("code", ["ApiKeyLimitExceeded", "ApiLimitExceeded"]) +def test_429_quota_exhausted_raises_without_retry( + code: str, + client: NewsDataApiClient, + mocked_responses: responses.RequestsMock, + no_sleep: None, +) -> None: + """A 429 whose error code means exhausted credits is never retried.""" + body = {"status": "error", "results": {"message": "limit", "code": code}} + mocked_responses.get(LATEST_URL, json=body, status=429) + with pytest.raises(NewsdataRateLimitError) as exc_info: + client.latest_api() + assert len(mocked_responses.calls) == 1 + assert exc_info.value.response_body == body + + def test_500_then_200_succeeds( client: NewsDataApiClient, mocked_responses: responses.RequestsMock,