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
jararaca/common/__init__.py
CHANGED
jararaca/core/__init__.py
CHANGED
jararaca/core/providers.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 dataclasses import dataclass
|
|
2
6
|
from typing import Any, Callable, Generic, Type, TypeVar
|
|
3
7
|
|
|
@@ -9,6 +13,10 @@ class Token(Generic[T]):
|
|
|
9
13
|
type_: Type[T]
|
|
10
14
|
name: str
|
|
11
15
|
|
|
16
|
+
@classmethod
|
|
17
|
+
def create(cls, type_: Type[T], name: str) -> "Token[T]":
|
|
18
|
+
return cls(type_=type_, name=name)
|
|
19
|
+
|
|
12
20
|
|
|
13
21
|
@dataclass
|
|
14
22
|
class ProviderSpec:
|
jararaca/core/uow.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 contextlib import asynccontextmanager
|
|
2
6
|
from typing import AsyncGenerator, Sequence
|
|
3
7
|
|
|
@@ -9,7 +13,7 @@ from jararaca.microservice import (
|
|
|
9
13
|
provide_app_context,
|
|
10
14
|
provide_container,
|
|
11
15
|
)
|
|
12
|
-
from jararaca.reflect.metadata import
|
|
16
|
+
from jararaca.reflect.metadata import start_transaction_metadata_context
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class ContainerInterceptor(AppInterceptor):
|
|
@@ -33,7 +37,6 @@ class UnitOfWorkContextProvider:
|
|
|
33
37
|
self.container = container
|
|
34
38
|
self.container_interceptor = ContainerInterceptor(container)
|
|
35
39
|
|
|
36
|
-
# TODO: Guarantee that the context is closed whenever an exception is raised
|
|
37
40
|
# TODO: Guarantee a unit of work workflow for the whole request, including all the interceptors
|
|
38
41
|
|
|
39
42
|
def factory_app_interceptors(self) -> Sequence[AppInterceptor]:
|
|
@@ -57,7 +60,9 @@ class UnitOfWorkContextProvider:
|
|
|
57
60
|
) -> AsyncGenerator[None, None]:
|
|
58
61
|
|
|
59
62
|
app_interceptors = self.factory_app_interceptors()
|
|
60
|
-
with
|
|
63
|
+
with start_transaction_metadata_context(
|
|
64
|
+
app_context.controller_member_reflect.metadata
|
|
65
|
+
):
|
|
61
66
|
ctxs = [self.container_interceptor.intercept(app_context)] + [
|
|
62
67
|
interceptor.intercept(app_context) for interceptor in app_interceptors
|
|
63
68
|
]
|
|
@@ -65,7 +70,36 @@ class UnitOfWorkContextProvider:
|
|
|
65
70
|
for ctx in ctxs:
|
|
66
71
|
await ctx.__aenter__()
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
exc_type = None
|
|
74
|
+
exc_value = None
|
|
75
|
+
exc_traceback = None
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
yield None
|
|
79
|
+
except BaseException as e:
|
|
80
|
+
exc_type = type(e)
|
|
81
|
+
exc_value = e
|
|
82
|
+
exc_traceback = e.__traceback__
|
|
83
|
+
raise
|
|
84
|
+
finally:
|
|
85
|
+
# Exit interceptors in reverse order, propagating exception info
|
|
86
|
+
for ctx in reversed(ctxs):
|
|
87
|
+
try:
|
|
88
|
+
suppressed = await ctx.__aexit__(
|
|
89
|
+
exc_type, exc_value, exc_traceback
|
|
90
|
+
)
|
|
91
|
+
# If an interceptor returns True, it suppresses the exception
|
|
92
|
+
if suppressed and exc_type is not None:
|
|
93
|
+
exc_type = None
|
|
94
|
+
exc_value = None
|
|
95
|
+
exc_traceback = None
|
|
96
|
+
except BaseException as exit_exc:
|
|
97
|
+
# If an interceptor raises an exception during cleanup,
|
|
98
|
+
# replace the original exception with the new one
|
|
99
|
+
exc_type = type(exit_exc)
|
|
100
|
+
exc_value = exit_exc
|
|
101
|
+
exc_traceback = exit_exc.__traceback__
|
|
102
|
+
|
|
103
|
+
# Re-raise the exception if it wasn't suppressed
|
|
104
|
+
if exc_type is not None and exc_value is not None:
|
|
105
|
+
raise exc_value.with_traceback(exc_traceback)
|
jararaca/di.py
CHANGED
jararaca/files/entity.py.mako
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from jararaca.core.providers import Token
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GlobalSchedulerConfig(BaseModel):
|
|
13
|
+
MAX_CONCURRENT_JOBS: int = 10
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
GlobalSchedulerConfigToken = Token.create(
|
|
17
|
+
GlobalSchedulerConfig, "GlobalSchedulerConfig"
|
|
18
|
+
)
|
|
19
|
+
GlobalSchedulerConfigAnnotated = Annotated[
|
|
20
|
+
GlobalSchedulerConfig, GlobalSchedulerConfigToken
|
|
21
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from jararaca.helpers.global_scheduler.config import GlobalSchedulerConfigAnnotated
|
|
8
|
+
from jararaca.helpers.global_scheduler.registry import (
|
|
9
|
+
GlobalScheduleFnType,
|
|
10
|
+
GlobalSchedulerRegistry,
|
|
11
|
+
)
|
|
12
|
+
from jararaca.messagebus.decorators import MessageBusController
|
|
13
|
+
from jararaca.scheduler.decorators import ScheduledAction
|
|
14
|
+
from jararaca.utils.env_parse_utils import get_env_str
|
|
15
|
+
|
|
16
|
+
SCHEDULER_CRON = get_env_str("SCHEDULER_CRON", "*/5 * * * *")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@MessageBusController()
|
|
20
|
+
class GlobalSchedulerController:
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: GlobalSchedulerConfigAnnotated):
|
|
23
|
+
self._config = config
|
|
24
|
+
|
|
25
|
+
@ScheduledAction(cron=SCHEDULER_CRON)
|
|
26
|
+
async def trigger_scheduled_actions(self) -> None:
|
|
27
|
+
"""Trigger all registered scheduled actions."""
|
|
28
|
+
|
|
29
|
+
taks = []
|
|
30
|
+
|
|
31
|
+
semaphore = asyncio.Semaphore(self._config.MAX_CONCURRENT_JOBS)
|
|
32
|
+
|
|
33
|
+
for action in GlobalSchedulerRegistry.get_registered_actions():
|
|
34
|
+
task = asyncio.create_task(self._run_with_semaphore(semaphore, action))
|
|
35
|
+
|
|
36
|
+
taks.append(task)
|
|
37
|
+
|
|
38
|
+
async def _run_with_semaphore(
|
|
39
|
+
self, semaphore: asyncio.Semaphore, action: GlobalScheduleFnType
|
|
40
|
+
) -> None:
|
|
41
|
+
async with semaphore:
|
|
42
|
+
await action()
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from typing import Any, Awaitable, Callable
|
|
7
|
+
|
|
8
|
+
GlobalScheduleFnType = Callable[[], Awaitable[Any]]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GlobalSchedulerRegistry:
|
|
12
|
+
"""Registry for the Global Scheduler helper."""
|
|
13
|
+
|
|
14
|
+
_REGISTRY: list[GlobalScheduleFnType] = []
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def register(cls, fn: GlobalScheduleFnType) -> None:
|
|
18
|
+
"""Register a scheduled action function.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
fn: The scheduled action function to register.
|
|
22
|
+
"""
|
|
23
|
+
cls._REGISTRY.append(fn)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def get_registered_actions(cls) -> list[GlobalScheduleFnType]:
|
|
27
|
+
"""Get the list of registered scheduled action functions.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A list of registered scheduled action functions.
|
|
31
|
+
"""
|
|
32
|
+
return cls._REGISTRY
|
jararaca/lifecycle.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
|
import logging
|
|
2
6
|
from contextlib import asynccontextmanager
|
|
3
7
|
from typing import AsyncContextManager, AsyncGenerator, Sequence
|
|
@@ -33,7 +37,7 @@ class AppLifecycle:
|
|
|
33
37
|
self.container.fill_providers(False)
|
|
34
38
|
lifecycle_ctxs: list[AsyncContextManager[None]] = []
|
|
35
39
|
|
|
36
|
-
logger.
|
|
40
|
+
logger.debug("Initializing interceptors lifecycle")
|
|
37
41
|
for interceptor_dep in self.app.interceptors:
|
|
38
42
|
interceptor: AppInterceptor
|
|
39
43
|
if not isinstance(interceptor_dep, AppInterceptor):
|
|
@@ -57,6 +61,6 @@ class AppLifecycle:
|
|
|
57
61
|
|
|
58
62
|
yield
|
|
59
63
|
|
|
60
|
-
logger.
|
|
64
|
+
logger.debug("Finalizing interceptors lifecycle")
|
|
61
65
|
for ctx in lifecycle_ctxs:
|
|
62
66
|
await ctx.__aexit__(None, None, None)
|
jararaca/messagebus/__init__.py
CHANGED
|
@@ -1,72 +1,140 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
|
|
1
6
|
import inspect
|
|
2
7
|
from dataclasses import dataclass
|
|
3
|
-
from typing import Any, Awaitable, Callable,
|
|
8
|
+
from typing import Any, Awaitable, Callable, Literal, TypeVar, cast, get_args
|
|
4
9
|
|
|
5
10
|
from jararaca.messagebus.message import INHERITS_MESSAGE_CO, Message, MessageOf
|
|
6
11
|
from jararaca.reflect.controller_inspect import (
|
|
7
12
|
ControllerMemberReflect,
|
|
8
13
|
inspect_controller,
|
|
9
14
|
)
|
|
15
|
+
from jararaca.reflect.decorators import (
|
|
16
|
+
FUNC_OR_TYPE_T,
|
|
17
|
+
GenericStackableDecorator,
|
|
18
|
+
StackableDecorator,
|
|
19
|
+
)
|
|
20
|
+
from jararaca.reflect.helpers import is_generic_alias
|
|
10
21
|
from jararaca.scheduler.decorators import ScheduledAction, ScheduledActionData
|
|
22
|
+
from jararaca.utils.env_parse_utils import get_env_float, get_env_int, is_env_truffy
|
|
23
|
+
from jararaca.utils.retry import RetryPolicy
|
|
11
24
|
|
|
12
|
-
|
|
13
|
-
|
|
25
|
+
AcceptableHandler = (
|
|
26
|
+
Callable[[Any, MessageOf[Any]], Awaitable[None]]
|
|
27
|
+
| Callable[[Any, Any], Awaitable[None]]
|
|
28
|
+
)
|
|
29
|
+
MessageHandlerT = TypeVar("MessageHandlerT", bound=AcceptableHandler)
|
|
14
30
|
|
|
31
|
+
DEFAULT_TIMEOUT = get_env_int("JARARACA_MESSAGEBUS_HANDLER_TIMEOUT")
|
|
32
|
+
DEFAULT_NACK_ON_EXCEPTION = is_env_truffy("JARARACA_MESSAGEBUS_NACK_ON_EXCEPTION")
|
|
33
|
+
DEFAULT_AUTO_ACK = is_env_truffy("JARARACA_MESSAGEBUS_AUTO_ACK")
|
|
34
|
+
DEFAULT_NACK_DELAY_ON_EXCEPTION = get_env_float(
|
|
35
|
+
"JARARACA_MESSAGEBUS_NACK_DELAY_ON_EXCEPTION"
|
|
36
|
+
)
|
|
15
37
|
|
|
16
|
-
class MessageHandler(Generic[INHERITS_MESSAGE_CO]):
|
|
17
38
|
|
|
18
|
-
|
|
39
|
+
class MessageHandler(GenericStackableDecorator[AcceptableHandler]):
|
|
19
40
|
|
|
20
41
|
def __init__(
|
|
21
42
|
self,
|
|
22
43
|
message: type[INHERITS_MESSAGE_CO],
|
|
23
|
-
|
|
44
|
+
*,
|
|
45
|
+
timeout: int | None = DEFAULT_TIMEOUT if DEFAULT_TIMEOUT is not False else None,
|
|
24
46
|
exception_handler: Callable[[BaseException], None] | None = None,
|
|
25
|
-
nack_on_exception: bool =
|
|
26
|
-
|
|
47
|
+
nack_on_exception: bool = DEFAULT_NACK_ON_EXCEPTION,
|
|
48
|
+
nack_delay_on_exception: float = DEFAULT_NACK_DELAY_ON_EXCEPTION or 5.0,
|
|
49
|
+
auto_ack: bool = DEFAULT_AUTO_ACK,
|
|
27
50
|
name: str | None = None,
|
|
51
|
+
retry_config: RetryPolicy | None = None,
|
|
28
52
|
) -> None:
|
|
29
53
|
self.message_type = message
|
|
30
54
|
|
|
31
55
|
self.timeout = timeout
|
|
32
56
|
self.exception_handler = exception_handler
|
|
33
|
-
self.
|
|
57
|
+
self.nack_on_exception = nack_on_exception
|
|
58
|
+
self.nack_delay_on_exception = nack_delay_on_exception
|
|
59
|
+
|
|
34
60
|
self.auto_ack = auto_ack
|
|
35
61
|
self.name = name
|
|
62
|
+
self.retry_config = retry_config
|
|
36
63
|
|
|
37
|
-
def __call__(
|
|
38
|
-
|
|
39
|
-
) -> Callable[[Any, MessageOf[INHERITS_MESSAGE_CO]], Awaitable[None]]:
|
|
40
|
-
|
|
41
|
-
MessageHandler[Any].register(func, self)
|
|
64
|
+
def __call__(self, subject: MessageHandlerT) -> MessageHandlerT:
|
|
65
|
+
return cast(MessageHandlerT, super().__call__(subject))
|
|
42
66
|
|
|
43
|
-
|
|
67
|
+
def pre_decorated(self, subject: FUNC_OR_TYPE_T) -> None:
|
|
68
|
+
MessageHandler.validate_decorated_fn(subject)
|
|
44
69
|
|
|
45
70
|
@staticmethod
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
def validate_decorated_fn(
|
|
72
|
+
subject: FUNC_OR_TYPE_T,
|
|
73
|
+
) -> tuple[Literal["WRAPPED", "DIRECT"], type[Message]]:
|
|
74
|
+
"""Validates that the decorated function has the correct signature
|
|
75
|
+
the decorated must follow one of the patterns:
|
|
50
76
|
|
|
51
|
-
|
|
77
|
+
async def handler(self, message: MessageOf[YourMessageType]) -> None:
|
|
78
|
+
...
|
|
52
79
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
async def handler(self, message: YourMessageType) -> None:
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
if not inspect.iscoroutinefunction(subject):
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
"Message handler '%s' must be a coroutine function"
|
|
88
|
+
% (subject.__qualname__)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
signature = inspect.signature(subject)
|
|
92
|
+
|
|
93
|
+
parameters = list(signature.parameters.values())
|
|
94
|
+
|
|
95
|
+
if len(parameters) != 2:
|
|
96
|
+
raise RuntimeError(
|
|
97
|
+
"Message handler '%s' must have exactly two parameters (self, message)"
|
|
98
|
+
% (subject.__qualname__)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
message_param = parameters[1]
|
|
102
|
+
|
|
103
|
+
if message_param.annotation is inspect.Parameter.empty:
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
"Message handler '%s' must have type annotation for the message parameter"
|
|
106
|
+
% (subject.__qualname__)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
annotation_type = message_param.annotation
|
|
110
|
+
mode: Literal["WRAPPED", "DIRECT"]
|
|
111
|
+
if is_generic_alias(annotation_type):
|
|
112
|
+
|
|
113
|
+
message_model_type = get_args(annotation_type)[0]
|
|
59
114
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
115
|
+
mode = "WRAPPED"
|
|
116
|
+
|
|
117
|
+
else:
|
|
118
|
+
message_model_type = annotation_type
|
|
119
|
+
|
|
120
|
+
mode = "DIRECT"
|
|
121
|
+
|
|
122
|
+
if not inspect.isclass(message_model_type) or not issubclass(
|
|
123
|
+
message_model_type, Message
|
|
124
|
+
):
|
|
125
|
+
raise RuntimeError(
|
|
126
|
+
"Message handler '%s' message parameter must be of type 'MessageOf[YourMessageType]' or 'YourMessageType' where 'YourMessageType' is a subclass of 'Message'"
|
|
127
|
+
% (subject.__qualname__)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return mode, message_model_type
|
|
63
131
|
|
|
64
132
|
|
|
65
133
|
@dataclass(frozen=True)
|
|
66
134
|
class MessageHandlerData:
|
|
67
135
|
message_type: type[Any]
|
|
68
|
-
spec: MessageHandler
|
|
69
|
-
instance_callable: Callable[
|
|
136
|
+
spec: MessageHandler
|
|
137
|
+
instance_callable: Callable[..., Awaitable[None]]
|
|
70
138
|
controller_member: ControllerMemberReflect
|
|
71
139
|
|
|
72
140
|
|
|
@@ -80,41 +148,51 @@ SCHEDULED_ACTION_DATA_SET = set[ScheduledActionData]
|
|
|
80
148
|
MESSAGE_HANDLER_DATA_SET = set[MessageHandlerData]
|
|
81
149
|
|
|
82
150
|
|
|
83
|
-
class MessageBusController:
|
|
84
|
-
|
|
85
|
-
MESSAGEBUS_ATTR = "__messagebus__"
|
|
151
|
+
class MessageBusController(StackableDecorator):
|
|
86
152
|
|
|
87
|
-
def __init__(
|
|
153
|
+
def __init__(
|
|
154
|
+
self,
|
|
155
|
+
*,
|
|
156
|
+
inherit_class_decorators: bool = True,
|
|
157
|
+
inherit_methods_decorators: bool = True,
|
|
158
|
+
) -> None:
|
|
88
159
|
self.messagebus_factory: (
|
|
89
160
|
Callable[[Any], tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]]
|
|
90
161
|
| None
|
|
91
162
|
) = None
|
|
92
163
|
|
|
164
|
+
self.inherit_class_decorators = inherit_class_decorators
|
|
165
|
+
self.inherit_methods_decorators = inherit_methods_decorators
|
|
166
|
+
|
|
93
167
|
def get_messagebus_factory(
|
|
94
168
|
self,
|
|
95
169
|
) -> Callable[
|
|
96
|
-
[
|
|
170
|
+
[FUNC_OR_TYPE_T], tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]
|
|
97
171
|
]:
|
|
98
172
|
if self.messagebus_factory is None:
|
|
99
173
|
raise Exception("MessageBus factory is not set")
|
|
100
174
|
return self.messagebus_factory
|
|
101
175
|
|
|
102
|
-
def
|
|
176
|
+
def post_decorated(self, subject: FUNC_OR_TYPE_T) -> None:
|
|
103
177
|
|
|
104
178
|
def messagebus_factory(
|
|
105
|
-
instance:
|
|
179
|
+
instance: FUNC_OR_TYPE_T,
|
|
106
180
|
) -> tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]:
|
|
107
181
|
handlers: MESSAGE_HANDLER_DATA_SET = set()
|
|
108
182
|
|
|
109
183
|
schedulers: SCHEDULED_ACTION_DATA_SET = set()
|
|
110
184
|
|
|
111
|
-
|
|
185
|
+
assert inspect.isclass(
|
|
186
|
+
subject
|
|
187
|
+
), "MessageBusController can only be applied to classes"
|
|
188
|
+
|
|
189
|
+
_, members = inspect_controller(subject)
|
|
112
190
|
|
|
113
191
|
for name, member in members.items():
|
|
114
|
-
message_handler_decoration = MessageHandler.
|
|
192
|
+
message_handler_decoration = MessageHandler.get_last(
|
|
115
193
|
member.member_function
|
|
116
194
|
)
|
|
117
|
-
scheduled_action_decoration = ScheduledAction.
|
|
195
|
+
scheduled_action_decoration = ScheduledAction.get_last(
|
|
118
196
|
member.member_function
|
|
119
197
|
)
|
|
120
198
|
|
|
@@ -123,7 +201,7 @@ class MessageBusController:
|
|
|
123
201
|
if not inspect.iscoroutinefunction(member.member_function):
|
|
124
202
|
raise Exception(
|
|
125
203
|
"Message incoming handler '%s' from '%s.%s' must be a coroutine function"
|
|
126
|
-
% (name,
|
|
204
|
+
% (name, subject.__module__, subject.__qualname__)
|
|
127
205
|
)
|
|
128
206
|
|
|
129
207
|
handlers.add(
|
|
@@ -138,7 +216,7 @@ class MessageBusController:
|
|
|
138
216
|
if not inspect.iscoroutinefunction(member.member_function):
|
|
139
217
|
raise Exception(
|
|
140
218
|
"Scheduled action handler '%s' from '%s.%s' must be a coroutine function"
|
|
141
|
-
% (name,
|
|
219
|
+
% (name, subject.__module__, subject.__qualname__)
|
|
142
220
|
)
|
|
143
221
|
instance_callable = getattr(instance, name)
|
|
144
222
|
|
|
@@ -153,21 +231,3 @@ class MessageBusController:
|
|
|
153
231
|
return handlers, schedulers
|
|
154
232
|
|
|
155
233
|
self.messagebus_factory = messagebus_factory
|
|
156
|
-
|
|
157
|
-
MessageBusController.register(cls_t, self)
|
|
158
|
-
|
|
159
|
-
return cls_t
|
|
160
|
-
|
|
161
|
-
@staticmethod
|
|
162
|
-
def register(func: type[DECORATED_T], messagebus: "MessageBusController") -> None:
|
|
163
|
-
|
|
164
|
-
setattr(func, MessageBusController.MESSAGEBUS_ATTR, messagebus)
|
|
165
|
-
|
|
166
|
-
@staticmethod
|
|
167
|
-
def get_messagebus(func: type[DECORATED_T]) -> "MessageBusController | None":
|
|
168
|
-
if not hasattr(func, MessageBusController.MESSAGEBUS_ATTR):
|
|
169
|
-
return None
|
|
170
|
-
|
|
171
|
-
return cast(
|
|
172
|
-
MessageBusController, getattr(func, MessageBusController.MESSAGEBUS_ATTR)
|
|
173
|
-
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import decimal
|
|
7
|
+
import typing
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from typing import Any, Dict, Generator
|
|
11
|
+
|
|
12
|
+
FieldArray = list["FieldValue"]
|
|
13
|
+
"""A data structure for holding an array of field values."""
|
|
14
|
+
|
|
15
|
+
FieldTable = typing.Dict[str, "FieldValue"]
|
|
16
|
+
FieldValue = (
|
|
17
|
+
bool
|
|
18
|
+
| bytes
|
|
19
|
+
| bytearray
|
|
20
|
+
| decimal.Decimal
|
|
21
|
+
| FieldArray
|
|
22
|
+
| FieldTable
|
|
23
|
+
| float
|
|
24
|
+
| int
|
|
25
|
+
| None
|
|
26
|
+
| str
|
|
27
|
+
| datetime.datetime
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
ImplicitHeaders = Dict[str, FieldValue]
|
|
31
|
+
|
|
32
|
+
implicit_headers_ctx = ContextVar[ImplicitHeaders | None](
|
|
33
|
+
"implicit_headers_ctx", default=None
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def use_implicit_headers() -> ImplicitHeaders | None:
|
|
38
|
+
return implicit_headers_ctx.get()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@contextmanager
|
|
42
|
+
def provide_implicit_headers(
|
|
43
|
+
implicit_headers: ImplicitHeaders,
|
|
44
|
+
) -> Generator[None, Any, None]:
|
|
45
|
+
token = implicit_headers_ctx.set(implicit_headers)
|
|
46
|
+
try:
|
|
47
|
+
yield
|
|
48
|
+
finally:
|
|
49
|
+
implicit_headers_ctx.reset(token)
|