jararaca 0.3.11a16__py3-none-any.whl → 0.3.12__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.
Potentially problematic release.
This version of jararaca might be problematic. Click here for more details.
- README.md +120 -0
- jararaca/__init__.py +106 -8
- jararaca/cli.py +216 -31
- jararaca/messagebus/worker.py +749 -272
- jararaca/microservice.py +42 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +82 -73
- jararaca/persistence/interceptors/constants.py +1 -0
- jararaca/persistence/interceptors/decorators.py +45 -0
- jararaca/presentation/server.py +57 -11
- jararaca/presentation/websocket/redis.py +113 -7
- jararaca/reflect/metadata.py +1 -1
- jararaca/rpc/http/__init__.py +97 -0
- jararaca/rpc/http/backends/__init__.py +10 -0
- jararaca/rpc/http/backends/httpx.py +39 -9
- jararaca/rpc/http/decorators.py +302 -6
- jararaca/scheduler/beat_worker.py +550 -91
- jararaca/tools/typescript/__init__.py +0 -0
- jararaca/tools/typescript/decorators.py +95 -0
- jararaca/tools/typescript/interface_parser.py +699 -156
- jararaca-0.3.12.dist-info/LICENSE +674 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/METADATA +4 -3
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/RECORD +26 -19
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/WHEEL +1 -1
- pyproject.toml +86 -0
- /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/entry_points.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import time
|
|
1
2
|
from urllib.parse import urljoin
|
|
2
3
|
|
|
3
4
|
import httpx
|
|
@@ -7,34 +8,63 @@ from jararaca.rpc.http.decorators import (
|
|
|
7
8
|
HttpRPCRequest,
|
|
8
9
|
HttpRPCResponse,
|
|
9
10
|
RPCRequestNetworkError,
|
|
11
|
+
TimeoutException,
|
|
10
12
|
)
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class HTTPXHttpRPCAsyncBackend(HttpRPCAsyncBackend):
|
|
14
16
|
|
|
15
|
-
def __init__(self, prefix_url: str = ""):
|
|
17
|
+
def __init__(self, prefix_url: str = "", default_timeout: float = 30.0):
|
|
16
18
|
self.prefix_url = prefix_url
|
|
19
|
+
self.default_timeout = default_timeout
|
|
17
20
|
|
|
18
21
|
async def request(
|
|
19
22
|
self,
|
|
20
23
|
request: HttpRPCRequest,
|
|
21
24
|
) -> HttpRPCResponse:
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
start_time = time.time()
|
|
27
|
+
|
|
28
|
+
# Prepare timeout
|
|
29
|
+
timeout = (
|
|
30
|
+
request.timeout if request.timeout is not None else self.default_timeout
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Prepare request kwargs
|
|
34
|
+
request_kwargs = {
|
|
35
|
+
"method": request.method,
|
|
36
|
+
"url": urljoin(self.prefix_url, request.url),
|
|
37
|
+
"headers": request.headers,
|
|
38
|
+
"params": request.query_params,
|
|
39
|
+
"timeout": timeout,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Handle different content types
|
|
43
|
+
if request.form_data and request.files:
|
|
44
|
+
# Multipart form data with files
|
|
45
|
+
request_kwargs["data"] = request.form_data
|
|
46
|
+
request_kwargs["files"] = request.files
|
|
47
|
+
elif request.form_data:
|
|
48
|
+
# Form data only
|
|
49
|
+
request_kwargs["data"] = request.form_data
|
|
50
|
+
elif request.body:
|
|
51
|
+
# Raw body content
|
|
52
|
+
request_kwargs["content"] = request.body
|
|
24
53
|
|
|
54
|
+
async with httpx.AsyncClient() as client:
|
|
25
55
|
try:
|
|
26
|
-
response = await client.request(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
headers=request.headers,
|
|
30
|
-
params=request.query_params,
|
|
31
|
-
content=request.body,
|
|
32
|
-
)
|
|
56
|
+
response = await client.request(**request_kwargs) # type: ignore[arg-type]
|
|
57
|
+
|
|
58
|
+
elapsed_time = time.time() - start_time
|
|
33
59
|
|
|
34
60
|
return HttpRPCResponse(
|
|
35
61
|
status_code=response.status_code,
|
|
36
62
|
data=response.content,
|
|
63
|
+
headers=dict(response.headers),
|
|
64
|
+
elapsed_time=elapsed_time,
|
|
37
65
|
)
|
|
66
|
+
except httpx.TimeoutException as err:
|
|
67
|
+
raise TimeoutException(f"Request timed out: {err}") from err
|
|
38
68
|
except httpx.NetworkError as err:
|
|
39
69
|
raise RPCRequestNetworkError(
|
|
40
70
|
request=request, backend_request=err.request
|
jararaca/rpc/http/decorators.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
2
5
|
from dataclasses import dataclass
|
|
3
6
|
from typing import (
|
|
4
7
|
Any,
|
|
5
8
|
Awaitable,
|
|
6
9
|
Callable,
|
|
10
|
+
Dict,
|
|
7
11
|
Iterable,
|
|
8
12
|
Literal,
|
|
13
|
+
Optional,
|
|
9
14
|
Protocol,
|
|
10
15
|
Type,
|
|
11
16
|
TypeVar,
|
|
@@ -15,6 +20,11 @@ from typing import (
|
|
|
15
20
|
from pydantic import BaseModel
|
|
16
21
|
|
|
17
22
|
DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Awaitable[Any]])
|
|
23
|
+
DECORATED_CLASS = TypeVar("DECORATED_CLASS", bound=Any)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TimeoutException(Exception):
|
|
27
|
+
"""Exception raised when a request times out"""
|
|
18
28
|
|
|
19
29
|
|
|
20
30
|
class HttpMapping:
|
|
@@ -95,7 +105,9 @@ class RequestAttribute:
|
|
|
95
105
|
return []
|
|
96
106
|
|
|
97
107
|
def __init__(
|
|
98
|
-
self,
|
|
108
|
+
self,
|
|
109
|
+
attribute_type: Literal["query", "header", "body", "param", "form", "file"],
|
|
110
|
+
name: str,
|
|
99
111
|
):
|
|
100
112
|
self.attribute_type = attribute_type
|
|
101
113
|
self.name = name
|
|
@@ -129,7 +141,105 @@ class PathParam(RequestAttribute):
|
|
|
129
141
|
super().__init__("param", name)
|
|
130
142
|
|
|
131
143
|
|
|
132
|
-
|
|
144
|
+
class FormData(RequestAttribute):
|
|
145
|
+
"""Decorator for form data parameters"""
|
|
146
|
+
|
|
147
|
+
def __init__(self, name: str):
|
|
148
|
+
super().__init__("form", name)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class File(RequestAttribute):
|
|
152
|
+
"""Decorator for file upload parameters"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, name: str):
|
|
155
|
+
super().__init__("file", name)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class Timeout:
|
|
159
|
+
"""Decorator for setting request timeout"""
|
|
160
|
+
|
|
161
|
+
TIMEOUT_ATTR = "__request_timeout__"
|
|
162
|
+
|
|
163
|
+
def __init__(self, seconds: float):
|
|
164
|
+
self.seconds = seconds
|
|
165
|
+
|
|
166
|
+
def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
167
|
+
setattr(func, self.TIMEOUT_ATTR, self)
|
|
168
|
+
return func
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def get(func: DECORATED_FUNC) -> Optional["Timeout"]:
|
|
172
|
+
return getattr(func, Timeout.TIMEOUT_ATTR, None)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class RetryConfig:
|
|
176
|
+
"""Configuration for retry behavior"""
|
|
177
|
+
|
|
178
|
+
def __init__(
|
|
179
|
+
self,
|
|
180
|
+
max_attempts: int = 3,
|
|
181
|
+
backoff_factor: float = 1.0,
|
|
182
|
+
retry_on_status_codes: Optional[list[int]] = None,
|
|
183
|
+
):
|
|
184
|
+
self.max_attempts = max_attempts
|
|
185
|
+
self.backoff_factor = backoff_factor
|
|
186
|
+
self.retry_on_status_codes = retry_on_status_codes or [500, 502, 503, 504]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class Retry:
|
|
190
|
+
"""Decorator for retry configuration"""
|
|
191
|
+
|
|
192
|
+
RETRY_ATTR = "__request_retry__"
|
|
193
|
+
|
|
194
|
+
def __init__(self, config: RetryConfig):
|
|
195
|
+
self.config = config
|
|
196
|
+
|
|
197
|
+
def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
198
|
+
setattr(func, self.RETRY_ATTR, self)
|
|
199
|
+
return func
|
|
200
|
+
|
|
201
|
+
@staticmethod
|
|
202
|
+
def get(func: DECORATED_FUNC) -> Optional["Retry"]:
|
|
203
|
+
return getattr(func, Retry.RETRY_ATTR, None)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ContentType:
|
|
207
|
+
"""Decorator for specifying content type"""
|
|
208
|
+
|
|
209
|
+
CONTENT_TYPE_ATTR = "__content_type__"
|
|
210
|
+
|
|
211
|
+
def __init__(self, content_type: str):
|
|
212
|
+
self.content_type = content_type
|
|
213
|
+
|
|
214
|
+
def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
215
|
+
setattr(func, self.CONTENT_TYPE_ATTR, self)
|
|
216
|
+
return func
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def get(func: DECORATED_FUNC) -> Optional["ContentType"]:
|
|
220
|
+
return getattr(func, ContentType.CONTENT_TYPE_ATTR, None)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ResponseMiddleware(Protocol):
|
|
224
|
+
"""Protocol for response middleware"""
|
|
225
|
+
|
|
226
|
+
def on_response(
|
|
227
|
+
self, request: "HttpRPCRequest", response: "HttpRPCResponse"
|
|
228
|
+
) -> "HttpRPCResponse": ...
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class RequestHook(Protocol):
|
|
232
|
+
"""Protocol for request hooks"""
|
|
233
|
+
|
|
234
|
+
def before_request(self, request: "HttpRPCRequest") -> "HttpRPCRequest": ...
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class ResponseHook(Protocol):
|
|
238
|
+
"""Protocol for response hooks"""
|
|
239
|
+
|
|
240
|
+
def after_response(
|
|
241
|
+
self, request: "HttpRPCRequest", response: "HttpRPCResponse"
|
|
242
|
+
) -> "HttpRPCResponse": ...
|
|
133
243
|
|
|
134
244
|
|
|
135
245
|
class RestClient:
|
|
@@ -159,9 +269,10 @@ class RestClient:
|
|
|
159
269
|
|
|
160
270
|
@dataclass
|
|
161
271
|
class HttpRPCResponse:
|
|
162
|
-
|
|
163
272
|
status_code: int
|
|
164
273
|
data: bytes
|
|
274
|
+
headers: Optional[Dict[str, str]] = None
|
|
275
|
+
elapsed_time: Optional[float] = None
|
|
165
276
|
|
|
166
277
|
|
|
167
278
|
@dataclass
|
|
@@ -171,6 +282,9 @@ class HttpRPCRequest:
|
|
|
171
282
|
headers: list[tuple[str, str]]
|
|
172
283
|
query_params: dict[str, str]
|
|
173
284
|
body: bytes | None
|
|
285
|
+
timeout: Optional[float] = None
|
|
286
|
+
form_data: Optional[Dict[str, Any]] = None
|
|
287
|
+
files: Optional[Dict[str, Any]] = None
|
|
174
288
|
|
|
175
289
|
|
|
176
290
|
class RPCRequestNetworkError(Exception):
|
|
@@ -275,15 +389,130 @@ class RequestMiddleware(Protocol):
|
|
|
275
389
|
def on_request(self, request: HttpRPCRequest) -> HttpRPCRequest: ...
|
|
276
390
|
|
|
277
391
|
|
|
392
|
+
class AuthenticationMiddleware(RequestMiddleware):
|
|
393
|
+
"""Base class for authentication middleware"""
|
|
394
|
+
|
|
395
|
+
def on_request(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
396
|
+
return self.add_auth(request)
|
|
397
|
+
|
|
398
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
399
|
+
raise NotImplementedError
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class BearerTokenAuth(AuthenticationMiddleware):
|
|
403
|
+
"""Bearer token authentication middleware"""
|
|
404
|
+
|
|
405
|
+
def __init__(self, token: str):
|
|
406
|
+
self.token = token
|
|
407
|
+
|
|
408
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
409
|
+
request.headers.append(("Authorization", f"Bearer {self.token}"))
|
|
410
|
+
return request
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class BasicAuth(AuthenticationMiddleware):
|
|
414
|
+
"""Basic authentication middleware"""
|
|
415
|
+
|
|
416
|
+
def __init__(self, username: str, password: str):
|
|
417
|
+
import base64
|
|
418
|
+
|
|
419
|
+
credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
|
|
420
|
+
self.credentials = credentials
|
|
421
|
+
|
|
422
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
423
|
+
request.headers.append(("Authorization", f"Basic {self.credentials}"))
|
|
424
|
+
return request
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class ApiKeyAuth(AuthenticationMiddleware):
|
|
428
|
+
"""API key authentication middleware"""
|
|
429
|
+
|
|
430
|
+
def __init__(self, api_key: str, header_name: str = "X-API-Key"):
|
|
431
|
+
self.api_key = api_key
|
|
432
|
+
self.header_name = header_name
|
|
433
|
+
|
|
434
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
435
|
+
request.headers.append((self.header_name, self.api_key))
|
|
436
|
+
return request
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class CacheMiddleware(RequestMiddleware):
|
|
440
|
+
"""Simple in-memory cache middleware"""
|
|
441
|
+
|
|
442
|
+
def __init__(self, ttl_seconds: int = 300):
|
|
443
|
+
self.cache: Dict[str, tuple[Any, float]] = {}
|
|
444
|
+
self.ttl_seconds = ttl_seconds
|
|
445
|
+
|
|
446
|
+
def _cache_key(self, request: HttpRPCRequest) -> str:
|
|
447
|
+
"""Generate cache key from request"""
|
|
448
|
+
key_data = {
|
|
449
|
+
"method": request.method,
|
|
450
|
+
"url": request.url,
|
|
451
|
+
"query_params": request.query_params,
|
|
452
|
+
"headers": sorted(request.headers),
|
|
453
|
+
}
|
|
454
|
+
return str(hash(json.dumps(key_data, sort_keys=True)))
|
|
455
|
+
|
|
456
|
+
def on_request(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
457
|
+
# Only cache GET requests
|
|
458
|
+
if request.method == "GET":
|
|
459
|
+
cache_key = self._cache_key(request)
|
|
460
|
+
if cache_key in self.cache:
|
|
461
|
+
cached_response, timestamp = self.cache[cache_key]
|
|
462
|
+
if time.time() - timestamp < self.ttl_seconds:
|
|
463
|
+
# Return cached response (this needs to be handled in the client builder)
|
|
464
|
+
setattr(request, "_cached_response", cached_response)
|
|
465
|
+
return request
|
|
466
|
+
|
|
467
|
+
|
|
278
468
|
class HttpRpcClientBuilder:
|
|
279
469
|
|
|
280
470
|
def __init__(
|
|
281
471
|
self,
|
|
282
472
|
backend: HttpRPCAsyncBackend,
|
|
283
473
|
middlewares: list[RequestMiddleware] = [],
|
|
474
|
+
response_middlewares: list[ResponseMiddleware] = [],
|
|
475
|
+
request_hooks: list[RequestHook] = [],
|
|
476
|
+
response_hooks: list[ResponseHook] = [],
|
|
284
477
|
):
|
|
285
478
|
self._backend = backend
|
|
286
479
|
self._middlewares = middlewares
|
|
480
|
+
self._response_middlewares = response_middlewares
|
|
481
|
+
self._request_hooks = request_hooks
|
|
482
|
+
self._response_hooks = response_hooks
|
|
483
|
+
|
|
484
|
+
async def _execute_with_retry(
|
|
485
|
+
self, request: HttpRPCRequest, retry_config: Optional[RetryConfig]
|
|
486
|
+
) -> HttpRPCResponse:
|
|
487
|
+
"""Execute request with retry logic"""
|
|
488
|
+
if not retry_config:
|
|
489
|
+
return await self._backend.request(request)
|
|
490
|
+
|
|
491
|
+
last_exception = None
|
|
492
|
+
for attempt in range(retry_config.max_attempts):
|
|
493
|
+
try:
|
|
494
|
+
response = await self._backend.request(request)
|
|
495
|
+
|
|
496
|
+
# Check if we should retry based on status code
|
|
497
|
+
if response.status_code in retry_config.retry_on_status_codes:
|
|
498
|
+
if attempt < retry_config.max_attempts - 1:
|
|
499
|
+
wait_time = retry_config.backoff_factor * (2**attempt)
|
|
500
|
+
await asyncio.sleep(wait_time)
|
|
501
|
+
continue
|
|
502
|
+
|
|
503
|
+
return response
|
|
504
|
+
|
|
505
|
+
except Exception as e:
|
|
506
|
+
last_exception = e
|
|
507
|
+
if attempt < retry_config.max_attempts - 1:
|
|
508
|
+
wait_time = retry_config.backoff_factor * (2**attempt)
|
|
509
|
+
await asyncio.sleep(wait_time)
|
|
510
|
+
continue
|
|
511
|
+
else:
|
|
512
|
+
raise
|
|
513
|
+
|
|
514
|
+
# This should never be reached, but just in case
|
|
515
|
+
raise last_exception or Exception("Retry failed")
|
|
287
516
|
|
|
288
517
|
def build(self, cls: type[T]) -> T:
|
|
289
518
|
rest_client = RestClient.get(cls)
|
|
@@ -314,10 +543,17 @@ class HttpRpcClientBuilder:
|
|
|
314
543
|
headers: list[tuple[str, str]] = []
|
|
315
544
|
query_params = {}
|
|
316
545
|
body: Any = None
|
|
546
|
+
form_data: Dict[str, Any] = {}
|
|
547
|
+
files: Dict[str, Any] = {}
|
|
317
548
|
compiled_path = (
|
|
318
549
|
rest_client.base_path.rstrip("/") + "/" + mapping.path.lstrip("/")
|
|
319
550
|
)
|
|
320
551
|
|
|
552
|
+
# Get decorators for this method
|
|
553
|
+
timeout_config = Timeout.get(method_call)
|
|
554
|
+
retry_config = Retry.get(method_call)
|
|
555
|
+
content_type_config = ContentType.get(method_call)
|
|
556
|
+
|
|
321
557
|
for attr in request_attributes:
|
|
322
558
|
if attr.attribute_type == "header":
|
|
323
559
|
headers.append((attr.name, compiled_kwargs[attr.name]))
|
|
@@ -329,17 +565,33 @@ class HttpRpcClientBuilder:
|
|
|
329
565
|
compiled_path = compiled_path.replace(
|
|
330
566
|
f":{attr.name}", str(compiled_kwargs[attr.name])
|
|
331
567
|
)
|
|
568
|
+
elif attr.attribute_type == "form":
|
|
569
|
+
form_data[attr.name] = compiled_kwargs[attr.name]
|
|
570
|
+
elif attr.attribute_type == "file":
|
|
571
|
+
files[attr.name] = compiled_kwargs[attr.name]
|
|
332
572
|
|
|
333
573
|
body_content: bytes | None = None
|
|
334
574
|
|
|
575
|
+
# Handle different content types
|
|
335
576
|
if body is not None:
|
|
336
577
|
if isinstance(body, BaseModel):
|
|
337
578
|
body_content = body.model_dump_json().encode()
|
|
338
|
-
|
|
579
|
+
if not content_type_config:
|
|
580
|
+
headers.append(("Content-Type", "application/json"))
|
|
339
581
|
elif isinstance(body, bytes):
|
|
340
582
|
body_content = body
|
|
583
|
+
elif isinstance(body, str):
|
|
584
|
+
body_content = body.encode()
|
|
585
|
+
elif isinstance(body, dict):
|
|
586
|
+
body_content = json.dumps(body).encode()
|
|
587
|
+
if not content_type_config:
|
|
588
|
+
headers.append(("Content-Type", "application/json"))
|
|
341
589
|
else:
|
|
342
|
-
raise ValueError("Invalid body type")
|
|
590
|
+
raise ValueError(f"Invalid body type: {type(body)}")
|
|
591
|
+
|
|
592
|
+
# Apply custom content type if specified
|
|
593
|
+
if content_type_config:
|
|
594
|
+
headers.append(("Content-Type", content_type_config.content_type))
|
|
343
595
|
|
|
344
596
|
request = HttpRPCRequest(
|
|
345
597
|
url=compiled_path,
|
|
@@ -347,12 +599,41 @@ class HttpRpcClientBuilder:
|
|
|
347
599
|
headers=headers,
|
|
348
600
|
query_params=query_params,
|
|
349
601
|
body=body_content,
|
|
602
|
+
timeout=timeout_config.seconds if timeout_config else None,
|
|
603
|
+
form_data=form_data if form_data else None,
|
|
604
|
+
files=files if files else None,
|
|
350
605
|
)
|
|
351
606
|
|
|
607
|
+
# Apply request hooks
|
|
608
|
+
for hook in self._request_hooks:
|
|
609
|
+
request = hook.before_request(request)
|
|
610
|
+
|
|
352
611
|
for middleware in self._middlewares:
|
|
353
612
|
request = middleware.on_request(request)
|
|
354
613
|
|
|
355
|
-
|
|
614
|
+
# Check for cached response
|
|
615
|
+
if hasattr(request, "_cached_response"):
|
|
616
|
+
response = getattr(request, "_cached_response")
|
|
617
|
+
else:
|
|
618
|
+
# Execute request with retry if configured
|
|
619
|
+
response = await self._execute_with_retry(
|
|
620
|
+
request, retry_config.config if retry_config else None
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Apply response middleware
|
|
624
|
+
for response_middleware in self._response_middlewares:
|
|
625
|
+
response = response_middleware.on_response(request, response)
|
|
626
|
+
|
|
627
|
+
# Apply response hooks
|
|
628
|
+
for response_hook in self._response_hooks:
|
|
629
|
+
response = response_hook.after_response(request, response)
|
|
630
|
+
|
|
631
|
+
# Cache response if using cache middleware and it's a GET request
|
|
632
|
+
if request.method == "GET":
|
|
633
|
+
for middleware in self._middlewares:
|
|
634
|
+
if isinstance(middleware, CacheMiddleware):
|
|
635
|
+
cache_key = middleware._cache_key(request)
|
|
636
|
+
middleware.cache[cache_key] = (response, time.time())
|
|
356
637
|
|
|
357
638
|
return_type = inspect.signature(method_call).return_annotation
|
|
358
639
|
|
|
@@ -407,13 +688,28 @@ __all__ = [
|
|
|
407
688
|
"Header",
|
|
408
689
|
"Body",
|
|
409
690
|
"PathParam",
|
|
691
|
+
"FormData",
|
|
692
|
+
"File",
|
|
693
|
+
"Timeout",
|
|
694
|
+
"RetryConfig",
|
|
695
|
+
"Retry",
|
|
696
|
+
"ContentType",
|
|
410
697
|
"RestClient",
|
|
411
698
|
"HttpRPCAsyncBackend",
|
|
412
699
|
"HttpRPCRequest",
|
|
413
700
|
"HttpRPCResponse",
|
|
414
701
|
"RPCRequestNetworkError",
|
|
702
|
+
"RPCUnhandleError",
|
|
415
703
|
"HttpRpcClientBuilder",
|
|
416
704
|
"RequestMiddleware",
|
|
705
|
+
"ResponseMiddleware",
|
|
706
|
+
"RequestHook",
|
|
707
|
+
"ResponseHook",
|
|
708
|
+
"AuthenticationMiddleware",
|
|
709
|
+
"BearerTokenAuth",
|
|
710
|
+
"BasicAuth",
|
|
711
|
+
"ApiKeyAuth",
|
|
712
|
+
"CacheMiddleware",
|
|
417
713
|
"TracedRequestMiddleware",
|
|
418
714
|
"GlobalHttpErrorHandler",
|
|
419
715
|
"RouteHttpErrorHandler",
|