jararaca 0.3.9__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 (35) 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/persistence/utilities.py +32 -20
  16. jararaca/presentation/decorators.py +96 -10
  17. jararaca/presentation/server.py +31 -4
  18. jararaca/presentation/websocket/context.py +30 -4
  19. jararaca/presentation/websocket/types.py +2 -2
  20. jararaca/presentation/websocket/websocket_interceptor.py +28 -4
  21. jararaca/reflect/__init__.py +0 -0
  22. jararaca/reflect/controller_inspect.py +75 -0
  23. jararaca/{tools → reflect}/metadata.py +25 -5
  24. jararaca/scheduler/{scheduler_v2.py → beat_worker.py} +49 -53
  25. jararaca/scheduler/decorators.py +55 -20
  26. jararaca/tools/app_config/interceptor.py +4 -2
  27. jararaca/utils/rabbitmq_utils.py +259 -5
  28. jararaca/utils/retry.py +141 -0
  29. {jararaca-0.3.9.dist-info → jararaca-0.3.11.dist-info}/METADATA +2 -1
  30. {jararaca-0.3.9.dist-info → jararaca-0.3.11.dist-info}/RECORD +33 -32
  31. {jararaca-0.3.9.dist-info → jararaca-0.3.11.dist-info}/WHEEL +1 -1
  32. jararaca/messagebus/worker_v2.py +0 -617
  33. jararaca/scheduler/scheduler.py +0 -161
  34. {jararaca-0.3.9.dist-info → jararaca-0.3.11.dist-info}/LICENSE +0 -0
  35. {jararaca-0.3.9.dist-info → jararaca-0.3.11.dist-info}/entry_points.txt +0 -0
jararaca/core/uow.py CHANGED
@@ -2,13 +2,14 @@ from contextlib import asynccontextmanager
2
2
  from typing import AsyncGenerator, Sequence
3
3
 
4
4
  from jararaca.microservice import (
5
- AppContext,
6
5
  AppInterceptor,
6
+ AppTransactionContext,
7
7
  Container,
8
8
  Microservice,
9
9
  provide_app_context,
10
10
  provide_container,
11
11
  )
12
+ from jararaca.reflect.metadata import provide_metadata
12
13
 
13
14
 
14
15
  class ContainerInterceptor(AppInterceptor):
@@ -17,7 +18,9 @@ class ContainerInterceptor(AppInterceptor):
17
18
  self.container = container
18
19
 
19
20
  @asynccontextmanager
20
- async def intercept(self, app_context: AppContext) -> AsyncGenerator[None, None]:
21
+ async def intercept(
22
+ self, app_context: AppTransactionContext
23
+ ) -> AsyncGenerator[None, None]:
21
24
 
22
25
  with provide_app_context(app_context), provide_container(self.container):
23
26
  yield None
@@ -49,18 +52,20 @@ class UnitOfWorkContextProvider:
49
52
  return interceptors
50
53
 
51
54
  @asynccontextmanager
52
- async def __call__(self, app_context: AppContext) -> AsyncGenerator[None, None]:
55
+ async def __call__(
56
+ self, app_context: AppTransactionContext
57
+ ) -> AsyncGenerator[None, None]:
53
58
 
54
59
  app_interceptors = self.factory_app_interceptors()
60
+ with provide_metadata(app_context.controller_member_reflect.metadata):
61
+ ctxs = [self.container_interceptor.intercept(app_context)] + [
62
+ interceptor.intercept(app_context) for interceptor in app_interceptors
63
+ ]
55
64
 
56
- ctxs = [self.container_interceptor.intercept(app_context)] + [
57
- interceptor.intercept(app_context) for interceptor in app_interceptors
58
- ]
65
+ for ctx in ctxs:
66
+ await ctx.__aenter__()
59
67
 
60
- for ctx in ctxs:
61
- await ctx.__aenter__()
62
-
63
- yield None
68
+ yield None
64
69
 
65
- for ctx in reversed(ctxs):
66
- await ctx.__aexit__(None, None, None)
70
+ for ctx in reversed(ctxs):
71
+ await ctx.__aexit__(None, None, None)
@@ -3,10 +3,14 @@ from dataclasses import dataclass
3
3
  from typing import Any, Awaitable, Callable, Generic, TypeVar, cast
4
4
 
5
5
  from jararaca.messagebus.message import INHERITS_MESSAGE_CO, Message, MessageOf
