Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/newsdataapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
)
from .websocket import NewsDataApiWebSocket

__version__ = "0.3.0"
__version__ = "0.3.1"

__all__ = [
"NewsDataApiClient",
Expand Down
18 changes: 17 additions & 1 deletion src/newsdataapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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``.

Expand Down Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading