api-shield 0.2.0__tar.gz → 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.
- api_shield-0.3.0/FEATURES.md +356 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/PKG-INFO +114 -3
- api_shield-0.3.0/PR_DESCRIPTION.md +34 -0
- api_shield-0.3.0/README.md +207 -0
- api_shield-0.3.0/docs/adapters/custom.md +520 -0
- api_shield-0.3.0/docs/adapters/fastapi.md +662 -0
- api_shield-0.3.0/docs/assets/dashboard.png +0 -0
- api_shield-0.3.0/docs/assets/openapi-maintenance.png +0 -0
- api_shield-0.3.0/docs/assets/openapi.png +0 -0
- api_shield-0.3.0/docs/changelog.md +99 -0
- api_shield-0.3.0/docs/guides/distributed.md +278 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/docs/guides/production.md +2 -2
- {api_shield-0.2.0 → api_shield-0.3.0}/docs/index.md +23 -14
- api_shield-0.3.0/docs/reference/backends.md +240 -0
- api_shield-0.3.0/docs/reference/cli.md +380 -0
- api_shield-0.3.0/docs/reference/decorators.md +454 -0
- api_shield-0.3.0/docs/reference/engine.md +517 -0
- api_shield-0.3.0/docs/reference/exceptions.md +146 -0
- api_shield-0.3.0/docs/reference/middleware.md +173 -0
- api_shield-0.3.0/docs/reference/models.md +191 -0
- api_shield-0.3.0/docs/reference/rate-limiting.md +465 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/docs/stylesheets/extra.css +23 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/docs/tutorial/admin-dashboard.md +32 -8
- {api_shield-0.2.0 → api_shield-0.3.0}/docs/tutorial/backends.md +26 -2
- api_shield-0.3.0/docs/tutorial/cli.md +199 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/docs/tutorial/first-decorator.md +8 -7
- {api_shield-0.2.0 → api_shield-0.3.0}/docs/tutorial/installation.md +8 -4
- {api_shield-0.2.0 → api_shield-0.3.0}/docs/tutorial/middleware.md +13 -7
- api_shield-0.3.0/docs/tutorial/rate-limiting.md +415 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/basic.py +3 -3
- api_shield-0.3.0/examples/fastapi/custom_responses.py +210 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/dependency_injection.py +56 -35
- api_shield-0.3.0/examples/fastapi/rate_limiting.py +295 -0
- api_shield-0.3.0/examples/fastapi/webhooks.py +257 -0
- api_shield-0.3.0/flask-adapter-design.md +568 -0
- api_shield-0.3.0/issues_to_resolve.txt +43 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/mkdocs.yml +7 -2
- {api_shield-0.2.0 → api_shield-0.3.0}/pyproject.toml +4 -1
- api_shield-0.3.0/rate-limits.md +1479 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/admin/api.py +82 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/admin/app.py +24 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/cli/client.py +59 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/cli/main.py +212 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/backends/base.py +104 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/backends/file.py +79 -14
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/backends/memory.py +61 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/backends/redis.py +185 -2
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/config.py +2 -2
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/engine.py +443 -14
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/exceptions.py +56 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/models.py +8 -3
- api_shield-0.3.0/shield/core/rate_limit/__init__.py +11 -0
- api_shield-0.3.0/shield/core/rate_limit/keys.py +229 -0
- api_shield-0.3.0/shield/core/rate_limit/limiter.py +246 -0
- api_shield-0.3.0/shield/core/rate_limit/models.py +284 -0
- api_shield-0.3.0/shield/core/rate_limit/storage.py +673 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/app.py +11 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/routes.py +257 -3
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/audit.html +3 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/base.html +31 -5
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/index.html +2 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/login.html +15 -8
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/audit_row.html +20 -0
- api_shield-0.3.0/shield/dashboard/templates/partials/modal_rl_delete.html +56 -0
- api_shield-0.3.0/shield/dashboard/templates/partials/modal_rl_edit.html +99 -0
- api_shield-0.3.0/shield/dashboard/templates/partials/modal_rl_reset.html +45 -0
- api_shield-0.3.0/shield/dashboard/templates/partials/pagination.html +63 -0
- api_shield-0.3.0/shield/dashboard/templates/partials/rate_limit_hits.html +16 -0
- api_shield-0.3.0/shield/dashboard/templates/partials/rate_limit_rows.html +61 -0
- api_shield-0.3.0/shield/dashboard/templates/rate_limits.html +53 -0
- api_shield-0.3.0/shield/dashboard/templates/rl_hits.html +50 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/__init__.py +4 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/decorators.py +349 -36
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/middleware.py +174 -30
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/router.py +64 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/admin/test_api.py +90 -0
- api_shield-0.3.0/tests/core/rate_limit/test_models.py +177 -0
- api_shield-0.3.0/tests/core/rate_limit/test_policy_persistence.py +210 -0
- api_shield-0.3.0/tests/core/rate_limit/test_storage.py +262 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_config.py +1 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_engine.py +209 -0
- api_shield-0.3.0/tests/fastapi/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_acceptance.py +3 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_dependencies.py +1 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_deprecated.py +1 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_middleware.py +1 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_openapi.py +1 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_openapi_maintenance.py +1 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_parameterized_routes.py +1 -1
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_plain_router.py +1 -1
- api_shield-0.3.0/tests/fastapi/test_rate_limit.py +286 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/test_cli.py +82 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/uv.lock +110 -1
- api_shield-0.3.0/version-changes.txt +3 -0
- api_shield-0.2.0/PR_DESCRIPTION.md +0 -15
- api_shield-0.2.0/README.md +0 -100
- api_shield-0.2.0/docs/PR_DESCRIPTION.md +0 -17
- api_shield-0.2.0/docs/adapters/custom.md +0 -230
- api_shield-0.2.0/docs/adapters/fastapi.md +0 -244
- api_shield-0.2.0/docs/changelog.md +0 -81
- api_shield-0.2.0/docs/reference/backends.md +0 -172
- api_shield-0.2.0/docs/reference/cli.md +0 -231
- api_shield-0.2.0/docs/reference/decorators.md +0 -218
- api_shield-0.2.0/docs/reference/engine.md +0 -301
- api_shield-0.2.0/docs/reference/exceptions.md +0 -91
- api_shield-0.2.0/docs/reference/middleware.md +0 -121
- api_shield-0.2.0/docs/reference/models.md +0 -137
- api_shield-0.2.0/docs/tutorial/cli.md +0 -203
- api_shield-0.2.0/issues_to_resolve.txt +0 -20
- {api_shield-0.2.0 → api_shield-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/.github/pull_request_template.md +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/.github/workflows/ci.yml +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/.github/workflows/docs.yml +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/.gitignore +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/.pre-commit-config.yaml +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/CONTRIBUTING.md +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/LICENSE +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/ROADMAP.md +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/SECURITY.md +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/api-shield-logo.svg +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/docs/assets/logo-full.svg +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/docs/assets/logo.svg +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/examples/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/custom_backend/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/custom_backend/sqlite_backend.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/global_maintenance.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/scheduled_maintenance.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/admin/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/admin/auth.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/cli/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/cli/config.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/backends/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/scheduler.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/webhooks.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/auth.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/audit_rows.html +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/global_maintenance.html +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/modal.html +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/modal_global_disable.html +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/modal_global_enable.html +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/route_row.html +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/routes_table.html +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/dependencies.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/openapi.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/admin/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/admin/test_auth.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/__init__.py +0 -0
- {api_shield-0.2.0/tests/dashboard → api_shield-0.3.0/tests/core/rate_limit}/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_backends.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_exceptions.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_models.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_scheduler.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_scheduler_polling.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_webhooks.py +0 -0
- {api_shield-0.2.0/tests/fastapi → api_shield-0.3.0/tests/dashboard}/__init__.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/dashboard/test_routes.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_decorators.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_global_maintenance.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_global_maintenance_docs.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_router.py +0 -0
- {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_startup_scan.py +0 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# api-shield — Feature Roadmap
|
|
2
|
+
|
|
3
|
+
This document captures the full set of proposed features across decorators, SaaS infrastructure, AI-powered analytics, and integrations.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Additional Decorators
|
|
8
|
+
|
|
9
|
+
### Access Control
|
|
10
|
+
|
|
11
|
+
#### `@ip_allowlist(*cidrs)`
|
|
12
|
+
Only allow requests from specific IP ranges. Requests from outside receive a silent 403 or 404.
|
|
13
|
+
```python
|
|
14
|
+
@router.get("/admin/reports")
|
|
15
|
+
@ip_allowlist("10.0.0.0/8", "192.168.1.0/24")
|
|
16
|
+
async def admin_reports(): ...
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
#### `@ip_blocklist(*cidrs)`
|
|
20
|
+
Inverse of `@ip_allowlist` — block specific IPs or ranges, allow everything else. Useful for abuse mitigation without touching the route handler.
|
|
21
|
+
|
|
22
|
+
#### `@geo_only(*country_codes)`
|
|
23
|
+
Restrict a route to specific countries based on IP geolocation. Returns a silent 404 elsewhere — same treatment as `@env_only` to avoid leaking that the route exists.
|
|
24
|
+
```python
|
|
25
|
+
@router.get("/checkout")
|
|
26
|
+
@geo_only("US", "CA", "GB")
|
|
27
|
+
async def checkout(): ...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
#### `@requires_role(*roles)`
|
|
31
|
+
Gate a route behind a role check. Reads `request.state.user_roles` (populated by your auth middleware). Returns 403 if the role is absent.
|
|
32
|
+
```python
|
|
33
|
+
@router.delete("/users/{id}")
|
|
34
|
+
@requires_role("admin", "superuser")
|
|
35
|
+
async def delete_user(id: int): ...
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
#### `@requires_header(name, value=None)`
|
|
39
|
+
Block requests missing a required header. Optionally assert an exact value. Useful for internal service-to-service APIs that should never be called by public clients.
|
|
40
|
+
```python
|
|
41
|
+
@router.post("/internal/sync")
|
|
42
|
+
@requires_header("X-Internal-Token")
|
|
43
|
+
async def internal_sync(): ...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### Traffic & Load Management
|
|
49
|
+
|
|
50
|
+
#### `@rate_limit(requests, window)`
|
|
51
|
+
Cap how many times a route can be called within a rolling time window. Exceeding the limit returns 429 with a `Retry-After` header. Window accepts human-readable strings (`"1m"`, `"1h"`, `"1d"`).
|
|
52
|
+
```python
|
|
53
|
+
@router.post("/auth/login")
|
|
54
|
+
@rate_limit(requests=5, window="1m")
|
|
55
|
+
async def login(): ...
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
#### `@quota(calls, period, per)`
|
|
59
|
+
Per-identity call quota — scoped to `"api_key"`, `"user_id"`, or `"ip"`. Resets at the end of each period (`"day"`, `"month"`). Returns 429 with remaining quota headers when exhausted.
|
|
60
|
+
```python
|
|
61
|
+
@router.post("/ai/generate")
|
|
62
|
+
@quota(calls=100, period="day", per="api_key")
|
|
63
|
+
async def generate(): ...
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
#### `@circuit_breaker(threshold, timeout)`
|
|
67
|
+
Automatically disables a route after `threshold` consecutive errors, then re-enables it after `timeout` seconds. Prevents a struggling downstream service from cascading failures across the API.
|
|
68
|
+
```python
|
|
69
|
+
@router.get("/payments/status")
|
|
70
|
+
@circuit_breaker(threshold=5, timeout=30)
|
|
71
|
+
async def payment_status(): ...
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### `@shed_load(above)`
|
|
75
|
+
Drop a configurable percentage of requests when a system health signal (CPU, memory, or custom) exceeds a threshold. Returns 503. The route remains "active" in shield state but sheds excess traffic automatically.
|
|
76
|
+
```python
|
|
77
|
+
@router.get("/recommendations")
|
|
78
|
+
@shed_load(above=0.8)
|
|
79
|
+
async def recommendations(): ...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### Time & Scheduling
|
|
85
|
+
|
|
86
|
+
#### `@available_between(start, end, tz, days)`
|
|
87
|
+
Route only responds during specified hours on specified days. Outside the window it returns 503 with a `Retry-After` pointing to the next available open time. Ideal for business-hours-only endpoints.
|
|
88
|
+
```python
|
|
89
|
+
@router.post("/support/ticket")
|
|
90
|
+
@available_between(start="09:00", end="17:00", tz="America/New_York", days=["mon","tue","wed","thu","fri"])
|
|
91
|
+
async def create_ticket(): ...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### `@sunset(date)`
|
|
95
|
+
Hard end-of-life — unlike `@deprecated` which warns, this blocks the route entirely (503) after the given date. Clients that ignored deprecation headers now receive a hard stop.
|
|
96
|
+
```python
|
|
97
|
+
@router.get("/v1/users")
|
|
98
|
+
@sunset(date="2026-01-01")
|
|
99
|
+
async def v1_users(): ...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### `@cooldown(duration)`
|
|
103
|
+
After a maintenance window ends, keep traffic throttled for a warmup period before returning to full capacity. Useful when the backend needs time to rebuild caches or warm up database connections.
|
|
104
|
+
```python
|
|
105
|
+
@router.get("/search")
|
|
106
|
+
@maintenance(reason="Index rebuild")
|
|
107
|
+
@cooldown(duration="10m")
|
|
108
|
+
async def search(): ...
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### Observability & Debugging
|
|
114
|
+
|
|
115
|
+
#### `@mock(response, status_code)`
|
|
116
|
+
Return a static mock response instead of running the handler. Zero backend calls. Invaluable for frontend teams when the real endpoint is not ready, or for chaos testing.
|
|
117
|
+
```python
|
|
118
|
+
@router.get("/recommendations")
|
|
119
|
+
@mock(response={"items": []}, status_code=200)
|
|
120
|
+
async def recommendations(): ...
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### `@latency(min_ms, max_ms)`
|
|
124
|
+
Inject artificial latency (random value within range) on every request. Useful for testing client timeout handling and loading states. Should be combined with `@env_only("dev", "staging")` to prevent accidental use in production.
|
|
125
|
+
```python
|
|
126
|
+
@router.get("/slow-service")
|
|
127
|
+
@env_only("dev", "staging")
|
|
128
|
+
@latency(min_ms=200, max_ms=800)
|
|
129
|
+
async def slow_service(): ...
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### `@audit_request(actor_from)`
|
|
133
|
+
Enhanced per-route audit logging — records full request metadata, response status, and actor identity. More granular than the engine-level audit log which only tracks state changes.
|
|
134
|
+
```python
|
|
135
|
+
@router.delete("/accounts/{id}")
|
|
136
|
+
@audit_request(actor_from="header:X-User-ID")
|
|
137
|
+
async def delete_account(id: int): ...
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### Feature Flagging & Experimentation
|
|
143
|
+
|
|
144
|
+
#### `@feature_flag(flag_name, provider)`
|
|
145
|
+
Route is only active when the named flag is enabled in an external feature flag system (LaunchDarkly, Unleash, or a local config dict). Integrates with the engine so it is controllable at runtime without redeployment.
|
|
146
|
+
```python
|
|
147
|
+
@router.get("/new-checkout")
|
|
148
|
+
@feature_flag("new_checkout_v2")
|
|
149
|
+
async def new_checkout(): ...
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### `@ab_test(variant, allocation)`
|
|
153
|
+
Serve this route handler to a specific slice of traffic as variant B. Pairs with a variant A handler on the same path. The engine tracks which sessions see which variant for analysis.
|
|
154
|
+
```python
|
|
155
|
+
@router.get("/pricing")
|
|
156
|
+
@ab_test(variant="B", allocation=0.3)
|
|
157
|
+
async def pricing_v2(): ...
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### `@beta(opt_in_header)`
|
|
161
|
+
Route is only reachable when the request includes a specific opt-in header. Everyone else receives a 404. Clean way to run an invite-only beta without a separate deployment or feature flag service.
|
|
162
|
+
```python
|
|
163
|
+
@router.get("/ai-search")
|
|
164
|
+
@beta(opt_in_header="X-Beta-Features")
|
|
165
|
+
async def ai_search(): ...
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### Data & Compliance
|
|
171
|
+
|
|
172
|
+
#### `@read_only`
|
|
173
|
+
Block all mutating methods (POST, PUT, PATCH, DELETE) on a route — only GET and HEAD pass through. Returns 405. Useful during live data migrations when reads must continue but writes must be frozen.
|
|
174
|
+
```python
|
|
175
|
+
@router.post("/orders")
|
|
176
|
+
@read_only
|
|
177
|
+
async def create_order(): ...
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### `@requires_consent(policy)`
|
|
181
|
+
Check that the requesting user has accepted a named policy before serving the response. Reads from `request.state.user_consents`. Returns 451 (Unavailable For Legal Reasons) if consent is missing.
|
|
182
|
+
```python
|
|
183
|
+
@router.get("/analytics/personal")
|
|
184
|
+
@requires_consent("gdpr_v2")
|
|
185
|
+
async def personal_analytics(): ...
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
#### `@pii_scrub(fields)`
|
|
189
|
+
Automatically redact specified fields from request logs and the audit trail for this route. The middleware strips listed fields before they reach the audit logger — they never appear in any log.
|
|
190
|
+
```python
|
|
191
|
+
@router.post("/users")
|
|
192
|
+
@pii_scrub(fields=["email", "phone", "ssn"])
|
|
193
|
+
async def create_user(): ...
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### Decorator Summary
|
|
199
|
+
|
|
200
|
+
| Category | Decorator | HTTP Response |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| Access | `@ip_allowlist(*cidrs)` | 403/404 if blocked |
|
|
203
|
+
| Access | `@ip_blocklist(*cidrs)` | 403 if blocked |
|
|
204
|
+
| Access | `@geo_only(*countries)` | 404 outside allowed countries |
|
|
205
|
+
| Access | `@requires_role(*roles)` | 403 if role missing |
|
|
206
|
+
| Access | `@requires_header(name, value)` | 400 if header missing |
|
|
207
|
+
| Traffic | `@rate_limit(requests, window)` | 429 + `Retry-After` |
|
|
208
|
+
| Traffic | `@quota(calls, period, per)` | 429 when exhausted |
|
|
209
|
+
| Traffic | `@circuit_breaker(threshold, timeout)` | 503 when open |
|
|
210
|
+
| Traffic | `@shed_load(above)` | 503 under stress |
|
|
211
|
+
| Time | `@available_between(start, end, tz, days)` | 503 + `Retry-After` |
|
|
212
|
+
| Time | `@sunset(date)` | 503 after date |
|
|
213
|
+
| Time | `@cooldown(duration)` | throttled after maintenance |
|
|
214
|
+
| Debug | `@mock(response, status_code)` | configurable |
|
|
215
|
+
| Debug | `@latency(min_ms, max_ms)` | normal response, delayed |
|
|
216
|
+
| Debug | `@audit_request(actor_from)` | normal response, extra logging |
|
|
217
|
+
| Experimentation | `@feature_flag(flag_name, provider)` | 503 if flag off |
|
|
218
|
+
| Experimentation | `@ab_test(variant, allocation)` | normal response |
|
|
219
|
+
| Experimentation | `@beta(opt_in_header)` | 404 without opt-in header |
|
|
220
|
+
| Compliance | `@read_only` | 405 on mutating methods |
|
|
221
|
+
| Compliance | `@requires_consent(policy)` | 451 if no consent |
|
|
222
|
+
| Compliance | `@pii_scrub(fields)` | normal response, scrubbed logs |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## SaaS Infrastructure
|
|
227
|
+
|
|
228
|
+
### Multi-tenancy
|
|
229
|
+
- Organizations and workspaces — one account manages multiple apps and environments
|
|
230
|
+
- Team roles: Owner, Admin, Viewer — scoped per workspace with granular permissions
|
|
231
|
+
- Per-workspace API keys replacing the single shared admin token
|
|
232
|
+
- Billing tiers:
|
|
233
|
+
- **Free**: memory backend, up to 5 routes, single user
|
|
234
|
+
- **Pro**: Redis backend, unlimited routes, team access, metrics history
|
|
235
|
+
- **Enterprise**: SSO/SAML, SLA, custom retention, on-premise deployment
|
|
236
|
+
|
|
237
|
+
### Cloud-hosted Backend
|
|
238
|
+
- Managed Redis or Postgres as the shield backend — users provision nothing
|
|
239
|
+
- CLI and ShieldAdmin point at the SaaS control plane, not their own server
|
|
240
|
+
- Local middleware reads state from the cloud backend; latency added is sub-millisecond via edge caching
|
|
241
|
+
- Fail-open guarantee preserved — if the cloud is unreachable, all routes pass through
|
|
242
|
+
|
|
243
|
+
### User & API Key Management
|
|
244
|
+
- Invite team members by email with role assignment
|
|
245
|
+
- Personal access tokens for CI/CD pipelines and automation
|
|
246
|
+
- Token scopes: `routes:read`, `routes:write`, `audit:read`, `admin`
|
|
247
|
+
- Token expiry and rotation policies
|
|
248
|
+
- Activity log per token
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## AI-Powered Analytics
|
|
253
|
+
|
|
254
|
+
### Metrics Collection
|
|
255
|
+
Before AI analysis is possible, `ShieldMiddleware` needs to emit structured metrics on every request:
|
|
256
|
+
|
|
257
|
+
- Request count per route (total, passed, blocked)
|
|
258
|
+
- Block reason distribution per route (maintenance / disabled / env-gated / rate-limited)
|
|
259
|
+
- Latency percentiles per route (p50, p95, p99)
|
|
260
|
+
- Requests blocked during a maintenance window (impact count)
|
|
261
|
+
- Traffic volume by hour-of-day and day-of-week (pattern data for scheduling AI)
|
|
262
|
+
|
|
263
|
+
Metrics are written to a time-series store (ClickHouse, TimescaleDB, or Postgres with partitioning) alongside the existing audit log.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
### AI Features
|
|
268
|
+
|
|
269
|
+
#### Optimal Maintenance Window Suggestions
|
|
270
|
+
Analyse historical traffic patterns per route and recommend the lowest-impact maintenance window.
|
|
271
|
+
> *"Route `GET /payments` sees its lowest traffic between 02:00–04:00 UTC on Sundays. Suggested window: Sunday 02:00–03:00 UTC."*
|
|
272
|
+
|
|
273
|
+
#### Anomaly Detection
|
|
274
|
+
Compare the current request rate against the historical baseline for each route and alert when:
|
|
275
|
+
- A route that normally receives 500 req/min drops to 0 — silent breakage
|
|
276
|
+
- A disabled route is being hammered — clients ignoring 503 responses
|
|
277
|
+
- A deprecated route traffic has not declined — clients not migrating to the successor
|
|
278
|
+
|
|
279
|
+
#### Rollout Safety Advisor
|
|
280
|
+
During a `@rollout(percentage=N)` deployment, continuously monitor the error rate on the canary slice vs the stable slice and surface actionable recommendations:
|
|
281
|
+
> *"Canary error rate is 4.2% vs stable baseline 0.3%. Recommend halting rollout and investigating before increasing traffic."*
|
|
282
|
+
|
|
283
|
+
#### Deprecation Candidate Detection
|
|
284
|
+
Scan routes with `ACTIVE` status and flag ones with steadily declining traffic over 30, 60, or 90 day windows as candidates for `@deprecated`. Surfaces the suggestion in the dashboard with traffic charts as evidence.
|
|
285
|
+
|
|
286
|
+
#### Impact Analysis Before State Changes
|
|
287
|
+
Before a route is disabled, surface estimated blast radius:
|
|
288
|
+
> *"`GET /auth/token` received 12,400 requests in the last hour. Disabling it will affect an estimated 8,200 active sessions and 3 dependent routes."*
|
|
289
|
+
|
|
290
|
+
#### Natural Language CLI Interface
|
|
291
|
+
Execute shield operations in plain English via a Claude-powered command:
|
|
292
|
+
```bash
|
|
293
|
+
shield ask "which routes have the most blocked traffic this week?"
|
|
294
|
+
shield ask "schedule maintenance on /payments for the quietest window this weekend"
|
|
295
|
+
shield ask "show me routes that haven't had any traffic in 30 days"
|
|
296
|
+
```
|
|
297
|
+
Backed by a Claude API call with current route state and metrics as context.
|
|
298
|
+
|
|
299
|
+
#### Incident Correlation
|
|
300
|
+
Cross-reference audit log entries (state changes) with error rate spikes automatically. Surfaces probable causes in the dashboard:
|
|
301
|
+
> *"Error rate on `GET /orders` spiked 340% at 14:22 UTC. `POST /inventory` was put in maintenance at 14:19 — likely cause."*
|
|
302
|
+
|
|
303
|
+
#### Route Health Scoring
|
|
304
|
+
Assign each route a health score (0–100) combining:
|
|
305
|
+
- Uptime percentage over the last 30 days
|
|
306
|
+
- Error rate trend
|
|
307
|
+
- Traffic volume trend
|
|
308
|
+
- Number of unplanned maintenance windows
|
|
309
|
+
- Time since last state change
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Dashboard Enhancements
|
|
314
|
+
|
|
315
|
+
| Enhancement | Description |
|
|
316
|
+
|---|---|
|
|
317
|
+
| Traffic sparklines | Inline 24h traffic chart per route row — no need to drill into a detail page |
|
|
318
|
+
| Maintenance impact counter | "Blocked 1,204 requests during last window" shown per route |
|
|
319
|
+
| Alert rules UI | Configure per-route alerts: notify Slack if error rate exceeds N% |
|
|
320
|
+
| Route health score | 0–100 score badge per route in the route table |
|
|
321
|
+
| Dependency graph | Visual map of which routes depend on which via FastAPI `Depends()` |
|
|
322
|
+
| Metrics detail page | Per-route page with full p50/p95/p99 latency, traffic, and block reason charts |
|
|
323
|
+
| Audit log filters | Filter by actor, action type, date range, and route directly in the UI |
|
|
324
|
+
| Scheduled window calendar | Calendar view of all upcoming maintenance windows across all routes |
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Integrations
|
|
329
|
+
|
|
330
|
+
| Integration | Value |
|
|
331
|
+
|---|---|
|
|
332
|
+
| **Prometheus** `/metrics` endpoint | Drop into any existing observability stack — Grafana dashboards work immediately |
|
|
333
|
+
| **Datadog** | Push route state and metrics as custom metrics and events |
|
|
334
|
+
| **Grafana** | Pre-built dashboard JSON for route health and traffic |
|
|
335
|
+
| **PagerDuty / OpsGenie** | Auto-page on anomaly detection alerts or unexpected route state changes |
|
|
336
|
+
| **Slack / Teams** | Rich formatted messages on state changes (webhook foundation already exists) |
|
|
337
|
+
| **GitHub Actions** | `shield disable` as a pre-deploy step, `shield enable` on success — zero-downtime deploys |
|
|
338
|
+
| **Terraform provider** | Declare route states as infrastructure-as-code alongside the rest of your infrastructure |
|
|
339
|
+
| **LaunchDarkly / Unleash** | Power `@feature_flag` with your existing flag provider |
|
|
340
|
+
| **Sentry** | Correlate shield state changes with error spikes in Sentry issues automatically |
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Suggested Build Priority
|
|
345
|
+
|
|
346
|
+
| Priority | Feature | Reason |
|
|
347
|
+
|---|---|---|
|
|
348
|
+
| 1 | Metrics collection in middleware | Everything else depends on this data existing |
|
|
349
|
+
| 2 | `@rate_limit` and `@circuit_breaker` | Highest immediate practical value for production APIs |
|
|
350
|
+
| 3 | `@available_between` and `@mock` | Common requests, low implementation complexity |
|
|
351
|
+
| 4 | Time-series metrics storage | Required before any AI feature ships |
|
|
352
|
+
| 5 | Dashboard traffic sparklines | Immediate visible value from metrics data |
|
|
353
|
+
| 6 | Optimal maintenance window AI | First AI feature — straightforward pattern analysis |
|
|
354
|
+
| 7 | Anomaly detection + alerts | The feature teams will pay for in a SaaS model |
|
|
355
|
+
| 8 | Multi-tenancy + billing | Once core AI value is proven |
|
|
356
|
+
| 9 | Natural language CLI | High delight factor, builds on all prior AI work |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-shield
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Route lifecycle management for APIs — maintenance mode, env gating, deprecation, and more
|
|
5
5
|
Project-URL: Homepage, https://github.com/Attakay78/api-shield
|
|
6
6
|
Project-URL: Repository, https://github.com/Attakay78/api-shield
|
|
@@ -32,6 +32,7 @@ Requires-Dist: aiofiles>=23.0; extra == 'all'
|
|
|
32
32
|
Requires-Dist: fastapi>=0.100; extra == 'all'
|
|
33
33
|
Requires-Dist: httpx>=0.27; extra == 'all'
|
|
34
34
|
Requires-Dist: jinja2>=3.1; extra == 'all'
|
|
35
|
+
Requires-Dist: limits>=5.8.0; extra == 'all'
|
|
35
36
|
Requires-Dist: python-multipart>=0.0.22; extra == 'all'
|
|
36
37
|
Requires-Dist: pyyaml>=6.0; extra == 'all'
|
|
37
38
|
Requires-Dist: redis[asyncio]>=5.0; extra == 'all'
|
|
@@ -51,6 +52,7 @@ Requires-Dist: aiofiles>=23.0; extra == 'dev'
|
|
|
51
52
|
Requires-Dist: anyio[trio]; extra == 'dev'
|
|
52
53
|
Requires-Dist: fastapi>=0.100; extra == 'dev'
|
|
53
54
|
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
55
|
+
Requires-Dist: limits>=5.8.0; extra == 'dev'
|
|
54
56
|
Requires-Dist: mkdocs-git-revision-date-localized-plugin>=1.2; extra == 'dev'
|
|
55
57
|
Requires-Dist: mkdocs-material>=9.5; extra == 'dev'
|
|
56
58
|
Requires-Dist: mkdocstrings[python]>=0.25; extra == 'dev'
|
|
@@ -67,6 +69,8 @@ Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
|
67
69
|
Requires-Dist: mkdocstrings[python]>=0.25; extra == 'docs'
|
|
68
70
|
Provides-Extra: fastapi
|
|
69
71
|
Requires-Dist: fastapi>=0.100; extra == 'fastapi'
|
|
72
|
+
Provides-Extra: rate-limit
|
|
73
|
+
Requires-Dist: limits>=5.8.0; extra == 'rate-limit'
|
|
70
74
|
Provides-Extra: redis
|
|
71
75
|
Requires-Dist: redis[asyncio]>=5.0; extra == 'redis'
|
|
72
76
|
Provides-Extra: toml
|
|
@@ -78,15 +82,39 @@ Description-Content-Type: text/markdown
|
|
|
78
82
|
<div align="center">
|
|
79
83
|
<img src="api-shield-logo.svg" alt="API Shield" width="600"/>
|
|
80
84
|
|
|
81
|
-
<p><strong>Route lifecycle management for Python web frameworks — maintenance mode, environment gating, deprecation, admin panels, and more. No restarts required.</strong></p>
|
|
85
|
+
<p><strong>Route(API) lifecycle management for Python web frameworks — maintenance mode, environment gating, deprecation, rate limiting, admin panels, and more. No restarts required.</strong></p>
|
|
82
86
|
|
|
83
|
-
<a href="https://pypi.org/project/api-shield"><img src="https://img.shields.io/pypi/v/api-shield?color=F59E0B&label=pypi" alt="PyPI"></a>
|
|
87
|
+
<a href="https://pypi.org/project/api-shield"><img src="https://img.shields.io/pypi/v/api-shield?color=F59E0B&label=pypi&cacheSeconds=300" alt="PyPI"></a>
|
|
84
88
|
<a href="https://pypi.org/project/api-shield"><img src="https://img.shields.io/pypi/pyversions/api-shield?color=F59E0B" alt="Python versions"></a>
|
|
85
89
|
<a href="LICENSE"><img src="https://img.shields.io/github/license/Attakay78/api-shield?color=F59E0B" alt="License"></a>
|
|
86
90
|
</div>
|
|
87
91
|
|
|
88
92
|
---
|
|
89
93
|
|
|
94
|
+
> [!WARNING]
|
|
95
|
+
> **Early Access** — `api-shield` is fully functional and ready to use. We are actively building on top of a solid foundation and your real-world experience is invaluable at this stage. If you have feedback, feature ideas, or suggestions, please [open an issue](https://github.com/Attakay78/api-shield/issues) — every voice helps shape the roadmap.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Key features
|
|
100
|
+
|
|
101
|
+
| Feature | Description |
|
|
102
|
+
|---|---|
|
|
103
|
+
| 🎨 **Decorator-first DX** | Route state lives next to the route definition, not in a separate config file |
|
|
104
|
+
| ⚡ **Zero-restart control** | State changes take effect immediately — no redeployment or server restart needed |
|
|
105
|
+
| 🛡️ **Fail-open by default** | If the backend is unreachable, requests pass through. Shield never takes down your API |
|
|
106
|
+
| 🔌 **Pluggable backends** | In-memory (default), file-based JSON, or Redis for multi-instance deployments |
|
|
107
|
+
| 🖥️ **Admin dashboard** | HTMX-powered UI with live SSE updates — no JS framework required |
|
|
108
|
+
| 🖱️ **REST API + CLI** | Full programmatic control from the terminal or CI pipelines — works over HTTPS remotely |
|
|
109
|
+
| 📄 **OpenAPI integration** | Disabled / env-gated routes hidden from `/docs`; deprecated routes flagged automatically |
|
|
110
|
+
| 📋 **Audit log** | Every state change is recorded: who, when, what route, old status → new status |
|
|
111
|
+
| ⏰ **Scheduled windows** | `asyncio`-native scheduler — maintenance windows activate and deactivate automatically |
|
|
112
|
+
| 🔔 **Webhooks** | Fire HTTP POST on every state change — built-in Slack formatter and custom formatters supported |
|
|
113
|
+
| 🎨 **Custom responses** | Return HTML, redirects, or any response shape for blocked routes — per-route or app-wide default |
|
|
114
|
+
| 🚦 **Rate limiting** | Per-IP, per-user, per-API-key, or global counters — tiered limits, burst allowance, runtime mutation |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
90
118
|
## Install
|
|
91
119
|
|
|
92
120
|
```bash
|
|
@@ -147,6 +175,86 @@ shield global enable --reason "Deploying v2" --exempt /health
|
|
|
147
175
|
| `@env_only("dev", "staging")` | Restricted to named environments | 404 elsewhere |
|
|
148
176
|
| `@deprecated(sunset, use_instead)` | Still works, injects deprecation headers | 200 |
|
|
149
177
|
| `@force_active` | Bypasses all shield checks | Always 200 |
|
|
178
|
+
| `@rate_limit("100/minute")` | Cap requests per IP, user, API key, or globally | 429 |
|
|
179
|
+
### Custom responses
|
|
180
|
+
|
|
181
|
+
By default, blocked routes return a structured JSON error body. You can replace it with anything — HTML, a redirect, plain text, or your own JSON — in two ways:
|
|
182
|
+
|
|
183
|
+
**Per-route** — pass `response=` directly on the decorator:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from starlette.requests import Request
|
|
187
|
+
from starlette.responses import HTMLResponse, RedirectResponse
|
|
188
|
+
from shield.fastapi import maintenance, disabled
|
|
189
|
+
|
|
190
|
+
def maintenance_page(request: Request, exc: Exception) -> HTMLResponse:
|
|
191
|
+
return HTMLResponse(
|
|
192
|
+
f"<h1>Down for maintenance</h1><p>{exc.reason}</p>", status_code=503
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
@router.get("/payments")
|
|
196
|
+
@maintenance(reason="DB migration", response=maintenance_page)
|
|
197
|
+
async def payments():
|
|
198
|
+
return {"payments": []}
|
|
199
|
+
|
|
200
|
+
@router.get("/orders")
|
|
201
|
+
@maintenance(reason="Upgrade in progress", response=lambda *_: RedirectResponse("/status"))
|
|
202
|
+
async def orders():
|
|
203
|
+
return {"orders": []}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Global default** — set once on `ShieldMiddleware`, applies to every route without a per-route factory:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
app.add_middleware(
|
|
210
|
+
ShieldMiddleware,
|
|
211
|
+
engine=engine,
|
|
212
|
+
responses={
|
|
213
|
+
"maintenance": maintenance_page, # all maintenance routes
|
|
214
|
+
"disabled": lambda req, exc: HTMLResponse(
|
|
215
|
+
f"<h1>Gone</h1><p>{exc.reason}</p>", status_code=503
|
|
216
|
+
),
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Resolution order: **per-route `response=`** → **global `responses[...]`** → **built-in JSON**. The factory can be sync or async and receives the live `Request` and the `ShieldException` that triggered the block.
|
|
222
|
+
|
|
223
|
+
## Rate limiting
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from shield.fastapi.decorators import rate_limit
|
|
227
|
+
|
|
228
|
+
@router.get("/public/posts")
|
|
229
|
+
@rate_limit("10/minute") # 10 req/min per IP
|
|
230
|
+
async def list_posts():
|
|
231
|
+
return {"posts": [...]}
|
|
232
|
+
|
|
233
|
+
@router.get("/users/me")
|
|
234
|
+
@rate_limit("100/minute", key="user") # per authenticated user
|
|
235
|
+
async def get_current_user():
|
|
236
|
+
...
|
|
237
|
+
|
|
238
|
+
@router.get("/reports")
|
|
239
|
+
@rate_limit( # tiered limits
|
|
240
|
+
{"free": "10/minute", "pro": "100/minute", "enterprise": "unlimited"},
|
|
241
|
+
key="user",
|
|
242
|
+
)
|
|
243
|
+
async def get_reports():
|
|
244
|
+
...
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Policies can be mutated at runtime without redeploying (`shield rl` and `shield rate-limits` are aliases):
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
shield rl set GET:/public/posts 20/minute # raise the limit live
|
|
251
|
+
shield rl reset GET:/public/posts # clear counters
|
|
252
|
+
shield rl hits # blocked requests log
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Requires `api-shield[rate-limit]`. Powered by [limits](https://limits.readthedocs.io/en/stable/).
|
|
256
|
+
|
|
257
|
+
---
|
|
150
258
|
|
|
151
259
|
## Backends
|
|
152
260
|
|
|
@@ -156,6 +264,8 @@ shield global enable --reason "Deploying v2" --exempt /health
|
|
|
156
264
|
| `FileBackend` | Yes | No |
|
|
157
265
|
| `RedisBackend` | Yes | Yes |
|
|
158
266
|
|
|
267
|
+
For rate limiting in multi-worker deployments, use `RedisBackend` — counters are atomic and shared across all processes.
|
|
268
|
+
|
|
159
269
|
---
|
|
160
270
|
|
|
161
271
|
## Documentation
|
|
@@ -166,6 +276,7 @@ Full documentation at **[attakay78.github.io/api-shield](https://attakay78.githu
|
|
|
166
276
|
|---|---|
|
|
167
277
|
| [Tutorial](https://attakay78.github.io/api-shield/tutorial/installation/) | Get started in 5 minutes |
|
|
168
278
|
| [Decorators reference](https://attakay78.github.io/api-shield/reference/decorators/) | All decorator options |
|
|
279
|
+
| [Rate limiting](https://attakay78.github.io/api-shield/tutorial/rate-limiting/) | Per-IP, per-user, tiered limits |
|
|
169
280
|
| [ShieldEngine reference](https://attakay78.github.io/api-shield/reference/engine/) | Programmatic control |
|
|
170
281
|
| [Backends](https://attakay78.github.io/api-shield/tutorial/backends/) | Memory, File, Redis, custom |
|
|
171
282
|
| [Admin dashboard](https://attakay78.github.io/api-shield/tutorial/admin-dashboard/) | Mounting ShieldAdmin |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Rate Limiting
|
|
2
|
+
|
|
3
|
+
Adds full rate limiting support to api-shield.
|
|
4
|
+
|
|
5
|
+
## What's new
|
|
6
|
+
|
|
7
|
+
**`@rate_limit` decorator**
|
|
8
|
+
|
|
9
|
+
- Limit requests per IP, user, API key, or globally with a single decorator
|
|
10
|
+
- Tiered limits (`{"free": "10/min", "pro": "100/min"}`) resolved from request state
|
|
11
|
+
- Burst allowance, exempt IPs, exempt roles
|
|
12
|
+
- Custom 429 response factory — per-route via `response=` or app-wide via `ShieldMiddleware(responses={"rate_limited": ...})`
|
|
13
|
+
- Works as `Depends()` without middleware
|
|
14
|
+
- Sync and async custom key functions supported
|
|
15
|
+
|
|
16
|
+
**Algorithms** — `fixed_window`, `sliding_window`, `moving_window`, `token_bucket` (via [limits](https://limits.readthedocs.io/en/stable/) >= 5.8.0)
|
|
17
|
+
|
|
18
|
+
**Storage** — auto-selected from the active backend: `MemoryRateLimitStorage`, `FileRateLimitStorage`, `RedisRateLimitStorage`. Pass `rate_limit_backend=` to use a different backend for counters.
|
|
19
|
+
|
|
20
|
+
**Dashboard** — Rate Limits tab at `/shield/rate-limits` with reset, edit, delete actions. Blocked requests log at `/shield/blocked`.
|
|
21
|
+
|
|
22
|
+
**CLI** — `shield rl list|set|reset|delete|hits` (`shield rate-limits` is an alias).
|
|
23
|
+
|
|
24
|
+
**Audit log** — policy changes recorded with action badges: `set`, `update`, `reset`, `delete`.
|
|
25
|
+
|
|
26
|
+
**Response headers** — `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` on every request; `Retry-After` on 429s.
|
|
27
|
+
|
|
28
|
+
## Other changes
|
|
29
|
+
|
|
30
|
+
- `mypy --strict` passes across all 36 source files
|
|
31
|
+
- `ruff check` passes with no errors
|
|
32
|
+
- `limits` version constraint updated to `>=5.8.0`
|
|
33
|
+
- Dependency injection example corrected — decorator and `Depends()` are mutually exclusive patterns, not combined
|
|
34
|
+
- Full documentation added: rate limiting tutorial, reference, and updates to all relevant pages (README, adapters, CLI, dashboard, backends, changelog)
|