6
- from jararaca.scheduler.decorators import ScheduledAction
6
+ from jararaca.reflect.controller_inspect import (
7
+ ControllerMemberReflect,
8
+ inspect_controller,
9
+ )
10
+ from jararaca.scheduler.decorators import ScheduledAction, ScheduledActionData
7
11
 
8
12
  DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
9
- DECORATED_CLASS = TypeVar("DECORATED_CLASS", bound=Any)
13
+ DECORATED_T = TypeVar("DECORATED_T", bound=Any)
10
14
 
11
15
 
12
16
  class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
@@ -20,6 +24,7 @@ class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
20
24
  exception_handler: Callable[[BaseException], None] | None = None,
21
25
  nack_on_exception: bool = False,
22
26
  auto_ack: bool = True,
27
+ name: str | None = None,
23
28
  ) -> None:
24
29
  self.message_type = message
25
30
 
@@ -27,6 +32,7 @@ class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
27
32
  self.exception_handler = exception_handler
28
33
  self.requeue_on_exception = nack_on_exception
29
34
  self.auto_ack = auto_ack
35
+ self.name = name
30
36
 
31
37
  def __call__(
32
38
  self, func: Callable[[Any, MessageOf[INHERITS_MESSAGE_CO]], Awaitable[None]]
@@ -60,7 +66,8 @@ class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
60
66
  class MessageHandlerData:
61
67
  message_type: type[Any]
62
68
  spec: MessageHandler[Message]
63
- callable: Callable[[MessageOf[Any]], Awaitable[None]]
69
+ instance_callable: Callable[[MessageOf[Any]], Awaitable[None]]
70
+ controller_member: ControllerMemberReflect
64
71
 
65
72
 
66
73
  @dataclass(frozen=True)
@@ -68,16 +75,9 @@ class ScheduleDispatchData:
68
75
  timestamp: float
69
76
 
70
77
 
71
- @dataclass(frozen=True)
72
- class ScheduledActionData:
73
- spec: ScheduledAction
74
- callable: Callable[
75
- ..., Awaitable[None]
76
- ] # Callable[[ScheduleDispatchData], Awaitable[None]]
77
-
78
+ SCHEDULED_ACTION_DATA_SET = set[ScheduledActionData]
78
79
 
79
80
  MESSAGE_HANDLER_DATA_SET = set[MessageHandlerData]
80
- SCHEDULED_ACTION_DATA_SET = set[ScheduledActionData]
81
81
 
82
82
 
83
83
  class MessageBusController:
@@ -93,55 +93,60 @@ class MessageBusController:
93
93
  def get_messagebus_factory(
94
94
  self,
95
95
  ) -> Callable[
96
- [DECORATED_CLASS], tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]
96
+ [DECORATED_T], tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]
97
97
  ]:
98
98
  if self.messagebus_factory is None:
99
99
  raise Exception("MessageBus factory is not set")
100
100
  return self.messagebus_factory
101
101
 
102
- def __call__(self, func: type[DECORATED_CLASS]) -> type[DECORATED_CLASS]:
102
+ def __call__(self, cls_t: type[DECORATED_T]) -> type[DECORATED_T]:
103
103
 
104
104
  def messagebus_factory(
105
- instance: DECORATED_CLASS,
105
+ instance: DECORATED_T,
106
106
  ) -> tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]:
107
107
  handlers: MESSAGE_HANDLER_DATA_SET = set()
108
108
 
109
109
  schedulers: SCHEDULED_ACTION_DATA_SET = set()
110
110
 
111
- members = inspect.getmembers(func, predicate=inspect.isfunction)
111
+ _, members = inspect_controller(cls_t)
112
112
 
113
- for name, member in members:
114
- message_handler_decoration = MessageHandler.get_message_incoming(member)
113
+ for name, member in members.items():
114
+ message_handler_decoration = MessageHandler.get_message_incoming(
115
+ member.member_function
116
+ )
115
117
  scheduled_action_decoration = ScheduledAction.get_scheduled_action(
116
- member
118
+ member.member_function
117
119
  )
118
120
 
119
121
  if message_handler_decoration is not None:
120
122
 
121
- if not inspect.iscoroutinefunction(member):
123
+ if not inspect.iscoroutinefunction(member.member_function):
122
124
  raise Exception(
123
125
  "Message incoming handler '%s' from '%s.%s' must be a coroutine function"
124
- % (name, func.__module__, func.__qualname__)
126
+ % (name, cls_t.__module__, cls_t.__qualname__)
125
127
  )
