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.
- jararaca/__init__.py +13 -4
- jararaca/broker_backend/__init__.py +102 -0
- jararaca/broker_backend/mapper.py +21 -0
- jararaca/broker_backend/redis_broker_backend.py +162 -0
- jararaca/cli.py +136 -44
- jararaca/messagebus/__init__.py +1 -1
- jararaca/messagebus/consumers/__init__.py +0 -0
- jararaca/messagebus/decorators.py +57 -21
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +56 -35
- jararaca/messagebus/interceptors/publisher_interceptor.py +34 -0
- jararaca/messagebus/message.py +27 -0
- jararaca/messagebus/publisher.py +31 -2
- jararaca/messagebus/worker.py +12 -16
- jararaca/messagebus/worker_v2.py +608 -0
- jararaca/microservice.py +1 -1
- jararaca/scheduler/decorators.py +34 -1
- jararaca/scheduler/scheduler.py +16 -9
- jararaca/scheduler/scheduler_v2.py +346 -0
- jararaca/scheduler/types.py +7 -0
- jararaca/utils/__init__.py +0 -0
- jararaca/utils/rabbitmq_utils.py +84 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.3.1.dist-info}/METADATA +3 -1
- {jararaca-0.2.37a12.dist-info → jararaca-0.3.1.dist-info}/RECORD +26 -16
- jararaca/messagebus/types.py +0 -30
- {jararaca-0.2.37a12.dist-info → jararaca-0.3.1.dist-info}/LICENSE +0 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.3.1.dist-info}/WHEEL +0 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.3.1.dist-info}/entry_points.txt +0 -0
jararaca/scheduler/scheduler.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import inspect
|
|
2
3
|
import logging
|
|
4
|
+
import time
|
|
3
5
|
from contextlib import asynccontextmanager
|
|
4
6
|
from dataclasses import dataclass
|
|
5
7
|
from datetime import UTC, datetime
|
|
@@ -11,6 +13,7 @@ from croniter import croniter
|
|
|
11
13
|
from jararaca.core.uow import UnitOfWorkContextProvider
|
|
12
14
|
from jararaca.di import Container
|
|
13
15
|
from jararaca.lifecycle import AppLifecycle
|
|
16
|
+
from jararaca.messagebus.decorators import ScheduleDispatchData
|
|
14
17
|
from jararaca.microservice import Microservice, SchedulerAppContext
|
|
15
18
|
from jararaca.scheduler.decorators import ScheduledAction
|
|
16
19
|
|
|
@@ -22,9 +25,6 @@ class SchedulerConfig:
|
|
|
22
25
|
interval: int
|
|
23
26
|
|
|
24
27
|
|
|
25
|
-
class SchedulerBackend: ...
|
|
26
|
-
|
|
27
|
-
|
|
28
28
|
def extract_scheduled_actions(
|
|
29
29
|
app: Microservice, container: Container
|
|
30
30
|
) -> list[tuple[Callable[..., Any], "ScheduledAction"]]:
|
|
@@ -52,12 +52,11 @@ class Scheduler:
|
|
|
52
52
|
def __init__(
|
|
53
53
|
self,
|
|
54
54
|
app: Microservice,
|
|
55
|
-
|
|
56
|
-
config: SchedulerConfig,
|
|
55
|
+
interval: int,
|
|
57
56
|
) -> None:
|
|
58
57
|
self.app = app
|
|
59
|
-
|
|
60
|
-
self.
|
|
58
|
+
|
|
59
|
+
self.interval = interval
|
|
61
60
|
self.container = Container(self.app)
|
|
62
61
|
self.uow_provider = UnitOfWorkContextProvider(app, self.container)
|
|
63
62
|
|
|
@@ -115,7 +114,15 @@ class Scheduler:
|
|
|
115
114
|
):
|
|
116
115
|
try:
|
|
117
116
|
async with ctx:
|
|
118
|
-
|
|
117
|
+
signature = inspect.signature(func)
|
|
118
|
+
if len(signature.parameters) > 0:
|
|
119
|
+
logging.warning(
|
|
120
|
+
f"Scheduled action {func.__module__}.{func.__qualname__} has parameters, but no arguments were provided. Must be using scheduler-v2"
|
|
121
|
+
)
|
|
122
|
+
await func(ScheduleDispatchData(time.time()))
|
|
123
|
+
else:
|
|
124
|
+
await func()
|
|
125
|
+
|
|
119
126
|
except BaseException as e:
|
|
120
127
|
if action_specs.exception_handler:
|
|
121
128
|
action_specs.exception_handler(e)
|
|
@@ -143,7 +150,7 @@ class Scheduler:
|
|
|
143
150
|
|
|
144
151
|
await self.process_task(func, scheduled_action)
|
|
145
152
|
|
|
146
|
-
await asyncio.sleep(self.
|
|
153
|
+
await asyncio.sleep(self.interval)
|
|
147
154
|
|
|
148
155
|
with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner:
|
|
149
156
|
runner.run(run_scheduled_actions())
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import logging
|
|
4
|
+
import signal
|
|
5
|
+
import time
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from types import FrameType
|
|
10
|
+
from typing import Any, AsyncGenerator, Callable
|
|
11
|
+
from urllib.parse import parse_qs
|
|
12
|
+
|
|
13
|
+
import aio_pika
|
|
14
|
+
import croniter
|
|
15
|
+
import urllib3
|
|
16
|
+
import urllib3.util
|
|
17
|
+
import uvloop
|
|
18
|
+
from aio_pika import connect_robust
|
|
19
|
+
from aio_pika.abc import AbstractChannel, AbstractRobustConnection
|
|
20
|
+
from aio_pika.pool import Pool
|
|
21
|
+
|
|
22
|
+
from jararaca.broker_backend import MessageBrokerBackend
|
|
23
|
+
from jararaca.broker_backend.mapper import get_message_broker_backend_from_url
|
|
24
|
+
from jararaca.core.uow import UnitOfWorkContextProvider
|
|
25
|
+
from jararaca.di import Container
|
|
26
|
+
from jararaca.lifecycle import AppLifecycle
|
|
27
|
+
from jararaca.microservice import Microservice
|
|
28
|
+
from jararaca.scheduler.decorators import ScheduledAction
|
|
29
|
+
from jararaca.scheduler.types import DelayedMessageData
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
SCHEDULED_ACTION_LIST = list[tuple[Callable[..., Any], "ScheduledAction"]]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def extract_scheduled_actions(
|
|
37
|
+
app: Microservice, container: Container
|
|
38
|
+
) -> SCHEDULED_ACTION_LIST:
|
|
39
|
+
scheduled_actions: SCHEDULED_ACTION_LIST = []
|
|
40
|
+
for controllers in app.controllers:
|
|
41
|
+
|
|
42
|
+
controller_instance: Any = container.get_by_type(controllers)
|
|
43
|
+
|
|
44
|
+
controller_scheduled_actions = ScheduledAction.get_type_scheduled_actions(
|
|
45
|
+
controller_instance
|
|
46
|
+
)
|
|
47
|
+
scheduled_actions.extend(controller_scheduled_actions)
|
|
48
|
+
|
|
49
|
+
return scheduled_actions
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# region Message Broker Dispatcher
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MessageBrokerDispatcher(ABC):
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def dispatch_scheduled_action(
|
|
59
|
+
self,
|
|
60
|
+
action_id: str,
|
|
61
|
+
timestamp: int,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Dispatch a message to the message broker.
|
|
65
|
+
This is used to send a message to the message broker
|
|
66
|
+
to trigger the scheduled action.
|
|
67
|
+
"""
|
|
68
|
+
raise NotImplementedError("dispatch() is not implemented yet.")
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
async def dispatch_delayed_message(
|
|
72
|
+
self,
|
|
73
|
+
delayed_message: DelayedMessageData,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Dispatch a delayed message to the message broker.
|
|
77
|
+
This is used to send a message to the message broker
|
|
78
|
+
to trigger the scheduled action.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
raise NotImplementedError("dispatch_delayed_message() is not implemented yet.")
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
async def initialize(self, scheduled_actions: SCHEDULED_ACTION_LIST) -> None:
|
|
85
|
+
raise NotImplementedError("initialize() is not implemented yet.")
|
|
86
|
+
|
|
87
|
+
async def dispose(self) -> None:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class RabbitMQBrokerDispatcher(MessageBrokerDispatcher):
|
|
92
|
+
|
|
93
|
+
def __init__(self, url: str) -> None:
|
|
94
|
+
self.url = url
|
|
95
|
+
|
|
96
|
+
self.conn_pool: "Pool[AbstractRobustConnection]" = Pool(
|
|
97
|
+
self._create_connection,
|
|
98
|
+
max_size=10,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self.channel_pool: "Pool[AbstractChannel]" = Pool(
|
|
102
|
+
self._create_channel,
|
|
103
|
+
max_size=10,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
splitted = urllib3.util.parse_url(url)
|
|
107
|
+
|
|
108
|
+
assert splitted.scheme in ["amqp", "amqps"], "Invalid URL scheme"
|
|
109
|
+
|
|
110
|
+
assert splitted.host, "Invalid URL host"
|
|
111
|
+
|
|
112
|
+
assert splitted.query, "Invalid URL query"
|
|
113
|
+
|
|
114
|
+
query_params: dict[str, list[str]] = parse_qs(splitted.query)
|
|
115
|
+
|
|
116
|
+
assert "exchange" in query_params, "Missing exchange parameter"
|
|
117
|
+
|
|
118
|
+
assert query_params["exchange"], "Empty exchange parameter"
|
|
119
|
+
|
|
120
|
+
self.exchange = str(query_params["exchange"][0])
|
|
121
|
+
|
|
122
|
+
async def _create_connection(self) -> AbstractRobustConnection:
|
|
123
|
+
"""
|
|
124
|
+
Create a connection to the RabbitMQ server.
|
|
125
|
+
This is used to send messages to the RabbitMQ server.
|
|
126
|
+
"""
|
|
127
|
+
connection = await connect_robust(self.url)
|
|
128
|
+
return connection
|
|
129
|
+
|
|
130
|
+
async def _create_channel(self) -> AbstractChannel:
|
|
131
|
+
"""
|
|
132
|
+
Create a channel to the RabbitMQ server.
|
|
133
|
+
This is used to send messages to the RabbitMQ server.
|
|
134
|
+
"""
|
|
135
|
+
async with self.conn_pool.acquire() as connection:
|
|
136
|
+
channel = await connection.channel()
|
|
137
|
+
return channel
|
|
138
|
+
|
|
139
|
+
async def dispatch_scheduled_action(self, action_id: str, timestamp: int) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Dispatch a message to the RabbitMQ server.
|
|
142
|
+
This is used to send a message to the RabbitMQ server
|
|
143
|
+
to trigger the scheduled action.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
logger.info(f"Dispatching message to {action_id} at {timestamp}")
|
|
147
|
+
async with self.channel_pool.acquire() as channel:
|
|
148
|
+
exchange = await channel.get_exchange(self.exchange)
|
|
149
|
+
|
|
150
|
+
await exchange.publish(
|
|
151
|
+
aio_pika.Message(body=str(timestamp).encode()),
|
|
152
|
+
routing_key=action_id,
|
|
153
|
+
)
|
|
154
|
+
logger.info(f"Dispatched message to {action_id} at {timestamp}")
|
|
155
|
+
|
|
156
|
+
async def dispatch_delayed_message(
|
|
157
|
+
self, delayed_message: DelayedMessageData
|
|
158
|
+
) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Dispatch a delayed message to the RabbitMQ server.
|
|
161
|
+
This is used to send a message to the RabbitMQ server
|
|
162
|
+
to trigger the scheduled action.
|
|
163
|
+
"""
|
|
164
|
+
async with self.channel_pool.acquire() as channel:
|
|
165
|
+
|
|
166
|
+
exchange = await channel.get_exchange(self.exchange)
|
|
167
|
+
await exchange.publish(
|
|
168
|
+
aio_pika.Message(
|
|
169
|
+
body=delayed_message.payload,
|
|
170
|
+
),
|
|
171
|
+
routing_key=f"{delayed_message.message_topic}.",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
async def initialize(self, scheduled_actions: SCHEDULED_ACTION_LIST) -> None:
|
|
175
|
+
"""
|
|
176
|
+
Initialize the RabbitMQ server.
|
|
177
|
+
This is used to create the exchange and queues for the scheduled actions.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
async with self.channel_pool.acquire() as channel:
|
|
181
|
+
|
|
182
|
+
await channel.set_qos(prefetch_count=1)
|
|
183
|
+
|
|
184
|
+
await channel.declare_exchange(
|
|
185
|
+
name=self.exchange,
|
|
186
|
+
type="topic",
|
|
187
|
+
durable=True,
|
|
188
|
+
auto_delete=False,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
for func, _ in scheduled_actions:
|
|
192
|
+
queue = await channel.declare_queue(
|
|
193
|
+
name=ScheduledAction.get_function_id(func),
|
|
194
|
+
durable=True,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
await queue.bind(
|
|
198
|
+
exchange=self.exchange,
|
|
199
|
+
routing_key=ScheduledAction.get_function_id(func),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
async def dispose(self) -> None:
|
|
203
|
+
await self.channel_pool.close()
|
|
204
|
+
await self.conn_pool.close()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def get_message_broker_dispatcher_from_url(url: str) -> MessageBrokerDispatcher:
|
|
208
|
+
"""
|
|
209
|
+
Factory function to create a message broker instance from a URL.
|
|
210
|
+
Currently, only RabbitMQ is supported.
|
|
211
|
+
"""
|
|
212
|
+
if url.startswith("amqp://") or url.startswith("amqps://"):
|
|
213
|
+
return RabbitMQBrokerDispatcher(url=url)
|
|
214
|
+
else:
|
|
215
|
+
raise ValueError(f"Unsupported message broker URL: {url}")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# endregion
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class SchedulerV2:
|
|
222
|
+
|
|
223
|
+
def __init__(
|
|
224
|
+
self,
|
|
225
|
+
app: Microservice,
|
|
226
|
+
interval: int,
|
|
227
|
+
broker_url: str,
|
|
228
|
+
backend_url: str,
|
|
229
|
+
) -> None:
|
|
230
|
+
self.app = app
|
|
231
|
+
|
|
232
|
+
self.broker: MessageBrokerDispatcher = get_message_broker_dispatcher_from_url(
|
|
233
|
+
broker_url
|
|
234
|
+
)
|
|
235
|
+
self.backend: MessageBrokerBackend = get_message_broker_backend_from_url(
|
|
236
|
+
backend_url
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
self.interval = interval
|
|
240
|
+
self.container = Container(self.app)
|
|
241
|
+
self.uow_provider = UnitOfWorkContextProvider(app, self.container)
|
|
242
|
+
|
|
243
|
+
self.shutdown_event = asyncio.Event()
|
|
244
|
+
|
|
245
|
+
self.lifecycle = AppLifecycle(app, self.container)
|
|
246
|
+
|
|
247
|
+
def run(self) -> None:
|
|
248
|
+
|
|
249
|
+
def on_signal_received(signal: int, frame_type: FrameType | None) -> None:
|
|
250
|
+
logger.info("Received shutdown signal")
|
|
251
|
+
self.shutdown_event.set()
|
|
252
|
+
|
|
253
|
+
signal.signal(signal.SIGINT, on_signal_received)
|
|
254
|
+
|
|
255
|
+
with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner:
|
|
256
|
+
runner.run(self.start_scheduler())
|
|
257
|
+
|
|
258
|
+
async def start_scheduler(self) -> None:
|
|
259
|
+
"""
|
|
260
|
+
Declares the scheduled actions and starts the scheduler.
|
|
261
|
+
This is the main entry point for the scheduler.
|
|
262
|
+
"""
|
|
263
|
+
async with self.lifecycle():
|
|
264
|
+
|
|
265
|
+
scheduled_actions = extract_scheduled_actions(self.app, self.container)
|
|
266
|
+
|
|
267
|
+
await self.broker.initialize(scheduled_actions)
|
|
268
|
+
|
|
269
|
+
await self.run_scheduled_actions(scheduled_actions)
|
|
270
|
+
|
|
271
|
+
async def run_scheduled_actions(
|
|
272
|
+
self, scheduled_actions: SCHEDULED_ACTION_LIST
|
|
273
|
+
) -> None:
|
|
274
|
+
|
|
275
|
+
while not self.shutdown_event.is_set():
|
|
276
|
+
now = int(time.time())
|
|
277
|
+
for func, scheduled_action in scheduled_actions:
|
|
278
|
+
if self.shutdown_event.is_set():
|
|
279
|
+
break
|
|
280
|
+
|
|
281
|
+
async with self.backend.lock():
|
|
282
|
+
|
|
283
|
+
last_dispatch_time: int | None = (
|
|
284
|
+
await self.backend.get_last_dispatch_time(
|
|
285
|
+
ScheduledAction.get_function_id(func)
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if last_dispatch_time is not None:
|
|
290
|
+
cron = croniter.croniter(
|
|
291
|
+
scheduled_action.cron, last_dispatch_time
|
|
292
|
+
)
|
|
293
|
+
next_run: datetime = cron.get_next(datetime).replace(tzinfo=UTC)
|
|
294
|
+
if next_run > datetime.now(UTC):
|
|
295
|
+
logger.info(
|
|
296
|
+
f"Skipping {func.__module__}.{func.__qualname__} until {next_run}"
|
|
297
|
+
)
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
if not scheduled_action.allow_overlap:
|
|
301
|
+
if (
|
|
302
|
+
await self.backend.get_in_execution_count(
|
|
303
|
+
ScheduledAction.get_function_id(func)
|
|
304
|
+
)
|
|
305
|
+
> 0
|
|
306
|
+
):
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
await self.broker.dispatch_scheduled_action(
|
|
310
|
+
ScheduledAction.get_function_id(func),
|
|
311
|
+
now,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
await self.backend.set_last_dispatch_time(
|
|
315
|
+
ScheduledAction.get_function_id(func), now
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
logger.info(
|
|
319
|
+
f"Scheduled {func.__module__}.{func.__qualname__} at {now}"
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
for (
|
|
323
|
+
delayed_message_data
|
|
324
|
+
) in await self.backend.dequeue_next_delayed_messages(now):
|
|
325
|
+
await self.broker.dispatch_delayed_message(delayed_message_data)
|
|
326
|
+
|
|
327
|
+
with contextlib.suppress(asyncio.TimeoutError):
|
|
328
|
+
await asyncio.wait_for(self.shutdown_event.wait(), self.interval)
|
|
329
|
+
|
|
330
|
+
# await self.shutdown_event.wait(self.interval)
|
|
331
|
+
|
|
332
|
+
logger.info("Scheduler stopped")
|
|
333
|
+
|
|
334
|
+
await self.backend.dispose()
|
|
335
|
+
await self.broker.dispose()
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@asynccontextmanager
|
|
339
|
+
async def none_context() -> AsyncGenerator[None, None]:
|
|
340
|
+
yield
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
logging.basicConfig(
|
|
344
|
+
level=logging.INFO,
|
|
345
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
346
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from aio_pika.abc import AbstractChannel, AbstractExchange, AbstractQueue
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RabbitmqUtils:
|
|
5
|
+
|
|
6
|
+
DEAD_LETTER_EXCHANGE = "dlx"
|
|
7
|
+
DEAD_LETTER_QUEUE = "dlq"
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
async def declare_dl_exchange(cls, channel: AbstractChannel) -> AbstractExchange:
|
|
11
|
+
"""
|
|
12
|
+
Declare a Dead Letter Exchange (DLX) for the given channel.
|
|
13
|
+
"""
|
|
14
|
+
await channel.set_qos(prefetch_count=1)
|
|
15
|
+
return await channel.declare_exchange(
|
|
16
|
+
cls.DEAD_LETTER_EXCHANGE,
|
|
17
|
+
type="direct",
|
|
18
|
+
durable=True,
|
|
19
|
+
auto_delete=False,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
async def declare_dl_queue(cls, channel: AbstractChannel) -> AbstractQueue:
|
|
24
|
+
"""
|
|
25
|
+
Declare a Dead Letter Queue (DLQ) for the given queue.
|
|
26
|
+
"""
|
|
27
|
+
await channel.set_qos(prefetch_count=1)
|
|
28
|
+
return await channel.declare_queue(
|
|
29
|
+
cls.DEAD_LETTER_QUEUE,
|
|
30
|
+
durable=True,
|
|
31
|
+
arguments={
|
|
32
|
+
"x-dead-letter-exchange": "",
|
|
33
|
+
"x-dead-letter-routing-key": cls.DEAD_LETTER_EXCHANGE,
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
async def delcare_dl_kit(
|
|
39
|
+
cls,
|
|
40
|
+
channel: AbstractChannel,
|
|
41
|
+
) -> tuple[AbstractExchange, AbstractQueue]:
|
|
42
|
+
"""
|
|
43
|
+
Declare a Dead Letter Exchange and Queue (DLX and DLQ) for the given channel.
|
|
44
|
+
"""
|
|
45
|
+
dlx = await cls.declare_dl_exchange(channel)
|
|
46
|
+
dlq = await cls.declare_dl_queue(channel)
|
|
47
|
+
await dlq.bind(dlx, routing_key=cls.DEAD_LETTER_EXCHANGE)
|
|
48
|
+
return dlx, dlq
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
async def declare_main_exchange(
|
|
52
|
+
cls,
|
|
53
|
+
channel: AbstractChannel,
|
|
54
|
+
exchange_name: str,
|
|
55
|
+
) -> AbstractExchange:
|
|
56
|
+
"""
|
|
57
|
+
Declare a main exchange for the given channel.
|
|
58
|
+
"""
|
|
59
|
+
await channel.set_qos(prefetch_count=1)
|
|
60
|
+
return await channel.declare_exchange(
|
|
61
|
+
exchange_name,
|
|
62
|
+
type="topic",
|
|
63
|
+
durable=True,
|
|
64
|
+
auto_delete=False,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
async def declare_queue(
|
|
69
|
+
cls,
|
|
70
|
+
channel: AbstractChannel,
|
|
71
|
+
queue_name: str,
|
|
72
|
+
) -> AbstractQueue:
|
|
73
|
+
"""
|
|
74
|
+
Declare a queue with the given name and properties.
|
|
75
|
+
"""
|
|
76
|
+
await channel.set_qos(prefetch_count=1)
|
|
77
|
+
return await channel.declare_queue(
|
|
78
|
+
queue_name,
|
|
79
|
+
durable=True,
|
|
80
|
+
arguments={
|
|
81
|
+
"x-dead-letter-exchange": cls.DEAD_LETTER_EXCHANGE,
|
|
82
|
+
"x-dead-letter-routing-key": cls.DEAD_LETTER_EXCHANGE,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: jararaca
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A simple and fast API framework for Python
|
|
5
5
|
Author: Lucas S
|
|
6
6
|
Author-email: me@luscasleo.dev
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
12
12
|
Provides-Extra: docs
|
|
13
13
|
Provides-Extra: http
|
|
14
14
|
Provides-Extra: opentelemetry
|
|
15
|
+
Provides-Extra: watch
|
|
15
16
|
Requires-Dist: aio-pika (>=9.4.3,<10.0.0)
|
|
16
17
|
Requires-Dist: croniter (>=3.0.3,<4.0.0)
|
|
17
18
|
Requires-Dist: fastapi (>=0.113.0,<0.114.0)
|
|
@@ -27,6 +28,7 @@ Requires-Dist: types-croniter (>=3.0.3.20240731,<4.0.0.0)
|
|
|
27
28
|
Requires-Dist: types-redis (>=4.6.0.20240903,<5.0.0.0)
|
|
28
29
|
Requires-Dist: uvicorn (>=0.30.6,<0.31.0)
|
|
29
30
|
Requires-Dist: uvloop (>=0.20.0,<0.21.0)
|
|
31
|
+
Requires-Dist: watchdog (>=3.0.0,<4.0.0) ; extra == "watch"
|
|
30
32
|
Requires-Dist: websockets (>=13.0.1,<14.0.0)
|
|
31
33
|
Project-URL: Repository, https://github.com/LuscasLeo/jararaca
|
|
32
34
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
jararaca/__init__.py,sha256=
|
|
1
|
+
jararaca/__init__.py,sha256=4WO-Th3vY1_0SKdLyviZN2rftiX7R5yPVNPGJ9Mf5Y0,15633
|
|
2
2
|
jararaca/__main__.py,sha256=-O3vsB5lHdqNFjUtoELDF81IYFtR-DSiiFMzRaiSsv4,67
|
|
3
|
-
jararaca/
|
|
3
|
+
jararaca/broker_backend/__init__.py,sha256=GzEIuHR1xzgCJD4FE3harNjoaYzxHMHoEL0_clUaC-k,3528
|
|
4
|
+
jararaca/broker_backend/mapper.py,sha256=vTsi7sWpNvlga1PWPFg0rCJ5joJ0cdzykkIc2Tuvenc,696
|
|
5
|
+
jararaca/broker_backend/redis_broker_backend.py,sha256=a7DHchy3NAiD71Ix8SwmQOUnniu7uup-Woa4ON_4J7I,5786
|
|
6
|
+
jararaca/cli.py,sha256=oaLqJlb-GAbhYpQHzl-P8ajPdo-G_ddIuSu2SsZNDh8,10244
|
|
4
7
|
jararaca/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
8
|
jararaca/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
9
|
jararaca/core/providers.py,sha256=wktH84FK7c1s2wNq-fudf1uMfi3CQBR0neU2czJ_L0U,434
|
|
@@ -8,15 +11,18 @@ jararaca/core/uow.py,sha256=WrA50VWzfJIyZHeUhSs8IOpSA4T-D8VV6YPLlFTF5V4,2026
|
|
|
8
11
|
jararaca/di.py,sha256=h3IsXdYZjJj8PJfnEDn0ZAwdd4EBfh8jU-wWO8ko_t4,76
|
|
9
12
|
jararaca/files/entity.py.mako,sha256=tjQ-1udAMvVqgRokhsrR4uy3P_OVnbk3XZ8X69ixWhE,3098
|
|
10
13
|
jararaca/lifecycle.py,sha256=qKlzLQQioS8QkxNJ_FC_5WbmT77cNbc_S7OcQeOoHkI,1895
|
|
11
|
-
jararaca/messagebus/__init__.py,sha256=
|
|
14
|
+
jararaca/messagebus/__init__.py,sha256=5jAqPqdcEMYBfQyfZDWPnplYdrfMyJLMcacf3qLyUhk,56
|
|
12
15
|
jararaca/messagebus/bus_message_controller.py,sha256=Xd_qwnX5jUvgBTCarHR36fvtol9lPTsYp2IIGKyQQaE,1487
|
|
13
|
-
jararaca/messagebus/
|
|
16
|
+
jararaca/messagebus/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
jararaca/messagebus/decorators.py,sha256=y-w4dWbP9ZW3ZJ4mE9iIaxw01ZC5snEbOuBY5NC-Bn0,5626
|
|
14
18
|
jararaca/messagebus/interceptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py,sha256=
|
|
16
|
-
jararaca/messagebus/
|
|
17
|
-
jararaca/messagebus/
|
|
18
|
-
jararaca/messagebus/
|
|
19
|
-
jararaca/
|
|
19
|
+
jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py,sha256=Aex87wopB34sMNzXBgi4KucVHjL2wYog3-IH75DAfDk,5387
|
|
20
|
+
jararaca/messagebus/interceptors/publisher_interceptor.py,sha256=fQFFW9hH6ZU3UOyR7kMPrNp9wA71qEy5XlgrBQdBMS4,1230
|
|
21
|
+
jararaca/messagebus/message.py,sha256=U6cyd2XknX8mtm0333slz5fanky2PFLWCmokAO56vvU,819
|
|
22
|
+
jararaca/messagebus/publisher.py,sha256=K7WsOMVTyLmdms3ZKEshqrQc_DhNreiFK-HnmOT9Ce0,1965
|
|
23
|
+
jararaca/messagebus/worker.py,sha256=SGd7Mng33ivcaAP7cwmF_2loQPuHIfTwkCZtCNJ5G-I,13523
|
|
24
|
+
jararaca/messagebus/worker_v2.py,sha256=F7Hl92h5PezxNUY3_lfv9jNhboyQouuk6hXocPGeyXY,20341
|
|
25
|
+
jararaca/microservice.py,sha256=C_Txqm3xSmdHIghJigKk6TOycL5eTJv9PF6XP7TA8j4,6638
|
|
20
26
|
jararaca/observability/decorators.py,sha256=XffBinFXdiNkY6eo8_1nkr_GapM0RUGBg0aicBIelag,2220
|
|
21
27
|
jararaca/observability/interceptor.py,sha256=GHkuGKFWftN7MDjvYeGFGEPnuJETNhtxRK6yuPrCrpU,1462
|
|
22
28
|
jararaca/observability/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -49,15 +55,19 @@ jararaca/rpc/http/backends/otel.py,sha256=Uc6CjHSCZ5hvnK1fNFv3ota5xzUFnvIl1JOpG3
|
|
|
49
55
|
jararaca/rpc/http/decorators.py,sha256=oUSzgMGI8w6SoKiz3GltDbd3BWAuyY60F23cdRRNeiw,11897
|
|
50
56
|
jararaca/rpc/http/httpx.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
51
57
|
jararaca/scheduler/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
-
jararaca/scheduler/decorators.py,sha256=
|
|
53
|
-
jararaca/scheduler/scheduler.py,sha256=
|
|
58
|
+
jararaca/scheduler/decorators.py,sha256=cRwnQHtlo9iC-2c30t7GGTEURFmdtDPkkVrr5cRVblM,3645
|
|
59
|
+
jararaca/scheduler/scheduler.py,sha256=ll3ifOZ9QZcyCX1jfuNagHMVAbhRoE9VAl0ox3MWnwo,5295
|
|
60
|
+
jararaca/scheduler/scheduler_v2.py,sha256=5-_O6EzG95y_3aIw5SA4W_ppbPIIszBrCYM7cFvXgBo,11166
|
|
61
|
+
jararaca/scheduler/types.py,sha256=4HEQOmVIDp-BYLSzqmqSFIio1bd51WFmgFPIzPpVu04,135
|
|
54
62
|
jararaca/tools/app_config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
63
|
jararaca/tools/app_config/decorators.py,sha256=-ckkMZ1dswOmECdo1rFrZ15UAku--txaNXMp8fd1Ndk,941
|
|
56
64
|
jararaca/tools/app_config/interceptor.py,sha256=nfFZiS80hrbnL7-XEYrwmp2rwaVYBqxvqu3Y-6o_ov4,2575
|
|
57
65
|
jararaca/tools/metadata.py,sha256=7nlCDYgItNybentPSSCc2MLqN7IpBd0VyQzfjfQycVI,1402
|
|
58
66
|
jararaca/tools/typescript/interface_parser.py,sha256=4SHt094P-QawMFHSyMCiujQf8Niw7xACIO1RHBM8-w4,29192
|
|
59
|
-
jararaca
|
|
60
|
-
jararaca
|
|
61
|
-
jararaca-0.
|
|
62
|
-
jararaca-0.
|
|
63
|
-
jararaca-0.
|
|
67
|
+
jararaca/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
|
+
jararaca/utils/rabbitmq_utils.py,sha256=zFqZE-j6TSWFOEPbkIaB2hy2sqsXup-5421jIiPLfXY,2543
|
|
69
|
+
jararaca-0.3.1.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
70
|
+
jararaca-0.3.1.dist-info/METADATA,sha256=TRaKZQ_5vbE3OS_sTSYCpq7aYkDX19zFlebpkenHo9A,4951
|
|
71
|
+
jararaca-0.3.1.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
|
|
72
|
+
jararaca-0.3.1.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
|
|
73
|
+
jararaca-0.3.1.dist-info/RECORD,,
|
jararaca/messagebus/types.py
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
from typing import ClassVar, Generic, Literal, Protocol, TypeVar
|
|
2
|
-
|
|
3
|
-
from pydantic import BaseModel
|
|
4
|
-
|
|
5
|
-
from jararaca.messagebus.publisher import use_publisher
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Message(BaseModel):
|
|
9
|
-
"""
|
|
10
|
-
Base class for messages representing tasks.
|
|
11
|
-
A Task is a message that represents a unit of work to be done.
|
|
12
|
-
It is published to a TaskPublisher and consumed by a TaskHandler, wrapped in TaskData.
|
|
13
|
-
Note: A Task is not an Event.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
MESSAGE_TOPIC: ClassVar[str] = "__unset__"
|
|
17
|
-
|
|
18
|
-
MESSAGE_TYPE: ClassVar[Literal["task", "event"]] = "task"
|
|
19
|
-
|
|
20
|
-
async def publish(self) -> None:
|
|
21
|
-
task_publisher = use_publisher()
|
|
22
|
-
await task_publisher.publish(self, self.MESSAGE_TOPIC)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
INHERITS_MESSAGE_CO = TypeVar("INHERITS_MESSAGE_CO", bound=Message, covariant=True)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class MessageOf(Protocol, Generic[INHERITS_MESSAGE_CO]):
|
|
29
|
-
|
|
30
|
-
def payload(self) -> INHERITS_MESSAGE_CO: ...
|
|
File without changes
|
|
File without changes
|
|
File without changes
|