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
|
@@ -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
|
|
@@ -85,13 +89,24 @@ class WebSocketConnectionManagerImpl(WebSocketConnectionManager):
|
|
|
85
89
|
await self.backend.broadcast(message)
|
|
86
90
|
|
|
87
91
|
async def _broadcast_from_backend(self, message: bytes) -> None:
|
|
88
|
-
|
|
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:
|
|
89
99
|
try:
|
|
90
100
|
if websocket.client_state == WebSocketState.CONNECTED:
|
|
91
101
|
await websocket.send_bytes(message)
|
|
92
102
|
except WebSocketDisconnect:
|
|
93
|
-
|
|
94
|
-
|
|
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)
|
|
95
110
|
|
|
96
111
|
async def send(self, rooms: list[str], message: WebSocketMessageBase) -> None:
|
|
97
112
|
|
|
@@ -103,16 +118,28 @@ class WebSocketConnectionManagerImpl(WebSocketConnectionManager):
|
|
|
103
118
|
)
|
|
104
119
|
|
|
105
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
|
|
106
122
|
async with self.lock:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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)
|
|
116
143
|
|
|
117
144
|
async def join(self, rooms: list[str], websocket: WebSocket) -> None:
|
|
118
145
|
for room in rooms:
|
|
@@ -222,13 +249,15 @@ class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
|
|
|
222
249
|
|
|
223
250
|
for controller_type in app.controllers:
|
|
224
251
|
|
|
225
|
-
rest_controller = RestController.
|
|
252
|
+
rest_controller = RestController.get_last(controller_type)
|
|
226
253
|
controller: Any = container.get_by_type(controller_type)
|
|
227
254
|
|
|
228
|
-
members = inspect.
|
|
255
|
+
members = inspect.getmembers_static(
|
|
256
|
+
controller_type, predicate=inspect.isfunction
|
|
257
|
+
)
|
|
229
258
|
|
|
230
259
|
for name, member in members:
|
|
231
|
-
if (ws_endpoint := WebSocketEndpoint.
|
|
260
|
+
if (ws_endpoint := WebSocketEndpoint.get_last(member)) is not None:
|
|
232
261
|
final_path = (
|
|
233
262
|
rest_controller.path + ws_endpoint.path
|
|
234
263
|
if rest_controller
|
|
@@ -236,7 +265,7 @@ class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
|
|
|
236
265
|
)
|
|
237
266
|
|
|
238
267
|
route_dependencies: list[Depends] = []
|
|
239
|
-
for middlewares_by_hook in UseMiddleware.
|
|
268
|
+
for middlewares_by_hook in UseMiddleware.get(
|
|
240
269
|
getattr(controller_type, name)
|
|
241
270
|
):
|
|
242
271
|
middleware_instance = container.get_by_type(
|
|
@@ -246,9 +275,7 @@ class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
|
|
|
246
275
|
Depends(middleware_instance.intercept)
|
|
247
276
|
)
|
|
248
277
|
|
|
249
|
-
for dependency in UseDependency.
|
|
250
|
-
getattr(controller_type, name)
|
|
251
|
-
):
|
|
278
|
+
for dependency in UseDependency.get(getattr(controller_type, name)):
|
|
252
279
|
route_dependencies.append(DependsF(dependency.dependency))
|
|
253
280
|
|
|
254
281
|
api_router.add_api_websocket_route(
|
jararaca/reflect/__init__.py
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
import inspect
|
|
2
6
|
from dataclasses import dataclass
|
|
3
7
|
from typing import Any, Callable, Mapping, Tuple, Type
|
|
4
8
|
|
|
5
9
|
from frozendict import frozendict
|
|
6
10
|
|
|
7
|
-
from jararaca.reflect.metadata import
|
|
11
|
+
from jararaca.reflect.metadata import SetMetadata, TransactionMetadata
|
|
8
12
|
|
|
9
13
|
|
|
10
14
|
@dataclass(frozen=True)
|
|
11
15
|
class ControllerReflect:
|
|
12
16
|
|
|
13
17
|
controller_class: Type[Any]
|
|
14
|
-
metadata: Mapping[str,
|
|
18
|
+
metadata: Mapping[str, TransactionMetadata]
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
@dataclass(frozen=True)
|
|
18
22
|
class ControllerMemberReflect:
|
|
19
23
|
controller_reflect: ControllerReflect
|
|
20
24
|
member_function: Callable[..., Any]
|
|
21
|
-
metadata: Mapping[str,
|
|
25
|
+
metadata: Mapping[str, TransactionMetadata]
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
def inspect_controller(
|
|
@@ -37,8 +41,8 @@ def inspect_controller(
|
|
|
37
41
|
|
|
38
42
|
controller_metadata_map = frozendict(
|
|
39
43
|
{
|
|
40
|
-
metadata.key:
|
|
41
|
-
value=metadata.value,
|
|
44
|
+
metadata.key: TransactionMetadata(
|
|
45
|
+
value=metadata.value, inherited_from_controller=False
|
|
42
46
|
)
|
|
43
47
|
for metadata in controller_metadata_list
|
|
44
48
|
}
|
|
@@ -55,21 +59,23 @@ def inspect_controller(
|
|
|
55
59
|
metadata=frozendict(
|
|
56
60
|
{
|
|
57
61
|
**{
|
|
58
|
-
key:
|
|
59
|
-
value=value.value,
|
|
62
|
+
key: TransactionMetadata(
|
|
63
|
+
value=value.value, inherited_from_controller=True
|
|
60
64
|
)
|
|
61
65
|
for key, value in controller_metadata_map.items()
|
|
62
66
|
},
|
|
63
67
|
**{
|
|
64
|
-
metadata.key:
|
|
65
|
-
value=metadata.value,
|
|
68
|
+
metadata.key: TransactionMetadata(
|
|
69
|
+
value=metadata.value, inherited_from_controller=False
|
|
66
70
|
)
|
|
67
71
|
for metadata in SetMetadata.get(member)
|
|
68
72
|
},
|
|
69
73
|
}
|
|
70
74
|
),
|
|
71
75
|
)
|
|
72
|
-
for name, member in inspect.
|
|
76
|
+
for name, member in inspect.getmembers_static(
|
|
77
|
+
controller, predicate=inspect.isfunction
|
|
78
|
+
)
|
|
73
79
|
}
|
|
74
80
|
|
|
75
81
|
return controller_reflect, members
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Generic, Self, TypedDict, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
FUNC_OR_TYPE_T = Callable[..., Any] | type
|
|
8
|
+
|
|
9
|
+
DECORATED_T = TypeVar("DECORATED_T", bound="FUNC_OR_TYPE_T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
S = TypeVar("S", bound="BaseStackableDecorator")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DecoratorMetadata(TypedDict):
|
|
16
|
+
decorators: "list[BaseStackableDecorator]"
|
|
17
|
+
decorators_by_type: "dict[Any, list[BaseStackableDecorator]]"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseStackableDecorator:
|
|
21
|
+
_ATTR_NAME: str = "__jararaca_stackable_decorator__"
|
|
22
|
+
|
|
23
|
+
def __call__(self, subject: Any) -> Any:
|
|
24
|
+
self.pre_decorated(subject)
|
|
25
|
+
self.register(subject, self)
|
|
26
|
+
self.post_decorated(subject)
|
|
27
|
+
return subject
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def decorator_key(cls) -> Any:
|
|
31
|
+
return cls
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_or_set_metadata(cls, subject: Any) -> DecoratorMetadata:
|
|
35
|
+
if cls._ATTR_NAME not in subject.__dict__:
|
|
36
|
+
setattr(
|
|
37
|
+
subject,
|
|
38
|
+
cls._ATTR_NAME,
|
|
39
|
+
DecoratorMetadata(decorators=[], decorators_by_type={}),
|
|
40
|
+
)
|
|
41
|
+
return cast(DecoratorMetadata, getattr(subject, cls._ATTR_NAME))
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def get_metadata(cls, subject: Any) -> DecoratorMetadata | None:
|
|
45
|
+
if hasattr(subject, cls._ATTR_NAME):
|
|
46
|
+
return cast(DecoratorMetadata, getattr(subject, cls._ATTR_NAME))
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def register(cls, subject: Any, decorator: "BaseStackableDecorator") -> None:
|
|
51
|
+
if not cls._ATTR_NAME:
|
|
52
|
+
raise NotImplementedError("Subclasses must define _ATTR_NAME")
|
|
53
|
+
|
|
54
|
+
metadata = cls.get_or_set_metadata(subject)
|
|
55
|
+
metadata["decorators"].append(decorator)
|
|
56
|
+
metadata["decorators_by_type"].setdefault(cls.decorator_key(), []).append(
|
|
57
|
+
decorator
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def get(cls, subject: Any) -> list[Self]:
|
|
62
|
+
metadata = cls.get_metadata(subject)
|
|
63
|
+
if metadata is None:
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
if cls is StackableDecorator:
|
|
67
|
+
return cast(list[Self], metadata["decorators"])
|
|
68
|
+
else:
|
|
69
|
+
return cast(
|
|
70
|
+
list[Self], metadata["decorators_by_type"].get(cls.decorator_key(), [])
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def extract_list(cls, subject: Any) -> list[Self]:
|
|
75
|
+
metadata = cls.get_metadata(subject)
|
|
76
|
+
if metadata is None:
|
|
77
|
+
return []
|
|
78
|
+
|
|
79
|
+
if cls is StackableDecorator:
|
|
80
|
+
return cast(list[Self], metadata["decorators"])
|
|
81
|
+
else:
|
|
82
|
+
return cast(
|
|
83
|
+
list[Self], metadata["decorators_by_type"].get(cls.decorator_key(), [])
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def get_fisrt(cls, subject: Any) -> Self | None:
|
|
88
|
+
decorators = cls.get(subject)
|
|
89
|
+
if decorators:
|
|
90
|
+
return decorators[0]
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def get_last(cls, subject: Any) -> Self | None:
|
|
95
|
+
decorators = cls.get(subject)
|
|
96
|
+
if decorators:
|
|
97
|
+
return decorators[-1]
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def pre_decorated(self, subject: FUNC_OR_TYPE_T) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Hook method called before the subject is decorated.
|
|
103
|
+
Can be overridden by subclasses to perform additional setup.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def post_decorated(self, subject: FUNC_OR_TYPE_T) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Hook method called after the subject has been decorated.
|
|
109
|
+
Can be overridden by subclasses to perform additional setup.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def get_all_from_type(cls, subject_type: type, inherit: bool = True) -> list[Self]:
|
|
114
|
+
"""
|
|
115
|
+
Retrieve all decorators of this type from the given class type.
|
|
116
|
+
"""
|
|
117
|
+
return resolve_class_decorators(subject_type, cls, inherit)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def get_bound_from_type(
|
|
121
|
+
cls, subject_type: type, inherit: bool = True, last: bool = False
|
|
122
|
+
) -> Self | None:
|
|
123
|
+
"""
|
|
124
|
+
Retrieve the first or last decorator of this type from the given class type.
|
|
125
|
+
"""
|
|
126
|
+
return resolve_bound_class_decorators(subject_type, cls, inherit, last=last)
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def get_all_from_method(
|
|
130
|
+
cls, cls_subject_type: type, method_name: str, inherit: bool = True
|
|
131
|
+
) -> list[Self]:
|
|
132
|
+
"""
|
|
133
|
+
Retrieve all decorators of this type from the given method.
|
|
134
|
+
"""
|
|
135
|
+
return resolve_method_decorators(cls_subject_type, method_name, cls, inherit)
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def get_bound_from_method(
|
|
139
|
+
cls,
|
|
140
|
+
cls_subject_type: type,
|
|
141
|
+
method_name: str,
|
|
142
|
+
inherit: bool = True,
|
|
143
|
+
last: bool = True,
|
|
144
|
+
) -> Self | None:
|
|
145
|
+
"""
|
|
146
|
+
Retrieve the first or last decorator of this type from the given method.
|
|
147
|
+
"""
|
|
148
|
+
return resolve_bound_method_decorator(
|
|
149
|
+
cls_subject_type, method_name, cls, inherit, last=last
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class StackableDecorator(BaseStackableDecorator):
|
|
154
|
+
|
|
155
|
+
def __call__(self, subject: DECORATED_T) -> DECORATED_T:
|
|
156
|
+
return cast(DECORATED_T, super().__call__(subject))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class GenericStackableDecorator(BaseStackableDecorator, Generic[DECORATED_T]):
|
|
160
|
+
|
|
161
|
+
def __call__(self, subject: DECORATED_T) -> DECORATED_T:
|
|
162
|
+
return cast(DECORATED_T, super().__call__(subject))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def resolve_class_decorators(
|
|
166
|
+
subject: Any, decorator_cls: type[S], inherit: bool = True
|
|
167
|
+
) -> list[S]:
|
|
168
|
+
"""
|
|
169
|
+
Resolve decorators for a class or instance, optionally inheriting from base classes.
|
|
170
|
+
"""
|
|
171
|
+
if not inherit:
|
|
172
|
+
return decorator_cls.get(subject)
|
|
173
|
+
|
|
174
|
+
# If subject is an instance, get its class
|
|
175
|
+
cls = subject if isinstance(subject, type) else type(subject)
|
|
176
|
+
|
|
177
|
+
collected: list[S] = []
|
|
178
|
+
# Iterate MRO in reverse to apply base class decorators first
|
|
179
|
+
for base in reversed(cls.mro()):
|
|
180
|
+
collected.extend(decorator_cls.get(base))
|
|
181
|
+
|
|
182
|
+
return collected
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def resolve_bound_class_decorators(
|
|
186
|
+
subject: Any, decorator_cls: type[S], inherit: bool = True, last: bool = False
|
|
187
|
+
) -> S | None:
|
|
188
|
+
"""
|
|
189
|
+
Retrieve the first or last decorator of a given type from a class or instance,
|
|
190
|
+
optionally inheriting from base classes.
|
|
191
|
+
"""
|
|
192
|
+
decorators = resolve_class_decorators(subject, decorator_cls, inherit)
|
|
193
|
+
if not decorators:
|
|
194
|
+
return None
|
|
195
|
+
return decorators[-1] if last else decorators[0]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def resolve_method_decorators(
|
|
199
|
+
cls: type,
|
|
200
|
+
method_name: str,
|
|
201
|
+
decorator_cls: type[S],
|
|
202
|
+
inherit: bool = True,
|
|
203
|
+
) -> list[S]:
|
|
204
|
+
"""
|
|
205
|
+
Resolve decorators for a method, optionally inheriting from base classes.
|
|
206
|
+
"""
|
|
207
|
+
if not inherit:
|
|
208
|
+
method = getattr(cls, method_name, None)
|
|
209
|
+
if method:
|
|
210
|
+
return decorator_cls.get(method)
|
|
211
|
+
return []
|
|
212
|
+
|
|
213
|
+
collected: list[S] = []
|
|
214
|
+
# Iterate MRO in reverse to apply base class decorators first
|
|
215
|
+
for base in reversed(cls.mro()):
|
|
216
|
+
if method_name in base.__dict__:
|
|
217
|
+
method = base.__dict__[method_name]
|
|
218
|
+
# Handle staticmethod/classmethod wrappers if necessary?
|
|
219
|
+
# Usually decorators are on the underlying function or the wrapper.
|
|
220
|
+
# getattr(cls, name) returns the bound/unbound method.
|
|
221
|
+
# base.__dict__[name] returns the raw object (function or descriptor).
|
|
222
|
+
|
|
223
|
+
# If it's a staticmethod object, it has no __dict__ usually, but we attach attributes to it?
|
|
224
|
+
# The decorator runs on the function BEFORE it becomes a staticmethod.
|
|
225
|
+
# So the attribute should be on the function object.
|
|
226
|
+
|
|
227
|
+
# However, when we access it via base.__dict__[method_name], we might get the staticmethod object.
|
|
228
|
+
# We need to unwrap it if possible.
|
|
229
|
+
|
|
230
|
+
if isinstance(method, (staticmethod, classmethod)):
|
|
231
|
+
method = method.__func__
|
|
232
|
+
|
|
233
|
+
collected.extend(decorator_cls.get(method))
|
|
234
|
+
|
|
235
|
+
return collected
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def resolve_bound_method_decorator(
|
|
239
|
+
cls: type,
|
|
240
|
+
method_name: str,
|
|
241
|
+
decorator_cls: type[S],
|
|
242
|
+
inherit: bool = True,
|
|
243
|
+
last: bool = False,
|
|
244
|
+
) -> S | None:
|
|
245
|
+
"""
|
|
246
|
+
Retrieve the first or last decorator of a given type from a method,
|
|
247
|
+
optionally inheriting from base classes.
|
|
248
|
+
"""
|
|
249
|
+
decorators = resolve_method_decorators(cls, method_name, decorator_cls, inherit)
|
|
250
|
+
if not decorators:
|
|
251
|
+
return None
|
|
252
|
+
return decorators[-1] if last else decorators[0]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from types import GenericAlias
|
|
10
|
+
|
|
11
|
+
from typing_extensions import TypeIs
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_generic_alias(subject: object) -> "TypeIs[GenericAlias]":
|
|
15
|
+
origin = get_origin(subject)
|
|
16
|
+
args = get_args(subject)
|
|
17
|
+
|
|
18
|
+
return origin is not None and len(args) > 0
|
jararaca/reflect/metadata.py
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
from contextlib import contextmanager, suppress
|
|
2
6
|
from contextvars import ContextVar
|
|
3
7
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, Awaitable, Callable, Mapping, TypeVar, Union
|
|
8
|
+
from typing import Any, Awaitable, Callable, Generator, Mapping, TypeVar, Union
|
|
9
|
+
|
|
10
|
+
from jararaca.reflect.decorators import StackableDecorator
|
|
5
11
|
|
|
6
12
|
DECORATED = TypeVar("DECORATED", bound=Union[Callable[..., Awaitable[Any]], type])
|
|
7
13
|
|
|
8
14
|
|
|
9
|
-
@dataclass
|
|
10
|
-
class
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class TransactionMetadata:
|
|
11
17
|
value: Any
|
|
12
|
-
|
|
18
|
+
"""The value of the metadata."""
|
|
19
|
+
|
|
20
|
+
inherited_from_controller: bool
|
|
21
|
+
"""Whether the metadata was inherited from a parent class."""
|
|
13
22
|
|
|
14
23
|
|
|
15
|
-
metadata_context: ContextVar[Mapping[str,
|
|
24
|
+
metadata_context: ContextVar[Mapping[str, TransactionMetadata]] = ContextVar(
|
|
16
25
|
"metadata_context"
|
|
17
26
|
)
|
|
18
27
|
|
|
19
28
|
|
|
20
|
-
def get_metadata(key: str) ->
|
|
29
|
+
def get_metadata(key: str) -> TransactionMetadata | None:
|
|
21
30
|
return metadata_context.get({}).get(key)
|
|
22
31
|
|
|
23
32
|
|
|
@@ -28,12 +37,14 @@ def get_metadata_value(key: str, default: Any | None = None) -> Any:
|
|
|
28
37
|
return metadata.value
|
|
29
38
|
|
|
30
39
|
|
|
31
|
-
def get_all_metadata() -> Mapping[str,
|
|
40
|
+
def get_all_metadata() -> Mapping[str, TransactionMetadata]:
|
|
32
41
|
return metadata_context.get({})
|
|
33
42
|
|
|
34
43
|
|
|
35
44
|
@contextmanager
|
|
36
|
-
def
|
|
45
|
+
def start_transaction_metadata_context(
|
|
46
|
+
metadata: Mapping[str, TransactionMetadata],
|
|
47
|
+
) -> Generator[None, Any, None]:
|
|
37
48
|
|
|
38
49
|
current_metadata = metadata_context.get({})
|
|
39
50
|
|
|
@@ -45,23 +56,21 @@ def provide_metadata(metadata: Mapping[str, ControllerInstanceMetadata]) -> Any:
|
|
|
45
56
|
metadata_context.reset(token)
|
|
46
57
|
|
|
47
58
|
|
|
48
|
-
|
|
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):
|
|
49
74
|
def __init__(self, key: str, value: Any) -> None:
|
|
50
75
|
self.key = key
|
|
51
76
|
self.value = value
|
|
52
|
-
|
|
53
|
-
METATADA_LIST = "__metadata_list__"
|
|
54
|
-
|
|
55
|
-
@staticmethod
|
|
56
|
-
def register_metadata(cls: DECORATED, value: "SetMetadata") -> None:
|
|
57
|
-
metadata_list = getattr(cls, SetMetadata.METATADA_LIST, [])
|
|
58
|
-
metadata_list.append(value)
|
|
59
|
-
setattr(cls, SetMetadata.METATADA_LIST, metadata_list)
|
|
60
|
-
|
|
61
|
-
@staticmethod
|
|
62
|
-
def get(cls: DECORATED) -> "list[SetMetadata]":
|
|
63
|
-
return cast(list[SetMetadata], getattr(cls, SetMetadata.METATADA_LIST, []))
|
|
64
|
-
|
|
65
|
-
def __call__(self, cls: DECORATED) -> DECORATED:
|
|
66
|
-
SetMetadata.register_metadata(cls, self)
|
|
67
|
-
return cls
|
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
|
+
RouteHttpErrorHandler,
|
|
47
|
+
RPCRequestNetworkError,
|
|
48
|
+
RPCRetryPolicy,
|
|
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
|
+
"RPCRetryPolicy",
|
|
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
|
+
]
|