jararaca 0.3.11a16__py3-none-any.whl → 0.4.0a5__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.
- README.md +121 -0
- jararaca/__init__.py +184 -12
- jararaca/__main__.py +4 -0
- jararaca/broker_backend/__init__.py +4 -0
- jararaca/broker_backend/mapper.py +4 -0
- jararaca/broker_backend/redis_broker_backend.py +9 -3
- jararaca/cli.py +272 -47
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +4 -0
- jararaca/core/uow.py +41 -7
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -0
- jararaca/lifecycle.py +6 -2
- jararaca/messagebus/__init__.py +4 -0
- jararaca/messagebus/bus_message_controller.py +4 -0
- jararaca/messagebus/consumers/__init__.py +3 -0
- jararaca/messagebus/decorators.py +33 -67
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +13 -4
- jararaca/messagebus/interceptors/publisher_interceptor.py +4 -0
- jararaca/messagebus/message.py +4 -0
- jararaca/messagebus/publisher.py +6 -0
- jararaca/messagebus/worker.py +850 -383
- jararaca/microservice.py +110 -1
- jararaca/observability/constants.py +7 -0
- jararaca/observability/decorators.py +170 -13
- jararaca/observability/fastapi_exception_handler.py +37 -0
- jararaca/observability/hooks.py +109 -0
- jararaca/observability/interceptor.py +4 -0
- jararaca/observability/providers/__init__.py +3 -0
- jararaca/observability/providers/otel.py +202 -11
- jararaca/persistence/base.py +38 -2
- jararaca/persistence/exports.py +4 -0
- jararaca/persistence/interceptors/__init__.py +3 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
- jararaca/persistence/interceptors/constants.py +5 -0
- jararaca/persistence/interceptors/decorators.py +50 -0
- jararaca/persistence/session.py +3 -0
- jararaca/persistence/sort_filter.py +4 -0
- jararaca/persistence/utilities.py +50 -20
- jararaca/presentation/__init__.py +3 -0
- jararaca/presentation/decorators.py +88 -86
- jararaca/presentation/exceptions.py +23 -0
- jararaca/presentation/hooks.py +4 -0
- jararaca/presentation/http_microservice.py +4 -0
- jararaca/presentation/server.py +97 -45
- jararaca/presentation/websocket/__init__.py +3 -0
- jararaca/presentation/websocket/base_types.py +4 -0
- jararaca/presentation/websocket/context.py +4 -0
- jararaca/presentation/websocket/decorators.py +8 -41
- jararaca/presentation/websocket/redis.py +280 -53
- jararaca/presentation/websocket/types.py +4 -0
- jararaca/presentation/websocket/websocket_interceptor.py +46 -19
- jararaca/reflect/__init__.py +3 -0
- jararaca/reflect/controller_inspect.py +16 -10
- jararaca/reflect/decorators.py +238 -0
- jararaca/reflect/metadata.py +34 -25
- jararaca/rpc/__init__.py +3 -0
- jararaca/rpc/http/__init__.py +101 -0
- jararaca/rpc/http/backends/__init__.py +14 -0
- jararaca/rpc/http/backends/httpx.py +43 -9
- jararaca/rpc/http/backends/otel.py +4 -0
- jararaca/rpc/http/decorators.py +378 -113
- jararaca/rpc/http/httpx.py +3 -0
- jararaca/scheduler/__init__.py +3 -0
- jararaca/scheduler/beat_worker.py +521 -105
- jararaca/scheduler/decorators.py +15 -22
- jararaca/scheduler/types.py +4 -0
- jararaca/tools/app_config/__init__.py +3 -0
- jararaca/tools/app_config/decorators.py +7 -19
- jararaca/tools/app_config/interceptor.py +6 -2
- jararaca/tools/typescript/__init__.py +3 -0
- jararaca/tools/typescript/decorators.py +120 -0
- jararaca/tools/typescript/interface_parser.py +1074 -173
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/rabbitmq_utils.py +65 -39
- jararaca/utils/retry.py +10 -3
- jararaca-0.4.0a5.dist-info/LICENSE +674 -0
- jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +11 -7
- jararaca-0.4.0a5.dist-info/RECORD +88 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
- pyproject.toml +131 -0
- jararaca-0.3.11a16.dist-info/RECORD +0 -74
- /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
jararaca/rpc/http/decorators.py
CHANGED
|
@@ -1,45 +1,50 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
1
6
|
import inspect
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
2
10
|
from dataclasses import dataclass
|
|
3
11
|
from typing import (
|
|
4
12
|
Any,
|
|
5
13
|
Awaitable,
|
|
6
14
|
Callable,
|
|
15
|
+
Dict,
|
|
7
16
|
Iterable,
|
|
8
17
|
Literal,
|
|
18
|
+
Optional,
|
|
9
19
|
Protocol,
|
|
10
|
-
Type,
|
|
11
20
|
TypeVar,
|
|
12
21
|
cast,
|
|
13
22
|
)
|
|
14
23
|
|
|
15
24
|
from pydantic import BaseModel
|
|
16
25
|
|
|
26
|
+
from jararaca.reflect.decorators import StackableDecorator
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
17
30
|
DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Awaitable[Any]])
|
|
31
|
+
DECORATED_CLASS = TypeVar("DECORATED_CLASS", bound=Any)
|
|
18
32
|
|
|
19
33
|
|
|
20
|
-
class
|
|
34
|
+
class TimeoutException(Exception):
|
|
35
|
+
"""Exception raised when a request times out"""
|
|
21
36
|
|
|
22
|
-
|
|
37
|
+
|
|
38
|
+
class HttpMapping(StackableDecorator):
|
|
23
39
|
|
|
24
40
|
def __init__(self, method: str, path: str, success_statuses: Iterable[int] = [200]):
|
|
25
41
|
self.method = method
|
|
26
42
|
self.path = path
|
|
27
43
|
self.success_statuses = success_statuses
|
|
28
44
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
@staticmethod
|
|
34
|
-
def register(funf: DECORATED_FUNC, instance: "HttpMapping") -> None:
|
|
35
|
-
setattr(funf, HttpMapping.HTTP_MAPPING_ATTR, instance)
|
|
36
|
-
|
|
37
|
-
@staticmethod
|
|
38
|
-
def get(funf: DECORATED_FUNC) -> "HttpMapping | None":
|
|
39
|
-
if hasattr(funf, HttpMapping.HTTP_MAPPING_ATTR):
|
|
40
|
-
return cast(HttpMapping, getattr(funf, HttpMapping.HTTP_MAPPING_ATTR))
|
|
41
|
-
|
|
42
|
-
return None
|
|
45
|
+
@classmethod
|
|
46
|
+
def decorator_key(cls) -> Any:
|
|
47
|
+
return HttpMapping
|
|
43
48
|
|
|
44
49
|
|
|
45
50
|
class Post(HttpMapping):
|
|
@@ -72,37 +77,19 @@ class Delete(HttpMapping):
|
|
|
72
77
|
super().__init__("DELETE", path)
|
|
73
78
|
|
|
74
79
|
|
|
75
|
-
class RequestAttribute:
|
|
76
|
-
|
|
77
|
-
REQUEST_ATTRIBUTE_ATTRS = "__request_attributes__"
|
|
78
|
-
|
|
79
|
-
@staticmethod
|
|
80
|
-
def register(cls: DECORATED_FUNC, instance: "RequestAttribute") -> None:
|
|
81
|
-
|
|
82
|
-
if not hasattr(cls, RequestAttribute.REQUEST_ATTRIBUTE_ATTRS):
|
|
83
|
-
setattr(cls, RequestAttribute.REQUEST_ATTRIBUTE_ATTRS, [])
|
|
84
|
-
|
|
85
|
-
getattr(cls, RequestAttribute.REQUEST_ATTRIBUTE_ATTRS).append(instance)
|
|
86
|
-
|
|
87
|
-
@staticmethod
|
|
88
|
-
def get(cls: DECORATED_FUNC) -> "list[RequestAttribute]":
|
|
89
|
-
if hasattr(cls, RequestAttribute.REQUEST_ATTRIBUTE_ATTRS):
|
|
90
|
-
return cast(
|
|
91
|
-
list[RequestAttribute],
|
|
92
|
-
getattr(cls, RequestAttribute.REQUEST_ATTRIBUTE_ATTRS),
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
return []
|
|
80
|
+
class RequestAttribute(StackableDecorator):
|
|
96
81
|
|
|
97
82
|
def __init__(
|
|
98
|
-
self,
|
|
83
|
+
self,
|
|
84
|
+
attribute_type: Literal["query", "header", "body", "param", "form", "file"],
|
|
85
|
+
name: str,
|
|
99
86
|
):
|
|
100
87
|
self.attribute_type = attribute_type
|
|
101
88
|
self.name = name
|
|
102
89
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return
|
|
90
|
+
@classmethod
|
|
91
|
+
def decorator_key(cls) -> Any:
|
|
92
|
+
return RequestAttribute
|
|
106
93
|
|
|
107
94
|
|
|
108
95
|
class Query(RequestAttribute):
|
|
@@ -129,39 +116,119 @@ class PathParam(RequestAttribute):
|
|
|
129
116
|
super().__init__("param", name)
|
|
130
117
|
|
|
131
118
|
|
|
132
|
-
|
|
119
|
+
class FormData(RequestAttribute):
|
|
120
|
+
"""Decorator for form data parameters"""
|
|
133
121
|
|
|
122
|
+
def __init__(self, name: str):
|
|
123
|
+
super().__init__("form", name)
|
|
134
124
|
|
|
135
|
-
class RestClient:
|
|
136
125
|
|
|
137
|
-
|
|
126
|
+
class File(RequestAttribute):
|
|
127
|
+
"""Decorator for file upload parameters"""
|
|
138
128
|
|
|
139
|
-
def __init__(self,
|
|
140
|
-
|
|
129
|
+
def __init__(self, name: str):
|
|
130
|
+
super().__init__("file", name)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Timeout:
|
|
134
|
+
"""Decorator for setting request timeout"""
|
|
135
|
+
|
|
136
|
+
TIMEOUT_ATTR = "__request_timeout__"
|
|
137
|
+
|
|
138
|
+
def __init__(self, seconds: float):
|
|
139
|
+
self.seconds = seconds
|
|
140
|
+
|
|
141
|
+
def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
142
|
+
setattr(func, self.TIMEOUT_ATTR, self)
|
|
143
|
+
return func
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def get(func: DECORATED_FUNC) -> Optional["Timeout"]:
|
|
147
|
+
return getattr(func, Timeout.TIMEOUT_ATTR, None)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class RetryConfig:
|
|
151
|
+
"""Configuration for retry behavior"""
|
|
152
|
+
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
max_attempts: int = 3,
|
|
156
|
+
backoff_factor: float = 1.0,
|
|
157
|
+
retry_on_status_codes: Optional[list[int]] = None,
|
|
158
|
+
):
|
|
159
|
+
self.max_attempts = max_attempts
|
|
160
|
+
self.backoff_factor = backoff_factor
|
|
161
|
+
self.retry_on_status_codes = retry_on_status_codes or [500, 502, 503, 504]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class Retry:
|
|
165
|
+
"""Decorator for retry configuration"""
|
|
166
|
+
|
|
167
|
+
RETRY_ATTR = "__request_retry__"
|
|
168
|
+
|
|
169
|
+
def __init__(self, config: RetryConfig):
|
|
170
|
+
self.config = config
|
|
171
|
+
|
|
172
|
+
def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
173
|
+
setattr(func, self.RETRY_ATTR, self)
|
|
174
|
+
return func
|
|
141
175
|
|
|
142
176
|
@staticmethod
|
|
143
|
-
def
|
|
144
|
-
|
|
177
|
+
def get(func: DECORATED_FUNC) -> Optional["Retry"]:
|
|
178
|
+
return getattr(func, Retry.RETRY_ATTR, None)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ContentType:
|
|
182
|
+
"""Decorator for specifying content type"""
|
|
183
|
+
|
|
184
|
+
CONTENT_TYPE_ATTR = "__content_type__"
|
|
185
|
+
|
|
186
|
+
def __init__(self, content_type: str):
|
|
187
|
+
self.content_type = content_type
|
|
188
|
+
|
|
189
|
+
def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
190
|
+
setattr(func, self.CONTENT_TYPE_ATTR, self)
|
|
191
|
+
return func
|
|
145
192
|
|
|
146
193
|
@staticmethod
|
|
147
|
-
def get(
|
|
148
|
-
|
|
149
|
-
|
|
194
|
+
def get(func: DECORATED_FUNC) -> Optional["ContentType"]:
|
|
195
|
+
return getattr(func, ContentType.CONTENT_TYPE_ATTR, None)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class ResponseMiddleware(Protocol):
|
|
199
|
+
"""Protocol for response middleware"""
|
|
200
|
+
|
|
201
|
+
def on_response(
|
|
202
|
+
self, request: "HttpRPCRequest", response: "HttpRPCResponse"
|
|
203
|
+
) -> "HttpRPCResponse": ...
|
|
204
|
+
|
|
150
205
|
|
|
151
|
-
|
|
206
|
+
class RequestHook(Protocol):
|
|
207
|
+
"""Protocol for request hooks"""
|
|
152
208
|
|
|
153
|
-
def
|
|
209
|
+
def before_request(self, request: "HttpRPCRequest") -> "HttpRPCRequest": ...
|
|
154
210
|
|
|
155
|
-
RestClient.register(cls, self)
|
|
156
211
|
|
|
157
|
-
|
|
212
|
+
class ResponseHook(Protocol):
|
|
213
|
+
"""Protocol for response hooks"""
|
|
214
|
+
|
|
215
|
+
def after_response(
|
|
216
|
+
self, request: "HttpRPCRequest", response: "HttpRPCResponse"
|
|
217
|
+
) -> "HttpRPCResponse": ...
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class RestClient(StackableDecorator):
|
|
221
|
+
|
|
222
|
+
def __init__(self, base_path: str) -> None:
|
|
223
|
+
self.base_path = base_path
|
|
158
224
|
|
|
159
225
|
|
|
160
226
|
@dataclass
|
|
161
227
|
class HttpRPCResponse:
|
|
162
|
-
|
|
163
228
|
status_code: int
|
|
164
229
|
data: bytes
|
|
230
|
+
headers: Optional[Dict[str, str]] = None
|
|
231
|
+
elapsed_time: Optional[float] = None
|
|
165
232
|
|
|
166
233
|
|
|
167
234
|
@dataclass
|
|
@@ -171,6 +238,9 @@ class HttpRPCRequest:
|
|
|
171
238
|
headers: list[tuple[str, str]]
|
|
172
239
|
query_params: dict[str, str]
|
|
173
240
|
body: bytes | None
|
|
241
|
+
timeout: Optional[float] = None
|
|
242
|
+
form_data: Optional[Dict[str, Any]] = None
|
|
243
|
+
files: Optional[Dict[str, Any]] = None
|
|
174
244
|
|
|
175
245
|
|
|
176
246
|
class RPCRequestNetworkError(Exception):
|
|
@@ -197,82 +267,110 @@ class HandleHttpErrorCallback(Protocol):
|
|
|
197
267
|
def __call__(self, request: HttpRPCRequest, response: HttpRPCResponse) -> Any: ...
|
|
198
268
|
|
|
199
269
|
|
|
200
|
-
class GlobalHttpErrorHandler:
|
|
270
|
+
class GlobalHttpErrorHandler(StackableDecorator):
|
|
201
271
|
|
|
202
|
-
|
|
272
|
+
def __init__(self, status_code: int, callback: HandleHttpErrorCallback):
|
|
273
|
+
self.status_code = status_code
|
|
274
|
+
self.callback = callback
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class RouteHttpErrorHandler(StackableDecorator):
|
|
203
278
|
|
|
204
279
|
def __init__(self, status_code: int, callback: HandleHttpErrorCallback):
|
|
205
280
|
self.status_code = status_code
|
|
206
281
|
self.callback = callback
|
|
207
282
|
|
|
208
|
-
def __call__(self, cls: Type[DECORATED_CLASS]) -> Type[DECORATED_CLASS]:
|
|
209
|
-
GlobalHttpErrorHandler.register(cls, self)
|
|
210
|
-
return cls
|
|
211
283
|
|
|
212
|
-
|
|
213
|
-
def register(
|
|
214
|
-
cls: Type[DECORATED_CLASS], instance: "GlobalHttpErrorHandler"
|
|
215
|
-
) -> None:
|
|
216
|
-
if not hasattr(cls, GlobalHttpErrorHandler.HTTP_ERROR_ATTR):
|
|
217
|
-
setattr(cls, GlobalHttpErrorHandler.HTTP_ERROR_ATTR, [])
|
|
284
|
+
class HttpRPCAsyncBackend(Protocol):
|
|
218
285
|
|
|
219
|
-
|
|
286
|
+
async def request(
|
|
287
|
+
self,
|
|
288
|
+
request: HttpRPCRequest,
|
|
289
|
+
) -> HttpRPCResponse: ...
|
|
220
290
|
|
|
221
|
-
@staticmethod
|
|
222
|
-
def get(cls: Type[DECORATED_CLASS]) -> "list[GlobalHttpErrorHandler]":
|
|
223
|
-
if hasattr(cls, GlobalHttpErrorHandler.HTTP_ERROR_ATTR):
|
|
224
|
-
return cast(
|
|
225
|
-
list[GlobalHttpErrorHandler],
|
|
226
|
-
getattr(cls, GlobalHttpErrorHandler.HTTP_ERROR_ATTR),
|
|
227
|
-
)
|
|
228
291
|
|
|
229
|
-
|
|
292
|
+
T = TypeVar("T")
|
|
230
293
|
|
|
231
294
|
|
|
232
|
-
class
|
|
295
|
+
class RequestMiddleware(Protocol):
|
|
233
296
|
|
|
234
|
-
|
|
297
|
+
def on_request(self, request: HttpRPCRequest) -> HttpRPCRequest: ...
|
|
235
298
|
|
|
236
|
-
def __init__(self, status_code: int, callback: HandleHttpErrorCallback):
|
|
237
|
-
self.status_code = status_code
|
|
238
|
-
self.callback = callback
|
|
239
299
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
return cls
|
|
300
|
+
class AuthenticationMiddleware(RequestMiddleware):
|
|
301
|
+
"""Base class for authentication middleware"""
|
|
243
302
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if not hasattr(cls, RouteHttpErrorHandler.ATTR):
|
|
247
|
-
setattr(cls, RouteHttpErrorHandler.ATTR, [])
|
|
303
|
+
def on_request(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
304
|
+
return self.add_auth(request)
|
|
248
305
|
|
|
249
|
-
|
|
306
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
307
|
+
raise NotImplementedError
|
|
250
308
|
|
|
251
|
-
@staticmethod
|
|
252
|
-
def get(cls: DECORATED_FUNC) -> "list[RouteHttpErrorHandler]":
|
|
253
|
-
if hasattr(cls, RouteHttpErrorHandler.ATTR):
|
|
254
|
-
return cast(
|
|
255
|
-
list[RouteHttpErrorHandler],
|
|
256
|
-
getattr(cls, RouteHttpErrorHandler.ATTR),
|
|
257
|
-
)
|
|
258
309
|
|
|
259
|
-
|
|
310
|
+
class BearerTokenAuth(AuthenticationMiddleware):
|
|
311
|
+
"""Bearer token authentication middleware"""
|
|
260
312
|
|
|
313
|
+
def __init__(self, token: str):
|
|
314
|
+
self.token = token
|
|
261
315
|
|
|
262
|
-
|
|
316
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
317
|
+
request.headers.append(("Authorization", f"Bearer {self.token}"))
|
|
318
|
+
return request
|
|
263
319
|
|
|
264
|
-
async def request(
|
|
265
|
-
self,
|
|
266
|
-
request: HttpRPCRequest,
|
|
267
|
-
) -> HttpRPCResponse: ...
|
|
268
320
|
|
|
321
|
+
class BasicAuth(AuthenticationMiddleware):
|
|
322
|
+
"""Basic authentication middleware"""
|
|
269
323
|
|
|
270
|
-
|
|
324
|
+
def __init__(self, username: str, password: str):
|
|
325
|
+
import base64
|
|
271
326
|
|
|
327
|
+
credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
|
|
328
|
+
self.credentials = credentials
|
|
272
329
|
|
|
273
|
-
|
|
330
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
331
|
+
request.headers.append(("Authorization", f"Basic {self.credentials}"))
|
|
332
|
+
return request
|
|
274
333
|
|
|
275
|
-
|
|
334
|
+
|
|
335
|
+
class ApiKeyAuth(AuthenticationMiddleware):
|
|
336
|
+
"""API key authentication middleware"""
|
|
337
|
+
|
|
338
|
+
def __init__(self, api_key: str, header_name: str = "X-API-Key"):
|
|
339
|
+
self.api_key = api_key
|
|
340
|
+
self.header_name = header_name
|
|
341
|
+
|
|
342
|
+
def add_auth(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
343
|
+
request.headers.append((self.header_name, self.api_key))
|
|
344
|
+
return request
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class CacheMiddleware(RequestMiddleware):
|
|
348
|
+
"""Simple in-memory cache middleware"""
|
|
349
|
+
|
|
350
|
+
def __init__(self, ttl_seconds: int = 300):
|
|
351
|
+
self.cache: Dict[str, tuple[Any, float]] = {}
|
|
352
|
+
self.ttl_seconds = ttl_seconds
|
|
353
|
+
|
|
354
|
+
def _cache_key(self, request: HttpRPCRequest) -> str:
|
|
355
|
+
"""Generate cache key from request"""
|
|
356
|
+
key_data = {
|
|
357
|
+
"method": request.method,
|
|
358
|
+
"url": request.url,
|
|
359
|
+
"query_params": request.query_params,
|
|
360
|
+
"headers": sorted(request.headers),
|
|
361
|
+
}
|
|
362
|
+
return str(hash(json.dumps(key_data, sort_keys=True)))
|
|
363
|
+
|
|
364
|
+
def on_request(self, request: HttpRPCRequest) -> HttpRPCRequest:
|
|
365
|
+
# Only cache GET requests
|
|
366
|
+
if request.method == "GET":
|
|
367
|
+
cache_key = self._cache_key(request)
|
|
368
|
+
if cache_key in self.cache:
|
|
369
|
+
cached_response, timestamp = self.cache[cache_key]
|
|
370
|
+
if time.time() - timestamp < self.ttl_seconds:
|
|
371
|
+
# Return cached response (this needs to be handled in the client builder)
|
|
372
|
+
setattr(request, "_cached_response", cached_response)
|
|
373
|
+
return request
|
|
276
374
|
|
|
277
375
|
|
|
278
376
|
class HttpRpcClientBuilder:
|
|
@@ -281,12 +379,79 @@ class HttpRpcClientBuilder:
|
|
|
281
379
|
self,
|
|
282
380
|
backend: HttpRPCAsyncBackend,
|
|
283
381
|
middlewares: list[RequestMiddleware] = [],
|
|
382
|
+
response_middlewares: list[ResponseMiddleware] = [],
|
|
383
|
+
request_hooks: list[RequestHook] = [],
|
|
384
|
+
response_hooks: list[ResponseHook] = [],
|
|
284
385
|
):
|
|
285
386
|
self._backend = backend
|
|
286
387
|
self._middlewares = middlewares
|
|
388
|
+
self._response_middlewares = response_middlewares
|
|
389
|
+
self._request_hooks = request_hooks
|
|
390
|
+
self._response_hooks = response_hooks
|
|
391
|
+
|
|
392
|
+
async def _execute_with_retry(
|
|
393
|
+
self, request: HttpRPCRequest, retry_config: Optional[RetryConfig]
|
|
394
|
+
) -> HttpRPCResponse:
|
|
395
|
+
"""Execute request with retry logic"""
|
|
396
|
+
if not retry_config:
|
|
397
|
+
logger.debug(
|
|
398
|
+
"Executing request without retry config: %s %s",
|
|
399
|
+
request.method,
|
|
400
|
+
request.url,
|
|
401
|
+
)
|
|
402
|
+
return await self._backend.request(request)
|
|
403
|
+
|
|
404
|
+
logger.debug(
|
|
405
|
+
"Executing request with retry config: %s %s (max_attempts=%s)",
|
|
406
|
+
request.method,
|
|
407
|
+
request.url,
|
|
408
|
+
retry_config.max_attempts,
|
|
409
|
+
)
|
|
410
|
+
last_exception = None
|
|
411
|
+
for attempt in range(retry_config.max_attempts):
|
|
412
|
+
try:
|
|
413
|
+
response = await self._backend.request(request)
|
|
414
|
+
|
|
415
|
+
# Check if we should retry based on status code
|
|
416
|
+
if response.status_code in retry_config.retry_on_status_codes:
|
|
417
|
+
logger.warning(
|
|
418
|
+
"Request failed with status %s, retrying (attempt %s/%s)",
|
|
419
|
+
response.status_code,
|
|
420
|
+
attempt + 1,
|
|
421
|
+
retry_config.max_attempts,
|
|
422
|
+
)
|
|
423
|
+
if attempt < retry_config.max_attempts - 1:
|
|
424
|
+
wait_time = retry_config.backoff_factor * (2**attempt)
|
|
425
|
+
await asyncio.sleep(wait_time)
|
|
426
|
+
continue
|
|
427
|
+
|
|
428
|
+
return response
|
|
429
|
+
|
|
430
|
+
except Exception as e:
|
|
431
|
+
last_exception = e
|
|
432
|
+
logger.warning(
|
|
433
|
+
"Request failed with exception: %s, retrying (attempt %s/%s)",
|
|
434
|
+
e,
|
|
435
|
+
attempt + 1,
|
|
436
|
+
retry_config.max_attempts,
|
|
437
|
+
)
|
|
438
|
+
if attempt < retry_config.max_attempts - 1:
|
|
439
|
+
wait_time = retry_config.backoff_factor * (2**attempt)
|
|
440
|
+
await asyncio.sleep(wait_time)
|
|
441
|
+
continue
|
|
442
|
+
else:
|
|
443
|
+
logger.error(
|
|
444
|
+
"Request failed after %s attempts: %s",
|
|
445
|
+
retry_config.max_attempts,
|
|
446
|
+
e,
|
|
447
|
+
)
|
|
448
|
+
raise
|
|
449
|
+
|
|
450
|
+
# This should never be reached, but just in case
|
|
451
|
+
raise last_exception or Exception("Retry failed")
|
|
287
452
|
|
|
288
453
|
def build(self, cls: type[T]) -> T:
|
|
289
|
-
rest_client = RestClient.
|
|
454
|
+
rest_client = RestClient.get_last(cls)
|
|
290
455
|
|
|
291
456
|
global_error_handlers = GlobalHttpErrorHandler.get(cls)
|
|
292
457
|
|
|
@@ -304,6 +469,12 @@ class HttpRpcClientBuilder:
|
|
|
304
469
|
call_parameters = [*call_signature.parameters.keys()][1:]
|
|
305
470
|
|
|
306
471
|
async def rpc_method(*args: Any, **kwargs: Any) -> Any:
|
|
472
|
+
logger.debug(
|
|
473
|
+
"Calling RPC method %s with args=%s kwargs=%s",
|
|
474
|
+
method_call.__name__,
|
|
475
|
+
args,
|
|
476
|
+
kwargs,
|
|
477
|
+
)
|
|
307
478
|
|
|
308
479
|
args_as_kwargs = dict(zip(call_parameters, args))
|
|
309
480
|
|
|
@@ -314,10 +485,17 @@ class HttpRpcClientBuilder:
|
|
|
314
485
|
headers: list[tuple[str, str]] = []
|
|
315
486
|
query_params = {}
|
|
316
487
|
body: Any = None
|
|
488
|
+
form_data: Dict[str, Any] = {}
|
|
489
|
+
files: Dict[str, Any] = {}
|
|
317
490
|
compiled_path = (
|
|
318
491
|
rest_client.base_path.rstrip("/") + "/" + mapping.path.lstrip("/")
|
|
319
492
|
)
|
|
320
493
|
|
|
494
|
+
# Get decorators for this method
|
|
495
|
+
timeout_config = Timeout.get(method_call)
|
|
496
|
+
retry_config = Retry.get(method_call)
|
|
497
|
+
content_type_config = ContentType.get(method_call)
|
|
498
|
+
|
|
321
499
|
for attr in request_attributes:
|
|
322
500
|
if attr.attribute_type == "header":
|
|
323
501
|
headers.append((attr.name, compiled_kwargs[attr.name]))
|
|
@@ -329,17 +507,33 @@ class HttpRpcClientBuilder:
|
|
|
329
507
|
compiled_path = compiled_path.replace(
|
|
330
508
|
f":{attr.name}", str(compiled_kwargs[attr.name])
|
|
331
509
|
)
|
|
510
|
+
elif attr.attribute_type == "form":
|
|
511
|
+
form_data[attr.name] = compiled_kwargs[attr.name]
|
|
512
|
+
elif attr.attribute_type == "file":
|
|
513
|
+
files[attr.name] = compiled_kwargs[attr.name]
|
|
332
514
|
|
|
333
515
|
body_content: bytes | None = None
|
|
334
516
|
|
|
517
|
+
# Handle different content types
|
|
335
518
|
if body is not None:
|
|
336
519
|
if isinstance(body, BaseModel):
|
|
337
520
|
body_content = body.model_dump_json().encode()
|
|
338
|
-
|
|
521
|
+
if not content_type_config:
|
|
522
|
+
headers.append(("Content-Type", "application/json"))
|
|
339
523
|
elif isinstance(body, bytes):
|
|
340
524
|
body_content = body
|
|
525
|
+
elif isinstance(body, str):
|
|
526
|
+
body_content = body.encode()
|
|
527
|
+
elif isinstance(body, dict):
|
|
528
|
+
body_content = json.dumps(body).encode()
|
|
529
|
+
if not content_type_config:
|
|
530
|
+
headers.append(("Content-Type", "application/json"))
|
|
341
531
|
else:
|
|
342
|
-
raise ValueError("Invalid body type")
|
|
532
|
+
raise ValueError(f"Invalid body type: {type(body)}")
|
|
533
|
+
|
|
534
|
+
# Apply custom content type if specified
|
|
535
|
+
if content_type_config:
|
|
536
|
+
headers.append(("Content-Type", content_type_config.content_type))
|
|
343
537
|
|
|
344
538
|
request = HttpRPCRequest(
|
|
345
539
|
url=compiled_path,
|
|
@@ -347,20 +541,75 @@ class HttpRpcClientBuilder:
|
|
|
347
541
|
headers=headers,
|
|
348
542
|
query_params=query_params,
|
|
349
543
|
body=body_content,
|
|
544
|
+
timeout=timeout_config.seconds if timeout_config else None,
|
|
545
|
+
form_data=form_data if form_data else None,
|
|
546
|
+
files=files if files else None,
|
|
350
547
|
)
|
|
351
548
|
|
|
549
|
+
logger.debug(
|
|
550
|
+
"Prepared request: %s %s\nHeaders: %s\nQuery Params: %s\nBody: %s",
|
|
551
|
+
request.method,
|
|
552
|
+
request.url,
|
|
553
|
+
request.headers,
|
|
554
|
+
request.query_params,
|
|
555
|
+
request.body,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
# Apply request hooks
|
|
559
|
+
for hook in self._request_hooks:
|
|
560
|
+
request = hook.before_request(request)
|
|
561
|
+
|
|
352
562
|
for middleware in self._middlewares:
|
|
353
563
|
request = middleware.on_request(request)
|
|
354
564
|
|
|
355
|
-
|
|
565
|
+
# Check for cached response
|
|
566
|
+
if hasattr(request, "_cached_response"):
|
|
567
|
+
logger.debug("Using cached response")
|
|
568
|
+
response = getattr(request, "_cached_response")
|
|
569
|
+
else:
|
|
570
|
+
# Execute request with retry if configured
|
|
571
|
+
logger.debug("Executing request...")
|
|
572
|
+
response = await self._execute_with_retry(
|
|
573
|
+
request, retry_config.config if retry_config else None
|
|
574
|
+
)
|
|
575
|
+
logger.debug("Received response: status=%s", response.status_code)
|
|
576
|
+
|
|
577
|
+
# Apply response middleware
|
|
578
|
+
for response_middleware in self._response_middlewares:
|
|
579
|
+
response = response_middleware.on_response(request, response)
|
|
580
|
+
|
|
581
|
+
# Apply response hooks
|
|
582
|
+
for response_hook in self._response_hooks:
|
|
583
|
+
response = response_hook.after_response(request, response)
|
|
584
|
+
|
|
585
|
+
# Cache response if using cache middleware and it's a GET request
|
|
586
|
+
if request.method == "GET":
|
|
587
|
+
for middleware in self._middlewares:
|
|
588
|
+
if isinstance(middleware, CacheMiddleware):
|
|
589
|
+
cache_key = middleware._cache_key(request)
|
|
590
|
+
middleware.cache[cache_key] = (response, time.time())
|
|
356
591
|
|
|
357
592
|
return_type = inspect.signature(method_call).return_annotation
|
|
358
593
|
|
|
359
594
|
if response.status_code not in mapping.success_statuses:
|
|
595
|
+
logger.warning(
|
|
596
|
+
"Response status %s not in success statuses %s",
|
|
597
|
+
response.status_code,
|
|
598
|
+
mapping.success_statuses,
|
|
599
|
+
)
|
|
360
600
|
for error_handler in route_error_handlers + global_error_handlers:
|
|
361
601
|
if error_handler.status_code == response.status_code:
|
|
602
|
+
logger.debug(
|
|
603
|
+
"Handling error with handler for status %s",
|
|
604
|
+
response.status_code,
|
|
605
|
+
)
|
|
362
606
|
return error_handler.callback(request, response)
|
|
363
607
|
|
|
608
|
+
logger.error(
|
|
609
|
+
"Unhandled RPC error: %s %s",
|
|
610
|
+
response.status_code,
|
|
611
|
+
response.data,
|
|
612
|
+
)
|
|
364
613
|
raise RPCUnhandleError(request, response, None)
|
|
365
614
|
|
|
366
615
|
if return_type is not inspect.Signature.empty:
|
|
@@ -379,9 +628,10 @@ class HttpRpcClientBuilder:
|
|
|
379
628
|
|
|
380
629
|
dummy = Dummy()
|
|
381
630
|
|
|
382
|
-
for attr_name in
|
|
383
|
-
|
|
384
|
-
|
|
631
|
+
for attr_name, method_call in inspect.getmembers(
|
|
632
|
+
cls, predicate=inspect.isfunction
|
|
633
|
+
):
|
|
634
|
+
if (mapping := HttpMapping.get_last(method_call)) is not None:
|
|
385
635
|
route_error_handlers = RouteHttpErrorHandler.get(method_call)
|
|
386
636
|
setattr(
|
|
387
637
|
dummy,
|
|
@@ -407,13 +657,28 @@ __all__ = [
|
|
|
407
657
|
"Header",
|
|
408
658
|
"Body",
|
|
409
659
|
"PathParam",
|
|
660
|
+
"FormData",
|
|
661
|
+
"File",
|
|
662
|
+
"Timeout",
|
|
663
|
+
"RetryConfig",
|
|
664
|
+
"Retry",
|
|
665
|
+
"ContentType",
|
|
410
666
|
"RestClient",
|
|
411
667
|
"HttpRPCAsyncBackend",
|
|
412
668
|
"HttpRPCRequest",
|
|
413
669
|
"HttpRPCResponse",
|
|
414
670
|
"RPCRequestNetworkError",
|
|
671
|
+
"RPCUnhandleError",
|
|
415
672
|
"HttpRpcClientBuilder",
|
|
416
673
|
"RequestMiddleware",
|
|
674
|
+
"ResponseMiddleware",
|
|
675
|
+
"RequestHook",
|
|
676
|
+
"ResponseHook",
|
|
677
|
+
"AuthenticationMiddleware",
|
|
678
|
+
"BearerTokenAuth",
|
|
679
|
+
"BasicAuth",
|
|
680
|
+
"ApiKeyAuth",
|
|
681
|
+
"CacheMiddleware",
|
|
417
682
|
"TracedRequestMiddleware",
|
|
418
683
|
"GlobalHttpErrorHandler",
|
|
419
684
|
"RouteHttpErrorHandler",
|
jararaca/rpc/http/httpx.py
CHANGED
jararaca/scheduler/__init__.py
CHANGED