fluxlimit 0.1.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.
- fluxlimit-0.1.0/.github/workflows/ci.yml +36 -0
- fluxlimit-0.1.0/.github/workflows/publish-testpypi.yml +38 -0
- fluxlimit-0.1.0/.github/workflows/publish.yml +72 -0
- fluxlimit-0.1.0/.gitignore +40 -0
- fluxlimit-0.1.0/LICENSE +21 -0
- fluxlimit-0.1.0/PKG-INFO +339 -0
- fluxlimit-0.1.0/README.md +286 -0
- fluxlimit-0.1.0/pyproject.toml +93 -0
- fluxlimit-0.1.0/src/fluxlimit/__init__.py +74 -0
- fluxlimit-0.1.0/src/fluxlimit/algorithms/__init__.py +22 -0
- fluxlimit-0.1.0/src/fluxlimit/algorithms/base.py +64 -0
- fluxlimit-0.1.0/src/fluxlimit/algorithms/fixed_window.py +64 -0
- fluxlimit-0.1.0/src/fluxlimit/algorithms/leaky_bucket.py +64 -0
- fluxlimit-0.1.0/src/fluxlimit/algorithms/sliding_window.py +64 -0
- fluxlimit-0.1.0/src/fluxlimit/algorithms/token_bucket.py +67 -0
- fluxlimit-0.1.0/src/fluxlimit/backends/__init__.py +13 -0
- fluxlimit-0.1.0/src/fluxlimit/backends/base.py +79 -0
- fluxlimit-0.1.0/src/fluxlimit/backends/memory.py +207 -0
- fluxlimit-0.1.0/src/fluxlimit/backends/postgres.py +305 -0
- fluxlimit-0.1.0/src/fluxlimit/backends/redis.py +335 -0
- fluxlimit-0.1.0/src/fluxlimit/core.py +225 -0
- fluxlimit-0.1.0/src/fluxlimit/exceptions.py +33 -0
- fluxlimit-0.1.0/src/fluxlimit/metrics.py +68 -0
- fluxlimit-0.1.0/src/fluxlimit/middleware/__init__.py +27 -0
- fluxlimit-0.1.0/src/fluxlimit/middleware/_helpers.py +40 -0
- fluxlimit-0.1.0/src/fluxlimit/middleware/asgi.py +129 -0
- fluxlimit-0.1.0/src/fluxlimit/middleware/flask.py +234 -0
- fluxlimit-0.1.0/tests/test_algorithms.py +75 -0
- fluxlimit-0.1.0/tests/test_core.py +60 -0
- fluxlimit-0.1.0/tests/test_fastapi.py +181 -0
- fluxlimit-0.1.0/tests/test_flux_flask.py +135 -0
- fluxlimit-0.1.0/tests/test_middleware_imports.py +36 -0
- fluxlimit-0.1.0/uv.lock +2499 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- name: Install uv
|
|
25
|
+
uses: astral-sh/setup-uv@v5
|
|
26
|
+
with:
|
|
27
|
+
enable-cache: true
|
|
28
|
+
|
|
29
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
30
|
+
run: uv python install ${{ matrix.python-version }}
|
|
31
|
+
|
|
32
|
+
- name: Install dependencies
|
|
33
|
+
run: uv sync --extra dev
|
|
34
|
+
|
|
35
|
+
- name: Run tests
|
|
36
|
+
run: uv run pytest -v --cov=fluxlimit --cov-report=term-missing
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish to TestPyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
id-token: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
publish:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
environment:
|
|
14
|
+
name: testpypi
|
|
15
|
+
url: https://test.pypi.org/p/fluxlimit
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Install uv
|
|
21
|
+
uses: astral-sh/setup-uv@v5
|
|
22
|
+
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
run: uv python install 3.12
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies
|
|
27
|
+
run: uv sync --extra dev
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: uv run pytest
|
|
31
|
+
|
|
32
|
+
- name: Build package
|
|
33
|
+
run: uv build
|
|
34
|
+
|
|
35
|
+
- name: Publish to TestPyPI
|
|
36
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
37
|
+
with:
|
|
38
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: publish-pypi
|
|
10
|
+
cancel-in-progress: false
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
build:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v5
|
|
23
|
+
with:
|
|
24
|
+
enable-cache: true
|
|
25
|
+
|
|
26
|
+
- name: Set up Python
|
|
27
|
+
run: uv python install 3.12
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: uv sync --extra dev --locked
|
|
31
|
+
|
|
32
|
+
- name: Verify release tag matches package version
|
|
33
|
+
if: github.event_name == 'release'
|
|
34
|
+
run: |
|
|
35
|
+
VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
|
|
36
|
+
TAG="${{ github.event.release.tag_name }}"
|
|
37
|
+
if [ "v${VERSION}" != "${TAG}" ]; then
|
|
38
|
+
echo "Version mismatch: pyproject.toml has ${VERSION}, release tag is ${TAG}"
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
- name: Run tests
|
|
43
|
+
run: uv run pytest
|
|
44
|
+
|
|
45
|
+
- name: Build package
|
|
46
|
+
run: uv build
|
|
47
|
+
|
|
48
|
+
- name: Upload dist artifacts
|
|
49
|
+
uses: actions/upload-artifact@v4
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
|
53
|
+
retention-days: 1
|
|
54
|
+
|
|
55
|
+
publish:
|
|
56
|
+
needs: build
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
environment:
|
|
59
|
+
name: pypi
|
|
60
|
+
url: https://pypi.org/p/fluxlimit
|
|
61
|
+
permissions:
|
|
62
|
+
id-token: write
|
|
63
|
+
|
|
64
|
+
steps:
|
|
65
|
+
- name: Download dist artifacts
|
|
66
|
+
uses: actions/download-artifact@v4
|
|
67
|
+
with:
|
|
68
|
+
name: dist
|
|
69
|
+
path: dist/
|
|
70
|
+
|
|
71
|
+
- name: Publish to PyPI
|
|
72
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Bytecode
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Virtual environments
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
env/
|
|
10
|
+
|
|
11
|
+
# Packaging / build
|
|
12
|
+
*.egg
|
|
13
|
+
*.egg-info/
|
|
14
|
+
.eggs/
|
|
15
|
+
build/
|
|
16
|
+
dist/
|
|
17
|
+
|
|
18
|
+
# Test & coverage
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
.coverage.*
|
|
22
|
+
htmlcov/
|
|
23
|
+
.tox/
|
|
24
|
+
.nox/
|
|
25
|
+
|
|
26
|
+
# Type checkers & linters
|
|
27
|
+
.mypy_cache/
|
|
28
|
+
.ruff_cache/
|
|
29
|
+
|
|
30
|
+
# IDE
|
|
31
|
+
.idea/
|
|
32
|
+
.vscode/
|
|
33
|
+
|
|
34
|
+
# OS
|
|
35
|
+
.DS_Store
|
|
36
|
+
Thumbs.db
|
|
37
|
+
|
|
38
|
+
# Local env / secrets
|
|
39
|
+
.env
|
|
40
|
+
.env.*
|
fluxlimit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anand Yadav
|
|
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.
|
fluxlimit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fluxlimit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Distributed rate limiting with fairness for Python - token bucket, leaky bucket, and window algorithms with Redis and PostgreSQL backends.
|
|
5
|
+
Project-URL: Homepage, https://github.com/yourusername/fluxlimit
|
|
6
|
+
Project-URL: Documentation, https://github.com/yourusername/fluxlimit#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/yourusername/fluxlimit/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/yourusername/fluxlimit/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Your Name <you@example.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: distributed,fastapi,postgresql,rate-limiter,rate-limiting,redis,throttling,token-bucket
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: System :: Networking
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: fastapi>=0.100
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: asyncpg>=0.29; extra == 'all'
|
|
30
|
+
Requires-Dist: flask>=2.0; extra == 'all'
|
|
31
|
+
Requires-Dist: prometheus-client>=0.19; extra == 'all'
|
|
32
|
+
Requires-Dist: redis>=5.0; extra == 'all'
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: black>=23.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: flask>=2.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: httpx>=0.25; extra == 'dev'
|
|
38
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
40
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
43
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
44
|
+
Provides-Extra: flask
|
|
45
|
+
Requires-Dist: flask>=2.0; extra == 'flask'
|
|
46
|
+
Provides-Extra: postgres
|
|
47
|
+
Requires-Dist: asyncpg>=0.29; extra == 'postgres'
|
|
48
|
+
Provides-Extra: prometheus
|
|
49
|
+
Requires-Dist: prometheus-client>=0.19; extra == 'prometheus'
|
|
50
|
+
Provides-Extra: redis
|
|
51
|
+
Requires-Dist: redis>=5.0; extra == 'redis'
|
|
52
|
+
Description-Content-Type: text/markdown
|
|
53
|
+
|
|
54
|
+
# fluxlimit
|
|
55
|
+
|
|
56
|
+
**Distributed rate limiting with fairness for Python.**
|
|
57
|
+
|
|
58
|
+
Production-ready rate limiting supporting token bucket, leaky bucket, fixed window, and sliding window algorithms — with strict distributed fairness via Redis Lua scripts or PostgreSQL advisory locks.
|
|
59
|
+
|
|
60
|
+
[](https://pypi.org/project/fluxlimit/)
|
|
61
|
+
[](https://pypi.org/project/fluxlimit/)
|
|
62
|
+
[](LICENSE)
|
|
63
|
+
|
|
64
|
+
## Why fluxlimit?
|
|
65
|
+
|
|
66
|
+
| Feature | slowapi | fastapi-limiter | **fluxlimit** |
|
|
67
|
+
|---------|---------|-----------------|---------------|
|
|
68
|
+
| Token bucket | ❌ | ❌ | ✅ |
|
|
69
|
+
| Leaky bucket | ❌ | ❌ | ✅ |
|
|
70
|
+
| Sliding window | ❌ | ❌ | ✅ |
|
|
71
|
+
| Distributed fairness | ⚠️ Best-effort | ⚠️ Best-effort | ✅ **Strict** |
|
|
72
|
+
| Cost-based limiting | ❌ | ❌ | ✅ |
|
|
73
|
+
| PostgreSQL backend | ❌ | ❌ | ✅ |
|
|
74
|
+
| Multiple rules per request | ❌ | ❌ | ✅ |
|
|
75
|
+
|
|
76
|
+
## Quick Start
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from fluxlimit import (
|
|
80
|
+
RateLimiter,
|
|
81
|
+
LimitRule,
|
|
82
|
+
TokenBucket,
|
|
83
|
+
TokenBucketConfig,
|
|
84
|
+
MemoryBackend,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
limiter = RateLimiter(
|
|
88
|
+
backend=MemoryBackend(),
|
|
89
|
+
default_rule=LimitRule(
|
|
90
|
+
key="api",
|
|
91
|
+
algorithm=TokenBucket(),
|
|
92
|
+
config=TokenBucketConfig(key="api", rate=10, burst=20),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async with limiter:
|
|
97
|
+
result = await limiter.check("user_123")
|
|
98
|
+
print(result.allowed, result.remaining)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
## FastAPI Integration
|
|
103
|
+
|
|
104
|
+
FastAPI middleware is included in the core install:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install fluxlimit
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from fastapi import FastAPI, Request
|
|
112
|
+
from fluxlimit import (
|
|
113
|
+
RateLimiter,
|
|
114
|
+
RateLimitMiddleware,
|
|
115
|
+
LimitRule,
|
|
116
|
+
TokenBucket,
|
|
117
|
+
TokenBucketConfig,
|
|
118
|
+
RedisBackend,
|
|
119
|
+
rate_limit,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
app = FastAPI()
|
|
123
|
+
|
|
124
|
+
limiter = RateLimiter(
|
|
125
|
+
backend=RedisBackend("redis://localhost:6379"),
|
|
126
|
+
default_rule=LimitRule(
|
|
127
|
+
key="api",
|
|
128
|
+
algorithm=TokenBucket(),
|
|
129
|
+
config=TokenBucketConfig(key="api", rate=100, burst=150),
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
app.add_middleware(
|
|
134
|
+
RateLimitMiddleware,
|
|
135
|
+
limiter=limiter,
|
|
136
|
+
skip_paths=["/health", "/health/*"],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
@app.on_event("startup")
|
|
140
|
+
async def startup():
|
|
141
|
+
await limiter.connect()
|
|
142
|
+
|
|
143
|
+
@app.on_event("shutdown")
|
|
144
|
+
async def shutdown():
|
|
145
|
+
await limiter.disconnect()
|
|
146
|
+
|
|
147
|
+
@app.get("/items")
|
|
148
|
+
async def get_items():
|
|
149
|
+
return {"message": "Hello World"}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
ASGI middleware lives in `fluxlimit.middleware.asgi`. You can also import explicitly:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from fluxlimit.middleware.asgi import RateLimitMiddleware, rate_limit
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Flask Integration
|
|
159
|
+
|
|
160
|
+
Install the Flask extra:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
pip install fluxlimit[flask]
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from flask import Flask
|
|
168
|
+
from fluxlimit import (
|
|
169
|
+
RateLimiter,
|
|
170
|
+
LimitRule,
|
|
171
|
+
TokenBucket,
|
|
172
|
+
TokenBucketConfig,
|
|
173
|
+
MemoryBackend,
|
|
174
|
+
Fluxlimit,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
app = Flask(__name__)
|
|
178
|
+
|
|
179
|
+
limiter = RateLimiter(
|
|
180
|
+
backend=MemoryBackend(),
|
|
181
|
+
default_rule=LimitRule(
|
|
182
|
+
key="api",
|
|
183
|
+
algorithm=TokenBucket(),
|
|
184
|
+
config=TokenBucketConfig(key="api", rate=10, burst=20),
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
fluxlimit = Fluxlimit(app, limiter=limiter, skip_paths=["/health", "/health/*"])
|
|
189
|
+
|
|
190
|
+
@app.route("/")
|
|
191
|
+
@fluxlimit.limit()
|
|
192
|
+
def index():
|
|
193
|
+
return {"message": "Hello"}
|
|
194
|
+
|
|
195
|
+
@app.route("/health")
|
|
196
|
+
@fluxlimit.exempt
|
|
197
|
+
def health():
|
|
198
|
+
return {"status": "ok"}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Flask integration lives in `fluxlimit.middleware.flask`.
|
|
202
|
+
|
|
203
|
+
## Cost-Based Limiting
|
|
204
|
+
Different endpoints can consume different amounts of quota:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from fluxlimit import rate_limit
|
|
208
|
+
|
|
209
|
+
# Expensive endpoint costs 5 tokens
|
|
210
|
+
@app.post("/analyze")
|
|
211
|
+
@rate_limit(limiter, cost=5)
|
|
212
|
+
async def analyze(request: Request):
|
|
213
|
+
return {"result": "..."}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Algorithms
|
|
217
|
+
|
|
218
|
+
### Token Bucket
|
|
219
|
+
Allows controlled bursts while maintaining average rate. Best for APIs that need burst tolerance.
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from fluxlimit import TokenBucket, TokenBucketConfig
|
|
223
|
+
|
|
224
|
+
TokenBucketConfig(key="api", rate=10, burst=20) # 10/sec sustained, 20 burst
|
|
225
|
+
```
|
|
226
|
+
### Leaky Bucket
|
|
227
|
+
Strict rate enforcement with no bursts. Best for protecting downstream services.
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from fluxlimit import LeakyBucket, LeakyBucketConfig
|
|
231
|
+
|
|
232
|
+
LeakyBucketConfig(key="api", rate=10, capacity=100) # 10/sec, queue of 100
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Fixed Window
|
|
236
|
+
Simple counter in fixed time windows. Low memory, but allows bursts at boundaries.
|
|
237
|
+
```python
|
|
238
|
+
from fluxlimit import FixedWindow, FixedWindowConfig
|
|
239
|
+
|
|
240
|
+
FixedWindowConfig(key="api", max_requests=100, window_seconds=60)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Sliding Window
|
|
244
|
+
Smooth rate limiting with no boundary bursts. Higher accuracy, more storage.
|
|
245
|
+
```python
|
|
246
|
+
from fluxlimit import SlidingWindow, SlidingWindowConfig
|
|
247
|
+
|
|
248
|
+
SlidingWindowConfig(key="api", max_requests=100, window_seconds=60)
|
|
249
|
+
```
|
|
250
|
+
## Backends
|
|
251
|
+
|
|
252
|
+
| Backend | Use Case | Distributed Fairness |
|
|
253
|
+
|---------|----------|----------------------|
|
|
254
|
+
| `MemoryBackend` | Single process, development | N/A |
|
|
255
|
+
| `RedisBackend` | Production, high throughput | ✅ Lua scripts |
|
|
256
|
+
| `PostgresBackend` | Teams using PostgreSQL | ✅ Advisory locks |
|
|
257
|
+
|
|
258
|
+
## PostgreSQL Setup
|
|
259
|
+
```sql
|
|
260
|
+
CREATE TABLE fluxlimit_rate_limits (
|
|
261
|
+
key TEXT PRIMARY KEY,
|
|
262
|
+
tokens FLOAT NOT NULL DEFAULT 0,
|
|
263
|
+
volume FLOAT NOT NULL DEFAULT 0,
|
|
264
|
+
count INTEGER NOT NULL DEFAULT 0,
|
|
265
|
+
window_start FLOAT NOT NULL DEFAULT 0,
|
|
266
|
+
last_update FLOAT NOT NULL DEFAULT 0,
|
|
267
|
+
algorithm VARCHAR(32) NOT NULL DEFAULT 'token_bucket'
|
|
268
|
+
);
|
|
269
|
+
```
|
|
270
|
+
## Installation
|
|
271
|
+
```bash
|
|
272
|
+
# Core (in-memory backend + FastAPI middleware)
|
|
273
|
+
pip install fluxlimit
|
|
274
|
+
|
|
275
|
+
# With Redis support
|
|
276
|
+
pip install fluxlimit[redis]
|
|
277
|
+
|
|
278
|
+
# With PostgreSQL support
|
|
279
|
+
pip install fluxlimit[postgres]
|
|
280
|
+
|
|
281
|
+
# With Prometheus metrics
|
|
282
|
+
pip install fluxlimit[prometheus]
|
|
283
|
+
|
|
284
|
+
# With Flask extension
|
|
285
|
+
pip install fluxlimit[flask]
|
|
286
|
+
|
|
287
|
+
# Everything
|
|
288
|
+
pip install fluxlimit[all]
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Testing
|
|
293
|
+
|
|
294
|
+
Install development dependencies from the project root:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
pip install ".[dev]"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Run the full test suite:
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
pytest
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Run with verbose output or target a specific file:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
pytest -v
|
|
310
|
+
pytest tests/test_core.py
|
|
311
|
+
pytest tests/test_algorithms.py::TestTokenBucket::test_refills_over_time
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Generate a coverage report:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
pytest --cov=fluxlimit --cov-report=term-missing
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
If you have not installed the package and only want to run tests against the source tree:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
PYTHONPATH=src pytest
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Test layout
|
|
327
|
+
|
|
328
|
+
| File | Covers |
|
|
329
|
+
|------|--------|
|
|
330
|
+
| `tests/test_core.py` | `RateLimiter` — allow/deny checks, `check_or_raise`, named rules |
|
|
331
|
+
| `tests/test_algorithms.py` | Token bucket burst/refill behavior, fixed window reset |
|
|
332
|
+
| `tests/test_fastapi.py` | ASGI middleware, `@rate_limit`, prefix skip, lazy imports |
|
|
333
|
+
| `tests/test_flux_flask.py` | Flask extension — decorators, exempt routes, skip paths |
|
|
334
|
+
| `tests/test_middleware_imports.py` | Lazy loading of middleware without eager framework imports |
|
|
335
|
+
|
|
336
|
+
Tests use `pytest-asyncio` with `asyncio_mode = auto` (configured in `pyproject.toml`). The in-memory backend is used by default, so no Redis or PostgreSQL instance is required.
|
|
337
|
+
|
|
338
|
+
## License
|
|
339
|
+
MIT
|