limitra 0.0.1__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.
Files changed (69) hide show
  1. limitra-0.0.1/.github/workflows/ci.yml +75 -0
  2. limitra-0.0.1/.github/workflows/python-package.yml +28 -0
  3. limitra-0.0.1/.gitignore +34 -0
  4. limitra-0.0.1/.mise-tasks/black +4 -0
  5. limitra-0.0.1/.mise-tasks/check +7 -0
  6. limitra-0.0.1/.mise-tasks/ci +60 -0
  7. limitra-0.0.1/.mise-tasks/doc +7 -0
  8. limitra-0.0.1/.mise-tasks/install +4 -0
  9. limitra-0.0.1/.mise-tasks/isort +4 -0
  10. limitra-0.0.1/.mise-tasks/mypy +4 -0
  11. limitra-0.0.1/.mise-tasks/pylint +4 -0
  12. limitra-0.0.1/.mise-tasks/test +4 -0
  13. limitra-0.0.1/.mise-tasks/test-no-redis +4 -0
  14. limitra-0.0.1/LICENSE +21 -0
  15. limitra-0.0.1/PKG-INFO +66 -0
  16. limitra-0.0.1/README.md +35 -0
  17. limitra-0.0.1/docs/getting-started/configuration.md +129 -0
  18. limitra-0.0.1/docs/getting-started/installation.md +30 -0
  19. limitra-0.0.1/docs/getting-started/reference.md +155 -0
  20. limitra-0.0.1/docs/guide/algorithms.md +186 -0
  21. limitra-0.0.1/docs/guide/backends.md +82 -0
  22. limitra-0.0.1/docs/guide/decorator.md +192 -0
  23. limitra-0.0.1/docs/guide/error-handling.md +175 -0
  24. limitra-0.0.1/docs/images/fixed_window.png +0 -0
  25. limitra-0.0.1/docs/images/leaky_bucket.png +0 -0
  26. limitra-0.0.1/docs/images/logo.png +0 -0
  27. limitra-0.0.1/docs/images/sliding_window.png +0 -0
  28. limitra-0.0.1/docs/images/token_bucket.png +0 -0
  29. limitra-0.0.1/docs/index.md +57 -0
  30. limitra-0.0.1/docs/integrations/django.md +53 -0
  31. limitra-0.0.1/docs/integrations/fastapi.md +63 -0
  32. limitra-0.0.1/docs/integrations/flask.md +59 -0
  33. limitra-0.0.1/docs/logo.png +0 -0
  34. limitra-0.0.1/docs/stylesheets/extra.css +3 -0
  35. limitra-0.0.1/limitra/__init__.py +27 -0
  36. limitra-0.0.1/limitra/_config.py +95 -0
  37. limitra-0.0.1/limitra/_decorator.py +279 -0
  38. limitra-0.0.1/limitra/_version.py +24 -0
  39. limitra-0.0.1/limitra/algorithms/__init__.py +23 -0
  40. limitra-0.0.1/limitra/algorithms/fixed_window.py +133 -0
  41. limitra-0.0.1/limitra/algorithms/leaky_bucket.py +139 -0
  42. limitra-0.0.1/limitra/algorithms/sliding_window.py +144 -0
  43. limitra-0.0.1/limitra/algorithms/token_bucket.py +136 -0
  44. limitra-0.0.1/limitra/base.py +152 -0
  45. limitra-0.0.1/limitra/exceptions.py +15 -0
  46. limitra-0.0.1/limitra/integrations/__init__.py +0 -0
  47. limitra-0.0.1/limitra/py.typed +0 -0
  48. limitra-0.0.1/limitra.egg-info/PKG-INFO +66 -0
  49. limitra-0.0.1/limitra.egg-info/SOURCES.txt +67 -0
  50. limitra-0.0.1/limitra.egg-info/dependency_links.txt +1 -0
  51. limitra-0.0.1/limitra.egg-info/top_level.txt +1 -0
  52. limitra-0.0.1/mise.toml +15 -0
  53. limitra-0.0.1/mkdocs.yml +65 -0
  54. limitra-0.0.1/pyproject.toml +90 -0
  55. limitra-0.0.1/requirements-dev.txt +18 -0
  56. limitra-0.0.1/requirements-docs.txt +1 -0
  57. limitra-0.0.1/setup.cfg +4 -0
  58. limitra-0.0.1/tests/__init__.py +0 -0
  59. limitra-0.0.1/tests/conftest.py +63 -0
  60. limitra-0.0.1/tests/integrations/__init__.py +0 -0
  61. limitra-0.0.1/tests/integrations/test_django.py +175 -0
  62. limitra-0.0.1/tests/integrations/test_fastapi.py +145 -0
  63. limitra-0.0.1/tests/integrations/test_flask.py +141 -0
  64. limitra-0.0.1/tests/redis/__init__.py +0 -0
  65. limitra-0.0.1/tests/redis/test_algorithms.py +172 -0
  66. limitra-0.0.1/tests/redis/test_fail_open.py +67 -0
  67. limitra-0.0.1/tests/shared.py +48 -0
  68. limitra-0.0.1/tests/test_algorithms.py +118 -0
  69. limitra-0.0.1/tests/test_limitra.py +600 -0
