jararaca 0.2.37a12__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 +267 -15
- jararaca/__main__.py +4 -0
- jararaca/broker_backend/__init__.py +106 -0
- jararaca/broker_backend/mapper.py +25 -0
- jararaca/broker_backend/redis_broker_backend.py +168 -0
- jararaca/cli.py +840 -103
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +4 -0
- jararaca/core/uow.py +55 -16
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -0
- jararaca/lifecycle.py +6 -2
- jararaca/messagebus/__init__.py +5 -1
- jararaca/messagebus/bus_message_controller.py +4 -0
- jararaca/messagebus/consumers/__init__.py +3 -0
- jararaca/messagebus/decorators.py +90 -85
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +95 -37
- jararaca/messagebus/interceptors/publisher_interceptor.py +42 -0
- jararaca/messagebus/message.py +31 -0
- jararaca/messagebus/publisher.py +47 -4
- jararaca/messagebus/worker.py +1615 -135
- jararaca/microservice.py +248 -36
- jararaca/observability/constants.py +7 -0
- jararaca/observability/decorators.py +177 -16
- jararaca/observability/fastapi_exception_handler.py +37 -0
- jararaca/observability/hooks.py +109 -0
- jararaca/observability/interceptor.py +8 -2
- jararaca/observability/providers/__init__.py +3 -0
- jararaca/observability/providers/otel.py +213 -18
- jararaca/persistence/base.py +40 -3
- jararaca/persistence/exports.py +4 -0
- jararaca/persistence/interceptors/__init__.py +3 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +187 -23
- 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 +74 -32
- jararaca/presentation/__init__.py +3 -0
- jararaca/presentation/decorators.py +170 -82
- jararaca/presentation/exceptions.py +23 -0
- jararaca/presentation/hooks.py +4 -0
- jararaca/presentation/http_microservice.py +4 -0
- jararaca/presentation/server.py +120 -41
- jararaca/presentation/websocket/__init__.py +3 -0
- jararaca/presentation/websocket/base_types.py +4 -0
- jararaca/presentation/websocket/context.py +34 -4
- jararaca/presentation/websocket/decorators.py +8 -41
- jararaca/presentation/websocket/redis.py +280 -53
- jararaca/presentation/websocket/types.py +6 -2
- jararaca/presentation/websocket/websocket_interceptor.py +74 -23
- jararaca/reflect/__init__.py +3 -0
- jararaca/reflect/controller_inspect.py +81 -0
- jararaca/reflect/decorators.py +238 -0
- jararaca/reflect/metadata.py +76 -0
- 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 +758 -0
- jararaca/scheduler/decorators.py +89 -28
- jararaca/scheduler/types.py +11 -0
- jararaca/tools/app_config/__init__.py +3 -0
- jararaca/tools/app_config/decorators.py +7 -19
- jararaca/tools/app_config/interceptor.py +10 -4
- jararaca/tools/typescript/__init__.py +3 -0
- jararaca/tools/typescript/decorators.py +120 -0
- jararaca/tools/typescript/interface_parser.py +1126 -189
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/rabbitmq_utils.py +372 -0
- jararaca/utils/retry.py +148 -0
- 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.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +14 -7
- jararaca-0.4.0a5.dist-info/RECORD +88 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
- pyproject.toml +131 -0
- jararaca/messagebus/types.py +0 -30
- jararaca/scheduler/scheduler.py +0 -154
- jararaca/tools/metadata.py +0 -47
- jararaca-0.2.37a12.dist-info/RECORD +0 -63
- /jararaca-0.2.37a12.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
import asyncio
|
|
2
6
|
import inspect
|
|
3
7
|
from contextlib import asynccontextmanager
|
|
@@ -13,9 +17,9 @@ from pydantic import BaseModel
|
|
|
13
17
|
from jararaca.core.uow import UnitOfWorkContextProvider
|
|
14
18
|
from jararaca.di import Container
|
|
15
19
|
from jararaca.microservice import (
|
|
16
|
-
AppContext,
|
|
17
20
|
AppInterceptor,
|
|
18
21
|
AppInterceptorWithLifecycle,
|
|
22
|
+
AppTransactionContext,
|
|
19
23
|
Microservice,
|
|
20
24
|
)
|
|
21
25
|
from jararaca.presentation.decorators import (
|
|
@@ -25,7 +29,9 @@ from jararaca.presentation.decorators import (
|
|
|
25
29
|
)
|
|
26
30
|
from jararaca.presentation.websocket.context import (
|
|
27
31
|
WebSocketConnectionManager,
|
|
32
|
+
WebSocketMessageSender,
|
|
28
33
|
provide_ws_manager,
|
|
34
|
+
provide_ws_message_sender,
|
|
29
35
|
)
|
|
30
36
|
from jararaca.presentation.websocket.decorators import (
|
|
31
37
|
INHERITS_WS_MESSAGE,
|
|
@@ -83,13 +89,24 @@ class WebSocketConnectionManagerImpl(WebSocketConnectionManager):
|
|
|
83
89
|
await self.backend.broadcast(message)
|
|
84
90
|
|
|
85
91
|
async def _broadcast_from_backend(self, message: bytes) -> None:
|
|
86
|
-
|
|
92
|
+
# Create a copy of the websockets set to avoid modification during iteration
|
|
93
|
+
async with self.lock:
|
|
94
|
+
websockets_to_send = list(self.all_websockets)
|
|
95
|
+
|
|
96
|
+
disconnected_websockets: list[WebSocket] = []
|
|
97
|
+
|
|
98
|
+
for websocket in websockets_to_send:
|
|
87
99
|
try:
|
|
88
100
|
if websocket.client_state == WebSocketState.CONNECTED:
|
|
89
101
|
await websocket.send_bytes(message)
|
|
90
102
|
except WebSocketDisconnect:
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
disconnected_websockets.append(websocket)
|
|
104
|
+
|
|
105
|
+
# Clean up disconnected websockets in a single lock acquisition
|
|
106
|
+
if disconnected_websockets:
|
|
107
|
+
async with self.lock:
|
|
108
|
+
for websocket in disconnected_websockets:
|
|
109
|
+
self.all_websockets.discard(websocket)
|
|
93
110
|
|
|
94
111
|
async def send(self, rooms: list[str], message: WebSocketMessageBase) -> None:
|
|
95
112
|
|
|
@@ -101,16 +118,28 @@ class WebSocketConnectionManagerImpl(WebSocketConnectionManager):
|
|
|
101
118
|
)
|
|
102
119
|
|
|
103
120
|
async def _send_from_backend(self, rooms: list[str], message: bytes) -> None:
|
|
121
|
+
# Create a copy of room memberships to avoid modification during iteration
|
|
104
122
|
async with self.lock:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
123
|
+
room_websockets: dict[str, list[WebSocket]] = {
|
|
124
|
+
room: list(self.rooms.get(room, set())) for room in rooms
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
disconnected_by_room: dict[str, list[WebSocket]] = {room: [] for room in rooms}
|
|
128
|
+
|
|
129
|
+
for room, websockets in room_websockets.items():
|
|
130
|
+
for websocket in websockets:
|
|
131
|
+
try:
|
|
132
|
+
if websocket.client_state == WebSocketState.CONNECTED:
|
|
133
|
+
await websocket.send_bytes(message)
|
|
134
|
+
except WebSocketDisconnect:
|
|
135
|
+
disconnected_by_room[room].append(websocket)
|
|
136
|
+
|
|
137
|
+
# Clean up disconnected websockets in a single lock acquisition
|
|
138
|
+
async with self.lock:
|
|
139
|
+
for room, disconnected_websockets in disconnected_by_room.items():
|
|
140
|
+
if room in self.rooms:
|
|
141
|
+
for websocket in disconnected_websockets:
|
|
142
|
+
self.rooms[room].discard(websocket)
|
|
114
143
|
|
|
115
144
|
async def join(self, rooms: list[str], websocket: WebSocket) -> None:
|
|
116
145
|
for room in rooms:
|
|
@@ -127,9 +156,23 @@ class WebSocketConnectionManagerImpl(WebSocketConnectionManager):
|
|
|
127
156
|
# async def setup_consumer(self, websocket: WebSocket) -> None: ...
|
|
128
157
|
|
|
129
158
|
|
|
159
|
+
class StagingWebSocketMessageSender(WebSocketMessageSender):
|
|
160
|
+
|
|
161
|
+
def __init__(self, connection: WebSocketConnectionManager) -> None:
|
|
162
|
+
self.connection = connection
|
|
163
|
+
self.staged_messages: list[tuple[list[str], WebSocketMessageBase]] = []
|
|
164
|
+
|
|
165
|
+
async def send(self, rooms: list[str], message: WebSocketMessageBase) -> None:
|
|
166
|
+
self.staged_messages.append((rooms, message))
|
|
167
|
+
|
|
168
|
+
async def flush(self) -> None:
|
|
169
|
+
for rooms, message in self.staged_messages:
|
|
170
|
+
await self.connection.send(rooms, message)
|
|
171
|
+
self.staged_messages.clear()
|
|
172
|
+
|
|
173
|
+
|
|
130
174
|
class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
|
|
131
175
|
"""
|
|
132
|
-
@Deprecated
|
|
133
176
|
WebSocketInterceptor is responsible for managing WebSocket connections and
|
|
134
177
|
intercepting WebSocket requests within the application. It integrates with
|
|
135
178
|
the application's lifecycle and provides a router for WebSocket endpoints.
|
|
@@ -169,10 +212,18 @@ class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
|
|
|
169
212
|
self.shutdown_event.set()
|
|
170
213
|
|
|
171
214
|
@asynccontextmanager
|
|
172
|
-
async def intercept(
|
|
215
|
+
async def intercept(
|
|
216
|
+
self, app_context: AppTransactionContext
|
|
217
|
+
) -> AsyncGenerator[None, None]:
|
|
173
218
|
|
|
174
|
-
|
|
219
|
+
staging_ws_messages_sender = StagingWebSocketMessageSender(
|
|
220
|
+
self.connection_manager
|
|
221
|
+
)
|
|
222
|
+
with provide_ws_manager(self.connection_manager), provide_ws_message_sender(
|
|
223
|
+
staging_ws_messages_sender
|
|
224
|
+
):
|
|
175
225
|
yield
|
|
226
|
+
await staging_ws_messages_sender.flush()
|
|
176
227
|
|
|
177
228
|
# def __wrap_with_uow_context_provider(
|
|
178
229
|
# self, uow: UnitOfWorkContextProvider, func: Callable[..., Any]
|
|
@@ -198,13 +249,15 @@ class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
|
|
|
198
249
|
|
|
199
250
|
for controller_type in app.controllers:
|
|
200
251
|
|
|
201
|
-
rest_controller = RestController.
|
|
252
|
+
rest_controller = RestController.get_last(controller_type)
|
|
202
253
|
controller: Any = container.get_by_type(controller_type)
|
|
203
254
|
|
|
204
|
-
members = inspect.
|
|
255
|
+
members = inspect.getmembers_static(
|
|
256
|
+
controller_type, predicate=inspect.isfunction
|
|
257
|
+
)
|
|
205
258
|
|
|
206
259
|
for name, member in members:
|
|
207
|
-
if (ws_endpoint := WebSocketEndpoint.
|
|
260
|
+
if (ws_endpoint := WebSocketEndpoint.get_last(member)) is not None:
|
|
208
261
|
final_path = (
|
|
209
262
|
rest_controller.path + ws_endpoint.path
|
|
210
263
|
if rest_controller
|
|
@@ -212,7 +265,7 @@ class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
|
|
|
212
265
|
)
|
|
213
266
|
|
|
214
267
|
route_dependencies: list[Depends] = []
|
|
215
|
-
for middlewares_by_hook in UseMiddleware.
|
|
268
|
+
for middlewares_by_hook in UseMiddleware.get(
|
|
216
269
|
getattr(controller_type, name)
|
|
217
270
|
):
|
|
218
271
|
middleware_instance = container.get_by_type(
|
|
@@ -222,9 +275,7 @@ class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
|
|
|
222
275
|
Depends(middleware_instance.intercept)
|
|
223
276
|
)
|
|
224
277
|
|
|
225
|
-
for dependency in UseDependency.
|
|
226
|
-
getattr(controller_type, name)
|
|
227
|
-
):
|
|
278
|
+
for dependency in UseDependency.get(getattr(controller_type, name)):
|
|
228
279
|
route_dependencies.append(DependsF(dependency.dependency))
|
|
229
280
|
|
|
230
281
|
api_router.add_api_websocket_route(
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Callable, Mapping, Tuple, Type
|
|
8
|
+
|
|
9
|
+
from frozendict import frozendict
|
|
10
|
+
|
|
11
|
+
from jararaca.reflect.metadata import SetMetadata, TransactionMetadata
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ControllerReflect:
|
|
16
|
+
|
|
17
|
+
controller_class: Type[Any]
|
|
18
|
+
metadata: Mapping[str, TransactionMetadata]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ControllerMemberReflect:
|
|
23
|
+
controller_reflect: ControllerReflect
|
|
24
|
+
member_function: Callable[..., Any]
|
|
25
|
+
metadata: Mapping[str, TransactionMetadata]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def inspect_controller(
|
|
29
|
+
controller: Type[Any],
|
|
30
|
+
) -> Tuple[ControllerReflect, Mapping[str, ControllerMemberReflect]]:
|
|
31
|
+
"""
|
|
32
|
+
Inspect a controller class to extract its metadata and member functions.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
controller (Type[Any]): The controller class to inspect.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Tuple[ControllerReflect, list[ControllerMemberReflect]]: A tuple containing the controller reflect and a list of member reflects.
|
|
39
|
+
"""
|
|
40
|
+
controller_metadata_list = SetMetadata.get(controller)
|
|
41
|
+
|
|
42
|
+
controller_metadata_map = frozendict(
|
|
43
|
+
{
|
|
44
|
+
metadata.key: TransactionMetadata(
|
|
45
|
+
value=metadata.value, inherited_from_controller=False
|
|
46
|
+
)
|
|
47
|
+
for metadata in controller_metadata_list
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
controller_reflect = ControllerReflect(
|
|
52
|
+
controller_class=controller, metadata=controller_metadata_map
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
members = {
|
|
56
|
+
name: ControllerMemberReflect(
|
|
57
|
+
controller_reflect=controller_reflect,
|
|
58
|
+
member_function=member,
|
|
59
|
+
metadata=frozendict(
|
|
60
|
+
{
|
|
61
|
+
**{
|
|
62
|
+
key: TransactionMetadata(
|
|
63
|
+
value=value.value, inherited_from_controller=True
|
|
64
|
+
)
|
|
65
|
+
for key, value in controller_metadata_map.items()
|
|
66
|
+
},
|
|
67
|
+
**{
|
|
68
|
+
metadata.key: TransactionMetadata(
|
|
69
|
+
value=metadata.value, inherited_from_controller=False
|
|
70
|
+
)
|
|
71
|
+
for metadata in SetMetadata.get(member)
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
for name, member in inspect.getmembers_static(
|
|
77
|
+
controller, predicate=inspect.isfunction
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return controller_reflect, members
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Self, TypedDict, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
DECORATED_T = TypeVar("DECORATED_T", bound="Callable[..., Any] | type")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
S = TypeVar("S", bound="StackableDecorator")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DecoratorMetadata(TypedDict):
|
|
14
|
+
decorators: "list[StackableDecorator]"
|
|
15
|
+
decorators_by_type: "dict[Any, list[StackableDecorator]]"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class StackableDecorator:
|
|
19
|
+
_ATTR_NAME: str = "__jararaca_stackable_decorator__"
|
|
20
|
+
|
|
21
|
+
def __call__(self, subject: DECORATED_T) -> DECORATED_T:
|
|
22
|
+
self.pre_decorated(subject)
|
|
23
|
+
self.register(subject, self)
|
|
24
|
+
self.post_decorated(subject)
|
|
25
|
+
return subject
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def decorator_key(cls) -> Any:
|
|
29
|
+
return cls
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def get_or_set_metadata(cls, subject: Any) -> DecoratorMetadata:
|
|
33
|
+
if cls._ATTR_NAME not in subject.__dict__:
|
|
34
|
+
setattr(
|
|
35
|
+
subject,
|
|
36
|
+
cls._ATTR_NAME,
|
|
37
|
+
DecoratorMetadata(decorators=[], decorators_by_type={}),
|
|
38
|
+
)
|
|
39
|
+
return cast(DecoratorMetadata, getattr(subject, cls._ATTR_NAME))
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def get_metadata(cls, subject: Any) -> DecoratorMetadata | None:
|
|
43
|
+
if hasattr(subject, cls._ATTR_NAME):
|
|
44
|
+
return cast(DecoratorMetadata, getattr(subject, cls._ATTR_NAME))
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def register(cls, subject: Any, decorator: "StackableDecorator") -> None:
|
|
49
|
+
if not cls._ATTR_NAME:
|
|
50
|
+
raise NotImplementedError("Subclasses must define _ATTR_NAME")
|
|
51
|
+
|
|
52
|
+
metadata = cls.get_or_set_metadata(subject)
|
|
53
|
+
metadata["decorators"].append(decorator)
|
|
54
|
+
metadata["decorators_by_type"].setdefault(cls.decorator_key(), []).append(
|
|
55
|
+
decorator
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def get(cls, subject: Any) -> list[Self]:
|
|
60
|
+
metadata = cls.get_metadata(subject)
|
|
61
|
+
if metadata is None:
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
if cls is StackableDecorator:
|
|
65
|
+
return cast(list[Self], metadata["decorators"])
|
|
66
|
+
else:
|
|
67
|
+
return cast(
|
|
68
|
+
list[Self], metadata["decorators_by_type"].get(cls.decorator_key(), [])
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def extract_list(cls, subject: Any) -> list[Self]:
|
|
73
|
+
metadata = cls.get_metadata(subject)
|
|
74
|
+
if metadata is None:
|
|
75
|
+
return []
|
|
76
|
+
|
|
77
|
+
if cls is StackableDecorator:
|
|
78
|
+
return cast(list[Self], metadata["decorators"])
|
|
79
|
+
else:
|
|
80
|
+
return cast(
|
|
81
|
+
list[Self], metadata["decorators_by_type"].get(cls.decorator_key(), [])
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def get_fisrt(cls, subject: Any) -> Self | None:
|
|
86
|
+
decorators = cls.get(subject)
|
|
87
|
+
if decorators:
|
|
88
|
+
return decorators[0]
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def get_last(cls, subject: Any) -> Self | None:
|
|
93
|
+
decorators = cls.get(subject)
|
|
94
|
+
if decorators:
|
|
95
|
+
return decorators[-1]
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def pre_decorated(self, subject: DECORATED_T) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Hook method called before the subject is decorated.
|
|
101
|
+
Can be overridden by subclasses to perform additional setup.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def post_decorated(self, subject: DECORATED_T) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Hook method called after the subject has been decorated.
|
|
107
|
+
Can be overridden by subclasses to perform additional setup.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def get_all_from_type(cls, subject_type: type, inherit: bool = True) -> list[Self]:
|
|
112
|
+
"""
|
|
113
|
+
Retrieve all decorators of this type from the given class type.
|
|
114
|
+
"""
|
|
115
|
+
return resolve_class_decorators(subject_type, cls, inherit)
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def get_bound_from_type(
|
|
119
|
+
cls, subject_type: type, inherit: bool = True, last: bool = False
|
|
120
|
+
) -> Self | None:
|
|
121
|
+
"""
|
|
122
|
+
Retrieve the first or last decorator of this type from the given class type.
|
|
123
|
+
"""
|
|
124
|
+
return resolve_bound_class_decorators(subject_type, cls, inherit, last=last)
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def get_all_from_method(
|
|
128
|
+
cls, cls_subject_type: type, method_name: str, inherit: bool = True
|
|
129
|
+
) -> list[Self]:
|
|
130
|
+
"""
|
|
131
|
+
Retrieve all decorators of this type from the given method.
|
|
132
|
+
"""
|
|
133
|
+
return resolve_method_decorators(cls_subject_type, method_name, cls, inherit)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def get_bound_from_method(
|
|
137
|
+
cls,
|
|
138
|
+
cls_subject_type: type,
|
|
139
|
+
method_name: str,
|
|
140
|
+
inherit: bool = True,
|
|
141
|
+
last: bool = True,
|
|
142
|
+
) -> Self | None:
|
|
143
|
+
"""
|
|
144
|
+
Retrieve the first or last decorator of this type from the given method.
|
|
145
|
+
"""
|
|
146
|
+
return resolve_bound_method_decorator(
|
|
147
|
+
cls_subject_type, method_name, cls, inherit, last=last
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def resolve_class_decorators(
|
|
152
|
+
subject: Any, decorator_cls: type[S], inherit: bool = True
|
|
153
|
+
) -> list[S]:
|
|
154
|
+
"""
|
|
155
|
+
Resolve decorators for a class or instance, optionally inheriting from base classes.
|
|
156
|
+
"""
|
|
157
|
+
if not inherit:
|
|
158
|
+
return decorator_cls.get(subject)
|
|
159
|
+
|
|
160
|
+
# If subject is an instance, get its class
|
|
161
|
+
cls = subject if isinstance(subject, type) else type(subject)
|
|
162
|
+
|
|
163
|
+
collected: list[S] = []
|
|
164
|
+
# Iterate MRO in reverse to apply base class decorators first
|
|
165
|
+
for base in reversed(cls.mro()):
|
|
166
|
+
collected.extend(decorator_cls.get(base))
|
|
167
|
+
|
|
168
|
+
return collected
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def resolve_bound_class_decorators(
|
|
172
|
+
subject: Any, decorator_cls: type[S], inherit: bool = True, last: bool = False
|
|
173
|
+
) -> S | None:
|
|
174
|
+
"""
|
|
175
|
+
Retrieve the first or last decorator of a given type from a class or instance,
|
|
176
|
+
optionally inheriting from base classes.
|
|
177
|
+
"""
|
|
178
|
+
decorators = resolve_class_decorators(subject, decorator_cls, inherit)
|
|
179
|
+
if not decorators:
|
|
180
|
+
return None
|
|
181
|
+
return decorators[-1] if last else decorators[0]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def resolve_method_decorators(
|
|
185
|
+
cls: type,
|
|
186
|
+
method_name: str,
|
|
187
|
+
decorator_cls: type[S],
|
|
188
|
+
inherit: bool = True,
|
|
189
|
+
) -> list[S]:
|
|
190
|
+
"""
|
|
191
|
+
Resolve decorators for a method, optionally inheriting from base classes.
|
|
192
|
+
"""
|
|
193
|
+
if not inherit:
|
|
194
|
+
method = getattr(cls, method_name, None)
|
|
195
|
+
if method:
|
|
196
|
+
return decorator_cls.get(method)
|
|
197
|
+
return []
|
|
198
|
+
|
|
199
|
+
collected: list[S] = []
|
|
200
|
+
# Iterate MRO in reverse to apply base class decorators first
|
|
201
|
+
for base in reversed(cls.mro()):
|
|
202
|
+
if method_name in base.__dict__:
|
|
203
|
+
method = base.__dict__[method_name]
|
|
204
|
+
# Handle staticmethod/classmethod wrappers if necessary?
|
|
205
|
+
# Usually decorators are on the underlying function or the wrapper.
|
|
206
|
+
# getattr(cls, name) returns the bound/unbound method.
|
|
207
|
+
# base.__dict__[name] returns the raw object (function or descriptor).
|
|
208
|
+
|
|
209
|
+
# If it's a staticmethod object, it has no __dict__ usually, but we attach attributes to it?
|
|
210
|
+
# The decorator runs on the function BEFORE it becomes a staticmethod.
|
|
211
|
+
# So the attribute should be on the function object.
|
|
212
|
+
|
|
213
|
+
# However, when we access it via base.__dict__[method_name], we might get the staticmethod object.
|
|
214
|
+
# We need to unwrap it if possible.
|
|
215
|
+
|
|
216
|
+
if isinstance(method, (staticmethod, classmethod)):
|
|
217
|
+
method = method.__func__
|
|
218
|
+
|
|
219
|
+
collected.extend(decorator_cls.get(method))
|
|
220
|
+
|
|
221
|
+
return collected
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def resolve_bound_method_decorator(
|
|
225
|
+
cls: type,
|
|
226
|
+
method_name: str,
|
|
227
|
+
decorator_cls: type[S],
|
|
228
|
+
inherit: bool = True,
|
|
229
|
+
last: bool = False,
|
|
230
|
+
) -> S | None:
|
|
231
|
+
"""
|
|
232
|
+
Retrieve the first or last decorator of a given type from a method,
|
|
233
|
+
optionally inheriting from base classes.
|
|
234
|
+
"""
|
|
235
|
+
decorators = resolve_method_decorators(cls, method_name, decorator_cls, inherit)
|
|
236
|
+
if not decorators:
|
|
237
|
+
return None
|
|
238
|
+
return decorators[-1] if last else decorators[0]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager, suppress
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Awaitable, Callable, Generator, Mapping, TypeVar, Union
|
|
9
|
+
|
|
10
|
+
from jararaca.reflect.decorators import StackableDecorator
|
|
11
|
+
|
|
12
|
+
DECORATED = TypeVar("DECORATED", bound=Union[Callable[..., Awaitable[Any]], type])
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class TransactionMetadata:
|
|
17
|
+
value: Any
|
|
18
|
+
"""The value of the metadata."""
|
|
19
|
+
|
|
20
|
+
inherited_from_controller: bool
|
|
21
|
+
"""Whether the metadata was inherited from a parent class."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
metadata_context: ContextVar[Mapping[str, TransactionMetadata]] = ContextVar(
|
|
25
|
+
"metadata_context"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_metadata(key: str) -> TransactionMetadata | None:
|
|
30
|
+
return metadata_context.get({}).get(key)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_metadata_value(key: str, default: Any | None = None) -> Any:
|
|
34
|
+
metadata = get_metadata(key)
|
|
35
|
+
if metadata is None:
|
|
36
|
+
return default
|
|
37
|
+
return metadata.value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_all_metadata() -> Mapping[str, TransactionMetadata]:
|
|
41
|
+
return metadata_context.get({})
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@contextmanager
|
|
45
|
+
def start_transaction_metadata_context(
|
|
46
|
+
metadata: Mapping[str, TransactionMetadata],
|
|
47
|
+
) -> Generator[None, Any, None]:
|
|
48
|
+
|
|
49
|
+
current_metadata = metadata_context.get({})
|
|
50
|
+
|
|
51
|
+
token = metadata_context.set({**current_metadata, **metadata})
|
|
52
|
+
try:
|
|
53
|
+
yield
|
|
54
|
+
finally:
|
|
55
|
+
with suppress(ValueError):
|
|
56
|
+
metadata_context.reset(token)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@contextmanager
|
|
60
|
+
def start_providing_metadata(
|
|
61
|
+
**metadata: Any,
|
|
62
|
+
) -> Generator[None, Any, None]:
|
|
63
|
+
|
|
64
|
+
with start_transaction_metadata_context(
|
|
65
|
+
{
|
|
66
|
+
key: TransactionMetadata(value=value, inherited_from_controller=False)
|
|
67
|
+
for key, value in metadata.items()
|
|
68
|
+
}
|
|
69
|
+
):
|
|
70
|
+
yield
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SetMetadata(StackableDecorator):
|
|
74
|
+
def __init__(self, key: str, value: Any) -> None:
|
|
75
|
+
self.key = key
|
|
76
|
+
self.value = value
|
jararaca/rpc/__init__.py
CHANGED
jararaca/rpc/http/__init__.py
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
# HTTP RPC Module - Complete REST Client Implementation
|
|
6
|
+
"""
|
|
7
|
+
This module provides a complete REST client implementation with support for:
|
|
8
|
+
- HTTP method decorators (@Get, @Post, @Put, @Patch, @Delete)
|
|
9
|
+
- Request parameter decorators (@Query, @Header, @PathParam, @Body, @FormData, @File)
|
|
10
|
+
- Configuration decorators (@Timeout, @Retry, @ContentType)
|
|
11
|
+
- Authentication middleware (BearerTokenAuth, BasicAuth, ApiKeyAuth)
|
|
12
|
+
- Caching and response middleware
|
|
13
|
+
- Request/response hooks for customization
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .backends.httpx import HTTPXHttpRPCAsyncBackend
|
|
17
|
+
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
|
|
18
|
+
ApiKeyAuth,
|
|
19
|
+
AuthenticationMiddleware,
|
|
20
|
+
BasicAuth,
|
|
21
|
+
BearerTokenAuth,
|
|
22
|
+
Body,
|
|
23
|
+
CacheMiddleware,
|
|
24
|
+
ContentType,
|
|
25
|
+
Delete,
|
|
26
|
+
File,
|
|
27
|
+
FormData,
|
|
28
|
+
Get,
|
|
29
|
+
GlobalHttpErrorHandler,
|
|
30
|
+
Header,
|
|
31
|
+
HttpMapping,
|
|
32
|
+
HttpRpcClientBuilder,
|
|
33
|
+
HttpRPCRequest,
|
|
34
|
+
HttpRPCResponse,
|
|
35
|
+
Patch,
|
|
36
|
+
PathParam,
|
|
37
|
+
Post,
|
|
38
|
+
Put,
|
|
39
|
+
Query,
|
|
40
|
+
RequestAttribute,
|
|
41
|
+
RequestHook,
|
|
42
|
+
ResponseHook,
|
|
43
|
+
ResponseMiddleware,
|
|
44
|
+
RestClient,
|
|
45
|
+
Retry,
|
|
46
|
+
RetryConfig,
|
|
47
|
+
RouteHttpErrorHandler,
|
|
48
|
+
RPCRequestNetworkError,
|
|
49
|
+
RPCUnhandleError,
|
|
50
|
+
Timeout,
|
|
51
|
+
TimeoutException,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
# HTTP Method decorators
|
|
56
|
+
"Get",
|
|
57
|
+
"Post",
|
|
58
|
+
"Put",
|
|
59
|
+
"Patch",
|
|
60
|
+
"Delete",
|
|
61
|
+
# Request parameter decorators
|
|
62
|
+
"Query",
|
|
63
|
+
"Header",
|
|
64
|
+
"PathParam",
|
|
65
|
+
"Body",
|
|
66
|
+
"FormData",
|
|
67
|
+
"File",
|
|
68
|
+
# Configuration decorators
|
|
69
|
+
"Timeout",
|
|
70
|
+
"Retry",
|
|
71
|
+
"ContentType",
|
|
72
|
+
# Client builder and core classes
|
|
73
|
+
"RestClient",
|
|
74
|
+
"HttpRpcClientBuilder",
|
|
75
|
+
"HttpMapping",
|
|
76
|
+
"RequestAttribute",
|
|
77
|
+
# Authentication classes
|
|
78
|
+
"BearerTokenAuth",
|
|
79
|
+
"BasicAuth",
|
|
80
|
+
"ApiKeyAuth",
|
|
81
|
+
"AuthenticationMiddleware",
|
|
82
|
+
# Middleware and hooks
|
|
83
|
+
"CacheMiddleware",
|
|
84
|
+
"ResponseMiddleware",
|
|
85
|
+
"RequestHook",
|
|
86
|
+
"ResponseHook",
|
|
87
|
+
# Configuration classes
|
|
88
|
+
"RetryConfig",
|
|
89
|
+
# Data structures
|
|
90
|
+
"HttpRPCRequest",
|
|
91
|
+
"HttpRPCResponse",
|
|
92
|
+
# Error handlers
|
|
93
|
+
"GlobalHttpErrorHandler",
|
|
94
|
+
"RouteHttpErrorHandler",
|
|
95
|
+
# Exceptions
|
|
96
|
+
"RPCRequestNetworkError",
|
|
97
|
+
"RPCUnhandleError",
|
|
98
|
+
"TimeoutException",
|
|
99
|
+
# Backend
|
|
100
|
+
"HTTPXHttpRPCAsyncBackend",
|
|
101
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
# HTTP RPC Backends
|
|
6
|
+
"""
|
|
7
|
+
Backend implementations for HTTP RPC clients.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .httpx import HTTPXHttpRPCAsyncBackend
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"HTTPXHttpRPCAsyncBackend",
|
|
14
|
+
]
|