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.
- jararaca/__init__.py +76 -5
- jararaca/cli.py +460 -116
- jararaca/core/uow.py +17 -12
- jararaca/messagebus/decorators.py +33 -30
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +30 -2
- jararaca/messagebus/interceptors/publisher_interceptor.py +7 -3
- jararaca/messagebus/publisher.py +14 -6
- jararaca/messagebus/worker.py +1102 -88
- jararaca/microservice.py +137 -34
- jararaca/observability/decorators.py +7 -3
- jararaca/observability/interceptor.py +4 -2
- jararaca/observability/providers/otel.py +14 -10
- jararaca/persistence/base.py +2 -1
- jararaca/persistence/interceptors/aiosqa_interceptor.py +167 -16
- jararaca/persistence/utilities.py +32 -20
- jararaca/presentation/decorators.py +96 -10
- jararaca/presentation/server.py +31 -4
- jararaca/presentation/websocket/context.py +30 -4
- jararaca/presentation/websocket/types.py +2 -2
- jararaca/presentation/websocket/websocket_interceptor.py +28 -4
- jararaca/reflect/__init__.py +0 -0
- jararaca/reflect/controller_inspect.py +75 -0
- jararaca/{tools → reflect}/metadata.py +25 -5
- jararaca/scheduler/{scheduler_v2.py → beat_worker.py} +49 -53
- jararaca/scheduler/decorators.py +55 -20
- jararaca/tools/app_config/interceptor.py +4 -2
- jararaca/utils/rabbitmq_utils.py +259 -5
- jararaca/utils/retry.py +141 -0
- {jararaca-0.3.9.dist-info → jararaca-0.3.11.dist-info}/METADATA +2 -1
- {jararaca-0.3.9.dist-info → jararaca-0.3.11.dist-info}/RECORD +33 -32
- {jararaca-0.3.9.dist-info → jararaca-0.3.11.dist-info}/WHEEL +1 -1
- jararaca/messagebus/worker_v2.py +0 -617
- jararaca/scheduler/scheduler.py +0 -161
- {jararaca-0.3.9.dist-info → jararaca-0.3.11.dist-info}/LICENSE +0 -0
- {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(
|
|
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__(
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
]
|
|
65
|
+
for ctx in ctxs:
|
|
66
|
+
await ctx.__aenter__()
|
|
59
67
|
|
|
60
|
-
|
|
61
|
-
await ctx.__aenter__()
|
|
62
|
-
|
|
63
|
-
yield None
|
|
68
|
+
yield None
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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,
|
|
102
|
+
def __call__(self, cls_t: type[DECORATED_T]) -> type[DECORATED_T]:
|
|
103
103
|
|
|
104
104
|
def messagebus_factory(
|
|
105
|
-
instance:
|
|
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 =
|
|
111
|
+
_, members = inspect_controller(cls_t)
|
|
112
112
|
|
|
113
|
-
for name, member in members:
|
|
114
|
-
message_handler_decoration = MessageHandler.get_message_incoming(
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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=
|
|
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(
|
|
157
|
+
MessageBusController.register(cls_t, self)
|
|
153
158
|
|
|
154
|
-
return
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
28
|
-
|
|
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()
|
jararaca/messagebus/publisher.py
CHANGED
|
@@ -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
|
|
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(
|
|
23
|
+
class MessagePublisher(ABC):
|
|
24
|
+
@abstractmethod
|
|
23
25
|
async def publish(self, message: IMessage, topic: str) -> None:
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]](
|