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.
- README.md +121 -0
- jararaca/__init__.py +189 -17
- jararaca/__main__.py +4 -0
- jararaca/broker_backend/__init__.py +4 -0
- jararaca/broker_backend/mapper.py +4 -0
- jararaca/broker_backend/redis_broker_backend.py +9 -3
- jararaca/cli.py +915 -51
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +8 -0
- jararaca/core/uow.py +41 -7
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -0
- jararaca/helpers/__init__.py +3 -0
- jararaca/helpers/global_scheduler/__init__.py +3 -0
- jararaca/helpers/global_scheduler/config.py +21 -0
- jararaca/helpers/global_scheduler/controller.py +42 -0
- jararaca/helpers/global_scheduler/registry.py +32 -0
- jararaca/lifecycle.py +6 -2
- jararaca/messagebus/__init__.py +4 -0
- jararaca/messagebus/bus_message_controller.py +4 -0
- jararaca/messagebus/consumers/__init__.py +3 -0
- jararaca/messagebus/decorators.py +121 -61
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +62 -11
- jararaca/messagebus/interceptors/message_publisher_collector.py +62 -0
- jararaca/messagebus/interceptors/publisher_interceptor.py +29 -3
- jararaca/messagebus/message.py +4 -0
- jararaca/messagebus/publisher.py +6 -0
- jararaca/messagebus/worker.py +1002 -459
- jararaca/microservice.py +113 -2
- jararaca/observability/constants.py +7 -0
- jararaca/observability/decorators.py +170 -13
- jararaca/observability/fastapi_exception_handler.py +37 -0
- jararaca/observability/hooks.py +109 -0
- jararaca/observability/interceptor.py +4 -0
- jararaca/observability/providers/__init__.py +3 -0
- jararaca/observability/providers/otel.py +225 -16
- jararaca/persistence/base.py +39 -3
- jararaca/persistence/exports.py +4 -0
- jararaca/persistence/interceptors/__init__.py +3 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
- jararaca/persistence/interceptors/constants.py +5 -0
- jararaca/persistence/interceptors/decorators.py +50 -0
- jararaca/persistence/session.py +3 -0
- jararaca/persistence/sort_filter.py +4 -0
- jararaca/persistence/utilities.py +73 -20
- jararaca/presentation/__init__.py +3 -0
- jararaca/presentation/decorators.py +88 -86
- jararaca/presentation/exceptions.py +23 -0
- jararaca/presentation/hooks.py +4 -0
- jararaca/presentation/http_microservice.py +4 -0
- jararaca/presentation/server.py +97 -45
- jararaca/presentation/websocket/__init__.py +3 -0
- jararaca/presentation/websocket/base_types.py +4 -0
- jararaca/presentation/websocket/context.py +4 -0
- jararaca/presentation/websocket/decorators.py +8 -41
- jararaca/presentation/websocket/redis.py +280 -53
- jararaca/presentation/websocket/types.py +4 -0
- jararaca/presentation/websocket/websocket_interceptor.py +46 -19
- jararaca/reflect/__init__.py +3 -0
- jararaca/reflect/controller_inspect.py +16 -10
- jararaca/reflect/decorators.py +252 -0
- jararaca/reflect/helpers.py +18 -0
- jararaca/reflect/metadata.py +34 -25
- jararaca/rpc/__init__.py +3 -0
- jararaca/rpc/http/__init__.py +101 -0
- jararaca/rpc/http/backends/__init__.py +14 -0
- jararaca/rpc/http/backends/httpx.py +43 -9
- jararaca/rpc/http/backends/otel.py +4 -0
- jararaca/rpc/http/decorators.py +380 -115
- jararaca/rpc/http/httpx.py +3 -0
- jararaca/scheduler/__init__.py +3 -0
- jararaca/scheduler/beat_worker.py +521 -105
- jararaca/scheduler/decorators.py +15 -22
- jararaca/scheduler/types.py +4 -0
- jararaca/tools/app_config/__init__.py +3 -0
- jararaca/tools/app_config/decorators.py +7 -19
- jararaca/tools/app_config/interceptor.py +6 -2
- jararaca/tools/typescript/__init__.py +3 -0
- jararaca/tools/typescript/decorators.py +120 -0
- jararaca/tools/typescript/interface_parser.py +1077 -174
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/env_parse_utils.py +133 -0
- jararaca/utils/rabbitmq_utils.py +112 -39
- jararaca/utils/retry.py +19 -14
- jararaca-0.4.0a19.dist-info/LICENSE +674 -0
- jararaca-0.4.0a19.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/METADATA +12 -7
- jararaca-0.4.0a19.dist-info/RECORD +96 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/WHEEL +1 -1
- pyproject.toml +132 -0
- jararaca-0.3.11a16.dist-info/RECORD +0 -74
- /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
- {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
|
import logging
|
|
2
6
|
from contextlib import asynccontextmanager
|
|
3
7
|
from datetime import datetime, timedelta
|
|
@@ -5,10 +9,12 @@ from datetime import tzinfo as _TzInfo
|
|
|
5
9
|
from typing import Any, AsyncGenerator
|
|
6
10
|
|
|
7
11
|
import aio_pika
|
|
12
|
+
import tenacity
|
|
8
13
|
from aio_pika.abc import AbstractConnection
|
|
9
14
|
from pydantic import BaseModel
|
|
10
15
|
|
|
11
16
|
from jararaca.broker_backend import MessageBrokerBackend
|
|
17
|
+
from jararaca.messagebus import implicit_headers
|
|
12
18
|
from jararaca.messagebus.interceptors.publisher_interceptor import (
|
|
13
19
|
MessageBusConnectionFactory,
|
|
14
20
|
)
|
|
@@ -41,9 +47,13 @@ class AIOPikaMessagePublisher(MessagePublisher):
|
|
|
41
47
|
if not exchange:
|
|
42
48
|
logging.warning(f"Exchange {self.exchange_name} not found")
|
|
43
49
|
return
|
|
44
|
-
routing_key = f"{topic}
|
|
50
|
+
routing_key = f"{topic}.#"
|
|
51
|
+
|
|
52
|
+
implicit_headers_data = implicit_headers.use_implicit_headers()
|
|
45
53
|
await exchange.publish(
|
|
46
|
-
aio_pika.Message(
|
|
54
|
+
aio_pika.Message(
|
|
55
|
+
body=message.model_dump_json().encode(), headers=implicit_headers_data
|
|
56
|
+
),
|
|
47
57
|
routing_key=routing_key,
|
|
48
58
|
)
|
|
49
59
|
|
|
@@ -62,7 +72,9 @@ class AIOPikaMessagePublisher(MessagePublisher):
|
|
|
62
72
|
)
|
|
63
73
|
)
|
|
64
74
|
|
|
65
|
-
async def schedule(
|
|
75
|
+
async def schedule(
|
|
76
|
+
self, message: IMessage, when: datetime, timezone: _TzInfo
|
|
77
|
+
) -> None:
|
|
66
78
|
if not self.message_broker_backend:
|
|
67
79
|
raise NotImplementedError(
|
|
68
80
|
"Schedule is not implemented for AIOPikaMessagePublisher"
|
|
@@ -101,6 +113,18 @@ class GenericPoolConfig(BaseModel):
|
|
|
101
113
|
max_size: int
|
|
102
114
|
|
|
103
115
|
|
|
116
|
+
default_retry = tenacity.retry(
|
|
117
|
+
wait=tenacity.wait_exponential_jitter(initial=1, max=60),
|
|
118
|
+
before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
default_retry_channel = tenacity.retry(
|
|
122
|
+
wait=tenacity.wait_exponential_jitter(initial=1, max=60),
|
|
123
|
+
before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
|
|
124
|
+
stop=tenacity.stop_after_attempt(5),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
104
128
|
class AIOPikaConnectionFactory(MessageBusConnectionFactory):
|
|
105
129
|
|
|
106
130
|
def __init__(
|
|
@@ -121,8 +145,9 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
|
|
|
121
145
|
|
|
122
146
|
if connection_pool_config:
|
|
123
147
|
|
|
148
|
+
@default_retry
|
|
124
149
|
async def get_connection() -> AbstractConnection:
|
|
125
|
-
return await aio_pika.
|
|
150
|
+
return await aio_pika.connect(self.url)
|
|
126
151
|
|
|
127
152
|
self.connection_pool = aio_pika.pool.Pool[AbstractConnection](
|
|
128
153
|
get_connection,
|
|
@@ -131,6 +156,7 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
|
|
|
131
156
|
|
|
132
157
|
if channel_pool_config:
|
|
133
158
|
|
|
159
|
+
@default_retry_channel
|
|
134
160
|
async def get_channel() -> aio_pika.abc.AbstractChannel:
|
|
135
161
|
async with self.acquire_connection() as connection:
|
|
136
162
|
return await connection.channel(publisher_confirms=False)
|
|
@@ -139,10 +165,14 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
|
|
|
139
165
|
get_channel, max_size=channel_pool_config.max_size
|
|
140
166
|
)
|
|
141
167
|
|
|
168
|
+
@default_retry
|
|
169
|
+
async def _connect(self) -> AbstractConnection:
|
|
170
|
+
return await aio_pika.connect(self.url)
|
|
171
|
+
|
|
142
172
|
@asynccontextmanager
|
|
143
173
|
async def acquire_connection(self) -> AsyncGenerator[AbstractConnection, Any]:
|
|
144
174
|
if not self.connection_pool:
|
|
145
|
-
async with await
|
|
175
|
+
async with await self._connect() as connection:
|
|
146
176
|
yield connection
|
|
147
177
|
else:
|
|
148
178
|
|
|
@@ -153,12 +183,33 @@ class AIOPikaConnectionFactory(MessageBusConnectionFactory):
|
|
|
153
183
|
async def acquire_channel(
|
|
154
184
|
self,
|
|
155
185
|
) -> AsyncGenerator[aio_pika.abc.AbstractChannel, Any]:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
186
|
+
|
|
187
|
+
async for attempt in tenacity.AsyncRetrying(
|
|
188
|
+
wait=tenacity.wait_exponential_jitter(initial=1, max=10),
|
|
189
|
+
before_sleep=tenacity.before_sleep_log(logger, logging.WARNING),
|
|
190
|
+
# stop=tenacity.stop_after_attempt(9000),
|
|
191
|
+
):
|
|
192
|
+
with attempt:
|
|
193
|
+
|
|
194
|
+
if not self.connection_pool or not self.channel_pool:
|
|
195
|
+
async with self.acquire_connection() as connection:
|
|
196
|
+
yield await connection.channel(publisher_confirms=False)
|
|
197
|
+
else:
|
|
198
|
+
|
|
199
|
+
async with self.connection_pool.acquire() as connection:
|
|
200
|
+
if not connection.connected.is_set():
|
|
201
|
+
await connection.connect()
|
|
202
|
+
|
|
203
|
+
async with connection.channel(
|
|
204
|
+
publisher_confirms=False
|
|
205
|
+
) as channel:
|
|
206
|
+
yield channel
|
|
207
|
+
# await connection.close()
|
|
208
|
+
if attempt.retry_state.attempt_number > 1:
|
|
209
|
+
logger.warning(
|
|
210
|
+
"Later successful connection attempt #%d",
|
|
211
|
+
attempt.retry_state.attempt_number,
|
|
212
|
+
)
|
|
162
213
|
|
|
163
214
|
@asynccontextmanager
|
|
164
215
|
async def provide_connection(self) -> AsyncGenerator[AIOPikaMessagePublisher, Any]:
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC, datetime, timedelta, tzinfo
|
|
7
|
+
|
|
8
|
+
from jararaca.messagebus.publisher import IMessage, MessagePublisher
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class CollectorDelayedMessageData:
|
|
13
|
+
message: IMessage
|
|
14
|
+
when: datetime
|
|
15
|
+
timezone: tzinfo
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MessagePublisherCollector(MessagePublisher):
|
|
19
|
+
|
|
20
|
+
def __init__(self) -> None:
|
|
21
|
+
self.staged_delayed_messages: list[CollectorDelayedMessageData] = []
|
|
22
|
+
self.staged_messages: list[IMessage] = []
|
|
23
|
+
|
|
24
|
+
async def publish(self, message: IMessage, topic: str) -> None:
|
|
25
|
+
self.staged_messages.append(message)
|
|
26
|
+
|
|
27
|
+
async def delay(self, message: IMessage, seconds: int) -> None:
|
|
28
|
+
self.staged_delayed_messages.append(
|
|
29
|
+
CollectorDelayedMessageData(
|
|
30
|
+
message=message,
|
|
31
|
+
when=datetime.now(UTC) + timedelta(seconds=seconds),
|
|
32
|
+
timezone=UTC,
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def schedule(
|
|
37
|
+
self, message: IMessage, when: datetime, timezone: tzinfo
|
|
38
|
+
) -> None:
|
|
39
|
+
self.staged_delayed_messages.append(
|
|
40
|
+
CollectorDelayedMessageData(
|
|
41
|
+
message=message,
|
|
42
|
+
when=when,
|
|
43
|
+
timezone=timezone,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def fill(self, publisher: MessagePublisher) -> None:
|
|
48
|
+
for message in self.staged_messages:
|
|
49
|
+
await publisher.publish(message, message.MESSAGE_TOPIC)
|
|
50
|
+
|
|
51
|
+
for delayed_message in self.staged_delayed_messages:
|
|
52
|
+
await publisher.schedule(
|
|
53
|
+
delayed_message.message,
|
|
54
|
+
delayed_message.when,
|
|
55
|
+
delayed_message.timezone,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def has_messages(self) -> bool:
|
|
59
|
+
return bool(self.staged_messages or self.staged_delayed_messages)
|
|
60
|
+
|
|
61
|
+
async def flush(self) -> None:
|
|
62
|
+
raise NotImplementedError("I'm just a poor little collector! :(")
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
from contextlib import asynccontextmanager
|
|
2
6
|
from typing import AsyncContextManager, AsyncGenerator, Protocol
|
|
3
7
|
|
|
4
8
|
from jararaca.broker_backend import MessageBrokerBackend
|
|
9
|
+
from jararaca.messagebus.interceptors.message_publisher_collector import (
|
|
10
|
+
MessagePublisherCollector,
|
|
11
|
+
)
|
|
5
12
|
from jararaca.messagebus.publisher import MessagePublisher, provide_message_publisher
|
|
6
13
|
from jararaca.microservice import AppInterceptor, AppTransactionContext
|
|
7
14
|
|
|
@@ -18,10 +25,15 @@ class MessageBusPublisherInterceptor(AppInterceptor):
|
|
|
18
25
|
connection_factory: MessageBusConnectionFactory,
|
|
19
26
|
connection_name: str = "default",
|
|
20
27
|
message_scheduler: MessageBrokerBackend | None = None,
|
|
28
|
+
*,
|
|
29
|
+
open_connection_at_end_of_transaction: bool = False,
|
|
21
30
|
):
|
|
22
31
|
self.connection_factory = connection_factory
|
|
23
32
|
self.connection_name = connection_name
|
|
24
33
|
self.message_scheduler = message_scheduler
|
|
34
|
+
self.open_connection_at_end_of_transaction = (
|
|
35
|
+
open_connection_at_end_of_transaction
|
|
36
|
+
)
|
|
25
37
|
|
|
26
38
|
@asynccontextmanager
|
|
27
39
|
async def intercept(
|
|
@@ -31,8 +43,22 @@ class MessageBusPublisherInterceptor(AppInterceptor):
|
|
|
31
43
|
yield
|
|
32
44
|
return
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
if self.open_connection_at_end_of_transaction:
|
|
47
|
+
|
|
48
|
+
collector = MessagePublisherCollector()
|
|
49
|
+
with provide_message_publisher(self.connection_name, collector):
|
|
36
50
|
yield
|
|
37
51
|
|
|
38
|
-
|
|
52
|
+
if collector.has_messages():
|
|
53
|
+
async with self.connection_factory.provide_connection() as connection:
|
|
54
|
+
await collector.fill(connection)
|
|
55
|
+
await connection.flush()
|
|
56
|
+
return
|
|
57
|
+
else:
|
|
58
|
+
|
|
59
|
+
yield
|
|
60
|
+
|
|
61
|
+
async with self.connection_factory.provide_connection() as connection:
|
|
62
|
+
with provide_message_publisher(self.connection_name, connection):
|
|
63
|
+
|
|
64
|
+
await connection.flush()
|
jararaca/messagebus/message.py
CHANGED
jararaca/messagebus/publisher.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
from abc import ABC, abstractmethod
|
|
2
6
|
from contextlib import contextmanager, suppress
|
|
3
7
|
from contextvars import ContextVar
|
|
@@ -19,6 +23,8 @@ class IMessage(BaseModel):
|
|
|
19
23
|
|
|
20
24
|
MESSAGE_TYPE: ClassVar[Literal["task", "event"]] = "task"
|
|
21
25
|
|
|
26
|
+
MESSAGE_CATEGORY: ClassVar[str] = "uncategorized"
|
|
27
|
+
|
|
22
28
|
|
|
23
29
|
class MessagePublisher(ABC):
|
|
24
30
|
@abstractmethod
|