moderato 0.3.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.
- moderato-0.3.0/LICENSE +21 -0
- moderato-0.3.0/PKG-INFO +545 -0
- moderato-0.3.0/README.md +509 -0
- moderato-0.3.0/moderato/__init__.py +75 -0
- moderato-0.3.0/moderato/algorithms/__init__.py +49 -0
- moderato-0.3.0/moderato/algorithms/base.py +62 -0
- moderato-0.3.0/moderato/algorithms/sliding_window.py +292 -0
- moderato-0.3.0/moderato/algorithms/token_bucket.py +226 -0
- moderato-0.3.0/moderato/backends/__init__.py +7 -0
- moderato-0.3.0/moderato/backends/redis.py +735 -0
- moderato-0.3.0/moderato/decorators.py +419 -0
- moderato-0.3.0/moderato/exceptions.py +57 -0
- moderato-0.3.0/moderato/limiter.py +850 -0
- moderato-0.3.0/moderato/metrics.py +405 -0
- moderato-0.3.0/moderato/middleware.py +235 -0
- moderato-0.3.0/moderato/models.py +99 -0
- moderato-0.3.0/moderato/py.typed +0 -0
- moderato-0.3.0/moderato/scripts/fixed_window.lua +66 -0
- moderato-0.3.0/moderato/scripts/sliding_window.lua +157 -0
- moderato-0.3.0/moderato/scripts/token_bucket.lua +109 -0
- moderato-0.3.0/moderato/utils.py +218 -0
- moderato-0.3.0/pyproject.toml +143 -0
moderato-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Arjun
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
moderato-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: moderato
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Async Redis-backed rate limiting library for Python
|
|
5
|
+
Home-page: https://github.com/Arjun-Aravind/moderato
|
|
6
|
+
Keywords: rate-limit,rate-limiting,redis,fastapi,async,api,throttle
|
|
7
|
+
Author: Arjun Aravind
|
|
8
|
+
Author-email: arjunaravind748@gmail.com
|
|
9
|
+
Requires-Python: >=3.9,<3.14
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Framework :: AsyncIO
|
|
12
|
+
Classifier: Framework :: FastAPI
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Session
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Provides-Extra: fastapi
|
|
25
|
+
Provides-Extra: metrics
|
|
26
|
+
Requires-Dist: fastapi (>=0.100) ; extra == "fastapi" or extra == "all"
|
|
27
|
+
Requires-Dist: prometheus-client (>=0.19) ; extra == "metrics" or extra == "all"
|
|
28
|
+
Requires-Dist: pydantic (>=2.0,<3.0)
|
|
29
|
+
Requires-Dist: redis (>=5.0.0,<6.0.0)
|
|
30
|
+
Requires-Dist: starlette (>=0.27) ; extra == "fastapi" or extra == "all"
|
|
31
|
+
Requires-Dist: typing-extensions (>=4.0)
|
|
32
|
+
Project-URL: Documentation, https://github.com/Arjun-Aravind/moderato#readme
|
|
33
|
+
Project-URL: Repository, https://github.com/Arjun-Aravind/moderato
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# Moderato
|
|
37
|
+
|
|
38
|
+
*In musical notation, **moderato** means "at a moderate pace." Moderato enforces your API's tempo.*
|
|
39
|
+
|
|
40
|
+
[](https://www.python.org)
|
|
41
|
+
[](LICENSE)
|
|
42
|
+
[](https://redis.io)
|
|
43
|
+
[](https://github.com/psf/black)
|
|
44
|
+
|
|
45
|
+
A Redis-backed rate limiting library for async Python applications.
|
|
46
|
+
|
|
47
|
+
[Features](#features) | [Quick Start](#quick-start) | [Algorithms](#algorithms) | [Documentation](#documentation) | [Examples](#examples)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## What is Moderato?
|
|
52
|
+
|
|
53
|
+
Moderato is a Redis-backed rate limiting library for async Python applications. It provides three algorithms, FastAPI integration, optional Prometheus metrics, cost-based limits, and tenant isolation.
|
|
54
|
+
|
|
55
|
+
**Use cases:**
|
|
56
|
+
- FastAPI applications requiring rate limiting
|
|
57
|
+
- Multi-tenant SaaS platforms with tier-based limits
|
|
58
|
+
- APIs that need Redis-backed limits across multiple application instances
|
|
59
|
+
- Services that need Prometheus counters and latency histograms
|
|
60
|
+
- Applications requiring atomic rate limit decisions
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
### Core Capabilities
|
|
67
|
+
- **Three Algorithms** - Fixed Window, Token Bucket & Sliding Window
|
|
68
|
+
- **Async-first design** - Built for FastAPI and modern async Python
|
|
69
|
+
- **Atomic decisions** - Redis Lua scripts keep checks and updates in one operation
|
|
70
|
+
- **Redis server time** - Consistent windows across application instances
|
|
71
|
+
- **Integer precision** - Uses integer math (x1000 multiplier) for accuracy
|
|
72
|
+
|
|
73
|
+
### Integrations and controls
|
|
74
|
+
- **Rate limit headers** - Standard headers on decorated FastAPI responses
|
|
75
|
+
- **Prometheus Metrics** - Optional counters and latency histograms
|
|
76
|
+
- **Multi-tenant support** - Isolated limits for different users/tiers/organizations
|
|
77
|
+
- **Decorator-based API** - Clean, declarative rate limiting
|
|
78
|
+
- **Cost-based limiting** - Weight expensive operations appropriately
|
|
79
|
+
- **Flexible configuration** - Customizable key extraction, algorithms, and costs
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Quick Start
|
|
84
|
+
|
|
85
|
+
### Installation
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install moderato
|
|
89
|
+
|
|
90
|
+
# With FastAPI/Starlette middleware integration
|
|
91
|
+
pip install 'moderato[fastapi]'
|
|
92
|
+
|
|
93
|
+
# With metrics support (optional)
|
|
94
|
+
pip install 'moderato[metrics]'
|
|
95
|
+
|
|
96
|
+
# Everything (optional)
|
|
97
|
+
pip install 'moderato[all]'
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Basic Example
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from contextlib import asynccontextmanager
|
|
104
|
+
|
|
105
|
+
from fastapi import FastAPI, Request
|
|
106
|
+
from moderato import RateLimiter, RateLimitHeadersMiddleware
|
|
107
|
+
|
|
108
|
+
limiter = RateLimiter(redis_url="redis://localhost:6379")
|
|
109
|
+
|
|
110
|
+
@asynccontextmanager
|
|
111
|
+
async def lifespan(app: FastAPI):
|
|
112
|
+
await limiter.connect()
|
|
113
|
+
yield
|
|
114
|
+
await limiter.close()
|
|
115
|
+
|
|
116
|
+
app = FastAPI(lifespan=lifespan)
|
|
117
|
+
app.add_middleware(RateLimitHeadersMiddleware)
|
|
118
|
+
|
|
119
|
+
@app.get("/api/users")
|
|
120
|
+
@limiter.limit("100/minute")
|
|
121
|
+
async def get_users(request: Request):
|
|
122
|
+
return {"users": ["Alice", "Bob"]}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This gives you:
|
|
126
|
+
- Rate limiting (100 requests/minute per IP)
|
|
127
|
+
- Rate limit headers on this decorated endpoint
|
|
128
|
+
- Proper 429 responses when exceeded
|
|
129
|
+
- Redis-backed, distributed-ready
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Algorithms
|
|
134
|
+
|
|
135
|
+
Moderato provides three tested algorithms. Choose based on the traffic behavior you want:
|
|
136
|
+
|
|
137
|
+
### Fixed Window (Default)
|
|
138
|
+
|
|
139
|
+
**Best for:** Simple rate limiting, strict per-window limits, lower memory usage
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
@limiter.limit("100/minute", algorithm="fixed_window")
|
|
143
|
+
async def endpoint(request: Request):
|
|
144
|
+
return {"data": "..."}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**How it works:**
|
|
148
|
+
- Time divided into fixed windows (e.g., 14:35:00 - 14:36:00)
|
|
149
|
+
- Counter increments per request
|
|
150
|
+
- Resets when window expires
|
|
151
|
+
|
|
152
|
+
**Pros:** Simple, low memory, strict limits
|
|
153
|
+
**Cons:** Possible boundary bursts (can get 2x at window edge)
|
|
154
|
+
|
|
155
|
+
### Token Bucket
|
|
156
|
+
|
|
157
|
+
**Best for:** Smoothing bursts while allowing capacity to refill continuously
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
@limiter.limit("100/minute", algorithm="token_bucket")
|
|
161
|
+
async def endpoint(request: Request):
|
|
162
|
+
return {"data": "..."}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**How it works:**
|
|
166
|
+
- Bucket holds tokens (capacity = 100)
|
|
167
|
+
- Tokens refill continuously (~1.67/second for 100/minute)
|
|
168
|
+
- Each request consumes tokens
|
|
169
|
+
|
|
170
|
+
**Pros:** Continuous refill and configurable burst capacity
|
|
171
|
+
**Cons:** State uses a Redis hash and allows bursts up to bucket capacity
|
|
172
|
+
|
|
173
|
+
### Sliding Window
|
|
174
|
+
|
|
175
|
+
**Best for:** Approximating a rolling window without storing every request timestamp
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
@limiter.limit("100/minute", algorithm="sliding_window")
|
|
179
|
+
async def endpoint(request: Request):
|
|
180
|
+
return {"data": "..."}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**How it works:**
|
|
184
|
+
- Combines current window with weighted portion of previous window
|
|
185
|
+
- Provides smooth transition between windows
|
|
186
|
+
- Approximates a rolling count with two fixed counters
|
|
187
|
+
|
|
188
|
+
**Pros:** Smooths fixed-window boundaries with constant Redis storage
|
|
189
|
+
**Cons:** It is an approximation rather than an exact request log
|
|
190
|
+
|
|
191
|
+
### Algorithm Comparison
|
|
192
|
+
|
|
193
|
+
| Feature | Fixed Window | Token Bucket | Sliding Window |
|
|
194
|
+
|---------|--------------|--------------|----------------|
|
|
195
|
+
| Simplicity | High | Medium | Medium |
|
|
196
|
+
| Boundary Bursts | Possible (2x) | None | None |
|
|
197
|
+
| Redis data | String counter | Hash with tokens and timestamp | Two string counters |
|
|
198
|
+
| Traffic behavior | Resets at boundaries | Continuous refill | Weighted window transition |
|
|
199
|
+
| Accuracy model | Exact fixed window | Exact token bucket state | Approximate rolling window |
|
|
200
|
+
|
|
201
|
+
**Recommendation:** Choose fixed window for simple quotas, token bucket for controlled bursts, and sliding window counter when fixed-window boundary bursts are undesirable.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Documentation
|
|
206
|
+
|
|
207
|
+
### Configuration
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from moderato import RateLimiter
|
|
211
|
+
|
|
212
|
+
limiter = RateLimiter(
|
|
213
|
+
redis_url="redis://localhost:6379", # Redis connection URL
|
|
214
|
+
key_prefix="myapp:ratelimit", # Prefix for Redis keys
|
|
215
|
+
default_algorithm="token_bucket", # Algorithm: "fixed_window", "token_bucket", or "sliding_window"
|
|
216
|
+
enable_metrics=False, # Enable Prometheus metrics
|
|
217
|
+
)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
| Parameter | Type | Default | Description |
|
|
221
|
+
|-----------|------|---------|-------------|
|
|
222
|
+
| `redis_url` | str | `"redis://localhost:6379"` | Redis connection string |
|
|
223
|
+
| `key_prefix` | str | `"ratelimit"` | Prefix for all Redis keys |
|
|
224
|
+
| `default_algorithm` | str | `"fixed_window"` | Default algorithm to use |
|
|
225
|
+
| `enable_metrics` | bool | `False` | Enable Prometheus metrics collection |
|
|
226
|
+
|
|
227
|
+
### Rate Limit Formats
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
"10/second" # 10 requests per second
|
|
231
|
+
"100/minute" # 100 requests per minute
|
|
232
|
+
"1000/hour" # 1000 requests per hour
|
|
233
|
+
"10000/day" # 10000 requests per day
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Decorator API
|
|
237
|
+
|
|
238
|
+
#### Basic IP-based limiting
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
@app.get("/api/data")
|
|
242
|
+
@limiter.limit("100/minute")
|
|
243
|
+
async def get_data(request: Request):
|
|
244
|
+
return {"data": "..."}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### Custom key extraction
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
@app.get("/api/user/{user_id}")
|
|
251
|
+
@limiter.limit(
|
|
252
|
+
"1000/hour",
|
|
253
|
+
key=lambda req: f"user:{req.path_params.get('user_id')}"
|
|
254
|
+
)
|
|
255
|
+
async def get_user_data(request: Request, user_id: str):
|
|
256
|
+
return {"user_id": user_id}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### Choose algorithm
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
@app.get("/api/smooth")
|
|
263
|
+
@limiter.limit("100/minute", algorithm="token_bucket")
|
|
264
|
+
async def smooth_endpoint(request: Request):
|
|
265
|
+
return {"data": "..."}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Share a limit across routes
|
|
269
|
+
|
|
270
|
+
Decorated endpoints use separate method-and-route scopes by default. Set `scope` to share one bucket:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
@limiter.limit("100/minute", scope="search-api")
|
|
274
|
+
async def search(request: Request):
|
|
275
|
+
...
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Manual `check()` calls use the `"global"` scope unless you pass one explicitly.
|
|
279
|
+
|
|
280
|
+
### Automatic Headers
|
|
281
|
+
|
|
282
|
+
For endpoints using `@limiter.limit(...)`, add the middleware to inject rate limit headers:
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
from moderato import RateLimitHeadersMiddleware
|
|
286
|
+
|
|
287
|
+
app.add_middleware(RateLimitHeadersMiddleware)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Headers added to decorated responses:**
|
|
291
|
+
- `X-RateLimit-Limit`: Maximum requests allowed
|
|
292
|
+
- `X-RateLimit-Remaining`: Requests remaining in current window
|
|
293
|
+
- `X-RateLimit-Reset`: Unix timestamp when the limit resets
|
|
294
|
+
|
|
295
|
+
**Additional headers on 429 responses:**
|
|
296
|
+
- `Retry-After`: Seconds to wait before retrying
|
|
297
|
+
|
|
298
|
+
### Prometheus Metrics
|
|
299
|
+
|
|
300
|
+
Install `moderato[metrics]`, then enable collection:
|
|
301
|
+
|
|
302
|
+
```python
|
|
303
|
+
limiter = RateLimiter(
|
|
304
|
+
redis_url="redis://localhost:6379",
|
|
305
|
+
enable_metrics=True # Enable Prometheus metrics
|
|
306
|
+
)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Metrics collected:**
|
|
310
|
+
- `moderato_checks_total` - Total rate limit checks
|
|
311
|
+
- `moderato_check_duration_seconds` - Check latency histogram
|
|
312
|
+
- `moderato_limit_exceeded_total` - Rate limit violations
|
|
313
|
+
- `moderato_backend_operations_total` - Redis operations
|
|
314
|
+
|
|
315
|
+
**Expose metrics endpoint:**
|
|
316
|
+
```python
|
|
317
|
+
from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
|
|
318
|
+
|
|
319
|
+
@app.get("/metrics")
|
|
320
|
+
async def metrics():
|
|
321
|
+
return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Multi-Tenant Setup
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
# Define tier-specific limits
|
|
328
|
+
TIER_LIMITS = {
|
|
329
|
+
"free": "100/hour",
|
|
330
|
+
"premium": "1000/hour",
|
|
331
|
+
"enterprise": "10000/hour",
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
@app.get("/api/data")
|
|
335
|
+
async def get_data(request: Request):
|
|
336
|
+
api_key = request.headers.get("X-API-Key", "anonymous")
|
|
337
|
+
tier = get_user_tier(api_key)
|
|
338
|
+
await limiter.check(
|
|
339
|
+
key=api_key,
|
|
340
|
+
rate=TIER_LIMITS[tier],
|
|
341
|
+
tenant_type=tier,
|
|
342
|
+
scope="GET:/api/data",
|
|
343
|
+
)
|
|
344
|
+
return {"data": "..."}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Cost-Based Limiting
|
|
348
|
+
|
|
349
|
+
```python
|
|
350
|
+
@app.post("/api/ml/inference")
|
|
351
|
+
@limiter.limit(
|
|
352
|
+
"100/minute",
|
|
353
|
+
cost=lambda req: 10 # This endpoint counts as 10 regular requests
|
|
354
|
+
)
|
|
355
|
+
async def ml_inference(request: Request):
|
|
356
|
+
return {"prediction": "..."}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Error Handling
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from moderato import RateLimitExceeded
|
|
363
|
+
|
|
364
|
+
@app.exception_handler(RateLimitExceeded)
|
|
365
|
+
async def rate_limit_handler(request: Request, exc: RateLimitExceeded):
|
|
366
|
+
return JSONResponse(
|
|
367
|
+
status_code=429,
|
|
368
|
+
content={
|
|
369
|
+
"error": "Rate limit exceeded",
|
|
370
|
+
"retry_after": exc.retry_after,
|
|
371
|
+
"limit": exc.limit,
|
|
372
|
+
},
|
|
373
|
+
headers={"Retry-After": str(exc.retry_after)},
|
|
374
|
+
)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Manual Checking
|
|
378
|
+
|
|
379
|
+
```python
|
|
380
|
+
# Direct rate limit check
|
|
381
|
+
try:
|
|
382
|
+
await limiter.check(key="user:123", rate="100/minute")
|
|
383
|
+
# Request allowed
|
|
384
|
+
except RateLimitExceeded as e:
|
|
385
|
+
# Rate limited
|
|
386
|
+
print(f"Retry after {e.retry_after} seconds")
|
|
387
|
+
|
|
388
|
+
# Get usage statistics
|
|
389
|
+
usage = await limiter.get_usage(key="user:123", rate="100/minute")
|
|
390
|
+
print(f"Current: {usage['current']}, Remaining: {usage['remaining']}")
|
|
391
|
+
|
|
392
|
+
# Reset rate limit
|
|
393
|
+
await limiter.reset(key="user:123")
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Performance
|
|
399
|
+
|
|
400
|
+
Run the benchmark suite against a local Redis instance rather than relying on hardware-independent throughput claims:
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
docker-compose -f docker-compose.dev.yml up -d
|
|
404
|
+
poetry install --with benchmarks
|
|
405
|
+
poetry run python benchmarks/performance.py --quick
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
The quick run reports throughput, latency percentiles, algorithm comparisons, and rate-limit accuracy. Run without `--quick` to include concurrent-client, Redis memory, and multi-tenant benchmarks. Results depend on Redis placement, network latency, hardware, Python version, and concurrency, so publish those details with any result.
|
|
409
|
+
|
|
410
|
+
**Implemented optimizations:**
|
|
411
|
+
- Cached Lua scripts with `EVALSHA` and `EVAL` fallback
|
|
412
|
+
- Redis connection pooling
|
|
413
|
+
- One atomic script call per rate limit decision
|
|
414
|
+
- Bounded, component-level hashing for long identifiers and scopes
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## Examples
|
|
419
|
+
|
|
420
|
+
See the [examples/](examples/) directory:
|
|
421
|
+
- [fastapi_app.py](examples/fastapi_app.py) - Complete FastAPI demo
|
|
422
|
+
- [multi_tenant.py](examples/multi_tenant.py) - Multi-tenant SaaS setup
|
|
423
|
+
- [algorithms_demo.py](examples/algorithms_demo.py) - Algorithm comparison
|
|
424
|
+
|
|
425
|
+
### Running Examples
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
# Start Redis
|
|
429
|
+
docker-compose -f docker-compose.dev.yml up -d
|
|
430
|
+
|
|
431
|
+
# FastAPI demo
|
|
432
|
+
poetry run uvicorn examples.fastapi_app:app --reload
|
|
433
|
+
|
|
434
|
+
# Multi-tenant demo
|
|
435
|
+
poetry run uvicorn examples.multi_tenant:app --reload --port 8001
|
|
436
|
+
|
|
437
|
+
# Algorithm comparison
|
|
438
|
+
poetry run python examples/algorithms_demo.py
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Architecture
|
|
444
|
+
|
|
445
|
+
```
|
|
446
|
+
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
447
|
+
│ FastAPI │────▶│ RateLimiter │────▶│ Redis │
|
|
448
|
+
│ App │ │ (Python) │ │ (Lua) │
|
|
449
|
+
└─────────────┘ └─────────────┘ └─────────────┘
|
|
450
|
+
│
|
|
451
|
+
┌───────┴───────┐
|
|
452
|
+
│ │
|
|
453
|
+
┌───────▼─────┐ ┌───────▼─────┐
|
|
454
|
+
│ Fixed │ │ Token │
|
|
455
|
+
│ Window │ │ Bucket │
|
|
456
|
+
└─────────────┘ └─────────────┘
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
For internals, read the algorithm implementations in `moderato/algorithms/`
|
|
460
|
+
and the atomic Lua scripts in `moderato/scripts/` — both are heavily tested
|
|
461
|
+
(`tests/`) and intentionally compact.
|
|
462
|
+
|
|
463
|
+
---
|
|
464
|
+
|
|
465
|
+
## Development
|
|
466
|
+
|
|
467
|
+
### Prerequisites
|
|
468
|
+
|
|
469
|
+
- Python 3.9–3.13
|
|
470
|
+
- Redis 7.0+
|
|
471
|
+
- Poetry
|
|
472
|
+
|
|
473
|
+
### Setup
|
|
474
|
+
|
|
475
|
+
```bash
|
|
476
|
+
git clone https://github.com/Arjun-Aravind/moderato.git
|
|
477
|
+
cd moderato
|
|
478
|
+
|
|
479
|
+
poetry install
|
|
480
|
+
docker-compose -f docker-compose.dev.yml up -d
|
|
481
|
+
poetry run pytest
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Commands
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
make test # Run tests
|
|
488
|
+
make test-cov # Run tests with coverage
|
|
489
|
+
make lint # Run linting
|
|
490
|
+
make format # Format code
|
|
491
|
+
make demo # Run algorithm demo
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## Testing
|
|
497
|
+
|
|
498
|
+
The test suite covers:
|
|
499
|
+
|
|
500
|
+
- All three algorithms (Fixed Window, Token Bucket, Sliding Window)
|
|
501
|
+
- Concurrent requests and race conditions
|
|
502
|
+
- Multi-tenant isolation
|
|
503
|
+
- Cost-based rate limiting
|
|
504
|
+
- Headers middleware
|
|
505
|
+
- Edge cases and error handling
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
# Run all tests
|
|
509
|
+
make test
|
|
510
|
+
|
|
511
|
+
# Run specific test file
|
|
512
|
+
poetry run pytest tests/test_token_bucket.py -v
|
|
513
|
+
|
|
514
|
+
# Run with coverage
|
|
515
|
+
make test-cov
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## Roadmap
|
|
521
|
+
|
|
522
|
+
- [x] Fixed Window algorithm
|
|
523
|
+
- [x] Token Bucket algorithm
|
|
524
|
+
- [x] Sliding Window algorithm
|
|
525
|
+
- [x] Automatic rate limit headers
|
|
526
|
+
- [x] Prometheus metrics
|
|
527
|
+
- [x] Multi-tenant support
|
|
528
|
+
- [x] Cost-based rate limiting
|
|
529
|
+
- [ ] Circuit breaker pattern
|
|
530
|
+
- [ ] Redis Cluster support
|
|
531
|
+
- [ ] Web dashboard
|
|
532
|
+
- [ ] Django integration
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
536
|
+
## Contributing
|
|
537
|
+
|
|
538
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## License
|
|
543
|
+
|
|
544
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
545
|
+
|