reqkey 0.1.0__py3-none-any.whl
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.
- reqkey/__init__.py +27 -0
- reqkey/_middleware.py +132 -0
- reqkey/asgi.py +612 -0
- reqkey/client.py +469 -0
- reqkey/django.py +628 -0
- reqkey/exceptions.py +42 -0
- reqkey/fastapi.py +5 -0
- reqkey/flask.py +5 -0
- reqkey/models.py +45 -0
- reqkey/py.typed +1 -0
- reqkey/wsgi.py +546 -0
- reqkey-0.1.0.dist-info/METADATA +679 -0
- reqkey-0.1.0.dist-info/RECORD +15 -0
- reqkey-0.1.0.dist-info/WHEEL +4 -0
- reqkey-0.1.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reqkey
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The official Python SDK for ReqKey API key validation and usage analytics
|
|
5
|
+
Project-URL: Homepage, https://reqkey.com
|
|
6
|
+
Project-URL: Documentation, https://reqkey.com/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/Req-Key/reqkey-python
|
|
8
|
+
Project-URL: Issues, https://github.com/Req-Key/reqkey-python/issues
|
|
9
|
+
Author-email: ReqKey <support@reqkey.com>
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 ReqKey
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Keywords: api-keys,authentication,fastapi,rate-limiting,usage-based-billing
|
|
34
|
+
Classifier: Development Status :: 3 - Alpha
|
|
35
|
+
Classifier: Framework :: Django
|
|
36
|
+
Classifier: Framework :: FastAPI
|
|
37
|
+
Classifier: Framework :: Flask
|
|
38
|
+
Classifier: Intended Audience :: Developers
|
|
39
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
40
|
+
Classifier: Programming Language :: Python :: 3
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
44
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
45
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
46
|
+
Classifier: Typing :: Typed
|
|
47
|
+
Requires-Python: >=3.10
|
|
48
|
+
Requires-Dist: httpx<1,>=0.27
|
|
49
|
+
Provides-Extra: all
|
|
50
|
+
Requires-Dist: django<6.1,>=5.2; extra == 'all'
|
|
51
|
+
Requires-Dist: fastapi<1,>=0.110; extra == 'all'
|
|
52
|
+
Requires-Dist: flask<4,>=3; extra == 'all'
|
|
53
|
+
Provides-Extra: asgi
|
|
54
|
+
Requires-Dist: starlette<1,>=0.37; extra == 'asgi'
|
|
55
|
+
Provides-Extra: dev
|
|
56
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
57
|
+
Requires-Dist: django-stubs<7,>=6.0.7; extra == 'dev'
|
|
58
|
+
Requires-Dist: django<6.1,>=5.2; extra == 'dev'
|
|
59
|
+
Requires-Dist: fastapi<1,>=0.110; extra == 'dev'
|
|
60
|
+
Requires-Dist: flask<4,>=3; extra == 'dev'
|
|
61
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
62
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
63
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
64
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
65
|
+
Provides-Extra: django
|
|
66
|
+
Requires-Dist: django<6.1,>=5.2; extra == 'django'
|
|
67
|
+
Provides-Extra: fastapi
|
|
68
|
+
Requires-Dist: fastapi<1,>=0.110; extra == 'fastapi'
|
|
69
|
+
Provides-Extra: flask
|
|
70
|
+
Requires-Dist: flask<4,>=3; extra == 'flask'
|
|
71
|
+
Provides-Extra: wsgi
|
|
72
|
+
Description-Content-Type: text/markdown
|
|
73
|
+
|
|
74
|
+
# ReqKey Python SDK
|
|
75
|
+
|
|
76
|
+
The official Python SDK for API key validation, credit metering, consumer rate
|
|
77
|
+
limits, and correlated API traffic analytics with ReqKey.
|
|
78
|
+
|
|
79
|
+
One distribution contains the shared sync/async client and optional framework
|
|
80
|
+
adapters. FastAPI, Starlette, Django, Flask, and generic ASGI/WSGI applications
|
|
81
|
+
can all use ready-made middleware without implementing ReqKey request logic.
|
|
82
|
+
|
|
83
|
+
## Supported integrations
|
|
84
|
+
|
|
85
|
+
| Application | Install | Middleware import |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| FastAPI / Starlette | `reqkey[fastapi]` | `reqkey.fastapi.ReqKeyMiddleware` |
|
|
88
|
+
| Generic ASGI | `reqkey[asgi]` | `reqkey.asgi.ReqKeyMiddleware` |
|
|
89
|
+
| Django (sync or async) | `reqkey[django]` | `reqkey.django.ReqKeyMiddleware` |
|
|
90
|
+
| Flask | `reqkey[flask]` | `reqkey.flask.ReqKeyMiddleware` |
|
|
91
|
+
| Bottle / Pyramid / generic WSGI | `reqkey` or `reqkey[wsgi]` | `reqkey.wsgi.ReqKeyMiddleware` |
|
|
92
|
+
| Scripts, workers, and custom frameworks | `reqkey` | `reqkey.ReqKey` / `reqkey.AsyncReqKey` |
|
|
93
|
+
|
|
94
|
+
Framework dependencies are optional. Installing plain `reqkey` installs only
|
|
95
|
+
the universal client and its HTTP dependency; it does not install Django,
|
|
96
|
+
Flask, FastAPI, or Starlette.
|
|
97
|
+
|
|
98
|
+
This release provides ready-made middleware for incoming requests in the
|
|
99
|
+
frameworks listed above. It does not yet include native Tornado or AWS Lambda
|
|
100
|
+
adapters, nor automatic instrumentation of outgoing calls made with `requests`
|
|
101
|
+
or `httpx`. Those applications can still use the direct `ReqKey` and
|
|
102
|
+
`AsyncReqKey` clients, but framework-specific request extraction remains
|
|
103
|
+
application code until a dedicated adapter is added.
|
|
104
|
+
|
|
105
|
+
## Install
|
|
106
|
+
|
|
107
|
+
Core client:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install reqkey
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
FastAPI middleware:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pip install "reqkey[fastapi]"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Django middleware:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install "reqkey[django]"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Flask middleware:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pip install "reqkey[flask]"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Every supported framework integration:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
pip install "reqkey[all]"
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Complete FastAPI integration
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import os
|
|
141
|
+
|
|
142
|
+
from fastapi import FastAPI, Request
|
|
143
|
+
from reqkey.fastapi import ReqKeyMiddleware
|
|
144
|
+
|
|
145
|
+
app = FastAPI()
|
|
146
|
+
|
|
147
|
+
app.add_middleware(
|
|
148
|
+
ReqKeyMiddleware,
|
|
149
|
+
|
|
150
|
+
# ReqKey project
|
|
151
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
152
|
+
api_id="api_payments",
|
|
153
|
+
|
|
154
|
+
# "validate", "ingest", or "both"
|
|
155
|
+
mode="both",
|
|
156
|
+
enabled=True,
|
|
157
|
+
|
|
158
|
+
# Where your consumer sends their ReqKey-issued key
|
|
159
|
+
key_location="header",
|
|
160
|
+
key_name="X-StartupName-Key",
|
|
161
|
+
key_scheme="raw",
|
|
162
|
+
|
|
163
|
+
# Usage cost
|
|
164
|
+
credits=1,
|
|
165
|
+
|
|
166
|
+
# No validation or analytics on these routes
|
|
167
|
+
exclude_paths=("/health", "/docs", "/openapi.json", "/redoc", "/cron/*"),
|
|
168
|
+
|
|
169
|
+
# Privacy-safe analytics defaults
|
|
170
|
+
capture_query_params=False,
|
|
171
|
+
capture_request_headers=False,
|
|
172
|
+
capture_response_headers=False,
|
|
173
|
+
capture_response_body=False,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.get("/health")
|
|
178
|
+
async def health() -> dict[str, bool]:
|
|
179
|
+
return {"ok": True}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@app.post("/payments")
|
|
183
|
+
async def create_payment(request: Request) -> dict[str, object]:
|
|
184
|
+
decision = request.state.reqkey
|
|
185
|
+
return {
|
|
186
|
+
"created": True,
|
|
187
|
+
"credits_remaining": decision.credits_remaining,
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The application contains no direct `/key/validate` or `/ingest` requests. The
|
|
192
|
+
middleware owns that internal work.
|
|
193
|
+
|
|
194
|
+
## Complete Django integration
|
|
195
|
+
|
|
196
|
+
Set the project credential in the server environment:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
export REQKEY_PROJECT_KEY="your_project_key"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Then configure `settings.py`:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
REQKEY = {
|
|
206
|
+
"API_ID": "api_payments",
|
|
207
|
+
"MODE": "both",
|
|
208
|
+
"KEY_LOCATION": "header",
|
|
209
|
+
"KEY_NAME": "X-StartupName-Key",
|
|
210
|
+
"KEY_SCHEME": "raw",
|
|
211
|
+
"CREDITS": 1,
|
|
212
|
+
"EXCLUDE_PATHS": ("/health", "/admin/*", "/static/*"),
|
|
213
|
+
"CAPTURE_QUERY_PARAMS": False,
|
|
214
|
+
"CAPTURE_REQUEST_HEADERS": False,
|
|
215
|
+
"CAPTURE_RESPONSE_HEADERS": False,
|
|
216
|
+
"CAPTURE_RESPONSE_BODY": False,
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
MIDDLEWARE = [
|
|
220
|
+
"django.middleware.security.SecurityMiddleware",
|
|
221
|
+
"reqkey.django.ReqKeyMiddleware",
|
|
222
|
+
# The rest of your middleware...
|
|
223
|
+
]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The same middleware class detects whether Django supplied a synchronous or
|
|
227
|
+
asynchronous request chain and uses `ReqKey` or `AsyncReqKey` accordingly.
|
|
228
|
+
After successful validation, a Django view can read:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
def create_payment(request):
|
|
232
|
+
decision = request.reqkey
|
|
233
|
+
return JsonResponse(
|
|
234
|
+
{
|
|
235
|
+
"created": True,
|
|
236
|
+
"credits_remaining": decision.credits_remaining,
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
All direct middleware keyword options are available in Django's `REQKEY`
|
|
242
|
+
mapping using uppercase names. `PROJECT_KEY` may be specified there, but an
|
|
243
|
+
environment variable is recommended so credentials do not enter source
|
|
244
|
+
control.
|
|
245
|
+
|
|
246
|
+
## Complete Flask integration
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
import os
|
|
250
|
+
|
|
251
|
+
from flask import Flask, request
|
|
252
|
+
from reqkey.flask import ReqKeyMiddleware
|
|
253
|
+
|
|
254
|
+
app = Flask(__name__)
|
|
255
|
+
app.wsgi_app = ReqKeyMiddleware(
|
|
256
|
+
app.wsgi_app,
|
|
257
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
258
|
+
api_id="api_payments",
|
|
259
|
+
mode="both",
|
|
260
|
+
key_name="X-StartupName-Key",
|
|
261
|
+
exclude_paths=("/health", "/static/*"),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@app.post("/payments")
|
|
266
|
+
def create_payment():
|
|
267
|
+
decision = request.environ["reqkey.decision"]
|
|
268
|
+
return {
|
|
269
|
+
"created": True,
|
|
270
|
+
"credits_remaining": decision.credits_remaining,
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Flask remains the application object; only its `wsgi_app` callable is wrapped.
|
|
275
|
+
The same `reqkey.wsgi.ReqKeyMiddleware` class works with Bottle, Pyramid, and
|
|
276
|
+
other PEP 3333 WSGI applications.
|
|
277
|
+
|
|
278
|
+
## Generic ASGI and WSGI
|
|
279
|
+
|
|
280
|
+
For any ASGI 3 application:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
import os
|
|
284
|
+
|
|
285
|
+
from reqkey.asgi import ReqKeyMiddleware
|
|
286
|
+
|
|
287
|
+
application = ReqKeyMiddleware(
|
|
288
|
+
application,
|
|
289
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
290
|
+
api_id="api_payments",
|
|
291
|
+
)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
For any WSGI application:
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
import os
|
|
298
|
+
|
|
299
|
+
from reqkey.wsgi import ReqKeyMiddleware
|
|
300
|
+
|
|
301
|
+
application = ReqKeyMiddleware(
|
|
302
|
+
application,
|
|
303
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
304
|
+
api_id="api_payments",
|
|
305
|
+
)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Request lifecycle
|
|
309
|
+
|
|
310
|
+
With `mode="both"`:
|
|
311
|
+
|
|
312
|
+
```text
|
|
313
|
+
Consumer request
|
|
314
|
+
→ extract consumer key
|
|
315
|
+
→ await /key/validate
|
|
316
|
+
→ denied: return 401 / 402 / 403 / 429
|
|
317
|
+
→ approved: run the FastAPI endpoint
|
|
318
|
+
→ collect response metadata
|
|
319
|
+
→ await /ingest with the validation requestId
|
|
320
|
+
→ release the response to the consumer
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
The framework adapters keep ingestion inside the request lifecycle instead of
|
|
324
|
+
starting an unreliable background task:
|
|
325
|
+
|
|
326
|
+
- ASGI awaits ingestion before sending the final response-completion message.
|
|
327
|
+
- WSGI ingests when the response iterable finishes, without buffering the full
|
|
328
|
+
response.
|
|
329
|
+
- Django's native middleware ingests before returning the response object.
|
|
330
|
+
|
|
331
|
+
Streaming responses are forwarded chunk by chunk. The middleware keeps only
|
|
332
|
+
enough data to reconstruct the first 1,000 response-body characters and holds
|
|
333
|
+
the final ASGI completion message until ingestion finishes. It does not retain
|
|
334
|
+
the complete stream in memory. An indefinitely open stream cannot produce a
|
|
335
|
+
completed analytics event until that stream eventually closes.
|
|
336
|
+
|
|
337
|
+
Django streaming responses are never consumed or buffered by the native
|
|
338
|
+
middleware. Their status and headers can be recorded, but response-body capture
|
|
339
|
+
is omitted because Django requires middleware to preserve the streaming
|
|
340
|
+
iterator.
|
|
341
|
+
|
|
342
|
+
## Configuration
|
|
343
|
+
|
|
344
|
+
| Input | Default | Purpose |
|
|
345
|
+
|---|---:|---|
|
|
346
|
+
| `project_key` | required | Project credential sent to ReqKey as the Bearer token. |
|
|
347
|
+
| `root_key` | — | Backward-compatible alias for `project_key`; never pass both. |
|
|
348
|
+
| `api_id` | required | ReqKey API being protected or observed. |
|
|
349
|
+
| `mode` | `"both"` | `"validate"`, `"ingest"`, or `"both"`. |
|
|
350
|
+
| `enabled` | `True` | Bypass the integration entirely when false. |
|
|
351
|
+
| `key_location` | `"header"` | `"header"`, `"query"`, or `"cookie"`. |
|
|
352
|
+
| `key_name` | `"X-API-Key"` | Customer-facing header, query parameter, or cookie name. |
|
|
353
|
+
| `key_scheme` | `"raw"` | `"raw"` or `"bearer"` for headers. |
|
|
354
|
+
| `credits` | `1` | Static cost or sync/async cost resolver. |
|
|
355
|
+
| `exclude_paths` | empty | Exact paths or trailing-`*` prefix patterns. |
|
|
356
|
+
| `skip_methods` | `("OPTIONS",)` | Methods that bypass ReqKey. |
|
|
357
|
+
| `failure_mode` | `"closed"` | Deny or allow when ReqKey itself is unavailable. |
|
|
358
|
+
| `error_messages` | built in | Override customer-facing denial messages. |
|
|
359
|
+
| `timeout` | `2.0` | Timeout for each ReqKey operation. |
|
|
360
|
+
|
|
361
|
+
`REQKEY_PROJECT_KEY` is the preferred environment-variable name.
|
|
362
|
+
`REQKEY_ROOT_KEY` remains supported by `ReqKey.from_env()` and
|
|
363
|
+
`AsyncReqKey.from_env()`.
|
|
364
|
+
|
|
365
|
+
The project credential must remain server-side. Do not expose it in browser
|
|
366
|
+
code or commit it to source control.
|
|
367
|
+
|
|
368
|
+
For a local-development switch, pass a boolean without changing the middleware
|
|
369
|
+
layout:
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
enabled=os.getenv("ENABLE_REQKEY", "true").lower() == "true"
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Choose validation, analytics, or both
|
|
376
|
+
|
|
377
|
+
Validation only:
|
|
378
|
+
|
|
379
|
+
```python
|
|
380
|
+
app.add_middleware(
|
|
381
|
+
ReqKeyMiddleware,
|
|
382
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
383
|
+
api_id="api_payments",
|
|
384
|
+
mode="validate",
|
|
385
|
+
)
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Traffic analytics only:
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
app.add_middleware(
|
|
392
|
+
ReqKeyMiddleware,
|
|
393
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
394
|
+
api_id="api_payments",
|
|
395
|
+
mode="ingest",
|
|
396
|
+
)
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
In ingest-only mode, the endpoint runs without ReqKey authentication and the
|
|
400
|
+
event is associated with `api_id`. If another middleware already performed
|
|
401
|
+
validation, provide its request ID for consumer correlation:
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
app.add_middleware(
|
|
405
|
+
ReqKeyMiddleware,
|
|
406
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
407
|
+
api_id="api_payments",
|
|
408
|
+
mode="ingest",
|
|
409
|
+
request_id_resolver=lambda request: getattr(
|
|
410
|
+
request.state,
|
|
411
|
+
"reqkey_request_id",
|
|
412
|
+
None,
|
|
413
|
+
),
|
|
414
|
+
)
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Validation plus correlated analytics:
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
app.add_middleware(
|
|
421
|
+
ReqKeyMiddleware,
|
|
422
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
423
|
+
api_id="api_payments",
|
|
424
|
+
mode="both",
|
|
425
|
+
)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## Choose where the consumer key comes from
|
|
429
|
+
|
|
430
|
+
Custom header, recommended:
|
|
431
|
+
|
|
432
|
+
```python
|
|
433
|
+
key_location="header"
|
|
434
|
+
key_name="X-StartupName-Key"
|
|
435
|
+
key_scheme="raw"
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Authorization Bearer token:
|
|
439
|
+
|
|
440
|
+
```python
|
|
441
|
+
key_location="header"
|
|
442
|
+
key_name="Authorization"
|
|
443
|
+
key_scheme="bearer"
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Query parameter:
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
key_location="query"
|
|
450
|
+
key_name="api_key"
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
Query parameters are supported for compatibility, but headers are recommended
|
|
454
|
+
because URLs are often retained in browser history, access logs, and proxy logs.
|
|
455
|
+
|
|
456
|
+
Cookie:
|
|
457
|
+
|
|
458
|
+
```python
|
|
459
|
+
key_location="cookie"
|
|
460
|
+
key_name="startup_key"
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
A custom sync or async extractor can handle another source:
|
|
464
|
+
|
|
465
|
+
```python
|
|
466
|
+
async def get_consumer_key(request: Request) -> str | None:
|
|
467
|
+
return request.headers.get("X-Custom-Key")
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
app.add_middleware(
|
|
471
|
+
ReqKeyMiddleware,
|
|
472
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
473
|
+
api_id="api_payments",
|
|
474
|
+
get_consumer_key=get_consumer_key,
|
|
475
|
+
)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Avoid reading an API key from the request body. Authentication middleware runs
|
|
479
|
+
before the endpoint and consuming the body can interfere with downstream body
|
|
480
|
+
parsing.
|
|
481
|
+
|
|
482
|
+
## Exclude or select endpoints
|
|
483
|
+
|
|
484
|
+
Exact and prefix exclusions:
|
|
485
|
+
|
|
486
|
+
```python
|
|
487
|
+
exclude_paths=(
|
|
488
|
+
"/health",
|
|
489
|
+
"/openapi.json",
|
|
490
|
+
"/docs/*",
|
|
491
|
+
"/cron/*",
|
|
492
|
+
)
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
For complete control, use a sync or async resolver:
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
def should_protect(request: Request) -> bool:
|
|
499
|
+
return request.url.path.startswith("/api/")
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
app.add_middleware(
|
|
503
|
+
ReqKeyMiddleware,
|
|
504
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
505
|
+
api_id="api_payments",
|
|
506
|
+
should_protect=should_protect,
|
|
507
|
+
)
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
The same selection controls validation and ingestion. Excluded traffic is not
|
|
511
|
+
validated, charged, or recorded.
|
|
512
|
+
|
|
513
|
+
## Dynamic credit costs
|
|
514
|
+
|
|
515
|
+
```python
|
|
516
|
+
def credits_for(request: Request) -> int:
|
|
517
|
+
if request.method == "POST" and request.url.path == "/images":
|
|
518
|
+
return 5
|
|
519
|
+
if request.url.path.startswith("/reports/"):
|
|
520
|
+
return 2
|
|
521
|
+
return 1
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
app.add_middleware(
|
|
525
|
+
ReqKeyMiddleware,
|
|
526
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
527
|
+
api_id="api_payments",
|
|
528
|
+
credits=credits_for,
|
|
529
|
+
)
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
## Analytics capture
|
|
533
|
+
|
|
534
|
+
Metadata sent by default:
|
|
535
|
+
|
|
536
|
+
- `requestId` when validation produced one
|
|
537
|
+
- `apiId`
|
|
538
|
+
- method and normalized endpoint path
|
|
539
|
+
- response status
|
|
540
|
+
- endpoint processing latency
|
|
541
|
+
- user agent
|
|
542
|
+
|
|
543
|
+
Additional data is opt-in:
|
|
544
|
+
|
|
545
|
+
```python
|
|
546
|
+
app.add_middleware(
|
|
547
|
+
ReqKeyMiddleware,
|
|
548
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
549
|
+
api_id="api_payments",
|
|
550
|
+
mode="both",
|
|
551
|
+
capture_query_params=True,
|
|
552
|
+
capture_request_headers=True,
|
|
553
|
+
capture_response_headers=True,
|
|
554
|
+
capture_response_body=True,
|
|
555
|
+
capture_client_ip=True,
|
|
556
|
+
excluded_headers=(
|
|
557
|
+
"X-RapidAPI-Proxy-Secret",
|
|
558
|
+
"X-Vercel-OIDC-Token",
|
|
559
|
+
),
|
|
560
|
+
)
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Authorization, cookies, `Set-Cookie`, common API-key headers, and the configured
|
|
564
|
+
consumer-key header are always removed from captured headers. Response-body
|
|
565
|
+
capture is limited to textual content and a hard maximum of 1,000 characters.
|
|
566
|
+
The limit is enforced again by the HTTP client before `/ingest` is called.
|
|
567
|
+
|
|
568
|
+
## Custom denial messages
|
|
569
|
+
|
|
570
|
+
```python
|
|
571
|
+
app.add_middleware(
|
|
572
|
+
ReqKeyMiddleware,
|
|
573
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
574
|
+
api_id="api_payments",
|
|
575
|
+
error_messages={
|
|
576
|
+
"missing_api_key": "Send your key in X-StartupName-Key.",
|
|
577
|
+
"invalid_api_key": "That key is invalid or disabled.",
|
|
578
|
+
"insufficient_credits": "Your account has no credits remaining.",
|
|
579
|
+
"access_denied": "This key cannot access the requested API.",
|
|
580
|
+
"rate_limited": "Too many requests. Please retry shortly.",
|
|
581
|
+
"reqkey_unavailable": "Authentication is temporarily unavailable.",
|
|
582
|
+
},
|
|
583
|
+
)
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
The middleware returns stable error identifiers alongside the messages and
|
|
587
|
+
preserves `Retry-After` for rate-limited decisions.
|
|
588
|
+
|
|
589
|
+
## Request state and response headers
|
|
590
|
+
|
|
591
|
+
After successful validation, FastAPI and Starlette routes can access:
|
|
592
|
+
|
|
593
|
+
```python
|
|
594
|
+
request.state.reqkey
|
|
595
|
+
request.state.reqkey_request_id
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Django views receive the same values as request attributes:
|
|
599
|
+
|
|
600
|
+
```python
|
|
601
|
+
request.reqkey
|
|
602
|
+
request.reqkey_request_id
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
WSGI applications, including Flask, receive namespaced environment values:
|
|
606
|
+
|
|
607
|
+
```python
|
|
608
|
+
request.environ["reqkey.decision"]
|
|
609
|
+
request.environ["reqkey.request_id"]
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
The response includes these values when available:
|
|
613
|
+
|
|
614
|
+
- `X-ReqKey-Request-ID`
|
|
615
|
+
- `X-ReqKey-Credits-Limit`
|
|
616
|
+
- `X-ReqKey-Credits-Remaining`
|
|
617
|
+
- `X-ReqKey-Validation-Time-Ms`
|
|
618
|
+
|
|
619
|
+
If browser JavaScript needs these headers, add them to the FastAPI CORS
|
|
620
|
+
middleware's `expose_headers` list.
|
|
621
|
+
|
|
622
|
+
## Availability behavior
|
|
623
|
+
|
|
624
|
+
Validation fails closed by default. When ReqKey times out or cannot be reached,
|
|
625
|
+
the middleware returns `503` without running the endpoint.
|
|
626
|
+
|
|
627
|
+
```python
|
|
628
|
+
failure_mode="open"
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
Fail-open applies only to ReqKey transport/service errors. Explicitly invalid,
|
|
632
|
+
disabled, exhausted, forbidden, and rate-limited keys are always denied. In
|
|
633
|
+
`mode="both"`, fail-open requests can still produce an API-level traffic event,
|
|
634
|
+
but it will not be correlated to a validated consumer.
|
|
635
|
+
|
|
636
|
+
The SDK does not automatically retry `/key/validate`, because validation can
|
|
637
|
+
deduct credits. Safe automatic retries require server-side idempotency.
|
|
638
|
+
|
|
639
|
+
## Direct sync and async clients
|
|
640
|
+
|
|
641
|
+
Async:
|
|
642
|
+
|
|
643
|
+
```python
|
|
644
|
+
from reqkey import AsyncReqKey
|
|
645
|
+
|
|
646
|
+
async with AsyncReqKey(project_key=os.environ["REQKEY_PROJECT_KEY"]) as reqkey:
|
|
647
|
+
decision = await reqkey.verify(
|
|
648
|
+
"consumer_key_...",
|
|
649
|
+
api_id="api_payments",
|
|
650
|
+
credits=1,
|
|
651
|
+
resource="/payments",
|
|
652
|
+
)
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Synchronous:
|
|
656
|
+
|
|
657
|
+
```python
|
|
658
|
+
from reqkey import ReqKey
|
|
659
|
+
|
|
660
|
+
with ReqKey(project_key=os.environ["REQKEY_PROJECT_KEY"]) as reqkey:
|
|
661
|
+
decision = reqkey.verify("consumer_key_...", api_id="api_payments")
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
ASGI and FastAPI use the asynchronous client and await both network operations.
|
|
665
|
+
WSGI uses the synchronous client. Django automatically selects the correct
|
|
666
|
+
client based on its middleware chain, avoiding blocking HTTP calls inside an
|
|
667
|
+
asynchronous Django deployment.
|
|
668
|
+
|
|
669
|
+
## Development
|
|
670
|
+
|
|
671
|
+
```bash
|
|
672
|
+
python -m venv .venv
|
|
673
|
+
. .venv/bin/activate
|
|
674
|
+
pip install -e ".[dev]"
|
|
675
|
+
ruff check .
|
|
676
|
+
mypy src/reqkey
|
|
677
|
+
pytest
|
|
678
|
+
python -m build
|
|
679
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
reqkey/__init__.py,sha256=YzNF3bU-WKMtBEwNXWMGqouH9WAGkaTmU7GZWTkPKF4,601
|
|
2
|
+
reqkey/_middleware.py,sha256=ytBYYjHzEarvCpUkOFTOOS69WAwv9xA0qIxxIRwKQhM,4620
|
|
3
|
+
reqkey/asgi.py,sha256=HIKr-J6ajG8fDrjRWnY1DhENACQ01MMy5E3GdCGcvk8,23207
|
|
4
|
+
reqkey/client.py,sha256=iGaPS2BHrGPxWRp3t0ExDJVqH5GQQiyhatMrID2uHNI,16662
|
|
5
|
+
reqkey/django.py,sha256=WpqK2rWCEzqSP4Fm4fhzechz8JSw41_stI9mCKXWIzA,24331
|
|
6
|
+
reqkey/exceptions.py,sha256=ate0koxfy0_Em-a_-T1naNIqHVzV4psHXcbp1SZAqd4,1055
|
|
7
|
+
reqkey/fastapi.py,sha256=qiVEMp0yW1E_I6o-owPp7Jm5OI-pEW7BTZhXqlSe6K4,176
|
|
8
|
+
reqkey/flask.py,sha256=CjQXRXphK7EWe79uhhnnqsLICNJ6ZRmYRGYe-qWFuBA,178
|
|
9
|
+
reqkey/models.py,sha256=YCh5TibeuJpKjs1WaD8PzvRjZdiVvMP4K7b0Ci9nbps,1218
|
|
10
|
+
reqkey/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
11
|
+
reqkey/wsgi.py,sha256=qYrDJ3RYWZHMdsTosNnd3opOVCzqMud0xHyqUL9eHDo,19662
|
|
12
|
+
reqkey-0.1.0.dist-info/METADATA,sha256=136QjWO2jv5fk-PNZlyNiXtptd2dGPhlmLi4uD7AEnY,19053
|
|
13
|
+
reqkey-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
14
|
+
reqkey-0.1.0.dist-info/licenses/LICENSE,sha256=bqplxNJLbhM3cn-TDm1LL73gTUdjBdNkot8KYUvzXkk,1064
|
|
15
|
+
reqkey-0.1.0.dist-info/RECORD,,
|