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.
@@ -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
+ [![PyPI Version](https://img.shields.io/pypi/v/throttlekit.svg)](https://pypi.org/project/throttlekit/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
33
+ [![Build Status](https://github.com/rowds/throttlekit/actions/workflows/test.yaml/badge.svg)](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)