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 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.
@@ -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
+ [![Python Version](https://img.shields.io/badge/python-3.9--3.13-blue)](https://www.python.org)
41
+ [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
42
+ [![Redis](https://img.shields.io/badge/redis-7%2B-red)](https://redis.io)
43
+ [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](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
+