mersal 0.1.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.
Files changed (187) hide show
  1. mersal/__init__.py +1 -0
  2. mersal/_activation/builtin_handler_activator.py +102 -0
  3. mersal/_activation/handler_activator.py +78 -0
  4. mersal/activation/__init__.py +14 -0
  5. mersal/app.py +338 -0
  6. mersal/configuration/__init__.py +3 -0
  7. mersal/configuration/default_plugin.py +161 -0
  8. mersal/configuration/dependency_resolver.py +55 -0
  9. mersal/configuration/standard_configurator.py +67 -0
  10. mersal/exceptions/__init__.py +11 -0
  11. mersal/exceptions/base_exceptions.py +49 -0
  12. mersal/handlers/__init__.py +3 -0
  13. mersal/handlers/message_handler.py +6 -0
  14. mersal/idempotency/__init__.py +11 -0
  15. mersal/idempotency/config.py +25 -0
  16. mersal/idempotency/const.py +4 -0
  17. mersal/idempotency/idempotency_checker_step.py +38 -0
  18. mersal/idempotency/message_tracker.py +17 -0
  19. mersal/idempotency/plugin.py +35 -0
  20. mersal/lifespan/__init__.py +4 -0
  21. mersal/lifespan/autosubscribe/__init__.py +3 -0
  22. mersal/lifespan/autosubscribe/autosubscribe_plugin.py +54 -0
  23. mersal/lifespan/default_lifespan_handler.py +15 -0
  24. mersal/lifespan/lifespan_handler.py +15 -0
  25. mersal/lifespan/lifespan_hooks_registration_plugin.py +48 -0
  26. mersal/messages/__init__.py +7 -0
  27. mersal/messages/batch_message.py +8 -0
  28. mersal/messages/logical_message.py +18 -0
  29. mersal/messages/message_completed_event.py +9 -0
  30. mersal/messages/message_completed_event_publisher.py +31 -0
  31. mersal/messages/message_headers.py +27 -0
  32. mersal/messages/transport_message.py +18 -0
  33. mersal/outbox/__init__.py +12 -0
  34. mersal/outbox/config.py +13 -0
  35. mersal/outbox/in_memory/__init__.py +5 -0
  36. mersal/outbox/in_memory/in_memory_outbox_storage.py +44 -0
  37. mersal/outbox/outbox_forwarder.py +86 -0
  38. mersal/outbox/outbox_incoming_step.py +20 -0
  39. mersal/outbox/outbox_message.py +32 -0
  40. mersal/outbox/outbox_message_batch.py +33 -0
  41. mersal/outbox/outbox_storage.py +44 -0
  42. mersal/outbox/outbox_transport_decorator.py +54 -0
  43. mersal/outbox/plugin.py +77 -0
  44. mersal/persistence/in_memory/__init__.py +15 -0
  45. mersal/persistence/in_memory/in_memory_message_tracker.py +19 -0
  46. mersal/persistence/in_memory/in_memory_saga_storage.py +98 -0
  47. mersal/persistence/in_memory/in_memory_subscription_storage.py +58 -0
  48. mersal/persistence/not_implemented/__init__.py +3 -0
  49. mersal/persistence/not_implemented/not_implemented_subscription_storage.py +21 -0
  50. mersal/pipeline/__init__.py +42 -0
  51. mersal/pipeline/default_pipeline.py +41 -0
  52. mersal/pipeline/incoming_step.py +11 -0
  53. mersal/pipeline/incoming_step_context.py +20 -0
  54. mersal/pipeline/iterative_pipeline_invoker.py +44 -0
  55. mersal/pipeline/message_context.py +48 -0
  56. mersal/pipeline/outgoing_step.py +11 -0
  57. mersal/pipeline/outgoing_step_context.py +20 -0
  58. mersal/pipeline/pipeline.py +23 -0
  59. mersal/pipeline/pipeline_injector.py +68 -0
  60. mersal/pipeline/pipeline_invoker.py +10 -0
  61. mersal/pipeline/receive/activate_handlers_step.py +70 -0
  62. mersal/pipeline/receive/deserialize_incoming_message_step.py +21 -0
  63. mersal/pipeline/receive/dispatch_incoming_message_step.py +19 -0
  64. mersal/pipeline/receive/handler_invoker.py +32 -0
  65. mersal/pipeline/receive/handler_invokers.py +24 -0
  66. mersal/pipeline/receive/saga_handler_invoker.py +26 -0
  67. mersal/pipeline/recursive_pipeline_invoker.py +38 -0
  68. mersal/pipeline/send/destination_addresses.py +12 -0
  69. mersal/pipeline/send/flow_correlation_step.py +59 -0
  70. mersal/pipeline/send/send_outgoing_message_step.py +23 -0
  71. mersal/pipeline/send/serialize_outgoing_message_step.py +19 -0
  72. mersal/pipeline/send/set_default_headers_step.py +33 -0
  73. mersal/pipeline/step_context.py +27 -0
  74. mersal/plugins/__init__.py +7 -0
  75. mersal/plugins/generic_registration_plugin.py +13 -0
  76. mersal/plugins/plugin.py +9 -0
  77. mersal/py.typed +0 -0
  78. mersal/retry/__init__.py +27 -0
  79. mersal/retry/default_retry_step.py +91 -0
  80. mersal/retry/default_retry_strategy.py +29 -0
  81. mersal/retry/error_handling/deadletter_queue_error_handler.py +29 -0
  82. mersal/retry/error_handling/error_handler.py +15 -0
  83. mersal/retry/error_tracking/error_tracker.py +16 -0
  84. mersal/retry/error_tracking/in_memory_error_tracker.py +29 -0
  85. mersal/retry/fail_fast/default_fail_fast_checker.py +23 -0
  86. mersal/retry/fail_fast/fail_fast_checker.py +8 -0
  87. mersal/retry/retry_step.py +8 -0
  88. mersal/retry/retry_strategy.py +9 -0
  89. mersal/retry/retry_strategy_settings.py +9 -0
  90. mersal/routing/__init__.py +7 -0
  91. mersal/routing/default/__init__.py +9 -0
  92. mersal/routing/default/config.py +15 -0
  93. mersal/routing/default/default_router.py +32 -0
  94. mersal/routing/default/plugin.py +29 -0
  95. mersal/routing/router.py +9 -0
  96. mersal/sagas/__init__.py +13 -0
  97. mersal/sagas/config.py +17 -0
  98. mersal/sagas/correlation_error_handler.py +21 -0
  99. mersal/sagas/correlation_property.py +15 -0
  100. mersal/sagas/correlator.py +32 -0
  101. mersal/sagas/default_correlation_error_handler.py +29 -0
  102. mersal/sagas/load_saga_data_step.py +134 -0
  103. mersal/sagas/plugin.py +55 -0
  104. mersal/sagas/saga.py +61 -0
  105. mersal/sagas/saga_data.py +15 -0
  106. mersal/sagas/saga_storage.py +41 -0
  107. mersal/serialization/__init__.py +9 -0
  108. mersal/serialization/identity_serializer/__init__.py +3 -0
  109. mersal/serialization/identity_serializer/identity_object_serializer.py +13 -0
  110. mersal/serialization/message_serializer.py +17 -0
  111. mersal/serialization/serializers.py +13 -0
  112. mersal/subscription/__init__.py +3 -0
  113. mersal/subscription/subscription_storage.py +14 -0
  114. mersal/threading/__init__.py +13 -0
  115. mersal/threading/anyio/anyio_periodic_async_task.py +49 -0
  116. mersal/threading/anyio/anyio_periodic_async_task_factory.py +22 -0
  117. mersal/threading/periodic_async_task.py +9 -0
  118. mersal/threading/periodic_async_task_factory.py +11 -0
  119. mersal/topic/__init__.py +4 -0
  120. mersal/topic/default_topic_name_convention.py +6 -0
  121. mersal/topic/topic_name_convention.py +7 -0
  122. mersal/transport/__init__.py +19 -0
  123. mersal/transport/ambient_context.py +20 -0
  124. mersal/transport/base_transport.py +40 -0
  125. mersal/transport/default_transaction_context.py +158 -0
  126. mersal/transport/default_transaction_context_with_app.py +16 -0
  127. mersal/transport/in_memory/__init__.py +10 -0
  128. mersal/transport/in_memory/in_memory_network.py +32 -0
  129. mersal/transport/in_memory/in_memory_transport.py +60 -0
  130. mersal/transport/in_memory/in_memory_transport_plugin.py +49 -0
  131. mersal/transport/outgoing_message.py +15 -0
  132. mersal/transport/transaction_context.py +32 -0
  133. mersal/transport/transaction_scope.py +46 -0
  134. mersal/transport/transport.py +21 -0
  135. mersal/transport/transport_bridge.py +46 -0
  136. mersal/transport/transport_decorator_plugin.py +23 -0
  137. mersal/types/__init__.py +22 -0
  138. mersal/types/callable_types.py +13 -0
  139. mersal/types/helper_types.py +6 -0
  140. mersal/unit_of_work/__init__.py +17 -0
  141. mersal/unit_of_work/config.py +49 -0
  142. mersal/unit_of_work/plugin.py +37 -0
  143. mersal/unit_of_work/unit_of_work_step.py +86 -0
  144. mersal/utils/__init__.py +9 -0
  145. mersal/utils/predicates.py +28 -0
  146. mersal/utils/retrier.py +24 -0
  147. mersal/utils/singleton.py +17 -0
  148. mersal/utils/sync.py +73 -0
  149. mersal/workers/__init__.py +3 -0
  150. mersal/workers/anyio/__init__.py +4 -0
  151. mersal/workers/anyio/anyio_worker.py +117 -0
  152. mersal/workers/anyio/anyio_worker_factory.py +34 -0
  153. mersal/workers/worker.py +23 -0
  154. mersal/workers/worker_factory.py +16 -0
  155. mersal-0.1.0.dist-info/METADATA +13 -0
  156. mersal-0.1.0.dist-info/RECORD +187 -0
  157. mersal-0.1.0.dist-info/WHEEL +4 -0
  158. mersal_testing/_internal/conftest.py +70 -0
  159. mersal_testing/_internal/docker-compose.yml +17 -0
  160. mersal_testing/app_runner_helper.py +22 -0
  161. mersal_testing/counter.py +41 -0
  162. mersal_testing/message_handlers/__init__.py +9 -0
  163. mersal_testing/message_handlers/message_handler_that_counts.py +14 -0
  164. mersal_testing/message_handlers/message_handler_that_sends_local_messages.py +32 -0
  165. mersal_testing/message_handlers/message_handler_that_stores_the_message.py +15 -0
  166. mersal_testing/message_handlers/message_handler_that_throws.py +13 -0
  167. mersal_testing/message_handlers/null_message_handler.py +10 -0
  168. mersal_testing/messages.py +12 -0
  169. mersal_testing/pipeline/pipeline_invoker_tests.py +137 -0
  170. mersal_testing/py.typed +2 -0
  171. mersal_testing/retry/error_tracker_base_tests.py +89 -0
  172. mersal_testing/test_doubles/__init__.py +31 -0
  173. mersal_testing/test_doubles/messages/__init__.py +0 -0
  174. mersal_testing/test_doubles/messages/logical_message_builder.py +52 -0
  175. mersal_testing/test_doubles/messages/transport_message_builder.py +19 -0
  176. mersal_testing/test_doubles/outbox/outbox_storage_test_double.py +61 -0
  177. mersal_testing/test_doubles/retry/error_handler_spy.py +20 -0
  178. mersal_testing/test_doubles/retry/error_tracker_test_double.py +28 -0
  179. mersal_testing/test_doubles/serialization/serializer_test_double.py +21 -0
  180. mersal_testing/test_doubles/transport/outgoing_message_builder.py +10 -0
  181. mersal_testing/test_doubles/transport/transaction_context_test_double.py +49 -0
  182. mersal_testing/test_doubles/transport/transport_spy.py +26 -0
  183. mersal_testing/test_doubles/transport/transport_test_double.py +25 -0
  184. mersal_testing/test_doubles/unit_of_work/unit_of_work_test_helper.py +24 -0
  185. mersal_testing/testing_utils.py +11 -0
  186. mersal_testing/transport/basic_transport_tests.py +170 -0
  187. mersal_testing/transport/transport_decorator_helper.py +36 -0