126
128
 
127
129
  handlers.add(
128
130
  MessageHandlerData(
129
131
  message_type=message_handler_decoration.message_type,
130
132
  spec=message_handler_decoration,
131
- callable=getattr(instance, name),
133
+ instance_callable=getattr(instance, name),
134
+ controller_member=member,
132
135
  )
133
136
  )
134
137
  elif scheduled_action_decoration is not None:
135
- if not inspect.iscoroutinefunction(member):
138
+ if not inspect.iscoroutinefunction(member.member_function):
136
139
  raise Exception(
137
140
  "Scheduled action handler '%s' from '%s.%s' must be a coroutine function"
138
- % (name, func.__module__, func.__qualname__)
141
+ % (name, cls_t.__module__, cls_t.__qualname__)
139
142
  )
143
+ instance_callable = getattr(instance, name)
140
144
 
141
145
  schedulers.add(
142
146
  ScheduledActionData(
147
+ controller_member=member,
143
148
  spec=scheduled_action_decoration,
144
- callable=getattr(instance, name),
149
+ callable=instance_callable,
145
150
  )
146
151
  )
147
152
 
@@ -149,19 +154,17 @@ class MessageBusController:
149
154
 
150
155
  self.messagebus_factory = messagebus_factory
151
156
 
152
- MessageBusController.register(func, self)
157
+ MessageBusController.register(cls_t, self)
153
158
 
154
- return func
159
+ return cls_t
155
160
 
156
161
  @staticmethod
157
- def register(
158
- func: type[DECORATED_CLASS], messagebus: "MessageBusController"
159
- ) -> None:
162
+ def register(func: type[DECORATED_T], messagebus: "MessageBusController") -> None:
160
163
 
161
164
  setattr(func, MessageBusController.MESSAGEBUS_ATTR, messagebus)
162
165
 
163
166
  @staticmethod
164
- def get_messagebus(func: type[DECORATED_CLASS]) -> "MessageBusController | None":
167
+ def get_messagebus(func: type[DECORATED_T]) -> "MessageBusController | None":
165
168
  if not hasattr(func, MessageBusController.MESSAGEBUS_ATTR):
166
169
  return None
167
170
 
