Skip to content

Commit 2d03481

Browse files
committed
Add extension rate limit helper
1 parent 7286579 commit 2d03481

5 files changed

Lines changed: 73 additions & 3 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- 🔁 **Version-aware routing** – register multiple handlers per event (`@sdk.webhook(..., version="v2")`) and propagate version headers on outbound calls.
1212
- 📦 **Manifest-aware defaults** – automatically loads `extension.yaml`/`manifest.yaml`, applies configuration defaults, and hydrates secrets from `KIKET_SECRET_*` environment variables.
1313
- 📇 **Custom data client** – call `/api/v1/ext/custom_data/...` with `context.endpoints.custom_data(project_id)` using the configured extension API key.
14+
- 📉 **Rate-limit helper** – introspect `/api/v1/ext/rate_limit` before enqueueing large jobs or retries.
1415
- 🧱 **Typed & documented** – designed for Python 3.11+ with Ruff linting, MyPy type hints, and rich docstrings.
1516
- 📊 **Telemetry & feedback hooks** – capture handler duration/success metrics automatically and forward them to your own feedback callback or a hosted endpoint.
1617

@@ -148,3 +149,22 @@ When you are ready to cut a release:
148149
- **Sample extension:** ship a production-grade marketing automation example demonstrating multi-event handlers, manifest-driven configuration, and deployment templates.
149150
- **Documentation:** publish quickstart, reference, cookbook, and tutorial content alongside SDK release.
150151
- **Early access:** package for PyPI, collect telemetry/feedback before general availability (telemetry hooks + publishing checklist now available).
152+
### Rate-Limit Helper
153+
154+
Need to throttle expensive work? Ask the runtime for the current window and remaining calls:
155+
156+
```python
157+
@sdk.webhook("automation.dispatch", version="v1")
158+
async def handle_dispatch(payload, context):
159+
limits = await context.endpoints.rate_limit()
160+
if limits["remaining"] < 5:
161+
await context.endpoints.notify(
162+
"Rate limit warning",
163+
f"Only {limits['remaining']} calls remain in this window",
164+
level="warning",
165+
)
166+
return {"deferred": True, "reset_in": limits["reset_in"]}
167+
168+
# Continue with the expensive call
169+
return {"ok": True}
170+
```

kiket_sdk/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from .client import KiketClient
44
from .config import ExtensionConfig
55
from .custom_data import ExtensionCustomDataClient
6-
from .endpoints import ExtensionEndpoints
6+
from .endpoints import ExtensionEndpoints, RateLimitInfo
77
from .notifications import (
88
ChannelValidationRequest,
99
ChannelValidationResponse,
@@ -25,6 +25,7 @@
2525
"KiketClient",
2626
"ExtensionConfig",
2727
"ExtensionEndpoints",
28+
"RateLimitInfo",
2829
"ExtensionCustomDataClient",
2930
"ExtensionSlaEventsClient",
3031
"ExtensionSecretManager",

kiket_sdk/endpoints.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
"""High-level client for Kiket extension endpoints."""
22
from __future__ import annotations
33

4-
from typing import Any
4+
from typing import Any, TypedDict
55

66
from .client import KiketClient
77
from .custom_data import ExtensionCustomDataClient
88
from .secrets import ExtensionSecretManager
99
from .sla import ExtensionSlaEventsClient
1010

1111

12+
class RateLimitInfo(TypedDict):
13+
"""Shape of the `/api/v1/ext/rate_limit` response."""
14+
15+
limit: int
16+
remaining: int
17+
window_seconds: int
18+
reset_in: int
19+
20+
1221
class ExtensionEndpoints:
1322
"""Typed helpers for calling common Kiket extension endpoints."""
1423

@@ -55,6 +64,18 @@ def sla_events(self, project_id: int | str) -> ExtensionSlaEventsClient:
5564
"""Return a helper for querying SLA alerts for a project."""
5665
return ExtensionSlaEventsClient(self._client, project_id)
5766

67+
async def rate_limit(self) -> RateLimitInfo:
68+
"""Fetch the current extension-specific rate limit window."""
69+
response = await self._client.get("/api/v1/ext/rate_limit")
70+
payload = response.json()
71+
data = payload.get("rate_limit") or {}
72+
return {
73+
"limit": int(data.get("limit", 0) or 0),
74+
"remaining": int(data.get("remaining", 0) or 0),
75+
"window_seconds": int(data.get("window_seconds", 0) or 0),
76+
"reset_in": int(data.get("reset_in", 0) or 0),
77+
}
78+
5879
def _version_headers(self) -> dict[str, str]:
5980
if not self._event_version:
6081
return {}

tests/test_endpoints.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,31 @@ def test_sla_helper_returns_client():
7373
endpoints = ExtensionEndpoints(client) # type: ignore[arg-type]
7474
helper = endpoints.sla_events(project_id="proj-1")
7575
assert isinstance(helper, ExtensionSlaEventsClient)
76+
77+
78+
@pytest.mark.asyncio
79+
async def test_rate_limit_returns_payload():
80+
async def handler(request: httpx.Request) -> httpx.Response:
81+
return httpx.Response(
82+
status_code=200,
83+
json={
84+
"rate_limit": {
85+
"limit": 600,
86+
"remaining": 42,
87+
"window_seconds": 60,
88+
"reset_in": 12,
89+
}
90+
},
91+
)
92+
93+
client = KiketClient(base_url="https://example.invalid", workspace_token="wk_test")
94+
client._client = httpx.AsyncClient(transport=MockTransport(handler), base_url=client.base_url) # type: ignore[attr-defined]
95+
96+
async with client as http_client:
97+
endpoints = ExtensionEndpoints(http_client)
98+
info = await endpoints.rate_limit()
99+
100+
assert info["limit"] == 600
101+
assert info["remaining"] == 42
102+
assert info["window_seconds"] == 60
103+
assert info["reset_in"] == 12

tests/test_telemetry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,4 @@ async def handler(payload, context): # noqa: ARG001
8181
assert len(events) == 1
8282
record = events[0]
8383
assert record.status == "error"
84-
assert record.metadata["message"] == "boom"
84+
assert record.metadata["error_message"] == "boom"

0 commit comments

Comments
 (0)