jararaca 0.3.10__py3-none-any.whl → 0.3.11__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.

Files changed (34) hide show
  1. jararaca/__init__.py +76 -5
  2. jararaca/cli.py +460 -116
  3. jararaca/core/uow.py +17 -12
  4. jararaca/messagebus/decorators.py +33 -30
  5. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +30 -2
  6. jararaca/messagebus/interceptors/publisher_interceptor.py +7 -3
  7. jararaca/messagebus/publisher.py +14 -6
  8. jararaca/messagebus/worker.py +1102 -88
  9. jararaca/microservice.py +137 -34
  10. jararaca/observability/decorators.py +7 -3
  11. jararaca/observability/interceptor.py +4 -2
  12. jararaca/observability/providers/otel.py +14 -10
  13. jararaca/persistence/base.py +2 -1
  14. jararaca/persistence/interceptors/aiosqa_interceptor.py +167 -16
  15. jararaca/presentation/decorators.py +96 -10
  16. jararaca/presentation/server.py +31 -4
  17. jararaca/presentation/websocket/context.py +30 -4
  18. jararaca/presentation/websocket/types.py +2 -2
  19. jararaca/presentation/websocket/websocket_interceptor.py +28 -4
  20. jararaca/reflect/__init__.py +0 -0
  21. jararaca/reflect/controller_inspect.py +75 -0
  22. jararaca/{tools → reflect}/metadata.py +25 -5
  23. jararaca/scheduler/{scheduler_v2.py → beat_worker.py} +49 -53
  24. jararaca/scheduler/decorators.py +55 -20
  25. jararaca/tools/app_config/interceptor.py +4 -2
  26. jararaca/utils/rabbitmq_utils.py +259 -5
  27. jararaca/utils/retry.py +141 -0
  28. {jararaca-0.3.10.dist-info → jararaca-0.3.11.dist-info}/METADATA +2 -1
  29. {jararaca-0.3.10.dist-info → jararaca-0.3.11.dist-info}/RECORD +32 -31
  30. {jararaca-0.3.10.dist-info → jararaca-0.3.11.dist-info}/WHEEL +1 -1
  31. jararaca/messagebus/worker_v2.py +0 -617
  32. jararaca/scheduler/scheduler.py +0 -161
  33. {jararaca-0.3.10.dist-info → jararaca-0.3.11.dist-info}/LICENSE +0 -0
  34. {jararaca-0.3.10.dist-info → jararaca-0.3.11.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,7 @@
1
- import inspect
2
- from typing import Any, Callable, Literal, Protocol, TypeVar, cast
1
+ from contextlib import contextmanager
2
+ from contextvars import ContextVar
3
+ from functools import wraps
4
+ from typing import Any, Awaitable, Callable, Literal, Protocol, TypeVar, cast
3
5
 
4
6
  from fastapi import APIRouter
5
7
  from fastapi import Depends as DependsF
@@ -9,6 +11,10 @@ from fastapi.params import Depends
9
11
  from jararaca.lifecycle import AppLifecycle
10
12
  from jararaca.presentation.http_microservice import HttpMiddleware
11
13
  from jararaca.presentation.websocket.decorators import WebSocketEndpoint
14
+ from jararaca.reflect.controller_inspect import (
15
+ ControllerMemberReflect,
16
+ inspect_controller,
17
+ )
12
18
 
13
19
  DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
14
20
  DECORATED_CLASS = TypeVar("DECORATED_CLASS", bound=Any)
@@ -73,15 +79,15 @@ class RestController:
73
79
  **(self.options or {}),
74
80
  )
75
81
 
76
- members = inspect.getmembers(cls, predicate=inspect.isfunction)
82
+ controller, members = inspect_controller(cls)
77
83
 
