reqkey 0.1.0__tar.gz → 0.2.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.
- {reqkey-0.1.0 → reqkey-0.2.0}/PKG-INFO +123 -4
- {reqkey-0.1.0 → reqkey-0.2.0}/README.md +122 -3
- {reqkey-0.1.0 → reqkey-0.2.0}/pyproject.toml +1 -1
- {reqkey-0.1.0 → reqkey-0.2.0}/src/reqkey/__init__.py +3 -2
- {reqkey-0.1.0 → reqkey-0.2.0}/src/reqkey/_middleware.py +66 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/src/reqkey/asgi.py +131 -7
- {reqkey-0.1.0 → reqkey-0.2.0}/src/reqkey/client.py +7 -1
- {reqkey-0.1.0 → reqkey-0.2.0}/src/reqkey/django.py +227 -11
- {reqkey-0.1.0 → reqkey-0.2.0}/src/reqkey/models.py +19 -2
- {reqkey-0.1.0 → reqkey-0.2.0}/src/reqkey/wsgi.py +129 -12
- {reqkey-0.1.0 → reqkey-0.2.0}/tests/test_client.py +2 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/tests/test_django.py +179 -4
- {reqkey-0.1.0 → reqkey-0.2.0}/tests/test_fastapi.py +89 -2
- reqkey-0.2.0/tests/test_middleware.py +35 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/tests/test_wsgi.py +122 -3
- {reqkey-0.1.0 → reqkey-0.2.0}/.github/workflows/ci.yml +0 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/.github/workflows/release.yml +0 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/.gitignore +0 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/LICENSE +0 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/examples/django_settings.py +0 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/examples/fastapi_app.py +0 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/examples/flask_app.py +0 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/src/reqkey/exceptions.py +0 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/src/reqkey/fastapi.py +0 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/src/reqkey/flask.py +0 -0
- {reqkey-0.1.0 → reqkey-0.2.0}/src/reqkey/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reqkey
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: The official Python SDK for ReqKey API key validation and usage analytics
|
|
5
5
|
Project-URL: Homepage, https://reqkey.com
|
|
6
6
|
Project-URL: Documentation, https://reqkey.com/docs
|
|
@@ -76,6 +76,9 @@ Description-Content-Type: text/markdown
|
|
|
76
76
|
The official Python SDK for API key validation, credit metering, consumer rate
|
|
77
77
|
limits, and correlated API traffic analytics with ReqKey.
|
|
78
78
|
|
|
79
|
+
**Website:** [reqkey.com](https://reqkey.com) ·
|
|
80
|
+
**Documentation:** [reqkey.com/docs](https://reqkey.com/docs)
|
|
81
|
+
|
|
79
82
|
One distribution contains the shared sync/async client and optional framework
|
|
80
83
|
adapters. FastAPI, Starlette, Django, Flask, and generic ASGI/WSGI applications
|
|
81
84
|
can all use ready-made middleware without implementing ReqKey request logic.
|
|
@@ -313,7 +316,7 @@ With `mode="both"`:
|
|
|
313
316
|
Consumer request
|
|
314
317
|
→ extract consumer key
|
|
315
318
|
→ await /key/validate
|
|
316
|
-
→ denied: return 401 / 402 / 403 / 429
|
|
319
|
+
→ denied: await /ingest, then return 401 / 402 / 403 / 429
|
|
317
320
|
→ approved: run the FastAPI endpoint
|
|
318
321
|
→ collect response metadata
|
|
319
322
|
→ await /ingest with the validation requestId
|
|
@@ -328,6 +331,11 @@ starting an unreliable background task:
|
|
|
328
331
|
response.
|
|
329
332
|
- Django's native middleware ingests before returning the response object.
|
|
330
333
|
|
|
334
|
+
Denied requests are recorded by default in `mode="both"`, including requests
|
|
335
|
+
with a missing key. This makes invalid-key attempts, exhausted consumers, and
|
|
336
|
+
rate-limit hammering visible in analytics. Set `ingest_denied_requests=False`
|
|
337
|
+
if you deliberately do not want those events or their ingestion cost.
|
|
338
|
+
|
|
331
339
|
Streaming responses are forwarded chunk by chunk. The middleware keeps only
|
|
332
340
|
enough data to reconstruct the first 1,000 response-body characters and holds
|
|
333
341
|
the final ASGI completion message until ingestion finishes. It does not retain
|
|
@@ -354,6 +362,10 @@ iterator.
|
|
|
354
362
|
| `credits` | `1` | Static cost or sync/async cost resolver. |
|
|
355
363
|
| `exclude_paths` | empty | Exact paths or trailing-`*` prefix patterns. |
|
|
356
364
|
| `skip_methods` | `("OPTIONS",)` | Methods that bypass ReqKey. |
|
|
365
|
+
| `consumer_name_resolver` | — | Resolve a display name from a request header, query parameter, or another trusted source. |
|
|
366
|
+
| `client_ip_resolver` | automatic | Override client-IP extraction for a trusted proxy setup. |
|
|
367
|
+
| `on_error` | — | Receive validation or ingestion service failures without exposing request secrets. |
|
|
368
|
+
| `ingest_denied_requests` | `True` | Record denied traffic in `mode="both"`. |
|
|
357
369
|
| `failure_mode` | `"closed"` | Deny or allow when ReqKey itself is unavailable. |
|
|
358
370
|
| `error_messages` | built in | Override customer-facing denial messages. |
|
|
359
371
|
| `timeout` | `2.0` | Timeout for each ReqKey operation. |
|
|
@@ -539,6 +551,7 @@ Metadata sent by default:
|
|
|
539
551
|
- response status
|
|
540
552
|
- endpoint processing latency
|
|
541
553
|
- user agent
|
|
554
|
+
- `consumerName` when a resolver supplies one
|
|
542
555
|
|
|
543
556
|
Additional data is opt-in:
|
|
544
557
|
|
|
@@ -565,6 +578,60 @@ consumer-key header are always removed from captured headers. Response-body
|
|
|
565
578
|
capture is limited to textual content and a hard maximum of 1,000 characters.
|
|
566
579
|
The limit is enforced again by the HTTP client before `/ingest` is called.
|
|
567
580
|
|
|
581
|
+
### Consumer names
|
|
582
|
+
|
|
583
|
+
Use `consumer_name_resolver` when an upstream gateway or your application
|
|
584
|
+
provides a useful customer label. The resolver may read any part of the
|
|
585
|
+
framework request and may be synchronous or asynchronous in ASGI/Django:
|
|
586
|
+
|
|
587
|
+
```python
|
|
588
|
+
def consumer_name(request: Request) -> str | None:
|
|
589
|
+
return (
|
|
590
|
+
request.headers.get("X-RapidAPI-User")
|
|
591
|
+
or request.query_params.get("consumerName")
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
app.add_middleware(
|
|
596
|
+
ReqKeyMiddleware,
|
|
597
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
598
|
+
api_id="api_payments",
|
|
599
|
+
consumer_name_resolver=consumer_name,
|
|
600
|
+
)
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
The resolved value is sent as analytics metadata. It is not used as the
|
|
604
|
+
consumer API key and does not change authorization.
|
|
605
|
+
|
|
606
|
+
For Django, set `CONSUMER_NAME_RESOLVER` in `REQKEY`. For WSGI, the resolver
|
|
607
|
+
receives the WSGI environment, so a header can be read as
|
|
608
|
+
`environ.get("HTTP_X_RAPIDAPI_USER")`.
|
|
609
|
+
|
|
610
|
+
### Client IPs and trusted proxies
|
|
611
|
+
|
|
612
|
+
Client-IP capture is opt-in with `capture_client_ip=True`. By default, the SDK
|
|
613
|
+
keeps the framework-provided peer address first: `request.client.host` for
|
|
614
|
+
ASGI, `REMOTE_ADDR` for WSGI, and `request.META["REMOTE_ADDR"]` for Django. It
|
|
615
|
+
only falls back when that value is absent, checking common proxy headers such
|
|
616
|
+
as `X-Forwarded-For`, `X-Real-IP`, Cloudflare, Vercel, Fly, Fastly, Azure,
|
|
617
|
+
App Engine, CloudFront, and standardized `Forwarded` headers.
|
|
618
|
+
|
|
619
|
+
If a trusted proxy always supplies the real client in a specific header, use a
|
|
620
|
+
resolver to override that default explicitly:
|
|
621
|
+
|
|
622
|
+
```python
|
|
623
|
+
app.add_middleware(
|
|
624
|
+
ReqKeyMiddleware,
|
|
625
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
626
|
+
api_id="api_payments",
|
|
627
|
+
capture_client_ip=True,
|
|
628
|
+
client_ip_resolver=lambda request: request.headers.get("CF-Connecting-IP"),
|
|
629
|
+
)
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
Only trust forwarding headers when your deployment proxy overwrites them;
|
|
633
|
+
otherwise a caller can spoof their value.
|
|
634
|
+
|
|
568
635
|
## Custom denial messages
|
|
569
636
|
|
|
570
637
|
```python
|
|
@@ -616,8 +683,9 @@ The response includes these values when available:
|
|
|
616
683
|
- `X-ReqKey-Credits-Remaining`
|
|
617
684
|
- `X-ReqKey-Validation-Time-Ms`
|
|
618
685
|
|
|
619
|
-
|
|
620
|
-
|
|
686
|
+
No CORS configuration is needed for server-to-server clients or for the server
|
|
687
|
+
itself to set these headers. Only cross-origin browser JavaScript that must read
|
|
688
|
+
them needs the names added to the CORS middleware's `expose_headers` list.
|
|
621
689
|
|
|
622
690
|
## Availability behavior
|
|
623
691
|
|
|
@@ -636,6 +704,44 @@ but it will not be correlated to a validated consumer.
|
|
|
636
704
|
The SDK does not automatically retry `/key/validate`, because validation can
|
|
637
705
|
deduct credits. Safe automatic retries require server-side idempotency.
|
|
638
706
|
|
|
707
|
+
### Failure alerts
|
|
708
|
+
|
|
709
|
+
Webhook payload formats differ between Discord, Slack, Teams, and custom alert
|
|
710
|
+
receivers, so the SDK does not post one supposed universal webhook format.
|
|
711
|
+
Instead, `on_error` receives a provider-neutral `MiddlewareErrorEvent` for
|
|
712
|
+
ReqKey validation or ingestion failures:
|
|
713
|
+
|
|
714
|
+
```python
|
|
715
|
+
from reqkey import MiddlewareErrorEvent
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
async def report_reqkey_failure(event: MiddlewareErrorEvent) -> None:
|
|
719
|
+
await send_to_your_alert_provider(
|
|
720
|
+
{
|
|
721
|
+
"operation": event.operation,
|
|
722
|
+
"message": event.message,
|
|
723
|
+
"method": event.method,
|
|
724
|
+
"path": event.path,
|
|
725
|
+
"request_id": event.request_id,
|
|
726
|
+
"status_code": event.status_code,
|
|
727
|
+
}
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
app.add_middleware(
|
|
732
|
+
ReqKeyMiddleware,
|
|
733
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
734
|
+
api_id="api_payments",
|
|
735
|
+
on_error=report_reqkey_failure,
|
|
736
|
+
)
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
The event intentionally excludes API keys, project credentials, headers,
|
|
740
|
+
query parameters, and bodies. Exceptions raised by the callback are logged and
|
|
741
|
+
do not replace the application response. WSGI callbacks are synchronous;
|
|
742
|
+
ASGI callbacks may be synchronous or asynchronous, and Django follows the
|
|
743
|
+
sync/async mode of its middleware chain.
|
|
744
|
+
|
|
639
745
|
## Direct sync and async clients
|
|
640
746
|
|
|
641
747
|
Async:
|
|
@@ -661,6 +767,19 @@ with ReqKey(project_key=os.environ["REQKEY_PROJECT_KEY"]) as reqkey:
|
|
|
661
767
|
decision = reqkey.verify("consumer_key_...", api_id="api_payments")
|
|
662
768
|
```
|
|
663
769
|
|
|
770
|
+
Direct ingestion also accepts an optional consumer label:
|
|
771
|
+
|
|
772
|
+
```python
|
|
773
|
+
reqkey.ingest(
|
|
774
|
+
decision.request_id,
|
|
775
|
+
api_id="api_payments",
|
|
776
|
+
method="POST",
|
|
777
|
+
path="/payments",
|
|
778
|
+
status_code=201,
|
|
779
|
+
consumer_name="rapidapi-user",
|
|
780
|
+
)
|
|
781
|
+
```
|
|
782
|
+
|
|
664
783
|
ASGI and FastAPI use the asynchronous client and await both network operations.
|
|
665
784
|
WSGI uses the synchronous client. Django automatically selects the correct
|
|
666
785
|
client based on its middleware chain, avoiding blocking HTTP calls inside an
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
The official Python SDK for API key validation, credit metering, consumer rate
|
|
4
4
|
limits, and correlated API traffic analytics with ReqKey.
|
|
5
5
|
|
|
6
|
+
**Website:** [reqkey.com](https://reqkey.com) ·
|
|
7
|
+
**Documentation:** [reqkey.com/docs](https://reqkey.com/docs)
|
|
8
|
+
|
|
6
9
|
One distribution contains the shared sync/async client and optional framework
|
|
7
10
|
adapters. FastAPI, Starlette, Django, Flask, and generic ASGI/WSGI applications
|
|
8
11
|
can all use ready-made middleware without implementing ReqKey request logic.
|
|
@@ -240,7 +243,7 @@ With `mode="both"`:
|
|
|
240
243
|
Consumer request
|
|
241
244
|
→ extract consumer key
|
|
242
245
|
→ await /key/validate
|
|
243
|
-
→ denied: return 401 / 402 / 403 / 429
|
|
246
|
+
→ denied: await /ingest, then return 401 / 402 / 403 / 429
|
|
244
247
|
→ approved: run the FastAPI endpoint
|
|
245
248
|
→ collect response metadata
|
|
246
249
|
→ await /ingest with the validation requestId
|
|
@@ -255,6 +258,11 @@ starting an unreliable background task:
|
|
|
255
258
|
response.
|
|
256
259
|
- Django's native middleware ingests before returning the response object.
|
|
257
260
|
|
|
261
|
+
Denied requests are recorded by default in `mode="both"`, including requests
|
|
262
|
+
with a missing key. This makes invalid-key attempts, exhausted consumers, and
|
|
263
|
+
rate-limit hammering visible in analytics. Set `ingest_denied_requests=False`
|
|
264
|
+
if you deliberately do not want those events or their ingestion cost.
|
|
265
|
+
|
|
258
266
|
Streaming responses are forwarded chunk by chunk. The middleware keeps only
|
|
259
267
|
enough data to reconstruct the first 1,000 response-body characters and holds
|
|
260
268
|
the final ASGI completion message until ingestion finishes. It does not retain
|
|
@@ -281,6 +289,10 @@ iterator.
|
|
|
281
289
|
| `credits` | `1` | Static cost or sync/async cost resolver. |
|
|
282
290
|
| `exclude_paths` | empty | Exact paths or trailing-`*` prefix patterns. |
|
|
283
291
|
| `skip_methods` | `("OPTIONS",)` | Methods that bypass ReqKey. |
|
|
292
|
+
| `consumer_name_resolver` | — | Resolve a display name from a request header, query parameter, or another trusted source. |
|
|
293
|
+
| `client_ip_resolver` | automatic | Override client-IP extraction for a trusted proxy setup. |
|
|
294
|
+
| `on_error` | — | Receive validation or ingestion service failures without exposing request secrets. |
|
|
295
|
+
| `ingest_denied_requests` | `True` | Record denied traffic in `mode="both"`. |
|
|
284
296
|
| `failure_mode` | `"closed"` | Deny or allow when ReqKey itself is unavailable. |
|
|
285
297
|
| `error_messages` | built in | Override customer-facing denial messages. |
|
|
286
298
|
| `timeout` | `2.0` | Timeout for each ReqKey operation. |
|
|
@@ -466,6 +478,7 @@ Metadata sent by default:
|
|
|
466
478
|
- response status
|
|
467
479
|
- endpoint processing latency
|
|
468
480
|
- user agent
|
|
481
|
+
- `consumerName` when a resolver supplies one
|
|
469
482
|
|
|
470
483
|
Additional data is opt-in:
|
|
471
484
|
|
|
@@ -492,6 +505,60 @@ consumer-key header are always removed from captured headers. Response-body
|
|
|
492
505
|
capture is limited to textual content and a hard maximum of 1,000 characters.
|
|
493
506
|
The limit is enforced again by the HTTP client before `/ingest` is called.
|
|
494
507
|
|
|
508
|
+
### Consumer names
|
|
509
|
+
|
|
510
|
+
Use `consumer_name_resolver` when an upstream gateway or your application
|
|
511
|
+
provides a useful customer label. The resolver may read any part of the
|
|
512
|
+
framework request and may be synchronous or asynchronous in ASGI/Django:
|
|
513
|
+
|
|
514
|
+
```python
|
|
515
|
+
def consumer_name(request: Request) -> str | None:
|
|
516
|
+
return (
|
|
517
|
+
request.headers.get("X-RapidAPI-User")
|
|
518
|
+
or request.query_params.get("consumerName")
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
app.add_middleware(
|
|
523
|
+
ReqKeyMiddleware,
|
|
524
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
525
|
+
api_id="api_payments",
|
|
526
|
+
consumer_name_resolver=consumer_name,
|
|
527
|
+
)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
The resolved value is sent as analytics metadata. It is not used as the
|
|
531
|
+
consumer API key and does not change authorization.
|
|
532
|
+
|
|
533
|
+
For Django, set `CONSUMER_NAME_RESOLVER` in `REQKEY`. For WSGI, the resolver
|
|
534
|
+
receives the WSGI environment, so a header can be read as
|
|
535
|
+
`environ.get("HTTP_X_RAPIDAPI_USER")`.
|
|
536
|
+
|
|
537
|
+
### Client IPs and trusted proxies
|
|
538
|
+
|
|
539
|
+
Client-IP capture is opt-in with `capture_client_ip=True`. By default, the SDK
|
|
540
|
+
keeps the framework-provided peer address first: `request.client.host` for
|
|
541
|
+
ASGI, `REMOTE_ADDR` for WSGI, and `request.META["REMOTE_ADDR"]` for Django. It
|
|
542
|
+
only falls back when that value is absent, checking common proxy headers such
|
|
543
|
+
as `X-Forwarded-For`, `X-Real-IP`, Cloudflare, Vercel, Fly, Fastly, Azure,
|
|
544
|
+
App Engine, CloudFront, and standardized `Forwarded` headers.
|
|
545
|
+
|
|
546
|
+
If a trusted proxy always supplies the real client in a specific header, use a
|
|
547
|
+
resolver to override that default explicitly:
|
|
548
|
+
|
|
549
|
+
```python
|
|
550
|
+
app.add_middleware(
|
|
551
|
+
ReqKeyMiddleware,
|
|
552
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
553
|
+
api_id="api_payments",
|
|
554
|
+
capture_client_ip=True,
|
|
555
|
+
client_ip_resolver=lambda request: request.headers.get("CF-Connecting-IP"),
|
|
556
|
+
)
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Only trust forwarding headers when your deployment proxy overwrites them;
|
|
560
|
+
otherwise a caller can spoof their value.
|
|
561
|
+
|
|
495
562
|
## Custom denial messages
|
|
496
563
|
|
|
497
564
|
```python
|
|
@@ -543,8 +610,9 @@ The response includes these values when available:
|
|
|
543
610
|
- `X-ReqKey-Credits-Remaining`
|
|
544
611
|
- `X-ReqKey-Validation-Time-Ms`
|
|
545
612
|
|
|
546
|
-
|
|
547
|
-
|
|
613
|
+
No CORS configuration is needed for server-to-server clients or for the server
|
|
614
|
+
itself to set these headers. Only cross-origin browser JavaScript that must read
|
|
615
|
+
them needs the names added to the CORS middleware's `expose_headers` list.
|
|
548
616
|
|
|
549
617
|
## Availability behavior
|
|
550
618
|
|
|
@@ -563,6 +631,44 @@ but it will not be correlated to a validated consumer.
|
|
|
563
631
|
The SDK does not automatically retry `/key/validate`, because validation can
|
|
564
632
|
deduct credits. Safe automatic retries require server-side idempotency.
|
|
565
633
|
|
|
634
|
+
### Failure alerts
|
|
635
|
+
|
|
636
|
+
Webhook payload formats differ between Discord, Slack, Teams, and custom alert
|
|
637
|
+
receivers, so the SDK does not post one supposed universal webhook format.
|
|
638
|
+
Instead, `on_error` receives a provider-neutral `MiddlewareErrorEvent` for
|
|
639
|
+
ReqKey validation or ingestion failures:
|
|
640
|
+
|
|
641
|
+
```python
|
|
642
|
+
from reqkey import MiddlewareErrorEvent
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
async def report_reqkey_failure(event: MiddlewareErrorEvent) -> None:
|
|
646
|
+
await send_to_your_alert_provider(
|
|
647
|
+
{
|
|
648
|
+
"operation": event.operation,
|
|
649
|
+
"message": event.message,
|
|
650
|
+
"method": event.method,
|
|
651
|
+
"path": event.path,
|
|
652
|
+
"request_id": event.request_id,
|
|
653
|
+
"status_code": event.status_code,
|
|
654
|
+
}
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
app.add_middleware(
|
|
659
|
+
ReqKeyMiddleware,
|
|
660
|
+
project_key=os.environ["REQKEY_PROJECT_KEY"],
|
|
661
|
+
api_id="api_payments",
|
|
662
|
+
on_error=report_reqkey_failure,
|
|
663
|
+
)
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
The event intentionally excludes API keys, project credentials, headers,
|
|
667
|
+
query parameters, and bodies. Exceptions raised by the callback are logged and
|
|
668
|
+
do not replace the application response. WSGI callbacks are synchronous;
|
|
669
|
+
ASGI callbacks may be synchronous or asynchronous, and Django follows the
|
|
670
|
+
sync/async mode of its middleware chain.
|
|
671
|
+
|
|
566
672
|
## Direct sync and async clients
|
|
567
673
|
|
|
568
674
|
Async:
|
|
@@ -588,6 +694,19 @@ with ReqKey(project_key=os.environ["REQKEY_PROJECT_KEY"]) as reqkey:
|
|
|
588
694
|
decision = reqkey.verify("consumer_key_...", api_id="api_payments")
|
|
589
695
|
```
|
|
590
696
|
|
|
697
|
+
Direct ingestion also accepts an optional consumer label:
|
|
698
|
+
|
|
699
|
+
```python
|
|
700
|
+
reqkey.ingest(
|
|
701
|
+
decision.request_id,
|
|
702
|
+
api_id="api_payments",
|
|
703
|
+
method="POST",
|
|
704
|
+
path="/payments",
|
|
705
|
+
status_code=201,
|
|
706
|
+
consumer_name="rapidapi-user",
|
|
707
|
+
)
|
|
708
|
+
```
|
|
709
|
+
|
|
591
710
|
ASGI and FastAPI use the asynchronous client and await both network operations.
|
|
592
711
|
WSGI uses the synchronous client. Django automatically selects the correct
|
|
593
712
|
client based on its middleware chain, avoiding blocking HTTP calls inside an
|
|
@@ -9,12 +9,13 @@ from .exceptions import (
|
|
|
9
9
|
ReqKeyTimeoutError,
|
|
10
10
|
ReqKeyTransportError,
|
|
11
11
|
)
|
|
12
|
-
from .models import VerificationReason, VerificationResult
|
|
12
|
+
from .models import MiddlewareErrorEvent, VerificationReason, VerificationResult
|
|
13
13
|
|
|
14
|
-
__version__ = "0.
|
|
14
|
+
__version__ = "0.2.0"
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
|
17
17
|
"AsyncReqKey",
|
|
18
|
+
"MiddlewareErrorEvent",
|
|
18
19
|
"ReqKey",
|
|
19
20
|
"ReqKeyAPIError",
|
|
20
21
|
"ReqKeyAuthenticationError",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import ipaddress
|
|
5
6
|
from collections.abc import Iterable, Mapping
|
|
6
7
|
from typing import Literal
|
|
7
8
|
|
|
@@ -32,6 +33,20 @@ DEFAULT_EXCLUDED_HEADERS = frozenset(
|
|
|
32
33
|
}
|
|
33
34
|
)
|
|
34
35
|
|
|
36
|
+
PROXY_CLIENT_IP_HEADERS = (
|
|
37
|
+
"x-forwarded-for",
|
|
38
|
+
"x-real-ip",
|
|
39
|
+
"cf-connecting-ip",
|
|
40
|
+
"true-client-ip",
|
|
41
|
+
"x-vercel-forwarded-for",
|
|
42
|
+
"fly-client-ip",
|
|
43
|
+
"fastly-client-ip",
|
|
44
|
+
"x-azure-clientip",
|
|
45
|
+
"x-appengine-user-ip",
|
|
46
|
+
"cloudfront-viewer-address",
|
|
47
|
+
"forwarded",
|
|
48
|
+
)
|
|
49
|
+
|
|
35
50
|
|
|
36
51
|
def validate_middleware_options(
|
|
37
52
|
*,
|
|
@@ -130,3 +145,54 @@ def filtered_headers(
|
|
|
130
145
|
for key, value in headers.items()
|
|
131
146
|
if key.lower() not in excluded
|
|
132
147
|
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def client_ip_from_headers(headers: Mapping[str, str]) -> str | None:
|
|
151
|
+
"""Return the first valid IP from common proxy headers.
|
|
152
|
+
|
|
153
|
+
Middleware adapters use this only when the framework did not provide a
|
|
154
|
+
client address. Applications that intentionally trust a proxy header can
|
|
155
|
+
override the behavior with ``client_ip_resolver``.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
normalized = {key.lower(): value for key, value in headers.items()}
|
|
159
|
+
for name in PROXY_CLIENT_IP_HEADERS:
|
|
160
|
+
value = normalized.get(name)
|
|
161
|
+
if not value:
|
|
162
|
+
continue
|
|
163
|
+
candidates = _forwarded_candidates(value) if name == "forwarded" else value.split(",")
|
|
164
|
+
for candidate in candidates:
|
|
165
|
+
parsed = _parse_ip(candidate)
|
|
166
|
+
if parsed is not None:
|
|
167
|
+
return parsed
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _forwarded_candidates(value: str) -> list[str]:
|
|
172
|
+
candidates: list[str] = []
|
|
173
|
+
for element in value.split(","):
|
|
174
|
+
for parameter in element.split(";"):
|
|
175
|
+
name, separator, raw = parameter.strip().partition("=")
|
|
176
|
+
if separator and name.lower() == "for":
|
|
177
|
+
candidates.append(raw)
|
|
178
|
+
return candidates
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _parse_ip(value: str) -> str | None:
|
|
182
|
+
candidate = value.strip().strip('"')
|
|
183
|
+
if not candidate or candidate.lower() == "unknown" or candidate.startswith("_"):
|
|
184
|
+
return None
|
|
185
|
+
if candidate.startswith("["):
|
|
186
|
+
end = candidate.find("]")
|
|
187
|
+
if end > 0:
|
|
188
|
+
candidate = candidate[1:end]
|
|
189
|
+
else:
|
|
190
|
+
try:
|
|
191
|
+
return str(ipaddress.ip_address(candidate))
|
|
192
|
+
except ValueError:
|
|
193
|
+
if candidate.count(":") == 1:
|
|
194
|
+
candidate = candidate.partition(":")[0]
|
|
195
|
+
try:
|
|
196
|
+
return str(ipaddress.ip_address(candidate))
|
|
197
|
+
except ValueError:
|
|
198
|
+
return None
|