jararaca 0.3.12a3__py3-none-any.whl → 0.3.12a5__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.
- jararaca/__init__.py +72 -8
- jararaca/messagebus/worker.py +102 -84
- jararaca/microservice.py +42 -0
- jararaca/presentation/server.py +57 -11
- 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/tools/typescript/interface_parser.py +1 -1
- {jararaca-0.3.12a3.dist-info → jararaca-0.3.12a5.dist-info}/METADATA +1 -1
- {jararaca-0.3.12a3.dist-info → jararaca-0.3.12a5.dist-info}/RECORD +15 -15
- pyproject.toml +1 -1
- {jararaca-0.3.12a3.dist-info → jararaca-0.3.12a5.dist-info}/LICENSE +0 -0
- {jararaca-0.3.12a3.dist-info → jararaca-0.3.12a5.dist-info}/WHEEL +0 -0
- {jararaca-0.3.12a3.dist-info → jararaca-0.3.12a5.dist-info}/entry_points.txt +0 -0
jararaca/__init__.py
CHANGED
|
@@ -45,26 +45,47 @@ if TYPE_CHECKING:
|
|
|
45
45
|
)
|
|
46
46
|
from jararaca.rpc.http.backends.httpx import HTTPXHttpRPCAsyncBackend
|
|
47
47
|
from jararaca.rpc.http.backends.otel import TracedRequestMiddleware
|
|
48
|
-
from jararaca.rpc.http.decorators import
|
|
48
|
+
from jararaca.rpc.http.decorators import ( # New request parameter decorators; Configuration decorators; Authentication classes; Middleware classes; Configuration classes; Exception classes
|
|
49
|
+
ApiKeyAuth,
|
|
50
|
+
AuthenticationMiddleware,
|
|
51
|
+
BasicAuth,
|
|
52
|
+
BearerTokenAuth,
|
|
53
|
+
Body,
|
|
54
|
+
CacheMiddleware,
|
|
55
|
+
ContentType,
|
|
56
|
+
)
|
|
49
57
|
from jararaca.rpc.http.decorators import Delete as HttpDelete
|
|
58
|
+
from jararaca.rpc.http.decorators import ( # New request parameter decorators; Configuration decorators; Authentication classes; Middleware classes; Configuration classes; Exception classes
|
|
59
|
+
File,
|
|
60
|
+
FormData,
|
|
61
|
+
)
|
|
50
62
|
from jararaca.rpc.http.decorators import Get as HttpGet
|
|
51
|
-
from jararaca.rpc.http.decorators import (
|
|
63
|
+
from jararaca.rpc.http.decorators import ( # New request parameter decorators; Configuration decorators; Authentication classes; Middleware classes; Configuration classes; Exception classes
|
|
52
64
|
GlobalHttpErrorHandler,
|
|
53
65
|
Header,
|
|
54
66
|
HttpMapping,
|
|
55
67
|
HttpRpcClientBuilder,
|
|
56
68
|
)
|
|
57
69
|
from jararaca.rpc.http.decorators import Patch as HttpPatch
|
|
58
|
-
from jararaca.rpc.http.decorators import
|
|
70
|
+
from jararaca.rpc.http.decorators import ( # New request parameter decorators; Configuration decorators; Authentication classes; Middleware classes; Configuration classes; Exception classes
|
|
71
|
+
PathParam,
|
|
72
|
+
)
|
|
59
73
|
from jararaca.rpc.http.decorators import Post as HttpPost
|
|
60
74
|
from jararaca.rpc.http.decorators import Put as HttpPut
|
|
61
|
-
from jararaca.rpc.http.decorators import (
|
|
75
|
+
from jararaca.rpc.http.decorators import ( # New request parameter decorators; Configuration decorators; Authentication classes; Middleware classes; Configuration classes; Exception classes
|
|
62
76
|
Query,
|
|
63
77
|
RequestAttribute,
|
|
78
|
+
RequestHook,
|
|
79
|
+
ResponseHook,
|
|
80
|
+
ResponseMiddleware,
|
|
64
81
|
RestClient,
|
|
82
|
+
Retry,
|
|
83
|
+
RetryConfig,
|
|
65
84
|
RouteHttpErrorHandler,
|
|
66
85
|
RPCRequestNetworkError,
|
|
67
86
|
RPCUnhandleError,
|
|
87
|
+
Timeout,
|
|
88
|
+
TimeoutException,
|
|
68
89
|
)
|
|
69
90
|
|
|
70
91
|
from .core.providers import ProviderSpec, Token
|
|
@@ -81,6 +102,8 @@ if TYPE_CHECKING:
|
|
|
81
102
|
from .messagebus.publisher import use_publisher
|
|
82
103
|
from .microservice import (
|
|
83
104
|
Microservice,
|
|
105
|
+
is_shutting_down,
|
|
106
|
+
request_shutdown,
|
|
84
107
|
use_app_context,
|
|
85
108
|
use_app_transaction_context,
|
|
86
109
|
use_app_tx_ctx_data,
|
|
@@ -186,6 +209,12 @@ if TYPE_CHECKING:
|
|
|
186
209
|
"QueryInjector",
|
|
187
210
|
"HttpMicroservice",
|
|
188
211
|
"use_current_container",
|
|
212
|
+
"use_app_context",
|
|
213
|
+
"use_app_transaction_context",
|
|
214
|
+
"use_app_tx_ctx_data",
|
|
215
|
+
"is_shutting_down",
|
|
216
|
+
"request_shutdown",
|
|
217
|
+
"Microservice",
|
|
189
218
|
"T_BASEMODEL",
|
|
190
219
|
"DatedEntity",
|
|
191
220
|
"BaseEntity",
|
|
@@ -208,7 +237,6 @@ if TYPE_CHECKING:
|
|
|
208
237
|
"MessageBusController",
|
|
209
238
|
"MessageHandler",
|
|
210
239
|
"ScheduledAction",
|
|
211
|
-
"Microservice",
|
|
212
240
|
"ProviderSpec",
|
|
213
241
|
"Token",
|
|
214
242
|
"AIOSqlAlchemySessionInterceptor",
|
|
@@ -242,9 +270,27 @@ if TYPE_CHECKING:
|
|
|
242
270
|
"provide_ws_manager",
|
|
243
271
|
"HttpRpcClientBuilder",
|
|
244
272
|
"HTTPXHttpRPCAsyncBackend",
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
"
|
|
273
|
+
# New request parameter decorators
|
|
274
|
+
"FormData",
|
|
275
|
+
"File",
|
|
276
|
+
# Configuration decorators
|
|
277
|
+
"Timeout",
|
|
278
|
+
"Retry",
|
|
279
|
+
"ContentType",
|
|
280
|
+
# Authentication classes
|
|
281
|
+
"BearerTokenAuth",
|
|
282
|
+
"BasicAuth",
|
|
283
|
+
"ApiKeyAuth",
|
|
284
|
+
# Middleware classes
|
|
285
|
+
"CacheMiddleware",
|
|
286
|
+
"AuthenticationMiddleware",
|
|
287
|
+
"ResponseMiddleware",
|
|
288
|
+
"RequestHook",
|
|
289
|
+
"ResponseHook",
|
|
290
|
+
# Configuration classes
|
|
291
|
+
"RetryConfig",
|
|
292
|
+
# Exception classes
|
|
293
|
+
"TimeoutException",
|
|
248
294
|
"AppTransactionContext",
|
|
249
295
|
"AppContext",
|
|
250
296
|
"ControllerMemberReflect",
|
|
@@ -440,9 +486,27 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
|
440
486
|
"provide_ws_manager": (__SPEC_PARENT__, "presentation.websocket.context", None),
|
|
441
487
|
"HttpRpcClientBuilder": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
442
488
|
"HTTPXHttpRPCAsyncBackend": (__SPEC_PARENT__, "rpc.http.backends.httpx", None),
|
|
489
|
+
# New HTTP RPC classes
|
|
490
|
+
"FormData": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
491
|
+
"File": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
492
|
+
"Timeout": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
493
|
+
"Retry": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
494
|
+
"ContentType": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
495
|
+
"BearerTokenAuth": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
496
|
+
"BasicAuth": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
497
|
+
"ApiKeyAuth": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
498
|
+
"CacheMiddleware": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
499
|
+
"AuthenticationMiddleware": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
500
|
+
"ResponseMiddleware": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
501
|
+
"RequestHook": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
502
|
+
"ResponseHook": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
503
|
+
"RetryConfig": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
504
|
+
"TimeoutException": (__SPEC_PARENT__, "rpc.http.decorators", None),
|
|
443
505
|
"use_app_context": (__SPEC_PARENT__, "microservice", None),
|
|
444
506
|
"use_app_transaction_context": (__SPEC_PARENT__, "microservice", None),
|
|
445
507
|
"use_app_tx_ctx_data": (__SPEC_PARENT__, "microservice", None),
|
|
508
|
+
"is_shutting_down": (__SPEC_PARENT__, "microservice", None),
|
|
509
|
+
"request_shutdown": (__SPEC_PARENT__, "microservice", None),
|
|
446
510
|
"AppContext": (__SPEC_PARENT__, "microservice", None),
|
|
447
511
|
"AppInterceptor": (__SPEC_PARENT__, "microservice", None),
|
|
448
512
|
"AppTransactionContext": (__SPEC_PARENT__, "microservice", None),
|
jararaca/messagebus/worker.py
CHANGED
|
@@ -49,6 +49,8 @@ from jararaca.microservice import (
|
|
|
49
49
|
MessageBusTransactionData,
|
|
50
50
|
Microservice,
|
|
51
51
|
SchedulerTransactionData,
|
|
52
|
+
ShutdownState,
|
|
53
|
+
provide_shutdown_state,
|
|
52
54
|
)
|
|
53
55
|
from jararaca.scheduler.decorators import ScheduledActionData
|
|
54
56
|
from jararaca.utils.rabbitmq_utils import RabbitmqUtils
|
|
@@ -129,6 +131,17 @@ class MessageBusConsumer(ABC):
|
|
|
129
131
|
"""Close all resources related to the consumer"""
|
|
130
132
|
|
|
131
133
|
|
|
134
|
+
class _WorkerShutdownState(ShutdownState):
|
|
135
|
+
def __init__(self, shutdown_event: asyncio.Event):
|
|
136
|
+
self.shutdown_event = shutdown_event
|
|
137
|
+
|
|
138
|
+
def request_shutdown(self) -> None:
|
|
139
|
+
self.shutdown_event.set()
|
|
140
|
+
|
|
141
|
+
def is_shutdown_requested(self) -> bool:
|
|
142
|
+
return self.shutdown_event.is_set()
|
|
143
|
+
|
|
144
|
+
|
|
132
145
|
class AioPikaMicroserviceConsumer(MessageBusConsumer):
|
|
133
146
|
def __init__(
|
|
134
147
|
self,
|
|
@@ -146,6 +159,7 @@ class AioPikaMicroserviceConsumer(MessageBusConsumer):
|
|
|
146
159
|
self.incoming_map: dict[str, MessageHandlerData] = {}
|
|
147
160
|
self.uow_context_provider = uow_context_provider
|
|
148
161
|
self.shutdown_event = asyncio.Event()
|
|
162
|
+
self.shutdown_state = _WorkerShutdownState(self.shutdown_event)
|
|
149
163
|
self.lock = asyncio.Lock()
|
|
150
164
|
self.tasks: set[asyncio.Task[Any]] = set()
|
|
151
165
|
self.connection: aio_pika.abc.AbstractConnection | None = None
|
|
@@ -832,18 +846,19 @@ class ScheduledMessageHandlerCallback:
|
|
|
832
846
|
args: tuple[Any, ...],
|
|
833
847
|
kwargs: dict[str, Any],
|
|
834
848
|
) -> None:
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
849
|
+
with provide_shutdown_state(self.consumer.shutdown_state):
|
|
850
|
+
async with self.consumer.uow_context_provider(
|
|
851
|
+
AppTransactionContext(
|
|
852
|
+
controller_member_reflect=scheduled_action.controller_member,
|
|
853
|
+
transaction_data=SchedulerTransactionData(
|
|
854
|
+
scheduled_to=datetime.now(UTC),
|
|
855
|
+
cron_expression=scheduled_action.spec.cron,
|
|
856
|
+
triggered_at=datetime.now(UTC),
|
|
857
|
+
),
|
|
858
|
+
)
|
|
859
|
+
):
|
|
845
860
|
|
|
846
|
-
|
|
861
|
+
await scheduled_action.callable(*args, **kwargs)
|
|
847
862
|
|
|
848
863
|
|
|
849
864
|
class MessageHandlerCallback:
|
|
@@ -1133,83 +1148,86 @@ class MessageHandlerCallback:
|
|
|
1133
1148
|
incoming_message_spec = MessageHandler.get_message_incoming(handler)
|
|
1134
1149
|
assert incoming_message_spec is not None
|
|
1135
1150
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1151
|
+
with provide_shutdown_state(self.consumer.shutdown_state):
|
|
1152
|
+
async with self.consumer.uow_context_provider(
|
|
1153
|
+
AppTransactionContext(
|
|
1154
|
+
controller_member_reflect=handler_data.controller_member,
|
|
1155
|
+
transaction_data=MessageBusTransactionData(
|
|
1156
|
+
message=builded_message,
|
|
1157
|
+
topic=routing_key,
|
|
1158
|
+
),
|
|
1159
|
+
)
|
|
1160
|
+
):
|
|
1161
|
+
ctx: AsyncContextManager[Any]
|
|
1162
|
+
if incoming_message_spec.timeout is not None:
|
|
1163
|
+
ctx = asyncio.timeout(incoming_message_spec.timeout)
|
|
1164
|
+
else:
|
|
1165
|
+
ctx = none_context()
|
|
1166
|
+
async with ctx:
|
|
1167
|
+
try:
|
|
1168
|
+
with provide_bus_message_controller(
|
|
1169
|
+
AioPikaMessageBusController(aio_pika_message)
|
|
1170
|
+
):
|
|
1171
|
+
await handler(builded_message)
|
|
1172
|
+
if not incoming_message_spec.auto_ack:
|
|
1173
|
+
with suppress(aio_pika.MessageProcessError):
|
|
1174
|
+
# Use channel context for acknowledgement
|
|
1175
|
+
async with self.consumer.get_channel_ctx(
|
|
1176
|
+
self.queue_name
|
|
1177
|
+
):
|
|
1178
|
+
await aio_pika_message.ack()
|
|
1179
|
+
except BaseException as base_exc:
|
|
1180
|
+
# Get message id for logging
|
|
1181
|
+
message_id = aio_pika_message.message_id or str(uuid.uuid4())
|
|
1182
|
+
|
|
1183
|
+
# Extract retry count from headers if available
|
|
1184
|
+
headers = aio_pika_message.headers or {}
|
|
1185
|
+
retry_count = int(str(headers.get("x-retry-count", 0)))
|
|
1186
|
+
|
|
1187
|
+
# Process exception handler if configured
|
|
1188
|
+
if incoming_message_spec.exception_handler is not None:
|
|
1189
|
+
try:
|
|
1190
|
+
incoming_message_spec.exception_handler(base_exc)
|
|
1191
|
+
except Exception as nested_exc:
|
|
1192
|
+
logger.exception(
|
|
1193
|
+
f"Error processing exception handler for message {message_id}: {base_exc} | {nested_exc}"
|
|
1194
|
+
)
|
|
1195
|
+
else:
|
|
1174
1196
|
logger.exception(
|
|
1175
|
-
f"Error processing
|
|
1197
|
+
f"Error processing message {message_id} on topic {routing_key}: {str(base_exc)}"
|
|
1176
1198
|
)
|
|
1177
|
-
else:
|
|
1178
|
-
logger.exception(
|
|
1179
|
-
f"Error processing message {message_id} on topic {routing_key}: {str(base_exc)}"
|
|
1180
|
-
)
|
|
1181
1199
|
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
else:
|
|
1197
|
-
# Message processed successfully, log and clean up any retry state
|
|
1198
|
-
message_id = aio_pika_message.message_id or str(uuid.uuid4())
|
|
1199
|
-
if message_id in self.retry_state:
|
|
1200
|
-
del self.retry_state[message_id]
|
|
1201
|
-
|
|
1202
|
-
# Log success with retry information if applicable
|
|
1203
|
-
headers = aio_pika_message.headers or {}
|
|
1204
|
-
if "x-retry-count" in headers:
|
|
1205
|
-
retry_count = int(str(headers.get("x-retry-count", 0)))
|
|
1206
|
-
logger.info(
|
|
1207
|
-
f"Message {message_id}#{self.queue_name} processed successfully after {retry_count} retries"
|
|
1208
|
-
)
|
|
1200
|
+
# Handle rejection with retry logic
|
|
1201
|
+
if incoming_message_spec.requeue_on_exception:
|
|
1202
|
+
# Use our retry with backoff mechanism
|
|
1203
|
+
await self.handle_reject_message(
|
|
1204
|
+
aio_pika_message,
|
|
1205
|
+
requeue=False, # Don't requeue directly, use our backoff mechanism
|
|
1206
|
+
retry_count=retry_count,
|
|
1207
|
+
exception=base_exc,
|
|
1208
|
+
)
|
|
1209
|
+
else:
|
|
1210
|
+
# Message shouldn't be retried, reject it
|
|
1211
|
+
await self.handle_reject_message(
|
|
1212
|
+
aio_pika_message, requeue=False, exception=base_exc
|
|
1213
|
+
)
|
|
1209
1214
|
else:
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1215
|
+
# Message processed successfully, log and clean up any retry state
|
|
1216
|
+
message_id = aio_pika_message.message_id or str(uuid.uuid4())
|
|
1217
|
+
if message_id in self.retry_state:
|
|
1218
|
+
del self.retry_state[message_id]
|
|
1219
|
+
|
|
1220
|
+
# Log success with retry information if applicable
|
|
1221
|
+
headers = aio_pika_message.headers or {}
|
|
1222
|
+
if "x-retry-count" in headers:
|
|
1223
|
+
retry_count = int(str(headers.get("x-retry-count", 0)))
|
|
1224
|
+
logger.info(
|
|
1225
|
+
f"Message {message_id}#{self.queue_name} processed successfully after {retry_count} retries"
|
|
1226
|
+
)
|
|
1227
|
+
else:
|
|
1228
|
+
logger.info(
|
|
1229
|
+
f"Message {message_id}#{self.queue_name} processed successfully"
|
|
1230
|
+
)
|
|
1213
1231
|
|
|
1214
1232
|
|
|
1215
1233
|
@asynccontextmanager
|
jararaca/microservice.py
CHANGED
|
@@ -325,6 +325,48 @@ def provide_container(container: Container) -> Generator[None, None, None]:
|
|
|
325
325
|
current_container_ctx.reset(token)
|
|
326
326
|
|
|
327
327
|
|
|
328
|
+
class ShutdownState(Protocol):
|
|
329
|
+
|
|
330
|
+
def request_shutdown(self) -> None: ...
|
|
331
|
+
|
|
332
|
+
def is_shutdown_requested(self) -> bool: ...
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
shutdown_state_ctx = ContextVar[ShutdownState]("shutdown_state")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def is_shutting_down() -> bool:
|
|
339
|
+
"""
|
|
340
|
+
Check if the application is in the process of shutting down.
|
|
341
|
+
"""
|
|
342
|
+
return shutdown_state_ctx.get().is_shutdown_requested()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def request_shutdown() -> None:
|
|
346
|
+
"""
|
|
347
|
+
Request the application to shut down.
|
|
348
|
+
This will set the shutdown event, allowing the application to gracefully shut down.
|
|
349
|
+
"""
|
|
350
|
+
shutdown_state_ctx.get().request_shutdown()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@contextmanager
|
|
354
|
+
def provide_shutdown_state(
|
|
355
|
+
state: ShutdownState,
|
|
356
|
+
) -> Generator[None, None, None]:
|
|
357
|
+
"""
|
|
358
|
+
Context manager to provide the shutdown state.
|
|
359
|
+
This is used to manage the shutdown event for the application.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
token = shutdown_state_ctx.set(state)
|
|
363
|
+
try:
|
|
364
|
+
yield
|
|
365
|
+
finally:
|
|
366
|
+
with suppress(ValueError):
|
|
367
|
+
shutdown_state_ctx.reset(token)
|
|
368
|
+
|
|
369
|
+
|
|
328
370
|
__all__ = [
|
|
329
371
|
"AppTransactionContext",
|
|
330
372
|
"AppInterceptor",
|
jararaca/presentation/server.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import signal
|
|
3
|
+
import threading
|
|
1
4
|
from contextlib import asynccontextmanager
|
|
5
|
+
from signal import SIGINT, SIGTERM
|
|
2
6
|
from typing import Any, AsyncGenerator
|
|
3
7
|
|
|
4
8
|
from fastapi import Depends, FastAPI, Request, WebSocket
|
|
@@ -10,7 +14,9 @@ from jararaca.lifecycle import AppLifecycle
|
|
|
10
14
|
from jararaca.microservice import (
|
|
11
15
|
AppTransactionContext,
|
|
12
16
|
HttpTransactionData,
|
|
17
|
+
ShutdownState,
|
|
13
18
|
WebSocketTransactionData,
|
|
19
|
+
provide_shutdown_state,
|
|
14
20
|
)
|
|
15
21
|
from jararaca.presentation.decorators import RestController
|
|
16
22
|
from jararaca.presentation.http_microservice import HttpMicroservice
|
|
@@ -76,10 +82,49 @@ class HttpAppLifecycle:
|
|
|
76
82
|
yield
|
|
77
83
|
|
|
78
84
|
|
|
85
|
+
class HttpShutdownState(ShutdownState):
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._requested = False
|
|
88
|
+
self.old_signal_handlers = {
|
|
89
|
+
SIGINT: signal.getsignal(SIGINT),
|
|
90
|
+
SIGTERM: signal.getsignal(SIGTERM),
|
|
91
|
+
}
|
|
92
|
+
self.thread_lock = threading.Lock()
|
|
93
|
+
|
|
94
|
+
def request_shutdown(self) -> None:
|
|
95
|
+
if not self._requested:
|
|
96
|
+
self._requested = True
|
|
97
|
+
os.kill(os.getpid(), SIGINT)
|
|
98
|
+
|
|
99
|
+
def is_shutdown_requested(self) -> bool:
|
|
100
|
+
return self._requested
|
|
101
|
+
|
|
102
|
+
def handle_signal(self, signum: int, frame: Any) -> None:
|
|
103
|
+
print(f"Received signal {signum}, initiating shutdown...")
|
|
104
|
+
if self._requested:
|
|
105
|
+
print("Shutdown already requested, ignoring signal.")
|
|
106
|
+
return
|
|
107
|
+
print("Requesting shutdown...")
|
|
108
|
+
self._requested = True
|
|
109
|
+
|
|
110
|
+
# remove the signal handler to prevent recursion
|
|
111
|
+
for sig in (SIGINT, SIGTERM):
|
|
112
|
+
if self.old_signal_handlers[sig] is not None:
|
|
113
|
+
signal.signal(sig, self.old_signal_handlers[sig])
|
|
114
|
+
|
|
115
|
+
signal.raise_signal(signum)
|
|
116
|
+
|
|
117
|
+
def setup_signal_handlers(self) -> None:
|
|
118
|
+
signal.signal(SIGINT, self.handle_signal)
|
|
119
|
+
signal.signal(SIGTERM, self.handle_signal)
|
|
120
|
+
|
|
121
|
+
|
|
79
122
|
class HttpUowContextProviderDependency:
|
|
80
123
|
|
|
81
124
|
def __init__(self, uow_provider: UnitOfWorkContextProvider) -> None:
|
|
82
125
|
self.uow_provider = uow_provider
|
|
126
|
+
self.shutdown_state = HttpShutdownState()
|
|
127
|
+
self.shutdown_state.setup_signal_handlers()
|
|
83
128
|
|
|
84
129
|
async def __call__(
|
|
85
130
|
self, websocket: WebSocket = None, request: Request = None # type: ignore
|
|
@@ -101,17 +146,18 @@ class HttpUowContextProviderDependency:
|
|
|
101
146
|
"ControllerMemberReflect, but got: {}".format(type(member))
|
|
102
147
|
)
|
|
103
148
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
with provide_shutdown_state(self.shutdown_state):
|
|
150
|
+
async with self.uow_provider(
|
|
151
|
+
AppTransactionContext(
|
|
152
|
+
controller_member_reflect=member,
|
|
153
|
+
transaction_data=(
|
|
154
|
+
HttpTransactionData(request=request)
|
|
155
|
+
if request
|
|
156
|
+
else WebSocketTransactionData(websocket=websocket)
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
):
|
|
160
|
+
yield
|
|
115
161
|
|
|
116
162
|
|
|
117
163
|
def create_http_server(
|
jararaca/rpc/http/__init__.py
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# HTTP RPC Module - Complete REST Client Implementation
|
|
2
|
+
"""
|
|
3
|
+
This module provides a complete REST client implementation with support for:
|
|
4
|
+
- HTTP method decorators (@Get, @Post, @Put, @Patch, @Delete)
|
|
5
|
+
- Request parameter decorators (@Query, @Header, @PathParam, @Body, @FormData, @File)
|
|
6
|
+
- Configuration decorators (@Timeout, @Retry, @ContentType)
|
|
7
|
+
- Authentication middleware (BearerTokenAuth, BasicAuth, ApiKeyAuth)
|
|
8
|
+
- Caching and response middleware
|
|
9
|
+
- Request/response hooks for customization
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .backends.httpx import HTTPXHttpRPCAsyncBackend
|
|
13
|
+
from .decorators import ( # HTTP Method decorators; Request parameter decorators; Configuration decorators; Client builder and core classes; Authentication classes; Middleware and hooks; Configuration classes; Data structures; Error handlers; Exceptions
|
|
14
|
+
ApiKeyAuth,
|
|
15
|
+
AuthenticationMiddleware,
|
|
16
|
+
BasicAuth,
|
|
17
|
+
BearerTokenAuth,
|
|
18
|
+
Body,
|
|
19
|
+
CacheMiddleware,
|
|
20
|
+
ContentType,
|
|
21
|
+
Delete,
|
|
22
|
+
File,
|
|
23
|
+
FormData,
|
|
24
|
+
Get,
|
|
25
|
+
GlobalHttpErrorHandler,
|
|
26
|
+
Header,
|
|
27
|
+
HttpMapping,
|
|
28
|
+
HttpRpcClientBuilder,
|
|
29
|
+
HttpRPCRequest,
|
|
30
|
+
HttpRPCResponse,
|
|
31
|
+
Patch,
|
|
32
|
+
PathParam,
|
|
33
|
+
Post,
|
|
34
|
+
Put,
|
|
35
|
+
Query,
|
|
36
|
+
RequestAttribute,
|
|
37
|
+
RequestHook,
|
|
38
|
+
ResponseHook,
|
|
39
|
+
ResponseMiddleware,
|
|
40
|
+
RestClient,
|
|
41
|
+
Retry,
|
|
42
|
+
RetryConfig,
|
|
43
|
+
RouteHttpErrorHandler,
|
|
44
|
+
RPCRequestNetworkError,
|
|
45
|
+
RPCUnhandleError,
|
|
46
|
+
Timeout,
|
|
47
|
+
TimeoutException,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
# HTTP Method decorators
|
|
52
|
+
"Get",
|
|
53
|
+
"Post",
|
|
54
|
+
"Put",
|
|
55
|
+
"Patch",
|
|
56
|
+
"Delete",
|
|
57
|
+
# Request parameter decorators
|
|
58
|
+
"Query",
|
|
59
|
+
"Header",
|
|
60
|
+
"PathParam",
|
|
61
|
+
"Body",
|
|
62
|
+
"FormData",
|
|
63
|
+
"File",
|
|
64
|
+
# Configuration decorators
|
|
65
|
+
"Timeout",
|
|
66
|
+
"Retry",
|
|
67
|
+
"ContentType",
|
|
68
|
+
# Client builder and core classes
|
|
69
|
+
"RestClient",
|
|
70
|
+
"HttpRpcClientBuilder",
|
|
71
|
+
"HttpMapping",
|
|
72
|
+
"RequestAttribute",
|
|
73
|
+
# Authentication classes
|
|
74
|
+
"BearerTokenAuth",
|
|
75
|
+
"BasicAuth",
|
|
76
|
+
"ApiKeyAuth",
|
|
77
|
+
"AuthenticationMiddleware",
|
|
78
|
+
# Middleware and hooks
|
|
79
|
+
"CacheMiddleware",
|
|
80
|
+
"ResponseMiddleware",
|
|
81
|
+
"RequestHook",
|
|
82
|
+
"ResponseHook",
|
|
83
|
+
# Configuration classes
|
|
84
|
+
"RetryConfig",
|
|
85
|
+
# Data structures
|
|
86
|
+
"HttpRPCRequest",
|
|
87
|
+
"HttpRPCResponse",
|
|
88
|
+
# Error handlers
|
|
89
|
+
"GlobalHttpErrorHandler",
|
|
90
|
+
"RouteHttpErrorHandler",
|
|
91
|
+
# Exceptions
|
|
92
|
+
"RPCRequestNetworkError",
|
|
93
|
+
"RPCUnhandleError",
|
|
94
|
+
"TimeoutException",
|
|
95
|
+
# Backend
|
|
96
|
+
"HTTPXHttpRPCAsyncBackend",
|
|
97
|
+
]
|
|
@@ -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",
|
|
@@ -406,7 +406,7 @@ export type ResponseType =
|
|
|
406
406
|
export interface HttpBackendRequest {
|
|
407
407
|
method: string;
|
|
408
408
|
path: string;
|
|
409
|
-
pathParams: { [key: string]:
|
|
409
|
+
pathParams: { [key: string]: any };
|
|
410
410
|
headers: { [key: string]: string };
|
|
411
411
|
query: { [key: string]: unknown };
|
|
412
412
|
body: unknown;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
2
2
|
README.md,sha256=2qMM__t_MoLKZr4IY9tXjo-Jn6LKjuHMb1qbyXpgL08,3401
|
|
3
|
-
pyproject.toml,sha256=
|
|
4
|
-
jararaca/__init__.py,sha256=
|
|
3
|
+
pyproject.toml,sha256=sh8QoSrsND1MonDpmTcmeZ9ycDIraZF48tey-OKC6ZQ,2040
|
|
4
|
+
jararaca/__init__.py,sha256=qffDJwYvbPqMdlx3fi5qFBVF2ZQ89W09znjTGTe9J6I,20939
|
|
5
5
|
jararaca/__main__.py,sha256=-O3vsB5lHdqNFjUtoELDF81IYFtR-DSiiFMzRaiSsv4,67
|
|
6
6
|
jararaca/broker_backend/__init__.py,sha256=GzEIuHR1xzgCJD4FE3harNjoaYzxHMHoEL0_clUaC-k,3528
|
|
7
7
|
jararaca/broker_backend/mapper.py,sha256=vTsi7sWpNvlga1PWPFg0rCJ5joJ0cdzykkIc2Tuvenc,696
|
|
@@ -23,8 +23,8 @@ jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py,sha256=_DEHwIH
|
|
|
23
23
|
jararaca/messagebus/interceptors/publisher_interceptor.py,sha256=ojy1bRhqMgrkQljcGGS8cd8-8pUjL8ZHjIUkdmaAnNM,1325
|
|
24
24
|
jararaca/messagebus/message.py,sha256=U6cyd2XknX8mtm0333slz5fanky2PFLWCmokAO56vvU,819
|
|
25
25
|
jararaca/messagebus/publisher.py,sha256=JTkxdKbvxvDWT8nK8PVEyyX061vYYbKQMxRHXrZtcEY,2173
|
|
26
|
-
jararaca/messagebus/worker.py,sha256=
|
|
27
|
-
jararaca/microservice.py,sha256=
|
|
26
|
+
jararaca/messagebus/worker.py,sha256=736rl6mBVxo-w5Y-jJe1w1ORPgVk16jlmTyVJueeAKs,55156
|
|
27
|
+
jararaca/microservice.py,sha256=4uQWPH1Ytxl2tg4IK-M_6T-28X89ku4ObJXJyg9CDr4,10663
|
|
28
28
|
jararaca/observability/decorators.py,sha256=MOIr2PttPYYvRwEdfQZEwD5RxKHOTv8UEy9n1YQVoKw,2281
|
|
29
29
|
jararaca/observability/interceptor.py,sha256=U4ZLM0f8j6Q7gMUKKnA85bnvD-Qa0ii79Qa_X8KsXAQ,1498
|
|
30
30
|
jararaca/observability/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -40,7 +40,7 @@ jararaca/presentation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
|
|
|
40
40
|
jararaca/presentation/decorators.py,sha256=dNcK1xYWhdHVBeWDa8dhqh4z8yQcPkJZTQkfgLXz6RI,11655
|
|
41
41
|
jararaca/presentation/hooks.py,sha256=WBbU5DG3-MAm2Ro2YraQyYG_HENfizYfyShL2ktHi6k,1980
|
|
42
42
|
jararaca/presentation/http_microservice.py,sha256=g771JosV6jTY3hQtG-HkLOo-T0e-r3b3rp1ddt99Qf0,533
|
|
43
|
-
jararaca/presentation/server.py,sha256=
|
|
43
|
+
jararaca/presentation/server.py,sha256=Im-tei9EynKm1DO-63UIGeTJK5ZvxIhpAxbsYGn7ZW0,6244
|
|
44
44
|
jararaca/presentation/websocket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
45
45
|
jararaca/presentation/websocket/base_types.py,sha256=AvUeeZ1TFhSiRMcYqZU1HaQNqSrcgTkC5R0ArP5dGmA,146
|
|
46
46
|
jararaca/presentation/websocket/context.py,sha256=A6K5W3kqo9Hgeh1m6JiI7Cdz5SfbXcaICSVX7u1ARZo,1903
|
|
@@ -53,11 +53,11 @@ jararaca/reflect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
|
|
|
53
53
|
jararaca/reflect/controller_inspect.py,sha256=UtV4pRIOqCoK4ogBTXQE0dyopEQ5LDFhwm-1iJvrkJc,2326
|
|
54
54
|
jararaca/reflect/metadata.py,sha256=oTi0zIjCYkeBhs12PNTLc8GmzR6qWHdl3drlmamXLJo,1897
|
|
55
55
|
jararaca/rpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
|
-
jararaca/rpc/http/__init__.py,sha256=
|
|
57
|
-
jararaca/rpc/http/backends/__init__.py,sha256=
|
|
58
|
-
jararaca/rpc/http/backends/httpx.py,sha256=
|
|
56
|
+
jararaca/rpc/http/__init__.py,sha256=Xp7-d-cVj7EK1JloSUbSnGBfER5YwCfp7LCU6wCAf1c,2396
|
|
57
|
+
jararaca/rpc/http/backends/__init__.py,sha256=Q1tIj1PTjB4__qTZndGMu4IjP5lbayLbQZJ4fZXcnAk,166
|
|
58
|
+
jararaca/rpc/http/backends/httpx.py,sha256=nNHXBortLt0tWW1qTXMPtSuO1mkpQcNh5eplP_8zYEM,2246
|
|
59
59
|
jararaca/rpc/http/backends/otel.py,sha256=Uc6CjHSCZ5hvnK1fNFv3ota5xzUFnvIl1JOpG380siA,807
|
|
60
|
-
jararaca/rpc/http/decorators.py,sha256=
|
|
60
|
+
jararaca/rpc/http/decorators.py,sha256=0BCcmP_jolIAB73VeXm3TKyVr3AWbzkXzUVMGqYtqSQ,22145
|
|
61
61
|
jararaca/rpc/http/httpx.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
62
62
|
jararaca/scheduler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
63
63
|
jararaca/scheduler/beat_worker.py,sha256=i_NyovjFhhLYuUivf2mwpVC27oQ9SGTFOHZ7Ec4jv6I,12076
|
|
@@ -66,12 +66,12 @@ jararaca/scheduler/types.py,sha256=4HEQOmVIDp-BYLSzqmqSFIio1bd51WFmgFPIzPpVu04,1
|
|
|
66
66
|
jararaca/tools/app_config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
67
|
jararaca/tools/app_config/decorators.py,sha256=-ckkMZ1dswOmECdo1rFrZ15UAku--txaNXMp8fd1Ndk,941
|
|
68
68
|
jararaca/tools/app_config/interceptor.py,sha256=HV8h4AxqUc_ACs5do4BSVlyxlRXzx7HqJtoVO9tfRnQ,2611
|
|
69
|
-
jararaca/tools/typescript/interface_parser.py,sha256=
|
|
69
|
+
jararaca/tools/typescript/interface_parser.py,sha256=foZ_A_lkyacmAq9TU6hUFv5ZnZcPZbbxcjtzFUu1A_o,31479
|
|
70
70
|
jararaca/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
71
71
|
jararaca/utils/rabbitmq_utils.py,sha256=ytdAFUyv-OBkaVnxezuJaJoLrmN7giZgtKeet_IsMBs,10918
|
|
72
72
|
jararaca/utils/retry.py,sha256=DzPX_fXUvTqej6BQ8Mt2dvLo9nNlTBm7Kx2pFZ26P2Q,4668
|
|
73
|
-
jararaca-0.3.
|
|
74
|
-
jararaca-0.3.
|
|
75
|
-
jararaca-0.3.
|
|
76
|
-
jararaca-0.3.
|
|
77
|
-
jararaca-0.3.
|
|
73
|
+
jararaca-0.3.12a5.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
74
|
+
jararaca-0.3.12a5.dist-info/METADATA,sha256=1voFAjmPXB7wfDeF9-B0GSVSfzPG-wV1NR5ay_Tjkls,4995
|
|
75
|
+
jararaca-0.3.12a5.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
76
|
+
jararaca-0.3.12a5.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
|
|
77
|
+
jararaca-0.3.12a5.dist-info/RECORD,,
|
pyproject.toml
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|