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.
Files changed (96) hide show
  1. README.md +121 -0
  2. jararaca/__init__.py +189 -17
  3. jararaca/__main__.py +4 -0
  4. jararaca/broker_backend/__init__.py +4 -0
  5. jararaca/broker_backend/mapper.py +4 -0
  6. jararaca/broker_backend/redis_broker_backend.py +9 -3
  7. jararaca/cli.py +915 -51
  8. jararaca/common/__init__.py +3 -0
  9. jararaca/core/__init__.py +3 -0
  10. jararaca/core/providers.py +8 -0
  11. jararaca/core/uow.py +41 -7
  12. jararaca/di.py +4 -0
  13. jararaca/files/entity.py.mako +4 -0
  14. jararaca/helpers/__init__.py +3 -0
  15. jararaca/helpers/global_scheduler/__init__.py +3 -0
  16. jararaca/helpers/global_scheduler/config.py +21 -0
  17. jararaca/helpers/global_scheduler/controller.py +42 -0
  18. jararaca/helpers/global_scheduler/registry.py +32 -0
  19. jararaca/lifecycle.py +6 -2
  20. jararaca/messagebus/__init__.py +4 -0
  21. jararaca/messagebus/bus_message_controller.py +4 -0
  22. jararaca/messagebus/consumers/__init__.py +3 -0
  23. jararaca/messagebus/decorators.py +121 -61
  24. jararaca/messagebus/implicit_headers.py +49 -0
  25. jararaca/messagebus/interceptors/__init__.py +3 -0
  26. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +62 -11
  27. jararaca/messagebus/interceptors/message_publisher_collector.py +62 -0
  28. jararaca/messagebus/interceptors/publisher_interceptor.py +29 -3
  29. jararaca/messagebus/message.py +4 -0
  30. jararaca/messagebus/publisher.py +6 -0
  31. jararaca/messagebus/worker.py +1002 -459
  32. jararaca/microservice.py +113 -2
  33. jararaca/observability/constants.py +7 -0
  34. jararaca/observability/decorators.py +170 -13
  35. jararaca/observability/fastapi_exception_handler.py +37 -0
  36. jararaca/observability/hooks.py +109 -0
  37. jararaca/observability/interceptor.py +4 -0
  38. jararaca/observability/providers/__init__.py +3 -0
  39. jararaca/observability/providers/otel.py +225 -16
  40. jararaca/persistence/base.py +39 -3
  41. jararaca/persistence/exports.py +4 -0
  42. jararaca/persistence/interceptors/__init__.py +3 -0
  43. jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
  44. jararaca/persistence/interceptors/constants.py +5 -0
  45. jararaca/persistence/interceptors/decorators.py +50 -0
  46. jararaca/persistence/session.py +3 -0
  47. jararaca/persistence/sort_filter.py +4 -0
  48. jararaca/persistence/utilities.py +73 -20
  49. jararaca/presentation/__init__.py +3 -0
  50. jararaca/presentation/decorators.py +88 -86
  51. jararaca/presentation/exceptions.py +23 -0
  52. jararaca/presentation/hooks.py +4 -0
  53. jararaca/presentation/http_microservice.py +4 -0
  54. jararaca/presentation/server.py +97 -45
  55. jararaca/presentation/websocket/__init__.py +3 -0
  56. jararaca/presentation/websocket/base_types.py +4 -0
  57. jararaca/presentation/websocket/context.py +4 -0
  58. jararaca/presentation/websocket/decorators.py +8 -41
  59. jararaca/presentation/websocket/redis.py +280 -53
  60. jararaca/presentation/websocket/types.py +4 -0
  61. jararaca/presentation/websocket/websocket_interceptor.py +46 -19
  62. jararaca/reflect/__init__.py +3 -0
  63. jararaca/reflect/controller_inspect.py +16 -10
  64. jararaca/reflect/decorators.py +252 -0
  65. jararaca/reflect/helpers.py +18 -0
  66. jararaca/reflect/metadata.py +34 -25
  67. jararaca/rpc/__init__.py +3 -0
  68. jararaca/rpc/http/__init__.py +101 -0
  69. jararaca/rpc/http/backends/__init__.py +14 -0
  70. jararaca/rpc/http/backends/httpx.py +43 -9
  71. jararaca/rpc/http/backends/otel.py +4 -0
  72. jararaca/rpc/http/decorators.py +380 -115
  73. jararaca/rpc/http/httpx.py +3 -0
  74. jararaca/scheduler/__init__.py +3 -0
  75. jararaca/scheduler/beat_worker.py +521 -105
  76. jararaca/scheduler/decorators.py +15 -22
  77. jararaca/scheduler/types.py +4 -0
  78. jararaca/tools/app_config/__init__.py +3 -0
  79. jararaca/tools/app_config/decorators.py +7 -19
  80. jararaca/tools/app_config/interceptor.py +6 -2
  81. jararaca/tools/typescript/__init__.py +3 -0
  82. jararaca/tools/typescript/decorators.py +120 -0
  83. jararaca/tools/typescript/interface_parser.py +1077 -174
  84. jararaca/utils/__init__.py +3 -0
  85. jararaca/utils/env_parse_utils.py +133 -0
  86. jararaca/utils/rabbitmq_utils.py +112 -39
  87. jararaca/utils/retry.py +19 -14
  88. jararaca-0.4.0a19.dist-info/LICENSE +674 -0
  89. jararaca-0.4.0a19.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
  90. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/METADATA +12 -7
  91. jararaca-0.4.0a19.dist-info/RECORD +96 -0
  92. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/WHEEL +1 -1
  93. pyproject.toml +132 -0
  94. jararaca-0.3.11a16.dist-info/RECORD +0 -74
  95. /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
  96. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
