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.
Files changed (167) hide show
  1. api_shield-0.3.0/FEATURES.md +356 -0
  2. {api_shield-0.2.0 → api_shield-0.3.0}/PKG-INFO +114 -3
  3. api_shield-0.3.0/PR_DESCRIPTION.md +34 -0
  4. api_shield-0.3.0/README.md +207 -0
  5. api_shield-0.3.0/docs/adapters/custom.md +520 -0
  6. api_shield-0.3.0/docs/adapters/fastapi.md +662 -0
  7. api_shield-0.3.0/docs/assets/dashboard.png +0 -0
  8. api_shield-0.3.0/docs/assets/openapi-maintenance.png +0 -0
  9. api_shield-0.3.0/docs/assets/openapi.png +0 -0
  10. api_shield-0.3.0/docs/changelog.md +99 -0
  11. api_shield-0.3.0/docs/guides/distributed.md +278 -0
  12. {api_shield-0.2.0 → api_shield-0.3.0}/docs/guides/production.md +2 -2
  13. {api_shield-0.2.0 → api_shield-0.3.0}/docs/index.md +23 -14
  14. api_shield-0.3.0/docs/reference/backends.md +240 -0
  15. api_shield-0.3.0/docs/reference/cli.md +380 -0
  16. api_shield-0.3.0/docs/reference/decorators.md +454 -0
  17. api_shield-0.3.0/docs/reference/engine.md +517 -0
  18. api_shield-0.3.0/docs/reference/exceptions.md +146 -0
  19. api_shield-0.3.0/docs/reference/middleware.md +173 -0
  20. api_shield-0.3.0/docs/reference/models.md +191 -0
  21. api_shield-0.3.0/docs/reference/rate-limiting.md +465 -0
  22. {api_shield-0.2.0 → api_shield-0.3.0}/docs/stylesheets/extra.css +23 -0
  23. {api_shield-0.2.0 → api_shield-0.3.0}/docs/tutorial/admin-dashboard.md +32 -8
  24. {api_shield-0.2.0 → api_shield-0.3.0}/docs/tutorial/backends.md +26 -2
  25. api_shield-0.3.0/docs/tutorial/cli.md +199 -0
  26. {api_shield-0.2.0 → api_shield-0.3.0}/docs/tutorial/first-decorator.md +8 -7
  27. {api_shield-0.2.0 → api_shield-0.3.0}/docs/tutorial/installation.md +8 -4
  28. {api_shield-0.2.0 → api_shield-0.3.0}/docs/tutorial/middleware.md +13 -7
  29. api_shield-0.3.0/docs/tutorial/rate-limiting.md +415 -0
  30. {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/basic.py +3 -3
  31. api_shield-0.3.0/examples/fastapi/custom_responses.py +210 -0
  32. {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/dependency_injection.py +56 -35
  33. api_shield-0.3.0/examples/fastapi/rate_limiting.py +295 -0
  34. api_shield-0.3.0/examples/fastapi/webhooks.py +257 -0
  35. api_shield-0.3.0/flask-adapter-design.md +568 -0
  36. api_shield-0.3.0/issues_to_resolve.txt +43 -0
  37. {api_shield-0.2.0 → api_shield-0.3.0}/mkdocs.yml +7 -2
  38. {api_shield-0.2.0 → api_shield-0.3.0}/pyproject.toml +4 -1
  39. api_shield-0.3.0/rate-limits.md +1479 -0
  40. {api_shield-0.2.0 → api_shield-0.3.0}/shield/admin/api.py +82 -0
  41. {api_shield-0.2.0 → api_shield-0.3.0}/shield/admin/app.py +24 -1
  42. {api_shield-0.2.0 → api_shield-0.3.0}/shield/cli/client.py +59 -0
  43. {api_shield-0.2.0 → api_shield-0.3.0}/shield/cli/main.py +212 -0
  44. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/backends/base.py +104 -0
  45. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/backends/file.py +79 -14
  46. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/backends/memory.py +61 -1
  47. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/backends/redis.py +185 -2
  48. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/config.py +2 -2
  49. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/engine.py +443 -14
  50. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/exceptions.py +56 -0
  51. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/models.py +8 -3
  52. api_shield-0.3.0/shield/core/rate_limit/__init__.py +11 -0
  53. api_shield-0.3.0/shield/core/rate_limit/keys.py +229 -0
  54. api_shield-0.3.0/shield/core/rate_limit/limiter.py +246 -0
  55. api_shield-0.3.0/shield/core/rate_limit/models.py +284 -0
  56. api_shield-0.3.0/shield/core/rate_limit/storage.py +673 -0
  57. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/app.py +11 -1
  58. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/routes.py +257 -3
  59. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/audit.html +3 -1
  60. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/base.html +31 -5
  61. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/index.html +2 -0
  62. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/login.html +15 -8
  63. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/audit_row.html +20 -0
  64. api_shield-0.3.0/shield/dashboard/templates/partials/modal_rl_delete.html +56 -0
  65. api_shield-0.3.0/shield/dashboard/templates/partials/modal_rl_edit.html +99 -0
  66. api_shield-0.3.0/shield/dashboard/templates/partials/modal_rl_reset.html +45 -0
  67. api_shield-0.3.0/shield/dashboard/templates/partials/pagination.html +63 -0
  68. api_shield-0.3.0/shield/dashboard/templates/partials/rate_limit_hits.html +16 -0
  69. api_shield-0.3.0/shield/dashboard/templates/partials/rate_limit_rows.html +61 -0
  70. api_shield-0.3.0/shield/dashboard/templates/rate_limits.html +53 -0
  71. api_shield-0.3.0/shield/dashboard/templates/rl_hits.html +50 -0
  72. {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/__init__.py +4 -0
  73. {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/decorators.py +349 -36
  74. {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/middleware.py +174 -30
  75. {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/router.py +64 -0
  76. {api_shield-0.2.0 → api_shield-0.3.0}/tests/admin/test_api.py +90 -0
  77. api_shield-0.3.0/tests/core/rate_limit/test_models.py +177 -0
  78. api_shield-0.3.0/tests/core/rate_limit/test_policy_persistence.py +210 -0
  79. api_shield-0.3.0/tests/core/rate_limit/test_storage.py +262 -0
  80. {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_config.py +1 -1
  81. {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_engine.py +209 -0
  82. api_shield-0.3.0/tests/fastapi/__init__.py +0 -0
  83. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_acceptance.py +3 -1
  84. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_dependencies.py +1 -1
  85. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_deprecated.py +1 -1
  86. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_middleware.py +1 -1
  87. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_openapi.py +1 -1
  88. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_openapi_maintenance.py +1 -1
  89. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_parameterized_routes.py +1 -1
  90. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_plain_router.py +1 -1
  91. api_shield-0.3.0/tests/fastapi/test_rate_limit.py +286 -0
  92. {api_shield-0.2.0 → api_shield-0.3.0}/tests/test_cli.py +82 -0
  93. {api_shield-0.2.0 → api_shield-0.3.0}/uv.lock +110 -1
  94. api_shield-0.3.0/version-changes.txt +3 -0
  95. api_shield-0.2.0/PR_DESCRIPTION.md +0 -15
  96. api_shield-0.2.0/README.md +0 -100
  97. api_shield-0.2.0/docs/PR_DESCRIPTION.md +0 -17
  98. api_shield-0.2.0/docs/adapters/custom.md +0 -230
  99. api_shield-0.2.0/docs/adapters/fastapi.md +0 -244
  100. api_shield-0.2.0/docs/changelog.md +0 -81
  101. api_shield-0.2.0/docs/reference/backends.md +0 -172
  102. api_shield-0.2.0/docs/reference/cli.md +0 -231
  103. api_shield-0.2.0/docs/reference/decorators.md +0 -218
  104. api_shield-0.2.0/docs/reference/engine.md +0 -301
  105. api_shield-0.2.0/docs/reference/exceptions.md +0 -91
  106. api_shield-0.2.0/docs/reference/middleware.md +0 -121
  107. api_shield-0.2.0/docs/reference/models.md +0 -137
  108. api_shield-0.2.0/docs/tutorial/cli.md +0 -203
  109. api_shield-0.2.0/issues_to_resolve.txt +0 -20
  110. {api_shield-0.2.0 → api_shield-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  111. {api_shield-0.2.0 → api_shield-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  112. {api_shield-0.2.0 → api_shield-0.3.0}/.github/pull_request_template.md +0 -0
  113. {api_shield-0.2.0 → api_shield-0.3.0}/.github/workflows/ci.yml +0 -0
  114. {api_shield-0.2.0 → api_shield-0.3.0}/.github/workflows/docs.yml +0 -0
  115. {api_shield-0.2.0 → api_shield-0.3.0}/.gitignore +0 -0
  116. {api_shield-0.2.0 → api_shield-0.3.0}/.pre-commit-config.yaml +0 -0
  117. {api_shield-0.2.0 → api_shield-0.3.0}/CONTRIBUTING.md +0 -0
  118. {api_shield-0.2.0 → api_shield-0.3.0}/LICENSE +0 -0
  119. {api_shield-0.2.0 → api_shield-0.3.0}/ROADMAP.md +0 -0
  120. {api_shield-0.2.0 → api_shield-0.3.0}/SECURITY.md +0 -0
  121. {api_shield-0.2.0 → api_shield-0.3.0}/api-shield-logo.svg +0 -0
  122. {api_shield-0.2.0 → api_shield-0.3.0}/docs/assets/logo-full.svg +0 -0
  123. {api_shield-0.2.0 → api_shield-0.3.0}/docs/assets/logo.svg +0 -0
  124. {api_shield-0.2.0 → api_shield-0.3.0}/examples/__init__.py +0 -0
  125. {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/__init__.py +0 -0
  126. {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/custom_backend/__init__.py +0 -0
  127. {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/custom_backend/sqlite_backend.py +0 -0
  128. {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/global_maintenance.py +0 -0
  129. {api_shield-0.2.0 → api_shield-0.3.0}/examples/fastapi/scheduled_maintenance.py +0 -0
  130. {api_shield-0.2.0 → api_shield-0.3.0}/shield/__init__.py +0 -0
  131. {api_shield-0.2.0 → api_shield-0.3.0}/shield/admin/__init__.py +0 -0
  132. {api_shield-0.2.0 → api_shield-0.3.0}/shield/admin/auth.py +0 -0
  133. {api_shield-0.2.0 → api_shield-0.3.0}/shield/cli/__init__.py +0 -0
  134. {api_shield-0.2.0 → api_shield-0.3.0}/shield/cli/config.py +0 -0
  135. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/__init__.py +0 -0
  136. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/backends/__init__.py +0 -0
  137. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/scheduler.py +0 -0
  138. {api_shield-0.2.0 → api_shield-0.3.0}/shield/core/webhooks.py +0 -0
  139. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/__init__.py +0 -0
  140. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/auth.py +0 -0
  141. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/audit_rows.html +0 -0
  142. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/global_maintenance.html +0 -0
  143. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/modal.html +0 -0
  144. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/modal_global_disable.html +0 -0
  145. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/modal_global_enable.html +0 -0
  146. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/route_row.html +0 -0
  147. {api_shield-0.2.0 → api_shield-0.3.0}/shield/dashboard/templates/partials/routes_table.html +0 -0
  148. {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/dependencies.py +0 -0
  149. {api_shield-0.2.0 → api_shield-0.3.0}/shield/fastapi/openapi.py +0 -0
  150. {api_shield-0.2.0 → api_shield-0.3.0}/tests/__init__.py +0 -0
  151. {api_shield-0.2.0 → api_shield-0.3.0}/tests/admin/__init__.py +0 -0
  152. {api_shield-0.2.0 → api_shield-0.3.0}/tests/admin/test_auth.py +0 -0
  153. {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/__init__.py +0 -0
  154. {api_shield-0.2.0/tests/dashboard → api_shield-0.3.0/tests/core/rate_limit}/__init__.py +0 -0
  155. {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_backends.py +0 -0
  156. {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_exceptions.py +0 -0
  157. {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_models.py +0 -0
  158. {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_scheduler.py +0 -0
  159. {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_scheduler_polling.py +0 -0
  160. {api_shield-0.2.0 → api_shield-0.3.0}/tests/core/test_webhooks.py +0 -0
  161. {api_shield-0.2.0/tests/fastapi → api_shield-0.3.0/tests/dashboard}/__init__.py +0 -0
  162. {api_shield-0.2.0 → api_shield-0.3.0}/tests/dashboard/test_routes.py +0 -0
  163. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_decorators.py +0 -0
  164. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_global_maintenance.py +0 -0
  165. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_global_maintenance_docs.py +0 -0
  166. {api_shield-0.2.0 → api_shield-0.3.0}/tests/fastapi/test_router.py +0 -0
  167. {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.2.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)