@@ -15,6 +15,8 @@ from jararaca.messagebus.interceptors.publisher_interceptor import (
15
15
  from jararaca.messagebus.publisher import IMessage, MessagePublisher
16
16
  from jararaca.scheduler.types import DelayedMessageData
17
17
 
18
+ logger = logging.getLogger(__name__)
19
+
18
20
 
19
21
  class AIOPikaMessagePublisher(MessagePublisher):
20
22
 
@@ -28,8 +30,13 @@ class AIOPikaMessagePublisher(MessagePublisher):
28
30
  self.channel = channel
29
31
  self.exchange_name = exchange_name
30
32
  self.message_broker_backend = message_broker_backend
33
+ self.staged_delayed_messages: list[DelayedMessageData] = []
34
+ self.staged_messages: list[IMessage] = []
31
35
 
32
36
  async def publish(self, message: IMessage, topic: str) -> None:
37
+ self.staged_messages.append(message)
38
+
39
+ async def _publish(self, message: IMessage, topic: str) -> None:
33
40
  exchange = await self.channel.get_exchange(self.exchange_name, ensure=False)
34
41
  if not exchange:
35
42
  logging.warning(f"Exchange {self.exchange_name} not found")
@@ -45,7 +52,7 @@ class AIOPikaMessagePublisher(MessagePublisher):
45
52
  raise NotImplementedError(
46
53
  "Delay is not implemented for AIOPikaMessagePublisher"
47
54
  )
48
- await self.message_broker_backend.enqueue_delayed_message(
55
+ self.staged_delayed_messages.append(
49
56
  DelayedMessageData(
50
57
  message_topic=message.MESSAGE_TOPIC,
51
58
  payload=message.model_dump_json().encode(),
@@ -60,7 +67,7 @@ class AIOPikaMessagePublisher(MessagePublisher):
60
67
  raise NotImplementedError(
61
68
  "Schedule is not implemented for AIOPikaMessagePublisher"
62
69
  )
63
- await self.message_broker_backend.enqueue_delayed_message(
70
+ self.staged_delayed_messages.append(
64
71
  DelayedMessageData(
65
72
  message_topic=message.MESSAGE_TOPIC,
66
73
  payload=message.model_dump_json().encode(),
@@ -68,6 +75,27 @@ class AIOPikaMessagePublisher(MessagePublisher):
68
75
  )
69
76
  )
70
77
 
78
+ async def flush(self) -> None:
79
+ for message in self.staged_messages:
80
+ logger.debug(
81
+ f"Publishing message {message.MESSAGE_TOPIC} with payload: {message.model_dump_json()}"
82
+ )
83
+ await self._publish(message, message.MESSAGE_TOPIC)
84
+
85
+ if len(self.staged_delayed_messages) > 0:
86
+ if not self.message_broker_backend:
87
+ raise NotImplementedError(
88
+ "MessageBrokerBackend is required to publish delayed messages"
89
+ )
90
+
91
+ for delayed_message in self.staged_delayed_messages:
92
+ logger.debug(
93
+ f"Scheduling delayed message {delayed_message.message_topic} with payload: {delayed_message.payload.decode()}"
94
+ )
95
+ await self.message_broker_backend.enqueue_delayed_message(
96
+ delayed_message
97
+ )
98
+
71
99
 
72
100
  class GenericPoolConfig(BaseModel):
73
101
  max_size: int
@@ -3,7 +3,7 @@ from typing import AsyncContextManager, AsyncGenerator, Protocol
3
3
 
4
4
  from jararaca.broker_backend import MessageBrokerBackend
5
5
  from jararaca.messagebus.publisher import MessagePublisher, provide_message_publisher
6
- from jararaca.microservice import AppContext, AppInterceptor
6
+ from jararaca.microservice import AppInterceptor, AppTransactionContext
7
7
 
8
8
 
9
9
  class MessageBusConnectionFactory(Protocol):
@@ -24,11 +24,15 @@ class MessageBusPublisherInterceptor(AppInterceptor):
24
24
  self.message_scheduler = message_scheduler
25
25
 
26
26
  @asynccontextmanager
27
- async def intercept(self, app_context: AppContext) -> AsyncGenerator[None, None]:
28
- if app_context.context_type == "websocket":
27
+ async def intercept(
28
+ self, app_context: AppTransactionContext
29
+ ) -> AsyncGenerator[None, None]:
30
+ if app_context.transaction_data.context_type == "websocket":
29
31
  yield
30
32
  return
31
33
 
32
34
  async with self.connection_factory.provide_connection() as connection:
33
35
  with provide_message_publisher(self.connection_name, connection):
34
36
  yield
37
+
38
+ await connection.flush()
@@ -1,7 +1,8 @@
1
+ from abc import ABC, abstractmethod
1
2
  from contextlib import contextmanager, suppress
2
3
  from contextvars import ContextVar
3
4
  from datetime import datetime, tzinfo
4
- from typing import Any, ClassVar, Generator, Literal, Protocol
5
+ from typing import Any, ClassVar, Generator, Literal
5
6
 
6
7
  from pydantic import BaseModel
7
8
 
@@ -19,24 +20,31 @@ class IMessage(BaseModel):
19
20
  MESSAGE_TYPE: ClassVar[Literal["task", "event"]] = "task"
20
21
 
21
22
 
22
- class MessagePublisher(Protocol):
23
+ class MessagePublisher(ABC):
24
+ @abstractmethod
23
25
  async def publish(self, message: IMessage, topic: str) -> None:
24
- raise NotImplementedError()
26
+ pass
25
27
 
28
+ @abstractmethod
26
29
  async def delay(self, message: IMessage, seconds: int) -> None:
27
30
  """
28
31
  Delay the message for a given number of seconds.
29
32
  """
30
33
 
31
- raise NotImplementedError()
32
-
34
+ @abstractmethod
33
35
  async def schedule(
34
36
  self, message: IMessage, when: datetime, timezone: tzinfo
35
37
  ) -> None:
36
38
  """
37
39
  Schedule the message for a given datetime.
38
40
  """
39
- raise NotImplementedError()
41
+
42
+ @abstractmethod
43
+ async def flush(self) -> None:
44
+ """
45
+ Publish all messages that have been delayed or scheduled.
46
+ This is typically called at the end of a request or task processing.
47
+ """
40
48
 
41
49
 
42
50
  message_publishers_ctx = ContextVar[dict[str, MessagePublisher]](