jararaca 0.2.37a12__py3-none-any.whl → 0.3.0__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 +80 -19
- 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 +51 -31
- 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.0.dist-info}/METADATA +3 -1
- {jararaca-0.2.37a12.dist-info → jararaca-0.3.0.dist-info}/RECORD +26 -16
- jararaca/messagebus/types.py +0 -30
- {jararaca-0.2.37a12.dist-info → jararaca-0.3.0.dist-info}/LICENSE +0 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.3.0.dist-info}/WHEEL +0 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.3.0.dist-info}/entry_points.txt +0 -0
jararaca/__init__.py
CHANGED
|
@@ -2,6 +2,7 @@ from importlib import import_module
|
|
|
2
2
|
from typing import TYPE_CHECKING
|
|
3
3
|
|
|
4
4
|
if TYPE_CHECKING:
|
|
5
|
+
from jararaca.broker_backend.redis_broker_backend import RedisMessageBrokerBackend
|
|
5
6
|
from jararaca.messagebus.bus_message_controller import (
|
|
6
7
|
ack,
|
|
7
8
|
nack,
|
|
@@ -60,10 +61,12 @@ if TYPE_CHECKING:
|
|
|
60
61
|
from .messagebus.interceptors.aiopika_publisher_interceptor import (
|
|
61
62
|
AIOPikaConnectionFactory,
|
|
62
63
|
GenericPoolConfig,
|
|
64
|
+
)
|
|
65
|
+
from .messagebus.interceptors.publisher_interceptor import (
|
|
63
66
|
MessageBusPublisherInterceptor,
|
|
64
67
|
)
|
|
68
|
+
from .messagebus.message import Message, MessageOf
|
|
65
69
|
from .messagebus.publisher import use_publisher
|
|
66
|
-
from .messagebus.types import Message, MessageOf
|
|
67
70
|
from .messagebus.worker import MessageBusWorker
|
|
68
71
|
from .microservice import Microservice, use_app_context, use_current_container
|
|
69
72
|
from .persistence.base import T_BASEMODEL, BaseEntity
|
|
@@ -115,6 +118,7 @@ if TYPE_CHECKING:
|
|
|
115
118
|
from .tools.app_config.interceptor import AppConfigurationInterceptor
|
|
116
119
|
|
|
117
120
|
__all__ = [
|
|
121
|
+
"RedisMessageBrokerBackend",
|
|
118
122
|
"FilterRuleApplier",
|
|
119
123
|
"SortRuleApplier",
|
|
120
124
|
"use_bus_message_controller",
|
|
@@ -216,6 +220,11 @@ if TYPE_CHECKING:
|
|
|
216
220
|
__SPEC_PARENT__: str = __spec__.parent # type: ignore
|
|
217
221
|
# A mapping of {<member name>: (package, <module name>)} defining dynamic imports
|
|
218
222
|
_dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
223
|
+
"RedisMessageBrokerBackend": (
|
|
224
|
+
__SPEC_PARENT__,
|
|
225
|
+
"broker_backend.redis_broker_backend",
|
|
226
|
+
None,
|
|
227
|
+
),
|
|
219
228
|
"FilterRuleApplier": (__SPEC_PARENT__, "persistence.sort_filter", None),
|
|
220
229
|
"SortRuleApplier": (__SPEC_PARENT__, "persistence.sort_filter", None),
|
|
221
230
|
"use_bus_message_controller": (
|
|
@@ -286,8 +295,8 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
|
286
295
|
),
|
|
287
296
|
"Identifiable": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
288
297
|
"IdentifiableEntity": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
289
|
-
"MessageOf": (__SPEC_PARENT__, "messagebus.
|
|
290
|
-
"Message": (__SPEC_PARENT__, "messagebus.
|
|
298
|
+
"MessageOf": (__SPEC_PARENT__, "messagebus.message", None),
|
|
299
|
+
"Message": (__SPEC_PARENT__, "messagebus.message", None),
|
|
291
300
|
"StringCriteria": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
292
301
|
"DateCriteria": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
293
302
|
"DateOrderedFilter": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
@@ -339,7 +348,7 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
|
339
348
|
),
|
|
340
349
|
"MessageBusPublisherInterceptor": (
|
|
341
350
|
__SPEC_PARENT__,
|
|
342
|
-
"messagebus.interceptors.
|
|
351
|
+
"messagebus.interceptors.publisher_interceptor",
|
|
343
352
|
None,
|
|
344
353
|
),
|
|
345
354
|
"RedisWebSocketConnectionBackend": (
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from typing import AsyncContextManager, AsyncGenerator, Iterable
|
|
4
|
+
|
|
5
|
+
from jararaca.scheduler.types import DelayedMessageData
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MessageBrokerBackend(ABC):
|
|
9
|
+
|
|
10
|
+
def lock(self) -> AsyncContextManager[None]:
|
|
11
|
+
"""
|
|
12
|
+
Acquire a lock for the message broker backend.
|
|
13
|
+
This is used to ensure that only one instance of the scheduler is running at a time.
|
|
14
|
+
"""
|
|
15
|
+
raise NotImplementedError(f"lock() is not implemented by {self.__class__}.")
|
|
16
|
+
|
|
17
|
+
async def get_last_dispatch_time(self, action_name: str) -> int | None:
|
|
18
|
+
"""
|
|
19
|
+
Get the last dispatch time of the scheduled action.
|
|
20
|
+
This is used to determine if the scheduled action should be executed again
|
|
21
|
+
or if it should be skipped.
|
|
22
|
+
"""
|
|
23
|
+
raise NotImplementedError(
|
|
24
|
+
f"get_last_dispatch_time() is not implemented by {self.__class__}."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
async def set_last_dispatch_time(self, action_name: str, timestamp: int) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Set the last dispatch time of the scheduled action.
|
|
30
|
+
This is used to determine if the scheduled action should be executed again
|
|
31
|
+
or if it should be skipped.
|
|
32
|
+
"""
|
|
33
|
+
raise NotImplementedError(
|
|
34
|
+
f"set_last_dispatch_time() is not implemented by {self.__class__}."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
async def get_in_execution_count(self, action_name: str) -> int:
|
|
38
|
+
"""
|
|
39
|
+
Get the number of scheduled actions in execution.
|
|
40
|
+
This is used to determine if the scheduled action should be executed again
|
|
41
|
+
or if it should be skipped.
|
|
42
|
+
"""
|
|
43
|
+
raise NotImplementedError(
|
|
44
|
+
f"get_in_execution_count() is not implemented by {self.__class__}."
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def in_execution(self, action_name: str) -> AsyncContextManager[None]:
|
|
48
|
+
"""
|
|
49
|
+
Acquire a lock for the scheduled action.
|
|
50
|
+
This is used to ensure that only one instance of the scheduled action is running at a time.
|
|
51
|
+
"""
|
|
52
|
+
raise NotImplementedError(
|
|
53
|
+
f"in_execution() is not implemented by {self.__class__}."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def dequeue_next_delayed_messages(
|
|
57
|
+
self, start_timestamp: int
|
|
58
|
+
) -> Iterable[DelayedMessageData]:
|
|
59
|
+
"""
|
|
60
|
+
Dequeue the next delayed messages from the message broker.
|
|
61
|
+
This is used to trigger the scheduled action.
|
|
62
|
+
"""
|
|
63
|
+
raise NotImplementedError(
|
|
64
|
+
f"dequeue_next_delayed_messages() is not implemented by {self.__class__}."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def enqueue_delayed_message(
|
|
68
|
+
self, delayed_message: DelayedMessageData
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Enqueue a delayed message to the message broker.
|
|
72
|
+
This is used to trigger the scheduled action.
|
|
73
|
+
"""
|
|
74
|
+
raise NotImplementedError(
|
|
75
|
+
f"enqueue_delayed_message() is not implemented by {self.__class__}."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def dispose(self) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Dispose of the message broker backend.
|
|
81
|
+
This is used to clean up resources used by the message broker backend.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class NullBackend(MessageBrokerBackend):
|
|
86
|
+
"""
|
|
87
|
+
A null backend that does nothing.
|
|
88
|
+
This is used for testing purposes.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
@asynccontextmanager
|
|
92
|
+
async def lock(self) -> AsyncGenerator[None, None]:
|
|
93
|
+
yield
|
|
94
|
+
|
|
95
|
+
async def get_last_dispatch_time(self, action_name: str) -> int:
|
|
96
|
+
return 0
|
|
97
|
+
|
|
98
|
+
async def set_last_dispatch_time(self, action_name: str, timestamp: int) -> None:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
async def dispose(self) -> None:
|
|
102
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from jararaca.broker_backend import MessageBrokerBackend
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_message_broker_backend_from_url(url: str) -> MessageBrokerBackend:
|
|
5
|
+
"""
|
|
6
|
+
Factory function to create a message broker backend instance from a URL.
|
|
7
|
+
Currently, only Redis is supported.
|
|
8
|
+
"""
|
|
9
|
+
if (
|
|
10
|
+
url.startswith("redis://")
|
|
11
|
+
or url.startswith("rediss://")
|
|
12
|
+
or url.startswith("redis-socket://")
|
|
13
|
+
or url.startswith("rediss+socket://")
|
|
14
|
+
):
|
|
15
|
+
from jararaca.broker_backend.redis_broker_backend import (
|
|
16
|
+
RedisMessageBrokerBackend,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
return RedisMessageBrokerBackend(url)
|
|
20
|
+
else:
|
|
21
|
+
raise ValueError(f"Unsupported message broker backend URL: {url}")
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
from typing import AsyncGenerator, Iterable
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
import redis.asyncio
|
|
8
|
+
|
|
9
|
+
from jararaca.broker_backend import MessageBrokerBackend
|
|
10
|
+
from jararaca.scheduler.types import DelayedMessageData
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RedisMessageBrokerBackend(MessageBrokerBackend):
|
|
16
|
+
def __init__(self, url: str) -> None:
|
|
17
|
+
self.redis = redis.asyncio.Redis.from_url(url)
|
|
18
|
+
self.last_dispatch_time_key = "last_dispatch_time:{action_name}"
|
|
19
|
+
self.last_execution_time_key = "last_execution_time:{action_name}"
|
|
20
|
+
self.execution_indicator_key = "in_execution:{action_name}:{timestamp}"
|
|
21
|
+
self.execution_indicator_expiration = 60 * 5
|
|
22
|
+
self.delayed_messages_key = "delayed_messages"
|
|
23
|
+
self.delayed_messages_metadata_key = "delayed_messages_metadata:{task_id}"
|
|
24
|
+
|
|
25
|
+
@asynccontextmanager
|
|
26
|
+
async def lock(self) -> AsyncGenerator[None, None]:
|
|
27
|
+
yield
|
|
28
|
+
|
|
29
|
+
async def get_last_dispatch_time(self, action_name: str) -> int | None:
|
|
30
|
+
|
|
31
|
+
key = self.last_dispatch_time_key.format(action_name=action_name)
|
|
32
|
+
last_execution_time = await self.redis.get(key)
|
|
33
|
+
if last_execution_time is None:
|
|
34
|
+
return None
|
|
35
|
+
return int(last_execution_time)
|
|
36
|
+
|
|
37
|
+
async def set_last_dispatch_time(self, action_name: str, timestamp: int) -> None:
|
|
38
|
+
key = self.last_dispatch_time_key.format(action_name=action_name)
|
|
39
|
+
await self.redis.set(key, timestamp)
|
|
40
|
+
|
|
41
|
+
async def get_last_execution_time(self, action_name: str) -> int | None:
|
|
42
|
+
key = self.last_execution_time_key.format(action_name=action_name)
|
|
43
|
+
last_execution_time = await self.redis.get(key)
|
|
44
|
+
if last_execution_time is None:
|
|
45
|
+
return None
|
|
46
|
+
return int(last_execution_time)
|
|
47
|
+
|
|
48
|
+
async def set_last_execution_time(self, action_name: str, timestamp: int) -> None:
|
|
49
|
+
key = self.last_execution_time_key.format(action_name=action_name)
|
|
50
|
+
await self.redis.set(key, timestamp)
|
|
51
|
+
|
|
52
|
+
async def get_in_execution_count(self, action_name: str) -> int:
|
|
53
|
+
key = self.execution_indicator_key.format(
|
|
54
|
+
action_name=action_name, timestamp="*"
|
|
55
|
+
)
|
|
56
|
+
in_execution_count = await self.redis.keys(key)
|
|
57
|
+
if in_execution_count is None:
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
return len(in_execution_count)
|
|
61
|
+
|
|
62
|
+
@asynccontextmanager
|
|
63
|
+
async def in_execution(self, action_name: str) -> AsyncGenerator[None, None]:
|
|
64
|
+
"""
|
|
65
|
+
Acquire a lock for the scheduled action.
|
|
66
|
+
This is used to ensure that only one instance of the scheduled action is running at a time.
|
|
67
|
+
"""
|
|
68
|
+
key = self.execution_indicator_key.format(
|
|
69
|
+
action_name=action_name, timestamp=int(time.time())
|
|
70
|
+
)
|
|
71
|
+
await self.redis.set(key, 1, ex=self.execution_indicator_expiration)
|
|
72
|
+
try:
|
|
73
|
+
yield
|
|
74
|
+
finally:
|
|
75
|
+
await self.redis.delete(key)
|
|
76
|
+
|
|
77
|
+
async def enqueue_delayed_message(
|
|
78
|
+
self, delayed_message: DelayedMessageData
|
|
79
|
+
) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Enqueue a delayed message to the message broker.
|
|
82
|
+
This is used to trigger the scheduled action.
|
|
83
|
+
"""
|
|
84
|
+
task_id = str(uuid4())
|
|
85
|
+
async with self.redis.pipeline() as pipe:
|
|
86
|
+
pipe.set(
|
|
87
|
+
self.delayed_messages_metadata_key.format(task_id=task_id),
|
|
88
|
+
delayed_message.model_dump_json().encode(),
|
|
89
|
+
)
|
|
90
|
+
pipe.zadd(
|
|
91
|
+
self.delayed_messages_key,
|
|
92
|
+
{task_id: delayed_message.dispatch_time},
|
|
93
|
+
nx=True,
|
|
94
|
+
)
|
|
95
|
+
await pipe.execute()
|
|
96
|
+
|
|
97
|
+
async def dequeue_next_delayed_messages(
|
|
98
|
+
self, start_timestamp: int
|
|
99
|
+
) -> Iterable[DelayedMessageData]:
|
|
100
|
+
"""
|
|
101
|
+
Dequeue the next delayed messages from the message broker.
|
|
102
|
+
This is used to trigger the scheduled action.
|
|
103
|
+
"""
|
|
104
|
+
tasks_ids = await self.redis.zrangebyscore(
|
|
105
|
+
name=self.delayed_messages_key,
|
|
106
|
+
max=start_timestamp,
|
|
107
|
+
min="-inf",
|
|
108
|
+
withscores=False,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if not tasks_ids:
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
tasks_bytes_data: list[bytes] = []
|
|
115
|
+
|
|
116
|
+
for task_id_bytes in tasks_ids:
|
|
117
|
+
metadata = await self.redis.get(
|
|
118
|
+
self.delayed_messages_metadata_key.format(
|
|
119
|
+
task_id=task_id_bytes.decode()
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
if metadata is None:
|
|
123
|
+
logger.warning(
|
|
124
|
+
f"Delayed message metadata not found for task_id: {task_id_bytes.decode()}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
tasks_bytes_data.append(metadata)
|
|
130
|
+
|
|
131
|
+
async with self.redis.pipeline() as pipe:
|
|
132
|
+
for task_id_bytes in tasks_ids:
|
|
133
|
+
pipe.zrem(self.delayed_messages_key, task_id_bytes.decode())
|
|
134
|
+
pipe.delete(
|
|
135
|
+
self.delayed_messages_metadata_key.format(
|
|
136
|
+
task_id=task_id_bytes.decode()
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
await pipe.execute()
|
|
140
|
+
|
|
141
|
+
delayed_messages: list[DelayedMessageData] = []
|
|
142
|
+
|
|
143
|
+
for task_bytes_data in tasks_bytes_data:
|
|
144
|
+
try:
|
|
145
|
+
delayed_message = DelayedMessageData.model_validate_json(
|
|
146
|
+
task_bytes_data.decode()
|
|
147
|
+
)
|
|
148
|
+
delayed_messages.append(delayed_message)
|
|
149
|
+
except Exception:
|
|
150
|
+
logger.error(
|
|
151
|
+
f"Error parsing delayed message: {task_bytes_data.decode()}"
|
|
152
|
+
)
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
return delayed_messages
|
|
156
|
+
|
|
157
|
+
async def dispose(self) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Dispose of the message broker backend.
|
|
160
|
+
This is used to close the connection to the message broker.
|
|
161
|
+
"""
|
|
162
|
+
await self.redis.close()
|
jararaca/cli.py
CHANGED
|
@@ -11,11 +11,13 @@ import click
|
|
|
11
11
|
import uvicorn
|
|
12
12
|
from mako.template import Template # type: ignore
|
|
13
13
|
|
|
14
|
-
from jararaca.messagebus
|
|
14
|
+
from jararaca.messagebus import worker as worker_v1
|
|
15
|
+
from jararaca.messagebus import worker_v2 as worker_v2_mod
|
|
15
16
|
from jararaca.microservice import Microservice
|
|
16
17
|
from jararaca.presentation.http_microservice import HttpMicroservice
|
|
17
18
|
from jararaca.presentation.server import create_http_server
|
|
18
|
-
from jararaca.scheduler.scheduler import Scheduler
|
|
19
|
+
from jararaca.scheduler.scheduler import Scheduler
|
|
20
|
+
from jararaca.scheduler.scheduler_v2 import SchedulerV2
|
|
19
21
|
from jararaca.tools.typescript.interface_parser import (
|
|
20
22
|
write_microservice_to_typescript_interface,
|
|
21
23
|
)
|
|
@@ -35,7 +37,10 @@ def find_item_by_module_path(
|
|
|
35
37
|
try:
|
|
36
38
|
module = importlib.import_module(module_name)
|
|
37
39
|
except ImportError as e:
|
|
38
|
-
|
|
40
|
+
if e.name == module_name:
|
|
41
|
+
raise ImportError("Module not found") from e
|
|
42
|
+
else:
|
|
43
|
+
raise
|
|
39
44
|
|
|
40
45
|
if not hasattr(module, app):
|
|
41
46
|
raise ValueError("module %s has no attribute %s" % (module, app))
|
|
@@ -73,7 +78,7 @@ def cli() -> None:
|
|
|
73
78
|
@click.option(
|
|
74
79
|
"--url",
|
|
75
80
|
type=str,
|
|
76
|
-
|
|
81
|
+
envvar="BROKER_URL",
|
|
77
82
|
)
|
|
78
83
|
@click.option(
|
|
79
84
|
"--username",
|
|
@@ -90,11 +95,6 @@ def cli() -> None:
|
|
|
90
95
|
type=str,
|
|
91
96
|
default="jararaca_ex",
|
|
92
97
|
)
|
|
93
|
-
@click.option(
|
|
94
|
-
"--queue",
|
|
95
|
-
type=str,
|
|
96
|
-
default="jararaca_q",
|
|
97
|
-
)
|
|
98
98
|
@click.option(
|
|
99
99
|
"--prefetch-count",
|
|
100
100
|
type=int,
|
|
@@ -106,7 +106,6 @@ def worker(
|
|
|
106
106
|
username: str | None,
|
|
107
107
|
password: str | None,
|
|
108
108
|
exchange: str,
|
|
109
|
-
queue: str,
|
|
110
109
|
prefetch_count: int,
|
|
111
110
|
) -> None:
|
|
112
111
|
|
|
@@ -134,30 +133,59 @@ def worker(
|
|
|
134
133
|
|
|
135
134
|
url = parsed_url.geturl()
|
|
136
135
|
|
|
137
|
-
config = AioPikaWorkerConfig(
|
|
136
|
+
config = worker_v1.AioPikaWorkerConfig(
|
|
138
137
|
url=url,
|
|
139
138
|
exchange=exchange,
|
|
140
|
-
queue=queue,
|
|
141
139
|
prefetch_count=prefetch_count,
|
|
142
140
|
)
|
|
143
141
|
|
|
144
|
-
MessageBusWorker(app, config=config).start_sync()
|
|
142
|
+
worker_v1.MessageBusWorker(app, config=config).start_sync()
|
|
145
143
|
|
|
146
144
|
|
|
147
145
|
@cli.command()
|
|
148
146
|
@click.argument(
|
|
149
147
|
"app_path",
|
|
150
148
|
type=str,
|
|
149
|
+
envvar="APP_PATH",
|
|
150
|
+
)
|
|
151
|
+
@click.option(
|
|
152
|
+
"--broker-url",
|
|
153
|
+
type=str,
|
|
154
|
+
envvar="BROKER_URL",
|
|
155
|
+
)
|
|
156
|
+
@click.option(
|
|
157
|
+
"--backend-url",
|
|
158
|
+
type=str,
|
|
159
|
+
envvar="BACKEND_URL",
|
|
160
|
+
)
|
|
161
|
+
def worker_v2(app_path: str, broker_url: str, backend_url: str) -> None:
|
|
162
|
+
|
|
163
|
+
app = find_microservice_by_module_path(app_path)
|
|
164
|
+
|
|
165
|
+
worker_v2_mod.MessageBusWorker(
|
|
166
|
+
app=app,
|
|
167
|
+
broker_url=broker_url,
|
|
168
|
+
backend_url=backend_url,
|
|
169
|
+
).start_sync()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@cli.command()
|
|
173
|
+
@click.argument(
|
|
174
|
+
"app_path",
|
|
175
|
+
type=str,
|
|
176
|
+
envvar="APP_PATH",
|
|
151
177
|
)
|
|
152
178
|
@click.option(
|
|
153
179
|
"--host",
|
|
154
180
|
type=str,
|
|
155
181
|
default="0.0.0.0",
|
|
182
|
+
envvar="HOST",
|
|
156
183
|
)
|
|
157
184
|
@click.option(
|
|
158
185
|
"--port",
|
|
159
186
|
type=int,
|
|
160
187
|
default=8000,
|
|
188
|
+
envvar="PORT",
|
|
161
189
|
)
|
|
162
190
|
def server(app_path: str, host: str, port: int) -> None:
|
|
163
191
|
|
|
@@ -180,9 +208,6 @@ def server(app_path: str, host: str, port: int) -> None:
|
|
|
180
208
|
uvicorn.run(asgi_app, host=host, port=port)
|
|
181
209
|
|
|
182
210
|
|
|
183
|
-
class NullBackend(SchedulerBackend): ...
|
|
184
|
-
|
|
185
|
-
|
|
186
211
|
@cli.command()
|
|
187
212
|
@click.argument(
|
|
188
213
|
"app_path",
|
|
@@ -199,7 +224,45 @@ def scheduler(
|
|
|
199
224
|
) -> None:
|
|
200
225
|
app = find_microservice_by_module_path(app_path)
|
|
201
226
|
|
|
202
|
-
Scheduler(app,
|
|
227
|
+
Scheduler(app, interval=interval).run()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@cli.command()
|
|
231
|
+
@click.argument(
|
|
232
|
+
"app_path",
|
|
233
|
+
type=str,
|
|
234
|
+
)
|
|
235
|
+
@click.option(
|
|
236
|
+
"--interval",
|
|
237
|
+
type=int,
|
|
238
|
+
default=1,
|
|
239
|
+
required=True,
|
|
240
|
+
)
|
|
241
|
+
@click.option(
|
|
242
|
+
"--broker-url",
|
|
243
|
+
type=str,
|
|
244
|
+
required=True,
|
|
245
|
+
)
|
|
246
|
+
@click.option(
|
|
247
|
+
"--backend-url",
|
|
248
|
+
type=str,
|
|
249
|
+
required=True,
|
|
250
|
+
)
|
|
251
|
+
def scheduler_v2(
|
|
252
|
+
interval: int,
|
|
253
|
+
broker_url: str,
|
|
254
|
+
backend_url: str,
|
|
255
|
+
app_path: str,
|
|
256
|
+
) -> None:
|
|
257
|
+
|
|
258
|
+
app = find_microservice_by_module_path(app_path)
|
|
259
|
+
scheduler = SchedulerV2(
|
|
260
|
+
app=app,
|
|
261
|
+
interval=interval,
|
|
262
|
+
backend_url=backend_url,
|
|
263
|
+
broker_url=broker_url,
|
|
264
|
+
)
|
|
265
|
+
scheduler.run()
|
|
203
266
|
|
|
204
267
|
|
|
205
268
|
@cli.command()
|
|
@@ -214,13 +277,11 @@ def scheduler(
|
|
|
214
277
|
@click.option(
|
|
215
278
|
"--watch",
|
|
216
279
|
is_flag=True,
|
|
217
|
-
help="Watch for file changes and regenerate TypeScript interfaces",
|
|
218
280
|
)
|
|
219
281
|
@click.option(
|
|
220
282
|
"--src-dir",
|
|
221
283
|
type=click.Path(exists=True, file_okay=False, dir_okay=True),
|
|
222
284
|
default="src",
|
|
223
|
-
help="Source directory to watch for changes (default: src)",
|
|
224
285
|
)
|
|
225
286
|
def gen_tsi(app_path: str, file_path: StreamWriter, watch: bool, src_dir: str) -> None:
|
|
226
287
|
"""Generate TypeScript interfaces from a Python microservice."""
|
jararaca/messagebus/__init__.py
CHANGED
|
File without changes
|
|
@@ -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.
|
|
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:
|
|
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[
|
|
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
|
-
|
|
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
|
-
|
|
114
|
+
message_handler_decoration = MessageHandler.get_message_incoming(member)
|
|
115
|
+
scheduled_action_decoration = ScheduledAction.get_scheduled_action(
|
|
116
|
+
member
|
|
117
|
+
)
|
|
94
118
|
|
|
95
|
-
if
|
|
96
|
-
continue
|
|
119
|
+
if message_handler_decoration is not None:
|
|
97
120
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|