jararaca 0.2.37a12__py3-none-any.whl → 0.3.1__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.

@@ -2,7 +2,8 @@ import inspect
2
2
  from dataclasses import dataclass
3
3
  from typing import Any, Awaitable, Callable, Generic, TypeVar, cast
4
4
 
5
- from jararaca.messagebus.types import INHERITS_MESSAGE_CO, Message, MessageOf
5
+ from jararaca.messagebus.message import INHERITS_MESSAGE_CO, Message, MessageOf
6
+ from jararaca.scheduler.decorators import ScheduledAction
6
7
 
7
8
  DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
8
9
  DECORATED_CLASS = TypeVar("DECORATED_CLASS", bound=Any)
@@ -45,7 +46,7 @@ class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
45
46
 
46
47
  @staticmethod
47
48
  def get_message_incoming(
48
- func: Callable[[MessageOf[Any]], Awaitable[Any]]
49
+ func: Callable[[MessageOf[Any]], Awaitable[Any]],
49
50
  ) -> "MessageHandler[Message] | None":
50
51
  if not hasattr(func, MessageHandler.MESSAGE_INCOMING_ATTR):
51
52
  return None
@@ -62,7 +63,21 @@ class MessageHandlerData:
62
63
  callable: Callable[[MessageOf[Any]], Awaitable[None]]
63
64
 
64
65
 
66
+ @dataclass(frozen=True)
67
+ class ScheduleDispatchData:
68
+ timestamp: float
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class ScheduledActionData:
73
+ spec: ScheduledAction
74
+ callable: Callable[
75
+ ..., Awaitable[None]
76
+ ] # Callable[[ScheduleDispatchData], Awaitable[None]]
77
+
78
+
65
79
  MESSAGE_HANDLER_DATA_SET = set[MessageHandlerData]
80
+ SCHEDULED_ACTION_DATA_SET = set[ScheduledActionData]
66
81
 
67
82
 
68
83
  class MessageBusController:
@@ -70,11 +85,16 @@ class MessageBusController:
70
85
  MESSAGEBUS_ATTR = "__messagebus__"
71
86
 
72
87
  def __init__(self) -> None:
73
- self.messagebus_factory: Callable[[Any], MESSAGE_HANDLER_DATA_SET] | None = None
88
+ self.messagebus_factory: (
89
+ Callable[[Any], tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]]
90
+ | None
91
+ ) = None
74
92
 
75
93
  def get_messagebus_factory(
76
94
  self,
77
- ) -> Callable[[DECORATED_CLASS], MESSAGE_HANDLER_DATA_SET]:
95
+ ) -> Callable[
96
+ [DECORATED_CLASS], tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]
97
+ ]:
78
98
  if self.messagebus_factory is None:
79
99
  raise Exception("MessageBus factory is not set")
80
100
  return self.messagebus_factory
@@ -83,33 +103,49 @@ class MessageBusController:
83
103
 
84
104
  def messagebus_factory(
85
105
  instance: DECORATED_CLASS,
86
- ) -> MESSAGE_HANDLER_DATA_SET:
106
+ ) -> tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]:
87
107
  handlers: MESSAGE_HANDLER_DATA_SET = set()
88
- inspect.signature(func)
108
+
109
+ schedulers: SCHEDULED_ACTION_DATA_SET = set()
89
110
 
90
111
  members = inspect.getmembers(func, predicate=inspect.isfunction)
91
112
 
92
113
  for name, member in members:
93
- message_incoming = MessageHandler.get_message_incoming(member)
114
+ message_handler_decoration = MessageHandler.get_message_incoming(member)
115
+ scheduled_action_decoration = ScheduledAction.get_scheduled_action(
116
+ member
117
+ )
94
118
 
95
- if message_incoming is None:
96
- continue
119
+ if message_handler_decoration is not None:
97
120
 
98
- if not inspect.iscoroutinefunction(member):
99
- raise Exception(
100
- "Message incoming handler '%s' from '%s.%s' must be a coroutine function"
101
- % (name, func.__module__, func.__qualname__)
102
- )
121
+ if not inspect.iscoroutinefunction(member):
122
+ raise Exception(
123
+ "Message incoming handler '%s' from '%s.%s' must be a coroutine function"
124
+ % (name, func.__module__, func.__qualname__)
125
+ )
103
126
 