jararaca/core/__init__.py CHANGED
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -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 provide_metadata
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 provide_metadata(app_context.controller_member_reflect.metadata):
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
- yield None
69
-
70
- for ctx in reversed(ctxs):
71
- await ctx.__aexit__(None, None, None)
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
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from jararaca.microservice import Container
2
6
 
3
7
  __all__ = [
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from datetime import datetime
2
6
  from typing import Annotated
3
7
  from uuid import UUID, uuid4
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -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.info("Initializing interceptors lifecycle")
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.info("Finalizing interceptors lifecycle")
64
+ logger.debug("Finalizing interceptors lifecycle")
61
65
  for ctx in lifecycle_ctxs:
62
66
  await ctx.__aexit__(None, None, None)
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from .message import MessageOf
2
6
 
3
7
  __all__ = ["MessageOf"]
@@ -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 contextmanager, suppress
2
6
  from contextvars import ContextVar
3
7
  from typing import Any, Generator, Protocol
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -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, Generic, TypeVar, cast
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
- DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
13
- DECORATED_T = TypeVar("DECORATED_T", bound=Any)
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
- MESSAGE_INCOMING_ATTR = "__message_incoming__"
39
+ class MessageHandler(GenericStackableDecorator[AcceptableHandler]):
19
40
 
20
41
  def __init__(
21
42
  self,
22
43
  message: type[INHERITS_MESSAGE_CO],
23
- timeout: int | None = None,
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 = False,
26
- auto_ack: bool = True,
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.requeue_on_exception = nack_on_exception
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
- self, func: Callable[[Any, MessageOf[INHERITS_MESSAGE_CO]], Awaitable[None]]
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
- return func
67
+ def pre_decorated(self, subject: FUNC_OR_TYPE_T) -> None:
68
+ MessageHandler.validate_decorated_fn(subject)
44
69
 
45
70
  @staticmethod
46
- def register(
47
- func: Callable[[Any, MessageOf[INHERITS_MESSAGE_CO]], Awaitable[None]],
48
- message_incoming: "MessageHandler[Any]",
49
- ) -> None:
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
- setattr(func, MessageHandler.MESSAGE_INCOMING_ATTR, message_incoming)
77
+ async def handler(self, message: MessageOf[YourMessageType]) -> None:
78
+ ...
52
79
 
53
- @staticmethod
54
- def get_message_incoming(
55
- func: Callable[[MessageOf[Any]], Awaitable[Any]],
56
- ) -> "MessageHandler[Message] | None":
57
- if not hasattr(func, MessageHandler.MESSAGE_INCOMING_ATTR):
58
- return None
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
- return cast(
61
- MessageHandler[Message], getattr(func, MessageHandler.MESSAGE_INCOMING_ATTR)
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[Message]
69
- instance_callable: Callable[[MessageOf[Any]], Awaitable[None]]
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__(self) -> None:
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
- [DECORATED_T], tuple[MESSAGE_HANDLER_DATA_SET, SCHEDULED_ACTION_DATA_SET]
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 __call__(self, cls_t: type[DECORATED_T]) -> type[DECORATED_T]:
176
+ def post_decorated(self, subject: FUNC_OR_TYPE_T) -> None:
103
177
 
104
178
  def messagebus_factory(
105
- instance: DECORATED_T,
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
- _, members = inspect_controller(cls_t)
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.get_message_incoming(
192
+ message_handler_decoration = MessageHandler.get_last(
115
193
  member.member_function
116
194
  )
117
- scheduled_action_decoration = ScheduledAction.get_scheduled_action(
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, cls_t.__module__, cls_t.__qualname__)
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, cls_t.__module__, cls_t.__qualname__)
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)
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later