@@ -0,0 +1,75 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+ branches: ["main"]
8
+
9
+ permissions: {}
10
+
11
+ jobs:
12
+ test:
13
+ name: Python ${{ matrix.python-version }}
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: read
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
21
+
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ with:
25
+ persist-credentials: false
26
+
27
+ - uses: actions/setup-python@v5
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+ allow-prereleases: true
31
+
32
+ - uses: astral-sh/setup-uv@v4
33
+
34
+ - uses: jdx/mise-action@v2
35
+ with:
36
+ install: false
37
+
38
+ - name: Install dependencies
39
+ run: uv pip install --system -e '.' -r requirements-dev.txt
40
+
41
+ - name: Lint
42
+ run: mise run pylint
43
+
44
+ - name: Type check
45
+ run: mise run mypy
46
+
47
+ - name: Run tests
48
+ run: mise run test
49
+
50
+ test-min-deps:
51
+ name: Min deps / Python 3.10
52
+ runs-on: ubuntu-latest
53
+ permissions:
54
+ contents: read
55
+
56
+ steps:
57
+ - uses: actions/checkout@v4
58
+ with:
59
+ persist-credentials: false
60
+
61
+ - uses: actions/setup-python@v5
62
+ with:
63
+ python-version: "3.10"
64
+
65
+ - uses: astral-sh/setup-uv@v4
66
+
67
+ - uses: jdx/mise-action@v2
68
+ with:
69
+ install: false
70
+
71
+ - name: Install minimum dependencies
72
+ run: uv pip install --system --resolution lowest-direct -e '.' -r requirements-dev.txt
73
+
74
+ - name: Run tests
75
+ run: mise run test
@@ -0,0 +1,28 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+
7
+ permissions: {}
8
+
9
+ jobs:
10
+ publish:
11
+ name: Build & publish
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ id-token: write
15
+ contents: read
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ persist-credentials: false
21
+
22
+ - uses: astral-sh/setup-uv@v4
23
+
24
+ - name: Build
25
+ run: uv build
26
+
27
+ - name: Publish
28
+ run: uv publish
@@ -0,0 +1,34 @@
1
+ # Generated by setuptools-scm
2
+ limitra/_version.py
3
+
4
+ # Virtual environment
5
+ .venv/
6
+
7
+ # Python bytecode
8
+ __pycache__/
9
+ *.py[cod]
10
+
11
+ # Build / distribution
12
+ build/
13
+ dist/
14
+ *.egg-info/
15
+ *.egg
16
+
17
+ # MkDocs
18
+ site/
19
+
20
+ # Test & coverage
21
+ .pytest_cache/
22
+ .coverage
23
+ .coverage.*
24
+ htmlcov/
25
+
26
+ # Type checking & linting
27
+ .mypy_cache/
28
+ .pylint.d/
29
+
30
+ # OS
31
+ .DS_Store
32
+
33
+ # mise
34
+ .mise.local.toml
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="Format code with black"
3
+ set -e
4
+ black limitra/ tests/
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="Run isort, black, pylint, and mypy in sequence"
3
+ set -e
4
+ isort limitra/ tests/
5
+ black limitra/ tests/
6
+ pylint limitra tests/
7
+ mypy limitra --strict
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="Run full CI workflow (lint + typecheck + tests) across all Python versions from the GitHub Actions matrix"
3
+ VERSIONS=("3.10" "3.11" "3.12" "3.13" "3.14")
4
+ FAILED=()
5
+
6
+ CI_BASE="$HOME/.cache/limitra-ci-$$"
7
+ mkdir -p "$CI_BASE"
8
+ trap 'rm -rf "$CI_BASE"' EXIT
9
+
10
+ export TMPDIR="$HOME/.cache/limitra-tmp"
11
+ export UV_CACHE_DIR="$HOME/.cache/uv"
12
+ mkdir -p "$TMPDIR" "$UV_CACHE_DIR"
13
+
14
+ for version in "${VERSIONS[@]}"; do
15
+ echo ""
16
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
17
+ echo " Python $version"
18
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
19
+
20
+ mise install python@$version
21
+
22
+ VENV="$CI_BASE/${version}/venv"
23
+ uv venv "$VENV" --python $version --seed -q
24
+ uv pip install -q -r requirements-dev.txt -e . --python "$VENV/bin/python"
25
+
26
+ echo "--- Lint ---"
27
+ if ! "$VENV/bin/pylint" limitra tests/; then
28
+ FAILED+=("$version:lint"); continue
29
+ fi
30
+
31
+ echo "--- Type check ---"
32
+ if ! "$VENV/bin/mypy" limitra --strict; then
33
+ FAILED+=("$version:mypy"); continue
34
+ fi
35
+
36
+ echo "--- Tests ---"
37
+ if ! "$VENV/bin/pytest" -v --tb=short; then
38
+ FAILED+=("$version:tests")
39
+ fi
40
+ done
41
+
42
+ echo ""
43
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
44
+ echo " Min deps / Python 3.10"
45
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
46
+
47
+ VENV="$CI_BASE/3.10-min/venv"
48
+ uv venv "$VENV" --python 3.10 --seed -q
49
+ uv pip install -q --resolution lowest-direct -r requirements-dev.txt -e . --python "$VENV/bin/python"
50
+
51
+ if ! "$VENV/bin/pytest" -v --tb=short; then
52
+ FAILED+=("3.10:min-deps")
53
+ fi
54
+
55
+ echo ""
56
+ if [ ${#FAILED[@]} -gt 0 ]; then
57
+ echo "FAILED: ${FAILED[*]}"
58
+ exit 1
59
+ fi
60
+ echo "All versions passed!"
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="Build and serve the documentation locally"
3
+ set -e
4
+ export TMPDIR="$HOME/.cache/mkdocs-tmp"
5
+ mkdir -p "$TMPDIR"
6
+ pip install -r requirements-docs.txt -q
7
+ mkdocs serve
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="Install project + dev dependencies into the venv"
3
+ set -e
4
+ uv pip install -e '.' -r requirements-dev.txt
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="Sort imports with isort"
3
+ set -e
4
+ isort limitra/ tests/
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="Type-check with mypy"
3
+ set -e
4
+ mypy limitra --strict
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="Lint with pylint"
3
+ set -e
4
+ pylint limitra tests/
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="Run the full test suite"
3
+ set -e
4
+ pytest tests/ -v
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ #MISE description="Run tests, skip Redis tests"
3
+ set -e
4
+ pytest tests/ -v -k 'not redis'
limitra-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Léo Kling
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.
limitra-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: limitra
3
+ Version: 0.0.1
4
+ Summary: Simple, framework-agnostic rate limiting for Python — sliding window, token bucket, leaky bucket, fixed window, memory and Redis backends
5
+ Author-email: Léo Kling <contact@leok.dev>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/leo-kling/limitra
8
+ Project-URL: Repository, https://github.com/leo-kling/limitra
9
+ Project-URL: Issues, https://github.com/leo-kling/limitra/issues
10
+ Keywords: rate-limit,rate-limiter,rate-limiting,ratelimit,throttle,throttling,sliding-window,token-bucket,leaky-bucket,fixed-window,fastapi,django,flask,redis,api
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
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: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Typing :: Typed
26
+ Classifier: Framework :: AsyncIO
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Dynamic: license-file
31
+
32
+ <p align="center">
33
+ <img src="https://limitra.pages.dev/logo.png" alt="Limitra" width="120" />
34
+ </p>
35
+
36
+ <h1 align="center">Limitra</h1>
37
+
38
+ <p align="center">
39
+ <img src="https://github.com/leo-kling/limitra/actions/workflows/ci.yml/badge.svg" alt="CI" />
40
+ <img src="https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13%20|%203.14-blue" alt="Python versions" />
41
+ <img src="https://img.shields.io/badge/license-MIT-lightgrey" alt="MIT license" />
42
+ </p>
43
+
44
+ <p align="center">Simple, framework-agnostic rate limiting for Python.</p>
45
+
46
+ ---
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install limitra
52
+ ```
53
+
54
+ ## Quick start
55
+
56
+ ```python
57
+ from limitra import LimitraConfig, rate_limit
58
+
59
+ LimitraConfig(redis_url="redis://localhost:6379", project="my-service")
60
+
61
+ @rate_limit(requests=10, window=60, key="user_id")
62
+ def create_post(user_id: str, content: str):
63
+ ...
64
+ ```
65
+
66
+ → Full documentation: **[limitra.leok.dev](https://limitra.leok.dev)**
@@ -0,0 +1,35 @@
1
+ <p align="center">
2
+ <img src="https://limitra.pages.dev/logo.png" alt="Limitra" width="120" />
3
+ </p>
4
+
5
+ <h1 align="center">Limitra</h1>
6
+
7
+ <p align="center">
8
+ <img src="https://github.com/leo-kling/limitra/actions/workflows/ci.yml/badge.svg" alt="CI" />
9
+ <img src="https://img.shields.io/badge/python-3.10%20|%203.11%20|%203.12%20|%203.13%20|%203.14-blue" alt="Python versions" />
10
+ <img src="https://img.shields.io/badge/license-MIT-lightgrey" alt="MIT license" />
11
+ </p>
12
+
13
+ <p align="center">Simple, framework-agnostic rate limiting for Python.</p>
14
+
15
+ ---
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install limitra
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```python
26
+ from limitra import LimitraConfig, rate_limit
27
+
28
+ LimitraConfig(redis_url="redis://localhost:6379", project="my-service")
29
+
30
+ @rate_limit(requests=10, window=60, key="user_id")
31
+ def create_post(user_id: str, content: str):
32
+ ...
33
+ ```
34
+
35
+ → Full documentation: **[limitra.leok.dev](https://limitra.leok.dev)**
@@ -0,0 +1,129 @@
1
+ # Configuration
2
+
3
+ ## How it works
4
+
5
+ Limitra uses two levels of configuration:
6
+
7
+ | | `LimitraConfig()` | `@rate_limit()` |
8
+ |---|---|---|
9
+ | **When** | Once at app startup | On each function to protect |
10
+ | **What** | Redis connection, namespace, defaults | The rule: how many requests, which window |
11
+ | **Scope** | Global — applies to all limiters | Local — this function only |
12
+
13
+ In practice: `LimitraConfig()` connects Limitra to Redis and sets global defaults. `@rate_limit()` defines the rule on a specific function and inherits everything set by `LimitraConfig()`.
14
+
15
+ ---
16
+
17
+ ## Full example
18
+
19
+ ```python
20
+ # main.py (or app.py, startup.py...)
21
+ from limitra import LimitraConfig, rate_limit, RateLimitExceeded
22
+
23
+ # 1. Global setup — call once at startup
24
+ LimitraConfig(
25
+ redis_url="redis://localhost:6379",
26
+ project="my-service", # isolates Redis keys for this service
27
+ )
28
+
29
+ # 2. Decorate each function to protect
30
+ @rate_limit(requests=10, window=60, key="user_id")
31
+ def create_post(user_id: str, content: str):
32
+ ...
33
+
34
+ @rate_limit(requests=1000, window=60)
35
+ def public_feed():
36
+ ...
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Without Redis — memory backend
42
+
43
+ If you don't need Redis (local development, single-process app), `LimitraConfig()` is not required. The memory backend is used by default:
44
+
45
+ ```python
46
+ from limitra import rate_limit
47
+
48
+ @rate_limit(requests=10, window=60, key="user_id", backend="memory")
49
+ def create_post(user_id: str, content: str):
50
+ ...
51
+ ```
52
+
53
+ !!! warning
54
+ The memory backend is not shared between processes or restarts. For production apps with multiple workers, use Redis.
55
+
56
+ ---
57
+
58
+ ## `LimitraConfig()` parameters
59
+
60
+ ### Redis connection
61
+
62
+ ```python
63
+ # Option A — URL
64
+ LimitraConfig(redis_url="redis://localhost:6379")
65
+ LimitraConfig(redis_url="rediss://user:pass@host:6380/0") # TLS
66
+
67
+ # Option B — pre-built client (useful with connection pooling)
68
+ import redis
69
+ client = redis.Redis(host="localhost", port=6379, db=0)
70
+ LimitraConfig(redis_client=client)
71
+ ```
72
+
73
+ | Parameter | Type | Default | Description |
74
+ |---|---|---|---|
75
+ | `redis_url` | `str` | `None` | Connection URL passed to `redis.from_url()` |
76
+ | `redis_client` | `Redis` | `None` | Pre-built Redis client |
77
+ | `redis_db` | `int` | `0` | Redis database index (only used with `redis_url`) |
78
+
79
+ ### Key namespace
80
+
81
+ ```python
82
+ LimitraConfig(
83
+ redis_url="redis://shared:6379",
84
+ project="user-svc", # → keys: user-svc:user:alice
85
+ prefix="rl", # → keys: rl:user-svc:user:alice
86
+ suffix="v2", # → keys: rl:user-svc:user:alice:v2
87
+ )
88
+ ```
89
+
90
+ | Parameter | Type | Default | Description |
91
+ |---|---|---|---|
92
+ | `project` | `str` | `None` | Namespace prefix — recommended to isolate microservices sharing the same Redis |
93
+ | `prefix` | `str` | `None` | Key prefix added before `project` |
94
+ | `suffix` | `str` | `None` | Key suffix added after the identifier |
95
+
96
+ ### Defaults
97
+
98
+ These values are used by `@rate_limit()` when the corresponding parameters are not specified.
99
+
100
+ | Parameter | Type | Default | Description |
101
+ |---|---|---|---|
102
+ | `default_algorithm` | `str` | `"sliding_window"` | Default algorithm |
103
+ | `default_backend` | `str` | `"redis"` | Default backend after `LimitraConfig()` is called |
104
+ | `fail_open` | `bool` | `False` | When `True`, allow requests if Redis is unreachable instead of raising an error |
105
+
106
+ ---
107
+
108
+ ## Overriding `LimitraConfig()` per function
109
+
110
+ Any `@rate_limit()` parameter overrides the global setting for that function only:
111
+
112
+ ```python
113
+ LimitraConfig(redis_url="redis://localhost:6379", project="my-svc")
114
+
115
+ # Uses all globals — project "my-svc", redis backend
116
+ @rate_limit(requests=100, window=60)
117
+ def normal_endpoint():
118
+ ...
119
+
120
+ # Overrides the backend for this function only
121
+ @rate_limit(requests=100, window=60, backend="memory")
122
+ def local_only_endpoint():
123
+ ...
124
+
125
+ # Overrides the project for this function only
126
+ @rate_limit(requests=5, window=60, project="billing")
127
+ def billing_endpoint():
128
+ ...
129
+ ```
@@ -0,0 +1,30 @@
1
+ # Installation
2
+
3
+ **Python 3.10+** is required.
4
+
5
+ ```bash
6
+ pip install limitra
7
+ ```
8
+
9
+ Limitra has zero runtime dependencies. The memory backend works out of the box.
10
+
11
+ ## Redis backend
12
+
13
+ Limitra does not ship `redis` as a dependency — if you're using Redis for rate limiting, you're most likely already using it elsewhere in your project.
14
+
15
+ Add it to your project's own dependencies:
16
+
17
+ ```bash
18
+ pip install redis # or: poetry add redis, uv add redis, etc.
19
+ ```
20
+
21
+ Limitra detects it automatically. Any `redis>=4.0` version works.
22
+
23
+ !!! note
24
+ If `redis` is not installed and you configure a Redis backend, Limitra raises `ImportError` with a clear message at startup.
25
+
26
+ ## Local Redis for development
27
+
28
+ ```bash
29
+ docker run -p 6379:6379 redis:alpine
30
+ ```