104
- handlers.add(
105
- MessageHandlerData(
106
- message_type=message_incoming.message_type,
107
- spec=message_incoming,
108
- callable=getattr(instance, name),
127
+ handlers.add(
128
+ MessageHandlerData(
129
+ message_type=message_handler_decoration.message_type,
130
+ spec=message_handler_decoration,
131
+ callable=getattr(instance, name),
132
+ )
133
+ )
134
+ elif scheduled_action_decoration is not None:
135
+ if not inspect.iscoroutinefunction(member):
136
+ raise Exception(
137
+ "Scheduled action handler '%s' from '%s.%s' must be a coroutine function"
138
+ % (name, func.__module__, func.__qualname__)
139
+ )
140
+
141
+ schedulers.add(
142
+ ScheduledActionData(
143
+ spec=scheduled_action_decoration,
144
+ callable=getattr(instance, name),
145
+ )
109
146
  )
110
- )
111
147
 
112
- return handlers
148
+ return handlers, schedulers
113
149
 
114
150
  self.messagebus_factory = messagebus_factory
115
151
 
@@ -1,57 +1,73 @@
1
+ import logging
1
2
  from contextlib import asynccontextmanager
2
- from typing import Any, AsyncContextManager, AsyncGenerator, Protocol
3
+ from datetime import datetime, timedelta
4
+ from datetime import tzinfo as _TzInfo
5
+ from typing import Any, AsyncGenerator
3
6
 
4
7
  import aio_pika
5
8
  from aio_pika.abc import AbstractConnection
6
9
  from pydantic import BaseModel
7
10
 
8
- from jararaca.messagebus.publisher import MessagePublisher, provide_message_publisher
9
- from jararaca.microservice import AppContext, AppInterceptor
11
+ from jararaca.broker_backend import MessageBrokerBackend
12
+ from jararaca.messagebus.interceptors.publisher_interceptor import (
13
+ MessageBusConnectionFactory,
14
+ )
15
+ from jararaca.messagebus.publisher import IMessage, MessagePublisher
16
+ from jararaca.scheduler.types import DelayedMessageData
10
17
 
11
18
 
12
- class MessageBusConnectionFactory(Protocol):
13
-
14
- def provide_connection(self) -> AsyncContextManager[MessagePublisher]: ...
15
-
16
-
17
- class MessageBusPublisherInterceptor(AppInterceptor):
19
+ class AIOPikaMessagePublisher(MessagePublisher):
18
20
 
19
21
  def __init__(
20
22
  self,
21
- connection_factory: MessageBusConnectionFactory,
22
- connection_name: str = "default",
23
+ channel: aio_pika.abc.AbstractChannel,
24
+ exchange_name: str,
25
+ message_broker_backend: MessageBrokerBackend | None = None,
23
26
  ):
24
- self.connection_factory = connection_factory
25
- self.connection_name = connection_name
26
-
27
- @asynccontextmanager
28
- async def intercept(self, app_context: AppContext) -> AsyncGenerator[None, None]:
29
- if app_context.context_type == "websocket":
30
- yield
31
- return
32
-
33
- async with self.connection_factory.provide_connection() as connection:
34
- with provide_message_publisher(self.connection_name, connection):
35
- yield
36
-
37
-
38
- class AIOPikaMessagePublisher(MessagePublisher):
39
27
 
40
- def __init__(self, channel: aio_pika.abc.AbstractChannel, exchange_name: str):
41
28
  self.channel = channel
42
29
  self.exchange_name = exchange_name
30
+ self.message_broker_backend = message_broker_backend
43
31
 
44
- async def publish(self, message: BaseModel, topic: str) -> None:
45
- exchange = await self.channel.declare_exchange(
46
- self.exchange_name,
47
- type=aio_pika.ExchangeType.TOPIC,
48
- )
49
- routing_key = f"{self.exchange_name}.{topic}."
32
+ async def publish(self, message: IMessage, topic: str) -> None:
33
+ exchange = await self.channel.get_exchange(self.exchange_name, ensure=False)
34
+ if not exchange:
35
+ logging.warning(f"Exchange {self.exchange_name} not found")
36
+ return
37
+ routing_key = f"{topic}."
50
38
  await exchange.publish(
51
39
  aio_pika.Message(body=message.model_dump_json().encode()),
52
40
  routing_key=routing_key,
53
41
  )
54
42
 
43
+ async def delay(self, message: IMessage, seconds: int) -> None:
44
+ if not self.message_broker_backend:
45
+ raise NotImplementedError(
46
+ "Delay is not implemented for AIOPikaMessagePublisher"
47
+ )
48
+ await self.message_broker_backend.enqueue_delayed_message(
49
+ DelayedMessageData(
50
+ message_topic=message.MESSAGE_TOPIC,
51
+ payload=message.model_dump_json().encode(),
52
+ dispatch_time=int(
53
+ (datetime.now(tz=None) + timedelta(seconds=seconds)).timestamp()
54
+ ),
55
+ )
56
+ )
57
+
58
+ async def schedule(self, message: IMessage, when: datetime, tz: _TzInfo) -> None:
59
+ if not self.message_broker_backend:
60
+ raise NotImplementedError(
61
+ "Schedule is not implemented for AIOPikaMessagePublisher"
62
+ )
63
+ await self.message_broker_backend.enqueue_delayed_message(
64
+ DelayedMessageData(
65
+ message_topic=message.MESSAGE_TOPIC,
66
+ payload=message.model_dump_json().encode(),
67
+ dispatch_time=int(when.timestamp()),
68
+ )
69
+ )
70
+
55
71
 
56
72
  class GenericPoolConfig(BaseModel):
57
73
  max_size: int
@@ -65,10 +81,11 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
65
81
  exchange: str,
66
82
  connection_pool_config: GenericPoolConfig | None = None,
67
83
  channel_pool_config: GenericPoolConfig | None = None,
84
+ message_broker_backend: MessageBrokerBackend | None = None,
68
85
  ):