mersal/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from typing import TYPE_CHECKING, TypeVar, cast
5
+
6
+ from mersal.pipeline import MessageContext
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Sequence
10
+
11
+ from mersal._activation.handler_activator import HandlerFactory
12
+ from mersal.app import Mersal
13
+ from mersal.handlers import MessageHandler
14
+ from mersal.transport import TransactionContext
15
+
16
+ # Define locally to avoid import cycle with handler_activator.py
17
+ MessageT = TypeVar("MessageT")
18
+
19
+ __all__ = ("BuiltinHandlerActivator",)
20
+
21
+
22
+ class BuiltinHandlerActivator:
23
+ """Default implementation of the handler activator pattern.
24
+
25
+ This class manages the registration and activation of message handlers
26
+ based on message types. It stores handler factories indexed by message type
27
+ and instantiates the appropriate handlers when a message needs to be processed.
28
+ """
29
+
30
+ def __init__(self) -> None:
31
+ """Initialize a new instance of the BuiltinHandlerActivator."""
32
+ self._handler_factories: dict[type, list[HandlerFactory]] = defaultdict(list)
33
+ self._app: Mersal | None = None
34
+
35
+ async def get_handlers(
36
+ self,
37
+ message: MessageT,
38
+ transaction_context: TransactionContext,
39
+ ) -> Sequence[MessageHandler[MessageT]]:
40
+ """Get handlers for the specified message.
41
+
42
+ Args:
43
+ message: The message to get handlers for
44
+ transaction_context: The current transaction context
45
+
46
+ Returns:
47
+ A sequence of message handlers that can process the message
48
+
49
+ Raises:
50
+ Exception: If called outside of a message context
51
+ """
52
+ message_context = MessageContext.current()
53
+ if not message_context:
54
+ raise Exception(
55
+ "BuiltinHandlerActivator get_handlers called outside of a transaction.",
56
+ )
57
+
58
+ return [x(message_context, self.app) for x in self._handler_factories.get(type(message), [])]
59
+
60
+ def register(
61
+ self,
62
+ message_type: type[MessageT],
63
+ factory: HandlerFactory[MessageT],
64
+ ) -> BuiltinHandlerActivator:
65
+ """Register a handler factory for a specific message type.
66
+
67
+ Args:
68
+ message_type: The type of message to register a handler for
69
+ factory: A factory function that creates a message handler
70
+
71
+ Returns:
72
+ The handler activator instance for method chaining
73
+ """
74
+ self._handler_factories[message_type].append(factory)
75
+ return self
76
+
77
+ @property
78
+ def registered_message_types(self) -> set[type]:
79
+ """Get the set of message types that have registered handlers.
80
+
81
+ Returns:
82
+ A set containing all registered message types
83
+ """
84
+ return set(self._handler_factories.keys())
85
+
86
+ @property
87
+ def app(self) -> Mersal:
88
+ """Get the associated application instance.
89
+
90
+ Returns:
91
+ The Mersal application instance
92
+ """
93
+ return cast("Mersal", self._app)
94
+
95
+ @app.setter
96
+ def app(self, value: Mersal) -> None:
97
+ """Set the associated application instance.
98
+
99
+ Args:
100
+ value: The Mersal application instance
101
+ """
102
+ self._app = value
@@ -0,0 +1,78 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Sequence
4
+ from typing import TYPE_CHECKING, Protocol, TypeAlias, TypeVar
5
+
6
+ from mersal.handlers import MessageHandler
7
+ from mersal.pipeline import MessageContext
8
+
9
+ # Define a type variable for messages
10
+ MessageT = TypeVar("MessageT")
11
+
12
+ # A factory function that creates message handlers
13
+ HandlerFactory: TypeAlias = Callable[
14
+ [MessageContext, "Mersal"],
15
+ MessageHandler[MessageT],
16
+ ]
17
+
18
+ if TYPE_CHECKING:
19
+ from mersal.app import Mersal
20
+ from mersal.transport import TransactionContext
21
+
22
+ __all__ = ("HandlerActivator",)
23
+
24
+
25
+ class HandlerActivator(Protocol):
26
+ """Protocol defining the interface for activating message handlers.
27
+
28
+ The HandlerActivator is responsible for registering message handlers
29
+ and returning the appropriate handlers for a given message type during
30
+ message processing.
31
+ """
32
+
33
+ async def get_handlers(
34
+ self,
35
+ message: MessageT,
36
+ transaction_context: TransactionContext,
37
+ ) -> Sequence[MessageHandler[MessageT]]:
38
+ """Retrieve handlers for the given message.
39
+
40
+ Args:
41
+ message: The message to get handlers for
42
+ transaction_context: The current transaction context
43
+
44
+ Returns:
45
+ A sequence of message handlers that can process the message
46
+ """
47
+ ...
48
+
49
+ def register(
50
+ self,
51
+ message_type: type[MessageT],
52
+ factory: HandlerFactory[MessageT],
53
+ ) -> HandlerActivator:
54
+ """Register a handler factory for a specific message type.
55
+
56
+ Args:
57
+ message_type: The type of message to register a handler for
58
+ factory: A factory function that creates a message handler
59
+
60
+ Returns:
61
+ The handler activator instance for method chaining
62
+ """
63
+ ...
64
+
65
+ @property
66
+ def registered_message_types(self) -> set[type]:
67
+ """Get the set of message types that have registered handlers.
68
+
69
+ Returns:
70
+ A set containing all registered message types
71
+ """
72
+ ...
73
+
74
+ @property
75
+ def app(self) -> Mersal: ...
76
+
77
+ @app.setter
78
+ def app(self, value: Mersal) -> None: ...
@@ -0,0 +1,14 @@
1
+ """Message handler activation module.
2
+
3
+ This module provides classes for registering and activating message handlers
4
+ based on message types. The handler activator pattern is core to how Mersal
5
+ dispatches messages to the appropriate handlers.
6
+ """
7
+
8
+ from mersal._activation.builtin_handler_activator import BuiltinHandlerActivator
9
+ from mersal._activation.handler_activator import HandlerActivator
10
+
11
+ __all__ = (
12
+ "BuiltinHandlerActivator",
13
+ "HandlerActivator",
14
+ )
mersal/app.py ADDED
@@ -0,0 +1,338 @@
1
+ import logging
2
+ from collections.abc import Mapping, Sequence
3
+ from contextlib import AsyncExitStack
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from typing_extensions import Self
7
+
8
+ from mersal.activation import HandlerActivator
9
+ from mersal.configuration.default_plugin import DefaultPlugin
10
+ from mersal.configuration.standard_configurator import (
11
+ InvalidConfigurationError,
12
+ StandardConfigurator,
13
+ )
14
+ from mersal.idempotency import IdempotencyConfig, IdempotencyPlugin
15
+ from mersal.lifespan import LifespanHandler
16
+ from mersal.lifespan.autosubscribe import AutosubscribeConfig
17
+ from mersal.messages import LogicalMessage, MessageHeaders
18
+ from mersal.outbox.config import OutboxConfig
19
+ from mersal.outbox.plugin import OutboxPlugin
20
+ from mersal.persistence.in_memory import InMemorySagaStorage
21
+ from mersal.pipeline import (
22
+ DestinationAddresses,
23
+ MessageIdGenerator,
24
+ OutgoingStepContext,
25
+ PipelineInvoker,
26
+ )
27
+ from mersal.plugins import Plugin, generic_registration_plugin
28
+ from mersal.retry import (
29
+ ErrorHandler,
30
+ ErrorTracker,
31
+ FailFastChecker,
32
+ RetryStrategySettings,
33
+ )
34
+ from mersal.retry.fail_fast.default_fail_fast_checker import (
35
+ DefaultFailFastCheckerExceptionsContainer,
36
+ )
37
+ from mersal.routing import Router
38
+ from mersal.routing.default import DefaultRouterRegistrationConfig
39
+ from mersal.routing.default.plugin import DefaultRouterRegistrationPlugin
40
+ from mersal.sagas import SagaConfig
41
+ from mersal.sagas.plugin import SagaPlugin
42
+ from mersal.serialization import (
43
+ MessageBodySerializer,
44
+ MessageHeadersSerializer,
45
+ Serializer,
46
+ )
47
+ from mersal.subscription import SubscriptionStorage
48
+ from mersal.topic import TopicNameConvention
49
+ from mersal.transport import (
50
+ AmbientContext,
51
+ DefaultTransactionContextWithOwningApp,
52
+ TransactionContext,
53
+ Transport,
54
+ )
55
+ from mersal.types import Empty, EmptyType, LifespanHook
56
+ from mersal.unit_of_work import UnitOfWorkConfig
57
+ from mersal.unit_of_work.plugin import UnitOfWorkPlugin
58
+ from mersal.utils.sync import AsyncCallable
59
+ from mersal.workers import WorkerFactory
60
+
61
+ if TYPE_CHECKING:
62
+ from mersal.workers.worker import Worker
63
+
64
+ __all__ = ("Mersal",)
65
+
66
+
67
+ class Mersal:
68
+ def __init__( # noqa: C901
69
+ self,
70
+ name: str,
71
+ handler_activator: HandlerActivator,
72
+ transport: Transport | None = None,
73
+ pipeline_invoker: PipelineInvoker | None = None,
74
+ router: Router | None = None,
75
+ worker_factory: WorkerFactory | None = None,
76
+ subscription_storage: SubscriptionStorage | None = None,
77
+ topic_name_convention: TopicNameConvention | None = None,
78
+ on_startup_hooks: Sequence[LifespanHook] | None = None,
79
+ on_shutdown_hooks: Sequence[LifespanHook] | None = None,
80
+ retry_strategy_settings: RetryStrategySettings | None = None,
81
+ error_tracker: ErrorTracker | None = None,
82
+ error_handler: ErrorHandler | None = None,
83
+ fail_fast_checker: FailFastChecker | None = None,
84
+ fail_fast_exceptions: Sequence[type[Exception]] | None = None,
85
+ plugins: Sequence[Plugin] | None = None,
86
+ idempotency: IdempotencyConfig | None = None,
87
+ saga: SagaConfig | EmptyType | None = None,
88
+ serializer: Serializer | None = None,
89
+ message_body_serializer: MessageBodySerializer | None = None,
90
+ message_headers_serializer: MessageHeadersSerializer | None = None,
91
+ default_router_registration: DefaultRouterRegistrationConfig | None = None,
92
+ autosubscribe: AutosubscribeConfig | EmptyType | None = None,
93
+ unit_of_work: UnitOfWorkConfig | None = None,
94
+ outbox: OutboxConfig | None = None,
95
+ pdb_on_exception: bool | None = None,
96
+ message_id_generator: MessageIdGenerator | None = None,
97
+ ):
98
+ if router and default_router_registration:
99
+ raise InvalidConfigurationError("Cannot provide both router and default_router_registration")
100
+
101
+ if fail_fast_checker and fail_fast_exceptions:
102
+ raise InvalidConfigurationError("Cannot provide both fail_fast_exceptions and fail_fast_checker")
103
+ self.name = name
104
+ self.handler_activator = handler_activator
105
+ self.configurator = StandardConfigurator()
106
+
107
+ plugins = list(plugins or [])
108
+ plugins.append(generic_registration_plugin(handler_activator, HandlerActivator))
109
+ if transport is not None:
110
+ plugins.append(generic_registration_plugin(transport, Transport))
111
+
112
+ if pipeline_invoker is not None:
113
+ plugins.append(generic_registration_plugin(pipeline_invoker, PipelineInvoker))
114
+ if router is not None:
115
+ plugins.append(generic_registration_plugin(router, Router))
116
+ if worker_factory is not None:
117
+ plugins.append(generic_registration_plugin(worker_factory, WorkerFactory))
118
+ if subscription_storage is not None:
119
+ plugins.append(generic_registration_plugin(subscription_storage, SubscriptionStorage))
120
+ if topic_name_convention is not None:
121
+ plugins.append(generic_registration_plugin(topic_name_convention, TopicNameConvention))
122
+ if retry_strategy_settings is not None:
123
+ plugins.append(generic_registration_plugin(retry_strategy_settings, RetryStrategySettings))
124
+ if error_tracker is not None:
125
+ plugins.append(generic_registration_plugin(error_tracker, ErrorTracker))
126
+ if error_handler is not None:
127
+ plugins.append(generic_registration_plugin(error_handler, ErrorHandler))
128
+ if fail_fast_checker is not None:
129
+ plugins.append(generic_registration_plugin(fail_fast_checker, FailFastChecker))
130
+ if fail_fast_exceptions is not None:
131
+ plugins.append(
132
+ generic_registration_plugin(
133
+ DefaultFailFastCheckerExceptionsContainer(exceptions=fail_fast_exceptions),
134
+ DefaultFailFastCheckerExceptionsContainer,
135
+ )
136
+ )
137
+
138
+ if serializer is not None:
139
+ plugins.append(generic_registration_plugin(serializer, Serializer))
140
+ if message_body_serializer is not None:
141
+ plugins.append(generic_registration_plugin(message_body_serializer, MessageBodySerializer))
142
+ if message_body_serializer is not None:
143
+ plugins.append(generic_registration_plugin(message_body_serializer, MessageHeadersSerializer))
144
+ if idempotency is not None:
145
+ plugins.append(IdempotencyPlugin(idempotency))
146
+
147
+ if saga is not None and saga is not Empty:
148
+ plugins.append(SagaPlugin(saga)) # type: ignore[arg-type]
149
+ elif saga is not Empty:
150
+ plugins.append(SagaPlugin(SagaConfig(storage=InMemorySagaStorage())))
151
+
152
+ if default_router_registration:
153
+ plugins.append(DefaultRouterRegistrationPlugin(default_router_registration))
154
+
155
+ if isinstance(autosubscribe, AutosubscribeConfig):
156
+ autosubscribe_plugin = autosubscribe.plugin
157
+ plugins.append(autosubscribe_plugin)
158
+ elif autosubscribe is not Empty and self.configurator.is_registered(SubscriptionStorage):
159
+ plugins.append(AutosubscribeConfig().plugin)
160
+
161
+ if unit_of_work is not None:
162
+ plugins.append(UnitOfWorkPlugin(unit_of_work))
163
+
164
+ if outbox is not None:
165
+ plugins.append(OutboxPlugin(outbox))
166
+ if message_id_generator is not None:
167
+ plugins.append(generic_registration_plugin(message_id_generator, MessageIdGenerator))
168
+
169
+ self.on_startup_hooks = list(on_startup_hooks or [])
170
+ self.on_shutdown_hooks = list(on_shutdown_hooks or [])
171
+
172
+ self.handler_activator.app = self
173
+ self.configurator.mersal = self
174
+ plugins.append(DefaultPlugin(pdb_on_exception=bool(pdb_on_exception)))
175
+ for plugin in plugins:
176
+ plugin(self.configurator)
177
+
178
+ lifespan_handler = self.configurator.get(LifespanHandler) # type: ignore[type-abstract]
179
+ self.on_startup_hooks.extend(lifespan_handler.on_startup_hooks)
180
+ self.on_shutdown_hooks.extend(lifespan_handler.on_shutdown_hooks)
181
+
182
+ self.configurator.resolve()
183
+ self.router = self.configurator.get(Router) # type: ignore[type-abstract]
184
+ self.logger = logging.getLogger("mersal")
185
+ self.transport = self.configurator.get(Transport) # type: ignore[type-abstract]
186
+ self.worker_factory = self.configurator.get(WorkerFactory) # type: ignore[type-abstract]
187
+ self.worker_factory.app = self
188
+ self.subscription_storage = self.configurator.get(SubscriptionStorage) # type: ignore[type-abstract]
189
+ self.topic_name_convention = self.configurator.get(TopicNameConvention) # type: ignore[type-abstract]
190
+ self.pipeline_invoker = self.configurator.get(PipelineInvoker) # type: ignore[type-abstract]
191
+ self.worker: Worker
192
+ self._create_worker()
193
+ self._exit_stack: AsyncExitStack | None = None
194
+
195
+ async def start(self) -> None:
196
+ for hook in self.on_startup_hooks:
197
+ await AsyncCallable(hook)()
198
+ self._exit_stack = AsyncExitStack()
199
+ await self._exit_stack.enter_async_context(self.worker)
200
+
201
+ async def stop(self) -> None:
202
+ if self._exit_stack:
203
+ await self._exit_stack.aclose()
204
+
205
+ self._exit_stack = None
206
+ for hook in self.on_shutdown_hooks:
207
+ await AsyncCallable(hook)()
208
+
209
+ async def __aenter__(self) -> Self:
210
+ await self.start()
211
+ return self
212
+
213
+ async def __aexit__(
214
+ self,
215
+ exc_type: type[BaseException] | None,
216
+ exc_val: BaseException | None,
217
+ exc_tb: Any | None,
218
+ ) -> None:
219
+ await self.stop()
220
+
221
+ async def send(
222
+ self,
223
+ command_message: Any,
224
+ headers: Mapping[str, Any] | None = None,
225
+ ) -> None:
226
+ logical_message = self._create_message(command_message, headers)
227
+
228
+ destination_address = await self.router.get_destination_address(logical_message)
229
+ self.logger.info(
230
+ "The app named '%r' is about to send %r to %r",
231
+ self.name,
232
+ type(command_message),
233
+ destination_address,
234
+ )
235
+ addresses = [destination_address]
236
+ await self._send(set(addresses), logical_message)
237
+
238
+ async def send_local(
239
+ self,
240
+ command_message: Any,
241
+ headers: Mapping[str, Any] | None = None,
242
+ ) -> None:
243
+ self.logger.info(
244
+ "The app named '%r' is about to send %r locally",
245
+ self.name,
246
+ type(command_message),
247
+ )
248
+
249
+ destination_address = self.transport.address
250
+ logical_message = self._create_message(command_message, headers)
251
+
252
+ addresses = [destination_address]
253
+ await self._send(set(addresses), logical_message)
254
+
255
+ async def publish(self, event_message: Any, headers: Mapping[str, Any] | None = None) -> None:
256
+ topic = self.topic_name_convention.get_topic_name(type(event_message))
257
+
258
+ await self._inner_publish(topic, event_message, headers)
259
+
260
+ async def subscribe(self, event_type: type) -> None:
261
+ topic = self.topic_name_convention.get_topic_name(event_type)
262
+ await self._subscribe(topic)
263
+
264
+ async def _send(
265
+ self,
266
+ destination_addresses: set[str],
267
+ logical_message: LogicalMessage,
268
+ ) -> None:
269
+ transaction_context = self._get_transaction_context()
270
+
271
+ if not transaction_context:
272
+ async with DefaultTransactionContextWithOwningApp(self) as transaction_context:
273
+ await self._invoke_send(destination_addresses, logical_message, transaction_context)
274
+ transaction_context.set_result(commit=True, ack=True)
275
+ try:
276
+ await transaction_context.complete()
277
+ except: # noqa: E722
278
+ self.logger.exception(
279
+ "Exception while trying to complete the transaction context when sending %r",
280
+ logical_message.message_label,
281
+ )
282
+ else:
283
+ await self._invoke_send(destination_addresses, logical_message, transaction_context)
284
+
285
+ def _get_transaction_context(self) -> TransactionContext | None:
286
+ ambient_transaction_context = AmbientContext().current
287
+
288
+ if not ambient_transaction_context:
289
+ return None
290
+
291
+ if (
292
+ not isinstance(ambient_transaction_context, DefaultTransactionContextWithOwningApp)
293
+ or ambient_transaction_context.app is not self
294
+ ):
295
+ return None
296
+
297
+ return ambient_transaction_context
298
+
299
+ async def _invoke_send(
300
+ self,
301
+ destination_addresses: set[str],
302
+ logical_message: LogicalMessage,
303
+ transaction_context: TransactionContext,
304
+ ) -> None:
305
+ self.logger.info(
306
+ "The app named '%r' is about to send %r with id: %r to destination addresses %r",
307
+ self.name,
308
+ str(type(logical_message.body)),
309
+ str(logical_message.headers.get("message_id", "N/A")),
310
+ str(destination_addresses),
311
+ )
312
+ context = OutgoingStepContext(
313
+ logical_message,
314
+ transaction_context,
315
+ DestinationAddresses(destination_addresses),
316
+ )
317
+ await self.pipeline_invoker(context)
318
+
319
+ async def _inner_publish(self, topic: str, event_message: Any, headers: Mapping[str, Any] | None) -> None:
320
+ message = self._create_message(event_message, headers)
321
+ subscriber_addresses = await self.subscription_storage.get_subscriber_addresses(topic)
322
+ await self._send(subscriber_addresses, message)
323
+
324
+ async def _subscribe(self, topic: str) -> None:
325
+ subscriber_address = self.transport.address
326
+ if self.subscription_storage.is_centralized:
327
+ await self.subscription_storage.register_subscriber(topic, subscriber_address)
328
+ else:
329
+ raise NotImplementedError("Can't handle transports that handle pub/sub natively yet")
330
+
331
+ def _create_message(self, body: Any, headers: Mapping[str, Any] | None) -> LogicalMessage:
332
+ _headers = MessageHeaders(headers) if headers else MessageHeaders()
333
+
334
+ return LogicalMessage(body, _headers)
335
+
336
+ def _create_worker(self) -> None:
337
+ self.logger.info("The app named '%r' is creating a worker", self.name)
338
+ self.worker = self.worker_factory.create_worker(self.name)
@@ -0,0 +1,3 @@
1
+ from .standard_configurator import ConfigurationCallback, StandardConfigurator
2
+
3
+ __all__ = ("ConfigurationCallback", "StandardConfigurator")