jararaca 0.2.37a12__py3-none-any.whl → 0.4.0a5__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 +267 -15
- jararaca/__main__.py +4 -0
- jararaca/broker_backend/__init__.py +106 -0
- jararaca/broker_backend/mapper.py +25 -0
- jararaca/broker_backend/redis_broker_backend.py +168 -0
- jararaca/cli.py +840 -103
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +4 -0
- jararaca/core/uow.py +55 -16
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -0
- jararaca/lifecycle.py +6 -2
- jararaca/messagebus/__init__.py +5 -1
- jararaca/messagebus/bus_message_controller.py +4 -0
- jararaca/messagebus/consumers/__init__.py +3 -0
- jararaca/messagebus/decorators.py +90 -85
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +95 -37
- jararaca/messagebus/interceptors/publisher_interceptor.py +42 -0
- jararaca/messagebus/message.py +31 -0
- jararaca/messagebus/publisher.py +47 -4
- jararaca/messagebus/worker.py +1615 -135
- jararaca/microservice.py +248 -36
- jararaca/observability/constants.py +7 -0
- jararaca/observability/decorators.py +177 -16
- jararaca/observability/fastapi_exception_handler.py +37 -0
- jararaca/observability/hooks.py +109 -0
- jararaca/observability/interceptor.py +8 -2
- jararaca/observability/providers/__init__.py +3 -0
- jararaca/observability/providers/otel.py +213 -18
- jararaca/persistence/base.py +40 -3
- jararaca/persistence/exports.py +4 -0
- jararaca/persistence/interceptors/__init__.py +3 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +187 -23
- 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 +74 -32
- jararaca/presentation/__init__.py +3 -0
- jararaca/presentation/decorators.py +170 -82
- jararaca/presentation/exceptions.py +23 -0
- jararaca/presentation/hooks.py +4 -0
- jararaca/presentation/http_microservice.py +4 -0
- jararaca/presentation/server.py +120 -41
- jararaca/presentation/websocket/__init__.py +3 -0
- jararaca/presentation/websocket/base_types.py +4 -0
- jararaca/presentation/websocket/context.py +34 -4
- jararaca/presentation/websocket/decorators.py +8 -41
- jararaca/presentation/websocket/redis.py +280 -53
- jararaca/presentation/websocket/types.py +6 -2
- jararaca/presentation/websocket/websocket_interceptor.py +74 -23
- jararaca/reflect/__init__.py +3 -0
- jararaca/reflect/controller_inspect.py +81 -0
- jararaca/reflect/decorators.py +238 -0
- jararaca/reflect/metadata.py +76 -0
- 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 +378 -113
- jararaca/rpc/http/httpx.py +3 -0
- jararaca/scheduler/__init__.py +3 -0
- jararaca/scheduler/beat_worker.py +758 -0
- jararaca/scheduler/decorators.py +89 -28
- jararaca/scheduler/types.py +11 -0
- jararaca/tools/app_config/__init__.py +3 -0
- jararaca/tools/app_config/decorators.py +7 -19
- jararaca/tools/app_config/interceptor.py +10 -4
- jararaca/tools/typescript/__init__.py +3 -0
- jararaca/tools/typescript/decorators.py +120 -0
- jararaca/tools/typescript/interface_parser.py +1126 -189
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/rabbitmq_utils.py +372 -0
- jararaca/utils/retry.py +148 -0
- jararaca-0.4.0a5.dist-info/LICENSE +674 -0
- jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +14 -7
- jararaca-0.4.0a5.dist-info/RECORD +88 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
- pyproject.toml +131 -0
- jararaca/messagebus/types.py +0 -30
- jararaca/scheduler/scheduler.py +0 -154
- jararaca/tools/metadata.py +0 -47
- jararaca-0.2.37a12.dist-info/RECORD +0 -63
- /jararaca-0.2.37a12.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.2.37a12.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from abc import ABC
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from typing import AsyncContextManager, AsyncGenerator, Iterable
|
|
8
|
+
|
|
9
|
+
from jararaca.scheduler.types import DelayedMessageData
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MessageBrokerBackend(ABC):
|
|
13
|
+
|
|
14
|
+
def lock(self) -> AsyncContextManager[None]:
|
|
15
|
+
"""
|
|
16
|
+
Acquire a lock for the message broker backend.
|
|
17
|
+
This is used to ensure that only one instance of the scheduler is running at a time.
|
|
18
|
+
"""
|
|
19
|
+
raise NotImplementedError(f"lock() is not implemented by {self.__class__}.")
|
|
20
|
+
|
|
21
|
+
async def get_last_dispatch_time(self, action_name: str) -> int | None:
|
|
22
|
+
"""
|
|
23
|
+
Get the last dispatch time of the scheduled action.
|
|
24
|
+
This is used to determine if the scheduled action should be executed again
|
|
25
|
+
or if it should be skipped.
|
|
26
|
+
"""
|
|
27
|
+
raise NotImplementedError(
|
|
28
|
+
f"get_last_dispatch_time() is not implemented by {self.__class__}."
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
async def set_last_dispatch_time(self, action_name: str, timestamp: int) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Set the last dispatch time of the scheduled action.
|
|
34
|
+
This is used to determine if the scheduled action should be executed again
|
|
35
|
+
or if it should be skipped.
|
|
36
|
+
"""
|
|
37
|
+
raise NotImplementedError(
|
|
38
|
+
f"set_last_dispatch_time() is not implemented by {self.__class__}."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
async def get_in_execution_count(self, action_name: str) -> int:
|
|
42
|
+
"""
|
|
43
|
+
Get the number of scheduled actions in execution.
|
|
44
|
+
This is used to determine if the scheduled action should be executed again
|
|
45
|
+
or if it should be skipped.
|
|
46
|
+
"""
|
|
47
|
+
raise NotImplementedError(
|
|
48
|
+
f"get_in_execution_count() is not implemented by {self.__class__}."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def in_execution(self, action_name: str) -> AsyncContextManager[None]:
|
|
52
|
+
"""
|
|
53
|
+
Acquire a lock for the scheduled action.
|
|
54
|
+
This is used to ensure that only one instance of the scheduled action is running at a time.
|
|
55
|
+
"""
|
|
56
|
+
raise NotImplementedError(
|
|
57
|
+
f"in_execution() is not implemented by {self.__class__}."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
async def dequeue_next_delayed_messages(
|
|
61
|
+
self, start_timestamp: int
|
|
62
|
+
) -> Iterable[DelayedMessageData]:
|
|
63
|
+
"""
|
|
64
|
+
Dequeue the next delayed messages from the message broker.
|
|
65
|
+
This is used to trigger the scheduled action.
|
|
66
|
+
"""
|
|
67
|
+
raise NotImplementedError(
|
|
68
|
+
f"dequeue_next_delayed_messages() is not implemented by {self.__class__}."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def enqueue_delayed_message(
|
|
72
|
+
self, delayed_message: DelayedMessageData
|
|
73
|
+
) -> None:
|
|
74
|
+
"""
|
|
75
|
+
Enqueue a delayed message to the message broker.
|
|
76
|
+
This is used to trigger the scheduled action.
|
|
77
|
+
"""
|
|
78
|
+
raise NotImplementedError(
|
|
79
|
+
f"enqueue_delayed_message() is not implemented by {self.__class__}."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
async def dispose(self) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Dispose of the message broker backend.
|
|
85
|
+
This is used to clean up resources used by the message broker backend.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class NullBackend(MessageBrokerBackend):
|
|
90
|
+
"""
|
|
91
|
+
A null backend that does nothing.
|
|
92
|
+
This is used for testing purposes.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
@asynccontextmanager
|
|
96
|
+
async def lock(self) -> AsyncGenerator[None, None]:
|
|
97
|
+
yield
|
|
98
|
+
|
|
99
|
+
async def get_last_dispatch_time(self, action_name: str) -> int:
|
|
100
|
+
return 0
|
|
101
|
+
|
|
102
|
+
async def set_last_dispatch_time(self, action_name: str, timestamp: int) -> None:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
async def dispose(self) -> None:
|
|
106
|
+
pass
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from jararaca.broker_backend import MessageBrokerBackend
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_message_broker_backend_from_url(url: str) -> MessageBrokerBackend:
|
|
9
|
+
"""
|
|
10
|
+
Factory function to create a message broker backend instance from a URL.
|
|
11
|
+
Currently, only Redis is supported.
|
|
12
|
+
"""
|
|
13
|
+
if (
|
|
14
|
+
url.startswith("redis://")
|
|
15
|
+
or url.startswith("rediss://")
|
|
16
|
+
or url.startswith("redis-socket://")
|
|
17
|
+
or url.startswith("rediss+socket://")
|
|
18
|
+
):
|
|
19
|
+
from jararaca.broker_backend.redis_broker_backend import (
|
|
20
|
+
RedisMessageBrokerBackend,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
return RedisMessageBrokerBackend(url)
|
|
24
|
+
else:
|
|
25
|
+
raise ValueError(f"Unsupported message broker backend URL: {url}")
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from typing import AsyncGenerator, Iterable
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
import redis.asyncio
|
|
12
|
+
|
|
13
|
+
from jararaca.broker_backend import MessageBrokerBackend
|
|
14
|
+
from jararaca.scheduler.types import DelayedMessageData
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RedisMessageBrokerBackend(MessageBrokerBackend):
|
|
20
|
+
def __init__(self, url: str) -> None:
|
|
21
|
+
self.redis = redis.asyncio.Redis.from_url(url)
|
|
22
|
+
self.last_dispatch_time_key = "last_dispatch_time:{action_name}"
|
|
23
|
+
self.last_execution_time_key = "last_execution_time:{action_name}"
|
|
24
|
+
self.execution_indicator_key = "in_execution:{action_name}:{timestamp}"
|
|
25
|
+
self.execution_indicator_expiration = 60 * 5
|
|
26
|
+
self.delayed_messages_key = "delayed_messages"
|
|
27
|
+
self.delayed_messages_metadata_key = "delayed_messages_metadata:{task_id}"
|
|
28
|
+
|
|
29
|
+
@asynccontextmanager
|
|
30
|
+
async def lock(self) -> AsyncGenerator[None, None]:
|
|
31
|
+
yield
|
|
32
|
+
|
|
33
|
+
async def get_last_dispatch_time(self, action_name: str) -> int | None:
|
|
34
|
+
|
|
35
|
+
key = self.last_dispatch_time_key.format(action_name=action_name)
|
|
36
|
+
last_execution_time = await self.redis.get(key)
|
|
37
|
+
if last_execution_time is None:
|
|
38
|
+
return None
|
|
39
|
+
return int(last_execution_time)
|
|
40
|
+
|
|
41
|
+
async def set_last_dispatch_time(self, action_name: str, timestamp: int) -> None:
|
|
42
|
+
key = self.last_dispatch_time_key.format(action_name=action_name)
|
|
43
|
+
await self.redis.set(key, timestamp)
|
|
44
|
+
|
|
45
|
+
async def get_last_execution_time(self, action_name: str) -> int | None:
|
|
46
|
+
key = self.last_execution_time_key.format(action_name=action_name)
|
|
47
|
+
last_execution_time = await self.redis.get(key)
|
|
48
|
+
if last_execution_time is None:
|
|
49
|
+
return None
|
|
50
|
+
return int(last_execution_time)
|
|
51
|
+
|
|
52
|
+
async def set_last_execution_time(self, action_name: str, timestamp: int) -> None:
|
|
53
|
+
key = self.last_execution_time_key.format(action_name=action_name)
|
|
54
|
+
await self.redis.set(key, timestamp)
|
|
55
|
+
|
|
56
|
+
async def get_in_execution_count(self, action_name: str) -> int:
|
|
57
|
+
key = self.execution_indicator_key.format(
|
|
58
|
+
action_name=action_name, timestamp="*"
|
|
59
|
+
)
|
|
60
|
+
in_execution_count = await self.redis.keys(key)
|
|
61
|
+
if in_execution_count is None:
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
return len(in_execution_count)
|
|
65
|
+
|
|
66
|
+
@asynccontextmanager
|
|
67
|
+
async def in_execution(self, action_name: str) -> AsyncGenerator[None, None]:
|
|
68
|
+
"""
|
|
69
|
+
Acquire a lock for the scheduled action.
|
|
70
|
+
This is used to ensure that only one instance of the scheduled action is running at a time.
|
|
71
|
+
"""
|
|
72
|
+
key = self.execution_indicator_key.format(
|
|
73
|
+
action_name=action_name, timestamp=int(time.time())
|
|
74
|
+
)
|
|
75
|
+
await self.redis.set(key, 1, ex=self.execution_indicator_expiration)
|
|
76
|
+
try:
|
|
77
|
+
yield
|
|
78
|
+
finally:
|
|
79
|
+
await self.redis.delete(key)
|
|
80
|
+
|
|
81
|
+
async def enqueue_delayed_message(
|
|
82
|
+
self, delayed_message: DelayedMessageData
|
|
83
|
+
) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Enqueue a delayed message to the message broker.
|
|
86
|
+
This is used to trigger the scheduled action.
|
|
87
|
+
"""
|
|
88
|
+
task_id = str(uuid4())
|
|
89
|
+
async with self.redis.pipeline() as pipe:
|
|
90
|
+
pipe.set(
|
|
91
|
+
self.delayed_messages_metadata_key.format(task_id=task_id),
|
|
92
|
+
delayed_message.model_dump_json().encode(),
|
|
93
|
+
)
|
|
94
|
+
pipe.zadd(
|
|
95
|
+
self.delayed_messages_key,
|
|
96
|
+
{task_id: delayed_message.dispatch_time},
|
|
97
|
+
nx=True,
|
|
98
|
+
)
|
|
99
|
+
await pipe.execute()
|
|
100
|
+
|
|
101
|
+
async def dequeue_next_delayed_messages(
|
|
102
|
+
self, start_timestamp: int
|
|
103
|
+
) -> Iterable[DelayedMessageData]:
|
|
104
|
+
"""
|
|
105
|
+
Dequeue the next delayed messages from the message broker.
|
|
106
|
+
This is used to trigger the scheduled action.
|
|
107
|
+
"""
|
|
108
|
+
tasks_ids = await self.redis.zrangebyscore(
|
|
109
|
+
name=self.delayed_messages_key,
|
|
110
|
+
max=start_timestamp,
|
|
111
|
+
min="-inf",
|
|
112
|
+
withscores=False,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if not tasks_ids:
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
tasks_bytes_data: list[bytes] = []
|
|
119
|
+
|
|
120
|
+
for task_id_bytes in tasks_ids:
|
|
121
|
+
metadata = await self.redis.get(
|
|
122
|
+
self.delayed_messages_metadata_key.format(
|
|
123
|
+
task_id=task_id_bytes.decode()
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
if metadata is None:
|
|
127
|
+
logger.warning(
|
|
128
|
+
f"Delayed message metadata not found for task_id: {task_id_bytes.decode()}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
tasks_bytes_data.append(metadata)
|
|
134
|
+
|
|
135
|
+
async with self.redis.pipeline() as pipe:
|
|
136
|
+
for task_id_bytes in tasks_ids:
|
|
137
|
+
pipe.zrem(self.delayed_messages_key, task_id_bytes.decode())
|
|
138
|
+
pipe.delete(
|
|
139
|
+
self.delayed_messages_metadata_key.format(
|
|
140
|
+
task_id=task_id_bytes.decode()
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
await pipe.execute()
|
|
144
|
+
|
|
145
|
+
delayed_messages: list[DelayedMessageData] = []
|
|
146
|
+
|
|
147
|
+
for task_bytes_data in tasks_bytes_data:
|
|
148
|
+
try:
|
|
149
|
+
delayed_message = DelayedMessageData.model_validate_json(
|
|
150
|
+
task_bytes_data.decode()
|
|
151
|
+
)
|
|
152
|
+
delayed_messages.append(delayed_message)
|
|
153
|
+
except Exception:
|
|
154
|
+
if logger.isEnabledFor(logging.ERROR):
|
|
155
|
+
logger.error(
|
|
156
|
+
"Error parsing delayed message: %s",
|
|
157
|
+
task_bytes_data.decode(),
|
|
158
|
+
)
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
return delayed_messages
|
|
162
|
+
|
|
163
|
+
async def dispose(self) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Dispose of the message broker backend.
|
|
166
|
+
This is used to close the connection to the message broker.
|
|
167
|
+
"""
|
|
168
|
+
await self.redis.close()
|