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