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 ADDED
@@ -0,0 +1,27 @@
1
+ """The official Python SDK for ReqKey."""
2
+
3
+ from .client import AsyncReqKey, ReqKey
4
+ from .exceptions import (
5
+ ReqKeyAPIError,
6
+ ReqKeyAuthenticationError,
7
+ ReqKeyConfigurationError,
8
+ ReqKeyError,
9
+ ReqKeyTimeoutError,
10
+ ReqKeyTransportError,
11
+ )
12
+ from .models import VerificationReason, VerificationResult
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ __all__ = [
17
+ "AsyncReqKey",
18
+ "ReqKey",
19
+ "ReqKeyAPIError",
20
+ "ReqKeyAuthenticationError",
21
+ "ReqKeyConfigurationError",
22
+ "ReqKeyError",
23
+ "ReqKeyTimeoutError",
24
+ "ReqKeyTransportError",
25
+ "VerificationReason",
26
+ "VerificationResult",
27
+ ]
reqkey/_middleware.py ADDED
@@ -0,0 +1,132 @@
1
+ """Framework-independent helpers shared by ReqKey middleware adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable, Mapping
6
+ from typing import Literal
7
+
8
+ from .exceptions import ReqKeyConfigurationError
9
+ from .models import VerificationReason, VerificationResult
10
+
11
+ Mode = Literal["validate", "ingest", "both"]
12
+ FailureMode = Literal["closed", "open"]
13
+ KeyLocation = Literal["header", "query", "cookie"]
14
+ KeyScheme = Literal["raw", "bearer"]
15
+
16
+ DEFAULT_ERROR_MESSAGES: dict[str, str] = {
17
+ "missing_api_key": "An API key is required.",
18
+ "invalid_api_key": "The API key is invalid or inactive.",
19
+ "insufficient_credits": "The API key has insufficient credits.",
20
+ "access_denied": "The API key is not allowed to access this API.",
21
+ "rate_limited": "The API key has exceeded its rate limit.",
22
+ "reqkey_unavailable": "API key verification is temporarily unavailable.",
23
+ }
24
+
25
+ DEFAULT_EXCLUDED_HEADERS = frozenset(
26
+ {
27
+ "authorization",
28
+ "cookie",
29
+ "proxy-authorization",
30
+ "set-cookie",
31
+ "x-api-key",
32
+ }
33
+ )
34
+
35
+
36
+ def validate_middleware_options(
37
+ *,
38
+ api_id: str,
39
+ mode: str,
40
+ key_location: str,
41
+ key_scheme: str,
42
+ failure_mode: str,
43
+ credits: object,
44
+ ) -> None:
45
+ if not api_id.strip():
46
+ raise ReqKeyConfigurationError("api_id cannot be empty.")
47
+ if mode not in {"validate", "ingest", "both"}:
48
+ raise ReqKeyConfigurationError("mode must be 'validate', 'ingest', or 'both'.")
49
+ if key_location not in {"header", "query", "cookie"}:
50
+ raise ReqKeyConfigurationError("key_location must be 'header', 'query', or 'cookie'.")
51
+ if key_scheme not in {"raw", "bearer"}:
52
+ raise ReqKeyConfigurationError("key_scheme must be 'raw' or 'bearer'.")
53
+ if key_location != "header" and key_scheme != "raw":
54
+ raise ReqKeyConfigurationError(
55
+ "Query-parameter and cookie keys must use the raw scheme."
56
+ )
57
+ if failure_mode not in {"closed", "open"}:
58
+ raise ReqKeyConfigurationError("failure_mode must be 'closed' or 'open'.")
59
+ if not callable(credits) and (
60
+ isinstance(credits, bool) or not isinstance(credits, int) or credits < 0
61
+ ):
62
+ raise ReqKeyConfigurationError(
63
+ "credits must be a non-negative integer or a callable."
64
+ )
65
+
66
+
67
+ def extract_credential(value: str | None, key_scheme: KeyScheme) -> str | None:
68
+ if value is None or not value.strip():
69
+ return None
70
+ value = value.strip()
71
+ if key_scheme == "bearer":
72
+ scheme, separator, credential = value.partition(" ")
73
+ if not separator or scheme.lower() != "bearer" or not credential.strip():
74
+ return None
75
+ return credential.strip()
76
+ return value
77
+
78
+
79
+ def validate_credit_cost(value: object) -> int:
80
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
81
+ raise ReqKeyConfigurationError(
82
+ "The credits resolver must return a non-negative integer."
83
+ )
84
+ return value
85
+
86
+
87
+ def path_matches(path: str, pattern: str) -> bool:
88
+ return path.startswith(pattern[:-1]) if pattern.endswith("*") else path == pattern
89
+
90
+
91
+ def denial(decision: VerificationResult) -> tuple[int, str]:
92
+ if decision.reason is VerificationReason.INSUFFICIENT_CREDITS:
93
+ return 402, "insufficient_credits"
94
+ if decision.reason is VerificationReason.RATE_LIMITED:
95
+ return 429, "rate_limited"
96
+ if decision.reason is VerificationReason.FORBIDDEN:
97
+ return 403, "access_denied"
98
+ return 401, "invalid_api_key"
99
+
100
+
101
+ def decision_headers(
102
+ decision: VerificationResult | None,
103
+ validation_time_ms: float | None,
104
+ ) -> dict[str, str]:
105
+ headers: dict[str, str] = {}
106
+ if decision is not None:
107
+ if decision.request_id:
108
+ headers["X-ReqKey-Request-ID"] = decision.request_id
109
+ if decision.credits_limit is not None:
110
+ headers["X-ReqKey-Credits-Limit"] = str(decision.credits_limit)
111
+ if decision.credits_remaining is not None:
112
+ headers["X-ReqKey-Credits-Remaining"] = str(decision.credits_remaining)
113
+ if validation_time_ms is not None:
114
+ headers["X-ReqKey-Validation-Time-Ms"] = f"{validation_time_ms:.3f}"
115
+ return headers
116
+
117
+
118
+ def excluded_header_names(key_name: str, extra: Iterable[str]) -> frozenset[str]:
119
+ return DEFAULT_EXCLUDED_HEADERS | {header.lower() for header in extra} | {
120
+ key_name.lower()
121
+ }
122
+
123
+
124
+ def filtered_headers(
125
+ headers: Mapping[str, str],
126
+ excluded: frozenset[str],
127
+ ) -> dict[str, str]:
128
+ return {
129
+ key: value
130
+ for key, value in headers.items()
131
+ if key.lower() not in excluded
132
+ }