Skip to content

Commit 571ca50

Browse files
committed
feat(http): implement rate-limit handling with auto-retry (#11)
1 parent ccbe656 commit 571ca50

8 files changed

Lines changed: 1575 additions & 15 deletions

File tree

poetry.lock

Lines changed: 749 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pytest = "^7.4.0"
1515
flake8 = "^6.1.0"
1616
black = "^23.7.0"
1717
isort = "^5.12.0"
18+
aiohttp = "^3.14.1"
1819

1920

2021
[build-system]

src/shade/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
from .errors import HTTPError, RateLimitError, ShadeError
12
from .gateway import Gateway
3+
from .http import AsyncHTTPClient, SyncHTTPClient
24

35
__version__ = "0.1.0"
46

5-
__all__ = ["Gateway"]
7+
__all__ = [
8+
"Gateway",
9+
"SyncHTTPClient",
10+
"AsyncHTTPClient",
11+
"ShadeError",
12+
"HTTPError",
13+
"RateLimitError",
14+
]

src/shade/errors.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
Shade SDK exceptions.
3+
"""
4+
from __future__ import annotations
5+
6+
from typing import Optional
7+
8+
9+
class ShadeError(Exception):
10+
"""Base exception for all Shade SDK errors."""
11+
12+
13+
class HTTPError(ShadeError):
14+
"""Raised for non-2xx responses that are not handled by a more specific error."""
15+
16+
def __init__(self, message: str, status_code: int) -> None:
17+
super().__init__(message)
18+
self.status_code = status_code
19+
20+
21+
class RateLimitError(HTTPError):
22+
"""
23+
Raised when the API returns HTTP 429 Too Many Requests and either:
24+
- auto-retry is disabled, or
25+
- ``max_retries`` has been exhausted.
26+
27+
Attributes
28+
----------
29+
retry_after : int | None
30+
Seconds to wait before the next attempt, parsed from the
31+
``Retry-After`` response header. ``None`` if the header was absent.
32+
"""
33+
34+
def __init__(self, message: str, retry_after: Optional[int] = None) -> None:
35+
super().__init__(message, status_code=429)
36+
self.retry_after = retry_after
37+
38+
def __str__(self) -> str: # pragma: no cover
39+
base = super().__str__()
40+
if self.retry_after is not None:
41+
return f"{base} (retry after {self.retry_after}s)"
42+
return base

src/shade/gateway.py

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,87 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Dict, Optional
4+
5+
from .http import AsyncHTTPClient, SyncHTTPClient, DEFAULT_MAX_RETRIES
6+
7+
18
class Gateway:
29
"""
310
Main entry point for the Shade Payment Gateway.
11+
12+
Parameters
13+
----------
14+
api_key : str
15+
Your Shade API key.
16+
base_url : str
17+
Override the default API base URL (useful for testing).
18+
max_retries : int
19+
Number of automatic retries on HTTP 429. Defaults to
20+
``DEFAULT_MAX_RETRIES`` (3). Set to ``0`` to disable.
21+
timeout : float
22+
Per-request socket timeout in seconds.
423
"""
5-
def __init__(self):
6-
pass
724

8-
def process_payment(self, amount: float, currency: str):
25+
_DEFAULT_BASE_URL = "https://api.shadeprotocol.io/v1"
26+
27+
def __init__(
28+
self,
29+
api_key: str = "",
30+
base_url: str = "",
31+
max_retries: int = DEFAULT_MAX_RETRIES,
32+
timeout: float = 30.0,
33+
) -> None:
34+
self.api_key = api_key
35+
self._base_url = base_url or self._DEFAULT_BASE_URL
36+
self._http = SyncHTTPClient(
37+
base_url=self._base_url,
38+
api_key=api_key,
39+
max_retries=max_retries,
40+
timeout=timeout,
41+
)
42+
self._async_http = AsyncHTTPClient(
43+
base_url=self._base_url,
44+
api_key=api_key,
45+
max_retries=max_retries,
46+
timeout=timeout,
47+
)
48+
49+
# ------------------------------------------------------------------
50+
# Sync API
51+
# ------------------------------------------------------------------
52+
53+
def process_payment(self, amount: float, currency: str) -> Dict[str, Any]:
954
"""
10-
Process a payment (placeholder).
55+
Process a payment (sync).
56+
57+
Parameters
58+
----------
59+
amount : float
60+
Payment amount.
61+
currency : str
62+
ISO 4217 currency code (e.g. ``"USD"``).
63+
64+
Returns
65+
-------
66+
dict
67+
API response body.
1168
"""
12-
print(f"Processing payment of {amount} {currency}...")
13-
return True
69+
return self._http.request(
70+
"POST",
71+
"/payments",
72+
{"amount": amount, "currency": currency},
73+
)
74+
75+
# ------------------------------------------------------------------
76+
# Async API
77+
# ------------------------------------------------------------------
78+
79+
async def process_payment_async(
80+
self, amount: float, currency: str
81+
) -> Dict[str, Any]:
82+
"""Async variant of :meth:`process_payment`."""
83+
return await self._async_http.request(
84+
"POST",
85+
"/payments",
86+
{"amount": amount, "currency": currency},
87+
)

0 commit comments

Comments
 (0)