Skip to content

Commit dd16711

Browse files
committed
feat: parse structured 429 responses for descriptive rate limit errors
RateLimitError now includes tier, detail dict, and human-readable message from the server (e.g. "Daily query limit reached (100/100). Contact hello@datons.com for Professional tier.") instead of the generic "Rate limit exceeded."
1 parent be21537 commit dd16711

2 files changed

Lines changed: 41 additions & 7 deletions

File tree

src/datons/client.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,18 @@ def _request(
9898
raise AuthenticationError()
9999
if response.status_code == 429:
100100
retry_after = response.headers.get("Retry-After")
101-
raise RateLimitError(int(retry_after) if retry_after else None)
101+
# Parse structured error body from server
102+
try:
103+
body = response.json()
104+
detail = body.get("detail", body)
105+
except Exception:
106+
detail = None
107+
tier = detail.get("tier") if isinstance(detail, dict) else None
108+
raise RateLimitError(
109+
int(retry_after) if retry_after else None,
110+
tier=tier,
111+
detail=detail,
112+
)
102113
if response.status_code >= 400:
103114
detail = response.text[:500]
104115
raise QueryError(response.status_code, detail)

src/datons/exceptions.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,34 @@ def __init__(self, status_code: int, detail: str = ""):
2121

2222

2323
class RateLimitError(DatonsError):
24-
"""Rate limit exceeded."""
25-
26-
def __init__(self, retry_after: int | None = None):
24+
"""Rate limit exceeded.
25+
26+
Attributes:
27+
retry_after: Seconds until the limit resets.
28+
tier: Current API key tier (e.g. 'explorer', 'professional').
29+
detail: Full error detail from the server.
30+
"""
31+
32+
def __init__(
33+
self,
34+
retry_after: int | None = None,
35+
*,
36+
tier: str | None = None,
37+
detail: dict | str | None = None,
38+
):
2739
self.retry_after = retry_after
28-
msg = "Rate limit exceeded."
29-
if retry_after:
30-
msg += f" Retry after {retry_after}s."
40+
self.tier = tier
41+
self.detail = detail
42+
43+
# Build a human-readable message from the server response
44+
if isinstance(detail, dict):
45+
msg = detail.get("error", "Rate limit exceeded.")
46+
upgrade = detail.get("upgrade")
47+
if upgrade:
48+
msg += f" {upgrade}"
49+
else:
50+
msg = "Rate limit exceeded."
51+
if retry_after:
52+
msg += f" Retry after {retry_after}s."
53+
3154
super().__init__(msg)

0 commit comments

Comments
 (0)