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
reqkey/client.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
"""Synchronous and asynchronous clients for the ReqKey HTTP API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
ReqKeyAPIError,
|
|
14
|
+
ReqKeyAuthenticationError,
|
|
15
|
+
ReqKeyConfigurationError,
|
|
16
|
+
ReqKeyTimeoutError,
|
|
17
|
+
ReqKeyTransportError,
|
|
18
|
+
)
|
|
19
|
+
from .models import VerificationReason, VerificationResult
|
|
20
|
+
|
|
21
|
+
DEFAULT_BASE_URL = "https://api.reqkey.com"
|
|
22
|
+
DEFAULT_TIMEOUT_SECONDS = 2.0
|
|
23
|
+
USER_AGENT = "reqkey-python/0.1.0"
|
|
24
|
+
MAX_BODY_CHARACTERS = 1000
|
|
25
|
+
_DECISION_STATUS_CODES = {200, 402, 403, 429}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _require_project_key(project_key: str | None, root_key: str | None) -> str:
|
|
29
|
+
if project_key is not None and root_key is not None:
|
|
30
|
+
raise ReqKeyConfigurationError("Pass project_key or root_key, not both.")
|
|
31
|
+
value = (project_key or root_key or "").strip()
|
|
32
|
+
if not value:
|
|
33
|
+
raise ReqKeyConfigurationError(
|
|
34
|
+
"A ReqKey project key is required. Pass project_key/root_key or set "
|
|
35
|
+
"REQKEY_PROJECT_KEY/REQKEY_ROOT_KEY."
|
|
36
|
+
)
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _validate_verify_input(key: str, credits: int) -> None:
|
|
41
|
+
if not key.strip():
|
|
42
|
+
raise ReqKeyConfigurationError("The consumer API key cannot be empty.")
|
|
43
|
+
if isinstance(credits, bool) or not isinstance(credits, int) or credits < 0:
|
|
44
|
+
raise ReqKeyConfigurationError("credits must be a non-negative integer.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _json_object(response: httpx.Response) -> dict[str, Any]:
|
|
48
|
+
try:
|
|
49
|
+
payload = response.json()
|
|
50
|
+
except ValueError as exc:
|
|
51
|
+
raise ReqKeyAPIError(
|
|
52
|
+
"ReqKey returned a non-JSON response.",
|
|
53
|
+
status_code=response.status_code,
|
|
54
|
+
) from exc
|
|
55
|
+
if not isinstance(payload, dict):
|
|
56
|
+
raise ReqKeyAPIError(
|
|
57
|
+
"ReqKey returned an unexpected response body.",
|
|
58
|
+
status_code=response.status_code,
|
|
59
|
+
)
|
|
60
|
+
return payload
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _error_message(payload: Mapping[str, Any], fallback: str) -> str:
|
|
64
|
+
for key in ("error", "message"):
|
|
65
|
+
value = payload.get(key)
|
|
66
|
+
if isinstance(value, str) and value:
|
|
67
|
+
return value
|
|
68
|
+
return fallback
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _number(value: Any) -> int | None:
|
|
72
|
+
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _float_number(value: Any) -> float | None:
|
|
76
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
77
|
+
return float(value)
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _reason_for(response: httpx.Response, payload: Mapping[str, Any]) -> VerificationReason:
|
|
82
|
+
if response.status_code == 402:
|
|
83
|
+
return VerificationReason.INSUFFICIENT_CREDITS
|
|
84
|
+
if response.status_code == 403:
|
|
85
|
+
return VerificationReason.FORBIDDEN
|
|
86
|
+
if response.status_code == 429 or payload.get("rateLimited") is True:
|
|
87
|
+
return VerificationReason.RATE_LIMITED
|
|
88
|
+
if payload.get("valid") is True:
|
|
89
|
+
return VerificationReason.VALID
|
|
90
|
+
if response.status_code == 200:
|
|
91
|
+
return VerificationReason.INVALID_KEY
|
|
92
|
+
return VerificationReason.DENIED
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _verification_result(response: httpx.Response) -> VerificationResult:
|
|
96
|
+
payload = _json_object(response)
|
|
97
|
+
|
|
98
|
+
if response.status_code == 401:
|
|
99
|
+
raise ReqKeyAuthenticationError(
|
|
100
|
+
_error_message(payload, "ReqKey rejected the project credential."),
|
|
101
|
+
status_code=response.status_code,
|
|
102
|
+
body=payload,
|
|
103
|
+
)
|
|
104
|
+
if response.status_code not in _DECISION_STATUS_CODES:
|
|
105
|
+
raise ReqKeyAPIError(
|
|
106
|
+
_error_message(payload, f"ReqKey returned HTTP {response.status_code}."),
|
|
107
|
+
status_code=response.status_code,
|
|
108
|
+
body=payload,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
retry_after = _float_number(payload.get("retryAfter"))
|
|
112
|
+
if retry_after is None:
|
|
113
|
+
retry_after = _float_number(response.headers.get("Retry-After"))
|
|
114
|
+
|
|
115
|
+
raw_allowed_apis = payload.get("allowedApis")
|
|
116
|
+
allowed_apis = (
|
|
117
|
+
tuple(str(item) for item in raw_allowed_apis)
|
|
118
|
+
if isinstance(raw_allowed_apis, list)
|
|
119
|
+
else ()
|
|
120
|
+
)
|
|
121
|
+
raw_rate_limit = payload.get("rateLimit")
|
|
122
|
+
rate_limit = dict(raw_rate_limit) if isinstance(raw_rate_limit, dict) else None
|
|
123
|
+
|
|
124
|
+
return VerificationResult(
|
|
125
|
+
valid=payload.get("valid") is True,
|
|
126
|
+
reason=_reason_for(response, payload),
|
|
127
|
+
status_code=response.status_code,
|
|
128
|
+
request_id=payload.get("requestId") if isinstance(payload.get("requestId"), str) else None,
|
|
129
|
+
message=payload.get("message") if isinstance(payload.get("message"), str) else None,
|
|
130
|
+
api_id=payload.get("apiId") if isinstance(payload.get("apiId"), str) else None,
|
|
131
|
+
api_name=payload.get("apiName") if isinstance(payload.get("apiName"), str) else None,
|
|
132
|
+
resource=payload.get("resource") if isinstance(payload.get("resource"), str) else None,
|
|
133
|
+
credits_remaining=_number(payload.get("creditsRemaining")),
|
|
134
|
+
credits_limit=_number(payload.get("creditsLimit")),
|
|
135
|
+
allowed_apis=allowed_apis,
|
|
136
|
+
retry_after=retry_after,
|
|
137
|
+
rate_limit=rate_limit,
|
|
138
|
+
raw=payload,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _verify_payload(
|
|
143
|
+
*,
|
|
144
|
+
key: str,
|
|
145
|
+
api_id: str | None,
|
|
146
|
+
credits: int,
|
|
147
|
+
resource: str | None,
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
_validate_verify_input(key, credits)
|
|
150
|
+
payload: dict[str, Any] = {"key": key, "credits": credits}
|
|
151
|
+
if api_id is not None:
|
|
152
|
+
payload["apiId"] = api_id
|
|
153
|
+
if resource is not None:
|
|
154
|
+
payload["resource"] = resource
|
|
155
|
+
return payload
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _ingest_payload(
|
|
159
|
+
*,
|
|
160
|
+
request_id: str | None = None,
|
|
161
|
+
api_id: str | None = None,
|
|
162
|
+
method: str | None = None,
|
|
163
|
+
endpoint: str | None = None,
|
|
164
|
+
path: str | None = None,
|
|
165
|
+
status_code: int | None = None,
|
|
166
|
+
latency_ms: int | None = None,
|
|
167
|
+
client_ip: str | None = None,
|
|
168
|
+
user_agent: str | None = None,
|
|
169
|
+
user_id: str | None = None,
|
|
170
|
+
query_params: Mapping[str, Any] | None = None,
|
|
171
|
+
request_headers: Mapping[str, str] | None = None,
|
|
172
|
+
response_headers: Mapping[str, str] | None = None,
|
|
173
|
+
request_body: str | None = None,
|
|
174
|
+
response_body: str | None = None,
|
|
175
|
+
timestamp: datetime | str | None = None,
|
|
176
|
+
) -> dict[str, Any]:
|
|
177
|
+
if request_id is not None and not request_id.strip():
|
|
178
|
+
raise ReqKeyConfigurationError("request_id cannot be empty when provided.")
|
|
179
|
+
if api_id is not None and not api_id.strip():
|
|
180
|
+
raise ReqKeyConfigurationError("api_id cannot be empty when provided.")
|
|
181
|
+
if request_id is None and api_id is None:
|
|
182
|
+
raise ReqKeyConfigurationError("Ingestion requires request_id, api_id, or both.")
|
|
183
|
+
payload: dict[str, Any] = {}
|
|
184
|
+
if request_id is not None:
|
|
185
|
+
payload["requestId"] = request_id
|
|
186
|
+
if api_id is not None:
|
|
187
|
+
payload["apiId"] = api_id
|
|
188
|
+
optional_values: dict[str, Any] = {
|
|
189
|
+
"method": method,
|
|
190
|
+
"endpoint": endpoint,
|
|
191
|
+
"path": path,
|
|
192
|
+
"statusCode": status_code,
|
|
193
|
+
"latencyMs": latency_ms,
|
|
194
|
+
"clientIp": client_ip,
|
|
195
|
+
"userAgent": user_agent,
|
|
196
|
+
"userId": user_id,
|
|
197
|
+
"queryParams": dict(query_params) if query_params is not None else None,
|
|
198
|
+
"requestHeaders": dict(request_headers) if request_headers is not None else None,
|
|
199
|
+
"responseHeaders": dict(response_headers) if response_headers is not None else None,
|
|
200
|
+
"requestBody": (
|
|
201
|
+
request_body[:MAX_BODY_CHARACTERS] if request_body is not None else None
|
|
202
|
+
),
|
|
203
|
+
"responseBody": (
|
|
204
|
+
response_body[:MAX_BODY_CHARACTERS] if response_body is not None else None
|
|
205
|
+
),
|
|
206
|
+
"timestamp": timestamp.isoformat() if isinstance(timestamp, datetime) else timestamp,
|
|
207
|
+
}
|
|
208
|
+
payload.update({key: value for key, value in optional_values.items() if value is not None})
|
|
209
|
+
return payload
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class _ClientConfig:
|
|
213
|
+
def __init__(
|
|
214
|
+
self,
|
|
215
|
+
project_key: str | None,
|
|
216
|
+
root_key: str | None,
|
|
217
|
+
base_url: str,
|
|
218
|
+
timeout: float,
|
|
219
|
+
) -> None:
|
|
220
|
+
self.project_key = _require_project_key(project_key, root_key)
|
|
221
|
+
self.base_url = base_url.rstrip("/")
|
|
222
|
+
if not self.base_url:
|
|
223
|
+
raise ReqKeyConfigurationError("base_url cannot be empty.")
|
|
224
|
+
if timeout <= 0:
|
|
225
|
+
raise ReqKeyConfigurationError("timeout must be greater than zero.")
|
|
226
|
+
self.timeout = timeout
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def headers(self) -> dict[str, str]:
|
|
230
|
+
return {
|
|
231
|
+
"Authorization": f"Bearer {self.project_key}",
|
|
232
|
+
"Content-Type": "application/json",
|
|
233
|
+
"User-Agent": USER_AGENT,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ReqKey:
|
|
238
|
+
"""Synchronous ReqKey client."""
|
|
239
|
+
|
|
240
|
+
def __init__(
|
|
241
|
+
self,
|
|
242
|
+
*,
|
|
243
|
+
project_key: str | None = None,
|
|
244
|
+
root_key: str | None = None,
|
|
245
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
246
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
247
|
+
http_client: httpx.Client | None = None,
|
|
248
|
+
) -> None:
|
|
249
|
+
self._config = _ClientConfig(project_key, root_key, base_url, timeout)
|
|
250
|
+
self._owns_client = http_client is None
|
|
251
|
+
self._client = http_client or httpx.Client(timeout=timeout, headers=self._config.headers)
|
|
252
|
+
|
|
253
|
+
@classmethod
|
|
254
|
+
def from_env(cls, **kwargs: Any) -> ReqKey:
|
|
255
|
+
"""Create a client using `REQKEY_PROJECT_KEY` or `REQKEY_ROOT_KEY`."""
|
|
256
|
+
|
|
257
|
+
return cls(
|
|
258
|
+
project_key=os.getenv("REQKEY_PROJECT_KEY") or os.getenv("REQKEY_ROOT_KEY"),
|
|
259
|
+
**kwargs,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def verify(
|
|
263
|
+
self,
|
|
264
|
+
key: str,
|
|
265
|
+
*,
|
|
266
|
+
api_id: str | None = None,
|
|
267
|
+
credits: int = 1,
|
|
268
|
+
resource: str | None = None,
|
|
269
|
+
) -> VerificationResult:
|
|
270
|
+
"""Validate a consumer key and atomically deduct credits."""
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
response = self._client.post(
|
|
274
|
+
f"{self._config.base_url}/key/validate",
|
|
275
|
+
json=_verify_payload(key=key, api_id=api_id, credits=credits, resource=resource),
|
|
276
|
+
headers=self._config.headers,
|
|
277
|
+
)
|
|
278
|
+
except httpx.TimeoutException as exc:
|
|
279
|
+
raise ReqKeyTimeoutError("The ReqKey validation request timed out.") from exc
|
|
280
|
+
except httpx.RequestError as exc:
|
|
281
|
+
raise ReqKeyTransportError(f"Could not reach ReqKey: {exc}") from exc
|
|
282
|
+
return _verification_result(response)
|
|
283
|
+
|
|
284
|
+
def ingest(
|
|
285
|
+
self,
|
|
286
|
+
request_id: str | None = None,
|
|
287
|
+
*,
|
|
288
|
+
api_id: str | None = None,
|
|
289
|
+
method: str | None = None,
|
|
290
|
+
endpoint: str | None = None,
|
|
291
|
+
path: str | None = None,
|
|
292
|
+
status_code: int | None = None,
|
|
293
|
+
latency_ms: int | None = None,
|
|
294
|
+
client_ip: str | None = None,
|
|
295
|
+
user_agent: str | None = None,
|
|
296
|
+
user_id: str | None = None,
|
|
297
|
+
query_params: Mapping[str, Any] | None = None,
|
|
298
|
+
request_headers: Mapping[str, str] | None = None,
|
|
299
|
+
response_headers: Mapping[str, str] | None = None,
|
|
300
|
+
request_body: str | None = None,
|
|
301
|
+
response_body: str | None = None,
|
|
302
|
+
timestamp: datetime | str | None = None,
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Send request/response metadata to ReqKey analytics."""
|
|
305
|
+
|
|
306
|
+
payload = _ingest_payload(
|
|
307
|
+
request_id=request_id,
|
|
308
|
+
api_id=api_id,
|
|
309
|
+
method=method,
|
|
310
|
+
endpoint=endpoint,
|
|
311
|
+
path=path,
|
|
312
|
+
status_code=status_code,
|
|
313
|
+
latency_ms=latency_ms,
|
|
314
|
+
client_ip=client_ip,
|
|
315
|
+
user_agent=user_agent,
|
|
316
|
+
user_id=user_id,
|
|
317
|
+
query_params=query_params,
|
|
318
|
+
request_headers=request_headers,
|
|
319
|
+
response_headers=response_headers,
|
|
320
|
+
request_body=request_body,
|
|
321
|
+
response_body=response_body,
|
|
322
|
+
timestamp=timestamp,
|
|
323
|
+
)
|
|
324
|
+
try:
|
|
325
|
+
response = self._client.post(
|
|
326
|
+
f"{self._config.base_url}/ingest",
|
|
327
|
+
json=payload,
|
|
328
|
+
headers=self._config.headers,
|
|
329
|
+
)
|
|
330
|
+
except httpx.TimeoutException as exc:
|
|
331
|
+
raise ReqKeyTimeoutError("The ReqKey ingestion request timed out.") from exc
|
|
332
|
+
except httpx.RequestError as exc:
|
|
333
|
+
raise ReqKeyTransportError(f"Could not reach ReqKey: {exc}") from exc
|
|
334
|
+
if response.status_code not in {200, 202}:
|
|
335
|
+
body = _json_object(response)
|
|
336
|
+
raise ReqKeyAPIError(
|
|
337
|
+
_error_message(body, f"ReqKey returned HTTP {response.status_code}."),
|
|
338
|
+
status_code=response.status_code,
|
|
339
|
+
body=body,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def close(self) -> None:
|
|
343
|
+
if self._owns_client:
|
|
344
|
+
self._client.close()
|
|
345
|
+
|
|
346
|
+
def __enter__(self) -> ReqKey:
|
|
347
|
+
return self
|
|
348
|
+
|
|
349
|
+
def __exit__(self, *_: object) -> None:
|
|
350
|
+
self.close()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class AsyncReqKey:
|
|
354
|
+
"""Asynchronous ReqKey client for FastAPI and other async applications."""
|
|
355
|
+
|
|
356
|
+
def __init__(
|
|
357
|
+
self,
|
|
358
|
+
*,
|
|
359
|
+
project_key: str | None = None,
|
|
360
|
+
root_key: str | None = None,
|
|
361
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
362
|
+
timeout: float = DEFAULT_TIMEOUT_SECONDS,
|
|
363
|
+
http_client: httpx.AsyncClient | None = None,
|
|
364
|
+
) -> None:
|
|
365
|
+
self._config = _ClientConfig(project_key, root_key, base_url, timeout)
|
|
366
|
+
self._owns_client = http_client is None
|
|
367
|
+
self._client = http_client or httpx.AsyncClient(
|
|
368
|
+
timeout=timeout,
|
|
369
|
+
headers=self._config.headers,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
@classmethod
|
|
373
|
+
def from_env(cls, **kwargs: Any) -> AsyncReqKey:
|
|
374
|
+
"""Create a client using `REQKEY_PROJECT_KEY` or `REQKEY_ROOT_KEY`."""
|
|
375
|
+
|
|
376
|
+
return cls(
|
|
377
|
+
project_key=os.getenv("REQKEY_PROJECT_KEY") or os.getenv("REQKEY_ROOT_KEY"),
|
|
378
|
+
**kwargs,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
async def verify(
|
|
382
|
+
self,
|
|
383
|
+
key: str,
|
|
384
|
+
*,
|
|
385
|
+
api_id: str | None = None,
|
|
386
|
+
credits: int = 1,
|
|
387
|
+
resource: str | None = None,
|
|
388
|
+
) -> VerificationResult:
|
|
389
|
+
"""Validate a consumer key and atomically deduct credits."""
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
response = await self._client.post(
|
|
393
|
+
f"{self._config.base_url}/key/validate",
|
|
394
|
+
json=_verify_payload(key=key, api_id=api_id, credits=credits, resource=resource),
|
|
395
|
+
headers=self._config.headers,
|
|
396
|
+
)
|
|
397
|
+
except httpx.TimeoutException as exc:
|
|
398
|
+
raise ReqKeyTimeoutError("The ReqKey validation request timed out.") from exc
|
|
399
|
+
except httpx.RequestError as exc:
|
|
400
|
+
raise ReqKeyTransportError(f"Could not reach ReqKey: {exc}") from exc
|
|
401
|
+
return _verification_result(response)
|
|
402
|
+
|
|
403
|
+
async def ingest(
|
|
404
|
+
self,
|
|
405
|
+
request_id: str | None = None,
|
|
406
|
+
*,
|
|
407
|
+
api_id: str | None = None,
|
|
408
|
+
method: str | None = None,
|
|
409
|
+
endpoint: str | None = None,
|
|
410
|
+
path: str | None = None,
|
|
411
|
+
status_code: int | None = None,
|
|
412
|
+
latency_ms: int | None = None,
|
|
413
|
+
client_ip: str | None = None,
|
|
414
|
+
user_agent: str | None = None,
|
|
415
|
+
user_id: str | None = None,
|
|
416
|
+
query_params: Mapping[str, Any] | None = None,
|
|
417
|
+
request_headers: Mapping[str, str] | None = None,
|
|
418
|
+
response_headers: Mapping[str, str] | None = None,
|
|
419
|
+
request_body: str | None = None,
|
|
420
|
+
response_body: str | None = None,
|
|
421
|
+
timestamp: datetime | str | None = None,
|
|
422
|
+
) -> None:
|
|
423
|
+
"""Send request/response metadata to ReqKey analytics."""
|
|
424
|
+
|
|
425
|
+
payload = _ingest_payload(
|
|
426
|
+
request_id=request_id,
|
|
427
|
+
api_id=api_id,
|
|
428
|
+
method=method,
|
|
429
|
+
endpoint=endpoint,
|
|
430
|
+
path=path,
|
|
431
|
+
status_code=status_code,
|
|
432
|
+
latency_ms=latency_ms,
|
|
433
|
+
client_ip=client_ip,
|
|
434
|
+
user_agent=user_agent,
|
|
435
|
+
user_id=user_id,
|
|
436
|
+
query_params=query_params,
|
|
437
|
+
request_headers=request_headers,
|
|
438
|
+
response_headers=response_headers,
|
|
439
|
+
request_body=request_body,
|
|
440
|
+
response_body=response_body,
|
|
441
|
+
timestamp=timestamp,
|
|
442
|
+
)
|
|
443
|
+
try:
|
|
444
|
+
response = await self._client.post(
|
|
445
|
+
f"{self._config.base_url}/ingest",
|
|
446
|
+
json=payload,
|
|
447
|
+
headers=self._config.headers,
|
|
448
|
+
)
|
|
449
|
+
except httpx.TimeoutException as exc:
|
|
450
|
+
raise ReqKeyTimeoutError("The ReqKey ingestion request timed out.") from exc
|
|
451
|
+
except httpx.RequestError as exc:
|
|
452
|
+
raise ReqKeyTransportError(f"Could not reach ReqKey: {exc}") from exc
|
|
453
|
+
if response.status_code not in {200, 202}:
|
|
454
|
+
body = _json_object(response)
|
|
455
|
+
raise ReqKeyAPIError(
|
|
456
|
+
_error_message(body, f"ReqKey returned HTTP {response.status_code}."),
|
|
457
|
+
status_code=response.status_code,
|
|
458
|
+
body=body,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
async def close(self) -> None:
|
|
462
|
+
if self._owns_client:
|
|
463
|
+
await self._client.aclose()
|
|
464
|
+
|
|
465
|
+
async def __aenter__(self) -> AsyncReqKey:
|
|
466
|
+
return self
|
|
467
|
+
|
|
468
|
+
async def __aexit__(self, *_: object) -> None:
|
|
469
|
+
await self.close()
|