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.
Files changed (96) hide show
  1. README.md +121 -0
  2. jararaca/__init__.py +189 -17
  3. jararaca/__main__.py +4 -0
  4. jararaca/broker_backend/__init__.py +4 -0
  5. jararaca/broker_backend/mapper.py +4 -0
  6. jararaca/broker_backend/redis_broker_backend.py +9 -3
  7. jararaca/cli.py +915 -51
  8. jararaca/common/__init__.py +3 -0
  9. jararaca/core/__init__.py +3 -0
  10. jararaca/core/providers.py +8 -0
  11. jararaca/core/uow.py +41 -7
  12. jararaca/di.py +4 -0
  13. jararaca/files/entity.py.mako +4 -0
  14. jararaca/helpers/__init__.py +3 -0
  15. jararaca/helpers/global_scheduler/__init__.py +3 -0
  16. jararaca/helpers/global_scheduler/config.py +21 -0
  17. jararaca/helpers/global_scheduler/controller.py +42 -0
  18. jararaca/helpers/global_scheduler/registry.py +32 -0
  19. jararaca/lifecycle.py +6 -2
  20. jararaca/messagebus/__init__.py +4 -0
  21. jararaca/messagebus/bus_message_controller.py +4 -0
  22. jararaca/messagebus/consumers/__init__.py +3 -0
  23. jararaca/messagebus/decorators.py +121 -61
  24. jararaca/messagebus/implicit_headers.py +49 -0
  25. jararaca/messagebus/interceptors/__init__.py +3 -0
  26. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +62 -11
  27. jararaca/messagebus/interceptors/message_publisher_collector.py +62 -0
  28. jararaca/messagebus/interceptors/publisher_interceptor.py +29 -3
  29. jararaca/messagebus/message.py +4 -0
  30. jararaca/messagebus/publisher.py +6 -0
  31. jararaca/messagebus/worker.py +1002 -459
  32. jararaca/microservice.py +113 -2
  33. jararaca/observability/constants.py +7 -0
  34. jararaca/observability/decorators.py +170 -13
  35. jararaca/observability/fastapi_exception_handler.py +37 -0
  36. jararaca/observability/hooks.py +109 -0
  37. jararaca/observability/interceptor.py +4 -0
  38. jararaca/observability/providers/__init__.py +3 -0
  39. jararaca/observability/providers/otel.py +225 -16
  40. jararaca/persistence/base.py +39 -3
  41. jararaca/persistence/exports.py +4 -0
  42. jararaca/persistence/interceptors/__init__.py +3 -0
  43. jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
  44. jararaca/persistence/interceptors/constants.py +5 -0
  45. jararaca/persistence/interceptors/decorators.py +50 -0
  46. jararaca/persistence/session.py +3 -0
  47. jararaca/persistence/sort_filter.py +4 -0
  48. jararaca/persistence/utilities.py +73 -20
  49. jararaca/presentation/__init__.py +3 -0
  50. jararaca/presentation/decorators.py +88 -86
  51. jararaca/presentation/exceptions.py +23 -0
  52. jararaca/presentation/hooks.py +4 -0
  53. jararaca/presentation/http_microservice.py +4 -0
  54. jararaca/presentation/server.py +97 -45
  55. jararaca/presentation/websocket/__init__.py +3 -0
  56. jararaca/presentation/websocket/base_types.py +4 -0
  57. jararaca/presentation/websocket/context.py +4 -0
  58. jararaca/presentation/websocket/decorators.py +8 -41
  59. jararaca/presentation/websocket/redis.py +280 -53
  60. jararaca/presentation/websocket/types.py +4 -0
  61. jararaca/presentation/websocket/websocket_interceptor.py +46 -19
  62. jararaca/reflect/__init__.py +3 -0
  63. jararaca/reflect/controller_inspect.py +16 -10
  64. jararaca/reflect/decorators.py +252 -0
  65. jararaca/reflect/helpers.py +18 -0
  66. jararaca/reflect/metadata.py +34 -25
  67. jararaca/rpc/__init__.py +3 -0
  68. jararaca/rpc/http/__init__.py +101 -0
  69. jararaca/rpc/http/backends/__init__.py +14 -0
  70. jararaca/rpc/http/backends/httpx.py +43 -9
  71. jararaca/rpc/http/backends/otel.py +4 -0
  72. jararaca/rpc/http/decorators.py +380 -115
  73. jararaca/rpc/http/httpx.py +3 -0
  74. jararaca/scheduler/__init__.py +3 -0
  75. jararaca/scheduler/beat_worker.py +521 -105
  76. jararaca/scheduler/decorators.py +15 -22
  77. jararaca/scheduler/types.py +4 -0
  78. jararaca/tools/app_config/__init__.py +3 -0
  79. jararaca/tools/app_config/decorators.py +7 -19
  80. jararaca/tools/app_config/interceptor.py +6 -2
  81. jararaca/tools/typescript/__init__.py +3 -0
  82. jararaca/tools/typescript/decorators.py +120 -0
  83. jararaca/tools/typescript/interface_parser.py +1077 -174
  84. jararaca/utils/__init__.py +3 -0
  85. jararaca/utils/env_parse_utils.py +133 -0
  86. jararaca/utils/rabbitmq_utils.py +112 -39
  87. jararaca/utils/retry.py +19 -14
  88. jararaca-0.4.0a19.dist-info/LICENSE +674 -0
  89. jararaca-0.4.0a19.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
  90. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/METADATA +12 -7
  91. jararaca-0.4.0a19.dist-info/RECORD +96 -0
  92. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/WHEEL +1 -1
  93. pyproject.toml +132 -0
  94. jararaca-0.3.11a16.dist-info/RECORD +0 -74
  95. /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
  96. {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
  from jararaca.presentation.websocket.base_types import WebSocketMessageBase
2
6
  from jararaca.presentation.websocket.context import use_ws_message_sender
3
7
 
@@ -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
- for websocket in self.all_websockets:
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
- async with self.lock: # TODO: check if this can cause concurrency slowdown issues
94
- self.all_websockets.remove(websocket)
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
- for room in rooms:
108
- for websocket in self.rooms.get(room, set()):
109
- try:
110
- if websocket.client_state == WebSocketState.CONNECTED:
111
- await websocket.send_bytes(message)
112
- except WebSocketDisconnect:
113
- async with self.lock:
114
- if websocket in self.rooms[room]:
115
- self.rooms[room].remove(websocket)
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.get_controller(controller_type)
252
+ rest_controller = RestController.get_last(controller_type)
226
253
  controller: Any = container.get_by_type(controller_type)
227
254
 
228
- members = inspect.getmembers(controller_type, predicate=inspect.isfunction)
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.get(member)) is not None:
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.get_middlewares(
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.get_dependencies(
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(
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -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 ControllerInstanceMetadata, SetMetadata
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, ControllerInstanceMetadata]
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, ControllerInstanceMetadata]
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: ControllerInstanceMetadata(
41
- value=metadata.value, inherited=False
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: ControllerInstanceMetadata(
59
- value=value.value, inherited=True
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: ControllerInstanceMetadata(
65
- value=metadata.value, inherited=False
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.getmembers(controller, predicate=inspect.isfunction)
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
@@ -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, cast
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 ControllerInstanceMetadata:
15
+ @dataclass(frozen=True)
16
+ class TransactionMetadata:
11
17
  value: Any
12
- inherited: bool
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, ControllerInstanceMetadata]] = ContextVar(
24
+ metadata_context: ContextVar[Mapping[str, TransactionMetadata]] = ContextVar(
16
25
  "metadata_context"
17
26
  )
18
27
 
19
28
 
20
- def get_metadata(key: str) -> ControllerInstanceMetadata | None:
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, ControllerInstanceMetadata]:
40
+ def get_all_metadata() -> Mapping[str, TransactionMetadata]:
32
41
  return metadata_context.get({})
33
42
 
34
43
 
35
44
  @contextmanager
36
- def provide_metadata(metadata: Mapping[str, ControllerInstanceMetadata]) -> Any:
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
- class SetMetadata:
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
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -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
+ ]