78
84
  router_members = [
79
- (name, mapping)
80
- for name, member in members
85
+ (name, mapping, member)
86
+ for name, member in members.items()
81
87
  if (
82
88
  mapping := (
83
- HttpMapping.get_http_mapping(member)
84
- or WebSocketEndpoint.get(member)
89
+ HttpMapping.get_http_mapping(member.member_function)
90
+ or WebSocketEndpoint.get(member.member_function)
85
91
  )
86
92
  )
87
93
  is not None
@@ -89,7 +95,7 @@ class RestController:
89
95
 
90
96
  router_members.sort(key=lambda x: x[1].order)
91
97
 
92
- for name, mapping in router_members:
98
+ for name, mapping, member in router_members:
93
99
  route_dependencies: list[Depends] = []
94
100
  for middlewares_by_hook in UseMiddleware.get_middlewares(
95
101
  getattr(instance, name)
@@ -104,12 +110,18 @@ class RestController:
104
110
  ):
105
111
  route_dependencies.append(DependsF(dependency.dependency))
106
112
 
113
+ instance_method = getattr(instance, name)
114
+ instance_method = wraps_with_attributes(
115
+ instance_method,
116
+ controller_member_reflect=member,
117
+ )
118
+
107
119
  if isinstance(mapping, HttpMapping):
108
120
  try:
109
121
  router.add_api_route(
110
122
  methods=[mapping.method],
111
123
  path=mapping.path,
112
- endpoint=getattr(instance, name),
124
+ endpoint=instance_method,
113
125
  dependencies=route_dependencies,
114
126
  **(mapping.options or {}),
115
127
  )
@@ -120,7 +132,7 @@ class RestController:
120
132
  else:
121
133
  router.add_api_websocket_route(
122
134
  path=mapping.path,
123
- endpoint=getattr(instance, name),
135
+ endpoint=instance_method,
124
136
  dependencies=route_dependencies,
125
137
  **(mapping.options or {}),
126
138
  )
@@ -299,3 +311,77 @@ class UseDependency:
299
311
  @staticmethod
300
312
  def get_dependencies(subject: DECORATED_FUNC) -> list["UseDependency"]:
301
313
  return getattr(subject, UseDependency.__DEPENDENCY_ATTR__, [])
314
+
315
+
316
+ def wraps_with_member_data(
317
+ controller_member: ControllerMemberReflect, func: Callable[..., Awaitable[Any]]
318
+ ) -> Callable[..., Any]:
319
+ """
320
+ A decorator that wraps a function and preserves its metadata.
321
+ This is useful for preserving metadata when using decorators.
322
+ """
323
+
324
+ @wraps(func)
325
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
326
+
327
+ with providing_controller_member(
328
+ controller_member=controller_member,
329
+ ):
330
+
331
+ return await func(*args, **kwargs)
332
+
333
+ # Copy metadata from the original function to the wrapper
334
+ # for attr in dir(func):
335
+ # if not attr.startswith("__"):
336
+ # setattr(wrapper, attr, getattr(func, attr))
337
+
338
+ return wrapper
339
+
340
+
341
+ controller_member_ctxvar = ContextVar[ControllerMemberReflect](
342
+ "controller_member_ctxvar"
343
+ )
344
+
345
+
346
+ @contextmanager
347
+ def providing_controller_member(
348
+ controller_member: ControllerMemberReflect,
349
+ ) -> Any:
350
+ """
351
+ Context manager to provide the controller member metadata.
352
+ This is used to preserve the metadata of the controller member
353
+ when using decorators.
354
+ """
355
+ token = controller_member_ctxvar.set(controller_member)
356
+ try:
357
+ yield
358
+ finally:
359
+ controller_member_ctxvar.reset(token)
360
+
361
+
362
+ def use_controller_member() -> ControllerMemberReflect:
363
+ """
364
+ Get the current controller member metadata.
365
+ This is used to access the metadata of the controller member
366
+ when using decorators.
367
+ """
368
+ return controller_member_ctxvar.get()
369
+
370
+
371
+ def wraps_with_attributes(
372
+ func: Callable[..., Awaitable[Any]], **attributes: Any
373
+ ) -> Callable[..., Awaitable[Any]]:
374
+ """
375
+ A decorator that wraps a function and preserves its attributes.
376
+ This is useful for preserving attributes when using decorators.
377
+ """
378
+
379
+ @wraps(func)
380
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
381
+ return await func(*args, **kwargs)
382
+
383
+ # Copy attributes from the original function to the wrapper
384
+ for key, value in attributes.items():
385
+ setattr(wrapper, key, value)
386
+
387
+ return wrapper
@@ -7,9 +7,14 @@ from starlette.types import ASGIApp
7
7
  from jararaca.core.uow import UnitOfWorkContextProvider
8
8
  from jararaca.di import Container
9
9
  from jararaca.lifecycle import AppLifecycle
10
- from jararaca.microservice import HttpAppContext, WebSocketAppContext
10
+ from jararaca.microservice import (
11
+ AppTransactionContext,
12
+ HttpTransactionData,
13
+ WebSocketTransactionData,
14
+ )
11
15
  from jararaca.presentation.decorators import RestController
12
16
  from jararaca.presentation.http_microservice import HttpMicroservice
17
+ from jararaca.reflect.controller_inspect import ControllerMemberReflect
13
18
 
14
19
 
15
20
  class HttpAppLifecycle:
@@ -79,10 +84,32 @@ class HttpUowContextProviderDependency:
79
84
  async def __call__(
80
85
  self, websocket: WebSocket = None, request: Request = None # type: ignore
81
86
  ) -> AsyncGenerator[None, None]:
87
+ if request:
88
+ endpoint = request.scope["endpoint"]
89
+ elif websocket:
90
+ endpoint = websocket.scope["endpoint"]
91
+ else:
92
+ raise ValueError("Either request or websocket must be provided.")
93
+
94
+ member = getattr(endpoint, "controller_member_reflect", None)
95
+
96
+ if member is None:
97
+ raise ValueError("The endpoint does not have a controller member reflect.")
98
+
99
+ assert isinstance(member, ControllerMemberReflect), (
100
+ "Expected endpoint.controller_member_reflect to be of type "
101
+ "ControllerMemberReflect, but got: {}".format(type(member))
102
+ )
103
+
82
104
  async with self.uow_provider(
83
- HttpAppContext(request=request)
84
- if request
85
- else WebSocketAppContext(websocket=websocket)
105
+ AppTransactionContext(
106
+ controller_member_reflect=member,
107
+ transaction_data=(
108
+ HttpTransactionData(request=request)
109
+ if request
110
+ else WebSocketTransactionData(websocket=websocket)
111
+ ),
112
+ )
86
113
  ):
87
114
  yield
88
115
 
@@ -16,12 +16,12 @@ class WebSocketConnectionManager(Protocol):
16
16
  async def remove_websocket(self, websocket: WebSocket) -> None: ...
17
17
 
18
18
 
19
- _ws_manage_ctx = ContextVar[WebSocketConnectionManager]("ws_manage_ctx")
19
+ _ws_conn_manager_ctx = ContextVar[WebSocketConnectionManager]("ws_manage_ctx")
20
20
 
21
21
 
22
22
  def use_ws_manager() -> WebSocketConnectionManager:
23
23
  try:
24
- return _ws_manage_ctx.get()
24
+ return _ws_conn_manager_ctx.get()
25
25
  except LookupError:
26
26
  raise RuntimeError("No WebSocketConnectionManager found")
27
27
 
@@ -30,9 +30,35 @@ def use_ws_manager() -> WebSocketConnectionManager:
30
30
  def provide_ws_manager(
31
31
  ws_manager: WebSocketConnectionManager,
32
32
  ) -> Generator[None, None, None]:
33
- token = _ws_manage_ctx.set(ws_manager)
33
+ token = _ws_conn_manager_ctx.set(ws_manager)
34
34
  try:
35
35
  yield
36
36
  finally:
37
37
  with suppress(ValueError):
38
- _ws_manage_ctx.reset(token)
38
+ _ws_conn_manager_ctx.reset(token)
39
+
40
+
41
+ class WebSocketMessageSender(Protocol):
42
+ async def send(self, rooms: list[str], message: WebSocketMessageBase) -> None: ...
43
+
44
+
45
+ _ws_msg_sender_ctx = ContextVar[WebSocketMessageSender]("ws_msg_sender_ctx")
46
+
47
+
48
+ def use_ws_message_sender() -> WebSocketMessageSender:
49
+ try:
50
+ return _ws_msg_sender_ctx.get()
51
+ except LookupError:
52
+ raise RuntimeError("No WebSocketMessageSender found")
53
+
54
+
55
+ @contextmanager
56
+ def provide_ws_message_sender(
57
+ ws_message_sender: WebSocketMessageSender,
58
+ ) -> Generator[None, None, None]:
59
+ token = _ws_msg_sender_ctx.set(ws_message_sender)
60
+ try:
61
+ yield
62
+ finally:
63
+ with suppress(ValueError):
64
+ _ws_msg_sender_ctx.reset(token)
@@ -1,8 +1,8 @@
1
1
  from jararaca.presentation.websocket.base_types import WebSocketMessageBase
2
- from jararaca.presentation.websocket.context import use_ws_manager
2
+ from jararaca.presentation.websocket.context import use_ws_message_sender
3
3
 
4
4
 
5
5
  class WebSocketMessage(WebSocketMessageBase):
6
6
 
7
7
  async def send(self, *rooms: str) -> None:
8
- await use_ws_manager().send(list(rooms), self)
8
+ await use_ws_message_sender().send(list(rooms), self)
@@ -13,9 +13,9 @@ from pydantic import BaseModel
13
13
  from jararaca.core.uow import UnitOfWorkContextProvider
14
14
  from jararaca.di import Container
15
15
  from jararaca.microservice import (
16
- AppContext,
17
16
  AppInterceptor,
18
17
  AppInterceptorWithLifecycle,
18
+ AppTransactionContext,
19
19
  Microservice,
20
20
  )
21
21
  from jararaca.presentation.decorators import (
@@ -25,7 +25,9 @@ from jararaca.presentation.decorators import (
25
25
  )
26
26
  from jararaca.presentation.websocket.context import (
27
27
  WebSocketConnectionManager,
28
+ WebSocketMessageSender,
28
29
  provide_ws_manager,
30
+ provide_ws_message_sender,
29
31
  )
30
32
  from jararaca.presentation.websocket.decorators import (
31
33
  INHERITS_WS_MESSAGE,
@@ -127,9 +129,23 @@ class WebSocketConnectionManagerImpl(WebSocketConnectionManager):
127
129
  # async def setup_consumer(self, websocket: WebSocket) -> None: ...
128
130
 
129
131
 
132
+ class StagingWebSocketMessageSender(WebSocketMessageSender):
133
+
134
+ def __init__(self, connection: WebSocketConnectionManager) -> None:
135
+ self.connection = connection
136
+ self.staged_messages: list[tuple[list[str], WebSocketMessageBase]] = []
137
+
138
+ async def send(self, rooms: list[str], message: WebSocketMessageBase) -> None:
139
+ self.staged_messages.append((rooms, message))
140
+
141
+ async def flush(self) -> None:
142
+ for rooms, message in self.staged_messages:
143
+ await self.connection.send(rooms, message)
144
+ self.staged_messages.clear()
145
+
146
+
130
147
  class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
131
148
  """
132
- @Deprecated
133
149
  WebSocketInterceptor is responsible for managing WebSocket connections and
134
150
  intercepting WebSocket requests within the application. It integrates with
135
151
  the application's lifecycle and provides a router for WebSocket endpoints.
@@ -169,10 +185,18 @@ class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
169
185
  self.shutdown_event.set()
170
186
 
171
187
  @asynccontextmanager
172
- async def intercept(self, app_context: AppContext) -> AsyncGenerator[None, None]:
188
+ async def intercept(
189
+ self, app_context: AppTransactionContext
190
+ ) -> AsyncGenerator[None, None]:
173
191
 
174
- with provide_ws_manager(self.connection_manager):
192
+ staging_ws_messages_sender = StagingWebSocketMessageSender(
193
+ self.connection_manager
194
+ )
195
+ with provide_ws_manager(self.connection_manager), provide_ws_message_sender(
196
+ staging_ws_messages_sender
197
+ ):
175
198
  yield
199
+ await staging_ws_messages_sender.flush()
176
200
 
177
201
  # def __wrap_with_uow_context_provider(
178
202
  # self, uow: UnitOfWorkContextProvider, func: Callable[..., Any]
File without changes
@@ -0,0 +1,75 @@
1
+ import inspect
2
+ from dataclasses import dataclass
3
+ from typing import Any, Callable, Mapping, Tuple, Type
4
+
5
+ from frozendict import frozendict
6
+
7
+ from jararaca.reflect.metadata import ControllerInstanceMetadata, SetMetadata
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ControllerReflect:
12
+
13
+ controller_class: Type[Any]
14
+ metadata: Mapping[str, ControllerInstanceMetadata]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ControllerMemberReflect:
19
+ controller_reflect: ControllerReflect
20
+ member_function: Callable[..., Any]
21
+ metadata: Mapping[str, ControllerInstanceMetadata]
22
+
23
+
24
+ def inspect_controller(
25
+ controller: Type[Any],
26
+ ) -> Tuple[ControllerReflect, Mapping[str, ControllerMemberReflect]]:
27
+ """
28
+ Inspect a controller class to extract its metadata and member functions.
29
+
30
+ Args:
31
+ controller (Type[Any]): The controller class to inspect.
32
+
33
+ Returns:
34
+ Tuple[ControllerReflect, list[ControllerMemberReflect]]: A tuple containing the controller reflect and a list of member reflects.
35
+ """
36
+ controller_metadata_list = SetMetadata.get(controller)
37
+
38
+ controller_metadata_map = frozendict(
39
+ {
40
+ metadata.key: ControllerInstanceMetadata(
41
+ value=metadata.value, inherited=False
42
+ )
43
+ for metadata in controller_metadata_list
44
+ }
45
+ )
46
+
47
+ controller_reflect = ControllerReflect(
48
+ controller_class=controller, metadata=controller_metadata_map
49
+ )
50
+
51
+ members = {
52
+ name: ControllerMemberReflect(
53
+ controller_reflect=controller_reflect,
54
+ member_function=member,
55
+ metadata=frozendict(
56
+ {
57
+ **{
58
+ key: ControllerInstanceMetadata(
59
+ value=value.value, inherited=True
60
+ )
61
+ for key, value in controller_metadata_map.items()
62
+ },
63
+ **{
64
+ metadata.key: ControllerInstanceMetadata(
65
+ value=metadata.value, inherited=False
66
+ )
67
+ for metadata in SetMetadata.get(member)
68
+ },
69
+ }
70
+ ),
71
+ )
72
+ for name, member in inspect.getmembers(controller, predicate=inspect.isfunction)
73
+ }
74
+
75
+ return controller_reflect, members
@@ -1,19 +1,39 @@
1
1
  from contextlib import contextmanager, suppress
2
2
  from contextvars import ContextVar
3
- from typing import Any, Awaitable, Callable, TypeVar, Union, cast
3
+ from dataclasses import dataclass
4
+ from typing import Any, Awaitable, Callable, Mapping, TypeVar, Union, cast
4
5
 
5
6
  DECORATED = TypeVar("DECORATED", bound=Union[Callable[..., Awaitable[Any]], type])
6
7
 
7
8
 
8
- metadata_context: ContextVar[dict[str, Any]] = ContextVar("metadata_context")
9
+ @dataclass
10
+ class ControllerInstanceMetadata:
11
+ value: Any
12
+ inherited: bool
9
13
 
10
14
 
11
- def get_metadata(key: str) -> Any | None:
15
+ metadata_context: ContextVar[Mapping[str, ControllerInstanceMetadata]] = ContextVar(
16
+ "metadata_context"
17
+ )
18
+
19
+
20
+ def get_metadata(key: str) -> ControllerInstanceMetadata | None:
12
21
  return metadata_context.get({}).get(key)
13
22
 
14
23
 
24
+ def get_metadata_value(key: str, default: Any | None = None) -> Any:
25
+ metadata = get_metadata(key)
26
+ if metadata is None:
27
+ return default
28
+ return metadata.value
29
+
30
+
31
+ def get_all_metadata() -> Mapping[str, ControllerInstanceMetadata]:
32
+ return metadata_context.get({})
33
+
34
+
15
35
  @contextmanager
16
- def provide_metadata(metadata: dict[str, Any]) -> Any:
36
+ def provide_metadata(metadata: Mapping[str, ControllerInstanceMetadata]) -> Any:
17
37
 
18
38
  current_metadata = metadata_context.get({})
19
39
 
@@ -39,7 +59,7 @@ class SetMetadata:
39
59
  setattr(cls, SetMetadata.METATADA_LIST, metadata_list)
40
60
 
41
61
  @staticmethod
42
- def get(cls: type) -> "list[SetMetadata]":
62
+ def get(cls: DECORATED) -> "list[SetMetadata]":
43
63
  return cast(list[SetMetadata], getattr(cls, SetMetadata.METATADA_LIST, []))
44
64
 
45
65
  def __call__(self, cls: DECORATED) -> DECORATED: