throttlekit 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- throttlekit-0.2.0/PKG-INFO +384 -0
- throttlekit-0.2.0/README.md +356 -0
- {throttlekit-0.1.0 → throttlekit-0.2.0}/pyproject.toml +26 -7
- throttlekit-0.2.0/src/throttlekit/__init__.py +17 -0
- throttlekit-0.2.0/src/throttlekit/backends/base.py +44 -0
- throttlekit-0.2.0/src/throttlekit/backends/redis.py +115 -0
- throttlekit-0.2.0/src/throttlekit/backends/sql.py +157 -0
- throttlekit-0.2.0/src/throttlekit/distributed.py +213 -0
- throttlekit-0.2.0/src/throttlekit/fastapi.py +166 -0
- {throttlekit-0.1.0 → throttlekit-0.2.0}/src/throttlekit/leaky_limiter.py +17 -1
- {throttlekit-0.1.0 → throttlekit-0.2.0}/src/throttlekit/limiter.py +30 -7
- throttlekit-0.2.0/src/throttlekit/py.typed +1 -0
- throttlekit-0.1.0/PKG-INFO +0 -205
- throttlekit-0.1.0/README.md +0 -185
- throttlekit-0.1.0/src/throttlekit/__init__.py +0 -2
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: throttlekit
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Fast asyncio-compatible token bucket rate limiter
|
|
5
|
+
Author: Roudrasekhar Majumder
|
|
6
|
+
Author-email: Roudrasekhar Majumder <roudra25@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
16
|
+
Requires-Dist: starlette>=1.3.1
|
|
17
|
+
Requires-Dist: typing-extensions>=4.12.2 ; python_full_version < '3.10'
|
|
18
|
+
Requires-Dist: fastapi>=0.136.3 ; extra == 'fastapi'
|
|
19
|
+
Requires-Dist: starlette>=1.3.1 ; extra == 'fastapi'
|
|
20
|
+
Requires-Dist: uvicorn>=0.22.0 ; extra == 'fastapi'
|
|
21
|
+
Requires-Dist: redis>=8.0.0 ; extra == 'redis'
|
|
22
|
+
Requires-Dist: sqlalchemy>=2.0.50 ; extra == 'sql'
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Provides-Extra: fastapi
|
|
25
|
+
Provides-Extra: redis
|
|
26
|
+
Provides-Extra: sql
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# 🔄 throttlekit
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/throttlekit/)
|
|
32
|
+
[](https://opensource.org/licenses/MIT)
|
|
33
|
+
[](https://github.com/rowds/throttlekit/actions)
|
|
34
|
+
|
|
35
|
+
A lightweight, high-performance, and feature-rich async rate limiting library for Python. Fully supports local (in-memory) and distributed (Redis, SQL) deployments, with first-class support for FastAPI and custom logging.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 🚀 Key Features
|
|
40
|
+
|
|
41
|
+
* ⚡ **Two Powerful Algorithms:**
|
|
42
|
+
* **Token Bucket:** Perfect for allowing bursts of requests up to a maximum limit, refilled at a constant rate.
|
|
43
|
+
* **Leaky Bucket (GCRA):** Enforces a steady flow of traffic, smooth pacing, and rejects/delays bursts.
|
|
44
|
+
* 🌐 **Distributed Coordination:** Share rate-limiting states across multiple instances/containers using:
|
|
45
|
+
* **Redis Backend:** High-performance, atomic, Lua-scripted rate limiting.
|
|
46
|
+
* **SQL Backend:** Database-backed rate limiting using SQLAlchemy async engine with row-level locking (supports PostgreSQL, MySQL, SQLite, etc.).
|
|
47
|
+
* 🛣️ **FastAPI/Starlette Ready:** Easily rate limit web endpoints using:
|
|
48
|
+
* **Dependency Injection (`Depends`):** Route-specific rate limits.
|
|
49
|
+
* **Application Middleware:** Global, application-wide rate limits.
|
|
50
|
+
* **Blocking (Throttling) vs. Non-blocking (Immediate HTTP 429 Rejection)** modes.
|
|
51
|
+
* 🚦 **Concurrency Limits:** Control the number of concurrent executions alongside rate limits.
|
|
52
|
+
* 🪵 **Custom Logging Support:** Fully integrates with Python's built-in `logging.Logger` for monitoring rate-limiting operations.
|
|
53
|
+
* 🛡️ **Clean Async Architecture:** Thread-safe, async-native, with graceful shutdowns via `.stop()`.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## ⚙️ Installation
|
|
58
|
+
|
|
59
|
+
Install the core package or include optional dependencies depending on your storage backend:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Core package (in-memory limiters only)
|
|
63
|
+
uv add throttlekit
|
|
64
|
+
|
|
65
|
+
# With Redis support
|
|
66
|
+
uv add "throttlekit[redis]"
|
|
67
|
+
|
|
68
|
+
# With SQL/SQLAlchemy support
|
|
69
|
+
uv add "throttlekit[sql]"
|
|
70
|
+
|
|
71
|
+
# With FastAPI/Starlette support
|
|
72
|
+
uv add "throttlekit[fastapi]"
|
|
73
|
+
|
|
74
|
+
# Install everything
|
|
75
|
+
uv add "throttlekit[redis,sql,fastapi]"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🛠️ Usage Patterns
|
|
81
|
+
|
|
82
|
+
All limiters (local and distributed) support three standard interaction patterns:
|
|
83
|
+
|
|
84
|
+
### 1️⃣ Decorator Pattern
|
|
85
|
+
Decorate async functions to apply rate limits automatically. Supports static keys or dynamic callable keys:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# Static key
|
|
89
|
+
@limiter.limit(key="my-key", block=False)
|
|
90
|
+
async def process_task():
|
|
91
|
+
return "success"
|
|
92
|
+
|
|
93
|
+
# Dynamic key (receives the decorated function's arguments)
|
|
94
|
+
@limiter.limit(key=lambda user_id, *args: f"user:{user_id}", block=True)
|
|
95
|
+
async def fetch_user_data(user_id: int):
|
|
96
|
+
return {"data": "..."}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 2️⃣ Context Manager Pattern
|
|
100
|
+
Enforce limits cleanly within a code block:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
# Using standard context manager
|
|
104
|
+
async with limiter(key="my-key", block=True):
|
|
105
|
+
await do_some_work()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 3️⃣ Manual Control Pattern
|
|
109
|
+
Call `.acquire()` directly in your application code:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
# Acquire token manually (blocks until a token is available)
|
|
113
|
+
await limiter.acquire("my-key")
|
|
114
|
+
|
|
115
|
+
# Non-blocking check
|
|
116
|
+
acquired = await limiter.acquire("my-key", block=False)
|
|
117
|
+
if not acquired:
|
|
118
|
+
raise RuntimeError("Rate limit exceeded")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## ⚡ Core Rate Limiters
|
|
124
|
+
|
|
125
|
+
### 1. In-Memory Limiters (Single Instance)
|
|
126
|
+
|
|
127
|
+
Best for single-server processes where rate limiting state resides only in memory.
|
|
128
|
+
|
|
129
|
+
#### Token Bucket (`TokenBucketRateLimiter`)
|
|
130
|
+
Allows bursty traffic. You can optionally enforce a maximum concurrency limit.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
import logging
|
|
134
|
+
from throttlekit import TokenBucketRateLimiter
|
|
135
|
+
|
|
136
|
+
logger = logging.getLogger("my_app")
|
|
137
|
+
|
|
138
|
+
# Refill 10 tokens every 60 seconds. Restrict concurrency to max 5 workers.
|
|
139
|
+
limiter = TokenBucketRateLimiter(
|
|
140
|
+
max_tokens=10,
|
|
141
|
+
refill_interval=60.0,
|
|
142
|
+
concurrency_limit=5,
|
|
143
|
+
logger=logger
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# You MUST start the background refill loop before using in-memory limiters
|
|
147
|
+
await limiter.start()
|
|
148
|
+
|
|
149
|
+
# Acquire tokens
|
|
150
|
+
async with limiter:
|
|
151
|
+
await fetch_api_data()
|
|
152
|
+
|
|
153
|
+
# Refund a token manually if an operation fails (optional)
|
|
154
|
+
limiter.release_token()
|
|
155
|
+
|
|
156
|
+
# Clean up / stop the background task gracefully on application shutdown
|
|
157
|
+
await limiter.stop()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### Leaky Bucket (`LeakyBucketRateLimiter`)
|
|
161
|
+
Enforces a smooth, consistent request rate. Requests are queued up to a maximum capacity.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from throttlekit import LeakyBucketRateLimiter
|
|
165
|
+
|
|
166
|
+
# Paced rate of 2 requests per second. Queue up to 100 requests.
|
|
167
|
+
limiter = LeakyBucketRateLimiter(
|
|
168
|
+
rate=2.0,
|
|
169
|
+
max_queue_size=100
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
await limiter.start()
|
|
173
|
+
|
|
174
|
+
# Paces execution: blocks if queue space is available; raises full queue exception if exceeded
|
|
175
|
+
async with limiter:
|
|
176
|
+
await send_notification()
|
|
177
|
+
|
|
178
|
+
await limiter.stop()
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### 2. Distributed Limiters (Multi-Container Deployments)
|
|
184
|
+
|
|
185
|
+
Best for microservices, Kubernetes pods, or multi-replica FastAPI nodes. State is persisted and synchronized through a shared backend.
|
|
186
|
+
|
|
187
|
+
#### Step A: Configure the Backend
|
|
188
|
+
|
|
189
|
+
##### Redis Backend (Recommended)
|
|
190
|
+
Uses highly efficient, atomic Lua scripts.
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
import redis.asyncio as aioredis
|
|
194
|
+
from throttlekit import RedisBackend
|
|
195
|
+
|
|
196
|
+
# Initialize async redis client
|
|
197
|
+
redis_client = aioredis.from_url("redis://localhost:6379")
|
|
198
|
+
backend = RedisBackend(redis_client)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
##### SQL Backend (SQLAlchemy compatible)
|
|
202
|
+
Supports PostgreSQL, MySQL, SQLite, etc. Uses row-level lock-based synchronization. Tables are automatically initialized.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
206
|
+
from throttlekit import SQLBackend
|
|
207
|
+
|
|
208
|
+
# Initialize SQLAlchemy async engine
|
|
209
|
+
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
|
|
210
|
+
backend = SQLBackend(engine, schema="my_custom_schema") # Optional schema argument
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### Step B: Instantiate the Distributed Limiter
|
|
214
|
+
|
|
215
|
+
##### Distributed Token Bucket (`DistributedTokenBucket`)
|
|
216
|
+
```python
|
|
217
|
+
from throttlekit import DistributedTokenBucket
|
|
218
|
+
|
|
219
|
+
# 10 tokens, refilling every 60 seconds.
|
|
220
|
+
# The 'name' namespace separates keys in the shared storage backend.
|
|
221
|
+
limiter = DistributedTokenBucket(
|
|
222
|
+
backend=backend,
|
|
223
|
+
max_tokens=10,
|
|
224
|
+
refill_interval=60.0,
|
|
225
|
+
name="api_user_limits"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Enforce rate limit globally for user "user_123"
|
|
229
|
+
await limiter.acquire(key="user_123", tokens=1, block=True)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
##### Distributed Leaky Bucket (`DistributedLeakyBucket` - GCRA)
|
|
233
|
+
```python
|
|
234
|
+
from throttlekit import DistributedLeakyBucket
|
|
235
|
+
|
|
236
|
+
# Paces at 5 requests per second, queue up to 10 requests.
|
|
237
|
+
limiter = DistributedLeakyBucket(
|
|
238
|
+
backend=backend,
|
|
239
|
+
rate=5.0,
|
|
240
|
+
max_queue_size=10,
|
|
241
|
+
name="sms_pacing"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
# Acquire space for phone number
|
|
245
|
+
await limiter.acquire(key="+1234567890", block=True)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 🌐 FastAPI & Starlette Integration
|
|
251
|
+
|
|
252
|
+
`throttlekit` comes with complete support for FastAPI route dependencies (`Depends`) and global middleware.
|
|
253
|
+
|
|
254
|
+
### 1️⃣ Route-level Dependency (`FastAPIRateLimiter`)
|
|
255
|
+
Use `FastAPIRateLimiter` to apply custom rate limits to specific endpoints or routers.
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from fastapi import FastAPI, Depends, Request
|
|
259
|
+
from throttlekit.fastapi import FastAPIRateLimiter
|
|
260
|
+
|
|
261
|
+
app = FastAPI()
|
|
262
|
+
|
|
263
|
+
# Custom function to resolve user ID or IP globally
|
|
264
|
+
def resolve_client_key(request: Request) -> str:
|
|
265
|
+
return request.headers.get("X-User-ID", request.client.host or "anonymous")
|
|
266
|
+
|
|
267
|
+
# Route limited by dynamic key: 2 requests per 10 seconds max.
|
|
268
|
+
# block=False returns HTTP 429 immediately if exceeded.
|
|
269
|
+
@app.get(
|
|
270
|
+
"/expensive-endpoint",
|
|
271
|
+
dependencies=[
|
|
272
|
+
Depends(FastAPIRateLimiter(
|
|
273
|
+
limiter=tb_limiter,
|
|
274
|
+
block=False,
|
|
275
|
+
detail="Too many expensive requests. Please wait.",
|
|
276
|
+
key=resolve_client_key
|
|
277
|
+
))
|
|
278
|
+
]
|
|
279
|
+
)
|
|
280
|
+
async def read_expensive_data():
|
|
281
|
+
return {"data": "rich content"}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 2️⃣ Global Application Middleware (`RateLimitMiddleware`)
|
|
285
|
+
Protect the entire application under a global rate-limiting policy.
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
from fastapi import FastAPI
|
|
289
|
+
from throttlekit.fastapi import RateLimitMiddleware
|
|
290
|
+
|
|
291
|
+
app = FastAPI()
|
|
292
|
+
|
|
293
|
+
# Enforce rate limits globally on all incoming paths
|
|
294
|
+
app.add_middleware(
|
|
295
|
+
RateLimitMiddleware,
|
|
296
|
+
limiter=tb_limiter,
|
|
297
|
+
block=False,
|
|
298
|
+
detail="Global rate limit exceeded",
|
|
299
|
+
key=lambda req: req.client.host or "global"
|
|
300
|
+
)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
> [!TIP]
|
|
304
|
+
> Use `block=True` in both dependency and middleware modes to **throttle** requests (making them wait/queue in line until a token is refilled) instead of rejecting them with an HTTP 429 error.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## ⚙️ Configuration Options Reference
|
|
309
|
+
|
|
310
|
+
### In-Memory Limiters
|
|
311
|
+
| Limiter Class | Parameter | Type | Default | Description |
|
|
312
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
313
|
+
| `TokenBucketRateLimiter` | `max_tokens` | `int` | *Required* | Max tokens the bucket can hold. |
|
|
314
|
+
| | `refill_interval` | `float` | `1.0` | Duration (seconds) to refill the bucket to max capacity. |
|
|
315
|
+
| | `concurrency_limit` | `Optional[int]` | `None` | Restricts concurrent active requests. |
|
|
316
|
+
| | `logger` | `Optional[logging.Logger]` | `None` | Custom logger instance. |
|
|
317
|
+
| `LeakyBucketRateLimiter` | `rate` | `float` | `1.0` | Target rate of requests per second. |
|
|
318
|
+
| | `max_queue_size` | `int` | `100` | Max queued requests allowed before failing. |
|
|
319
|
+
| | `logger` | `Optional[logging.Logger]` | `None` | Custom logger instance. |
|
|
320
|
+
|
|
321
|
+
### Distributed Limiters
|
|
322
|
+
| Limiter Class | Parameter | Type | Default | Description |
|
|
323
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
324
|
+
| `DistributedTokenBucket` | `backend` | `BaseBackend` | *Required* | RedisBackend or SQLBackend storage instance. |
|
|
325
|
+
| | `max_tokens` | `int` | *Required* | Max tokens the bucket holds. |
|
|
326
|
+
| | `refill_interval` | `float` | `1.0` | Time (seconds) to completely refill the bucket. |
|
|
327
|
+
| | `name` | `str` | `"default"` | Unique namespace key for backend storage. |
|
|
328
|
+
| | `logger` | `Optional[logging.Logger]` | `None` | Custom logger instance. |
|
|
329
|
+
| `DistributedLeakyBucket` | `backend` | `BaseBackend` | *Required* | RedisBackend or SQLBackend storage instance. |
|
|
330
|
+
| | `rate` | `float` | `1.0` | Requests allowed per second. |
|
|
331
|
+
| | `max_queue_size` | `int` | `100` | Queue limit capacity (GCRA buffer size). |
|
|
332
|
+
| | `name` | `str` | `"default"` | Unique namespace key for backend storage. |
|
|
333
|
+
| | `logger` | `Optional[logging.Logger]` | `None` | Custom logger instance. |
|
|
334
|
+
|
|
335
|
+
### Backends
|
|
336
|
+
| Backend Class | Parameter | Type | Default | Description |
|
|
337
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
338
|
+
| `RedisBackend` | `redis_client` | `Redis` | *Required* | Active async Redis connection client. |
|
|
339
|
+
| `SQLBackend` | `engine` | `AsyncEngine` | *Required* | Active SQLAlchemy async database engine. |
|
|
340
|
+
| | `table_name` | `str` | `"throttlekit_buckets"` | Base name of the database tables. |
|
|
341
|
+
| | `schema` | `Optional[str]` | `None` | Optional target database schema. |
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 🪵 Custom Logging
|
|
346
|
+
|
|
347
|
+
`throttlekit` limiters support passing standard loggers. All internal debug, warning, and startup logs will flow through your logger instance:
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
import logging
|
|
351
|
+
from throttlekit import TokenBucketRateLimiter
|
|
352
|
+
|
|
353
|
+
# Define a custom logging format
|
|
354
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
355
|
+
custom_logger = logging.getLogger("throttlekit_custom")
|
|
356
|
+
|
|
357
|
+
limiter = TokenBucketRateLimiter(
|
|
358
|
+
max_tokens=10,
|
|
359
|
+
refill_interval=1.0,
|
|
360
|
+
logger=custom_logger
|
|
361
|
+
)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## 🧪 Testing
|
|
367
|
+
|
|
368
|
+
We maintain 100% code coverage on the package modules. Run the test suite:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
# Run pytest with coverage reporting
|
|
372
|
+
uv run pytest --cov=src/throttlekit --cov-report=term-missing
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
To run the integration tests targeting local Redis (at `localhost:6379`) and multiprocessing interpreters:
|
|
376
|
+
```bash
|
|
377
|
+
uv run pytest tests/test_integration.py
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 📜 License
|
|
383
|
+
|
|
384
|
+
MIT License © [Roudrasekhar Majumder](https://github.com/rowds)
|