69
86
  self.url = url
70
87
  self.exchange = exchange
71
-
88
+ self.message_broker_backend = message_broker_backend
72
89
  self.connection_pool: aio_pika.pool.Pool[AbstractConnection] | None = None
73
90
  self.channel_pool: aio_pika.pool.Pool[aio_pika.abc.AbstractChannel] | None = (
74
91
  None
@@ -124,7 +141,11 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
124
141
  await tx.select()
125
142
 
126
143
  try:
127
- yield AIOPikaMessagePublisher(channel, exchange_name=self.exchange)
144
+ yield AIOPikaMessagePublisher(
145
+ channel,
146
+ exchange_name=self.exchange,
147
+ message_broker_backend=self.message_broker_backend,
148
+ )
128
149
  await tx.commit()
129
150
  except Exception as e:
130
151
  await tx.rollback()
@@ -0,0 +1,34 @@
1
+ from contextlib import asynccontextmanager
2
+ from typing import AsyncContextManager, AsyncGenerator, Protocol
3
+
4
+ from jararaca.broker_backend import MessageBrokerBackend
5
+ from jararaca.messagebus.publisher import MessagePublisher, provide_message_publisher
6
+ from jararaca.microservice import AppContext, AppInterceptor
7
+
8
+
9
+ class MessageBusConnectionFactory(Protocol):
10
+
11
+ def provide_connection(self) -> AsyncContextManager[MessagePublisher]: ...
12
+
13
+
14
+ class MessageBusPublisherInterceptor(AppInterceptor):
15
+
16
+ def __init__(
17
+ self,
18
+ connection_factory: MessageBusConnectionFactory,
19
+ connection_name: str = "default",
20
+ message_scheduler: MessageBrokerBackend | None = None,
21
+ ):
22
+ self.connection_factory = connection_factory
23
+ self.connection_name = connection_name
24
+ self.message_scheduler = message_scheduler
25
+
26
+ @asynccontextmanager
27
+ async def intercept(self, app_context: AppContext) -> AsyncGenerator[None, None]:
28
+ if app_context.context_type == "websocket":
29
+ yield
30
+ return
31
+
32
+ async with self.connection_factory.provide_connection() as connection:
33
+ with provide_message_publisher(self.connection_name, connection):
34
+ yield
@@ -0,0 +1,27 @@
1
+ from datetime import datetime, tzinfo
2
+ from typing import Generic, Protocol, TypeVar
3
+
4
+ from jararaca.messagebus.publisher import IMessage, use_publisher
5
+
6
+
7
+ class Message(IMessage):
8
+
9
+ async def publish(self) -> None:
10
+ task_publisher = use_publisher()
11
+ await task_publisher.publish(self, self.MESSAGE_TOPIC)
12
+
13
+ async def delay(self, seconds: int) -> None:
14
+ task_publisher = use_publisher()
15
+ await task_publisher.delay(self, seconds)
16
+
17
+ async def schedule(self, when: datetime, tz: tzinfo) -> None:
18
+ task_publisher = use_publisher()
19
+ await task_publisher.schedule(self, when, tz)
20
+
21
+
22
+ INHERITS_MESSAGE_CO = TypeVar("INHERITS_MESSAGE_CO", bound=Message, covariant=True)
23
+
24
+
25
+ class MessageOf(Protocol, Generic[INHERITS_MESSAGE_CO]):
26
+
27
+ def payload(self) -> INHERITS_MESSAGE_CO: ...
@@ -1,12 +1,41 @@
1
1
  from contextlib import contextmanager, suppress
2
2
  from contextvars import ContextVar
3
- from typing import Any, Generator, Protocol
3
+ from datetime import datetime, tzinfo
4
+ from typing import Any, ClassVar, Generator, Literal, Protocol
4
5
 
5
6
  from pydantic import BaseModel
6
7
 
7
8
 
9
+ class IMessage(BaseModel):
10
+ """
11
+ Base class for messages representing tasks.
12
+ A Task is a message that represents a unit of work to be done.
13
+ It is published to a TaskPublisher and consumed by a TaskHandler, wrapped in TaskData.
14
+ Note: A Task is not an Event.
15
+ """
16
+
17
+ MESSAGE_TOPIC: ClassVar[str] = "__unset__"
18
+
19
+ MESSAGE_TYPE: ClassVar[Literal["task", "event"]] = "task"
20
+
21
+
8
22
  class MessagePublisher(Protocol):
9
- async def publish(self, message: BaseModel, topic: str) -> None:
23
+ async def publish(self, message: IMessage, topic: str) -> None:
24
+ raise NotImplementedError()
25
+
26
+ async def delay(self, message: IMessage, seconds: int) -> None:
27
+ """
28
+ Delay the message for a given number of seconds.
29
+ """
30
+
31
+ raise NotImplementedError()
32
+
33
+ async def schedule(
34
+ self, message: IMessage, when: datetime, timezone: tzinfo
35
+ ) -> None:
36
+ """
37
+ Schedule the message for a given datetime.
38
+ """
10
39
  raise NotImplementedError()
11
40
 
12
41
 
@@ -24,8 +24,9 @@ from jararaca.messagebus.decorators import (
24
24
  MessageHandler,
25
25
  MessageHandlerData,
26
26
  )
27
- from jararaca.messagebus.types import Message, MessageOf
27
+ from jararaca.messagebus.message import Message, MessageOf
28
28
  from jararaca.microservice import MessageBusAppContext, Microservice
29
+ from jararaca.utils.rabbitmq_utils import RabbitmqUtils
29
30
 
30
31
  logger = logging.getLogger(__name__)
31
32
 
@@ -33,7 +34,6 @@ logger = logging.getLogger(__name__)
33
34
  @dataclass
34
35
  class AioPikaWorkerConfig:
35
36
  url: str
36
- queue: str
37
37
  exchange: str
38
38
  prefetch_count: int
39
39
 
@@ -99,32 +99,28 @@ class AioPikaMicroserviceConsumer:
99
99
 
100
100
  await channel.set_qos(prefetch_count=self.config.prefetch_count)
101
101
 
102
- main_x = await channel.declare_exchange(self.config.exchange, type="topic")
103
-
104
- dlx = await channel.declare_exchange("dlx", type="direct")
105
-
106
- dlq = await channel.declare_queue("dlq")
102
+ main_ex = await RabbitmqUtils.declare_main_exchange(
103
+ channel=channel, exchange_name=self.config.exchange
104
+ )
107
105
 
108
- await dlq.bind(dlx, routing_key="dlq")
106
+ dlx, dlq = await RabbitmqUtils.delcare_dl_kit(channel=channel)
109
107
 
110
108
  for handler in self.message_handler_set:
111
109
 
112
- queue_name = f"{self.config.exchange}.{handler.message_type.MESSAGE_TOPIC}.{handler.callable.__module__}.{handler.callable.__qualname__}"
113
- routing_key = (
114
- f"{self.config.exchange}.{handler.message_type.MESSAGE_TOPIC}.#"
115
- )
110
+ queue_name = f"{handler.message_type.MESSAGE_TOPIC}.{handler.callable.__module__}.{handler.callable.__qualname__}"
111
+ routing_key = f"{handler.message_type.MESSAGE_TOPIC}.#"
116
112
 
117
113
  self.incoming_map[queue_name] = handler
118
114
 
119
115
  queue = await channel.declare_queue(
120
116
  queue_name,
121
117
  arguments={
122
- "x-dead-letter-exchange": "dlx",
123
- "x-dead-letter-routing-key": "dlq",
118
+ "x-dead-letter-exchange": dlx.name,
119
+ "x-dead-letter-routing-key": dlq.name,
124
120
  },
125
121
  )
126
122
 
127
- await queue.bind(exchange=self.config.exchange, routing_key=routing_key)
123
+ await queue.bind(exchange=main_ex, routing_key=routing_key)
128
124
 
129
125
  await queue.consume(
130
126
  callback=MessageHandlerCallback(
@@ -343,7 +339,7 @@ class MessageBusWorker:
343
339
  instance: Any = self.container.get_by_type(instance_type)
344
340
 
345
341
  factory = controller.get_messagebus_factory()
346
- handlers = factory(instance)
342
+ handlers, _ = factory(instance)
347
343
 
348
344
  message_handler_data_map: dict[str, MessageHandlerData] = {}
349
345