pymediate 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.
pymediate/__init__.py ADDED
@@ -0,0 +1,106 @@
1
+ """PyMediate - A type-safe mediator pattern implementation for Python.
2
+
3
+ PyMediate is a modern implementation of the Mediator Pattern that provides
4
+ type-safe request routing with automatic response type inference. It's designed
5
+ for Python 3.12+ and integrates seamlessly with dataclasses and dependency
6
+ injection frameworks.
7
+
8
+ Key Features:
9
+ - Type-safe: Full runtime validation with mypy support
10
+ - Zero convention: Uses type inspection instead of naming conventions
11
+ - Async/await support: Built-in async handlers and mediators via pymediate.aio
12
+ - DI ready: Built-in dependency-injector integration
13
+ - Dataclass friendly: Works seamlessly with @dataclass and Request[T]
14
+ - Well tested: 95%+ coverage enforced in CI
15
+
16
+ Quick Example:
17
+ ```python
18
+ from dataclasses import dataclass
19
+ from pymediate import Request, Handler, Mediator, Services
20
+
21
+ @dataclass
22
+ class UserCreated:
23
+ user_id: int
24
+ username: str
25
+
26
+ @dataclass
27
+ class CreateUser(Request[UserCreated]):
28
+ username: str
29
+ email: str
30
+
31
+ class CreateUserHandler(Handler[CreateUser]):
32
+ def __call__(self, req: CreateUser) -> UserCreated:
33
+ return UserCreated(user_id=1, username=req.username)
34
+
35
+ services = Services()
36
+ services.add(CreateUserHandler())
37
+ provider = services.provider()
38
+ mediator = Mediator(provider)
39
+
40
+ response = mediator.send(CreateUser(username="alice", email="alice@example.com"))
41
+ print(f"User {response.username} created with ID {response.user_id}")
42
+ ```
43
+
44
+ Main Components:
45
+ - Request: Base class for all requests
46
+ - Handler: Base class for synchronous handlers
47
+ - Mediator: Routes requests to handlers (sync version)
48
+ - ServiceProvider: Protocol for resolving service instances
49
+ - Services: Builder for registering services
50
+
51
+ Async Support:
52
+ For asynchronous operations, use the async variants from pymediate.aio:
53
+ ```python
54
+ from pymediate import Services
55
+ from pymediate.aio import Handler, Mediator
56
+
57
+ class AsyncHandler(Handler[CreateUser]):
58
+ async def __call__(self, req: CreateUser) -> UserCreated:
59
+ # Can use await here
60
+ result = await async_database_operation(req)
61
+ return UserCreated(user_id=result.id, username=req.username)
62
+
63
+ services = Services()
64
+ services.add(AsyncHandler())
65
+ provider = services.provider()
66
+ mediator = Mediator(provider)
67
+ response = await mediator.send(CreateUser(username="alice", email="alice@example.com"))
68
+ ```
69
+
70
+ For more information, see the documentation at https://sina-al.github.io/pymediate/
71
+ """
72
+
73
+ from .errors import (
74
+ HandlerAlreadyRegisteredError,
75
+ HandlerNotFoundError,
76
+ InvalidHandlerSignatureError,
77
+ InvalidRequestTypeError,
78
+ PyMediateError,
79
+ ResponseTypeMismatchError,
80
+ )
81
+ from .handler import Handler
82
+ from .mediator import Mediator
83
+ from .pipeline import PipelineBehavior
84
+ from .request import Request
85
+ from .service import ServiceNotFoundError, ServiceProvider, Services
86
+
87
+ __all__ = [
88
+ "Request",
89
+ "Handler",
90
+ "Mediator",
91
+ # Service Provider
92
+ "ServiceProvider",
93
+ "Services",
94
+ "ServiceNotFoundError",
95
+ # Pipeline
96
+ "PipelineBehavior",
97
+ # Errors
98
+ "PyMediateError",
99
+ "HandlerNotFoundError",
100
+ "HandlerAlreadyRegisteredError",
101
+ "InvalidHandlerSignatureError",
102
+ "InvalidRequestTypeError",
103
+ "ResponseTypeMismatchError",
104
+ ]
105
+
106
+ __version__ = "0.1.0"
@@ -0,0 +1,10 @@
1
+ """Internal implementation details for PyMediate.
2
+
3
+ This package contains internal utilities, base classes, and registries that are
4
+ not part of the public API. Users should not import from this package directly.
5
+
6
+ Warning:
7
+ The contents of this package are internal implementation details and may
8
+ change without notice in any release. Only import from the main `pymediate`
9
+ package or `pymediate.aio` for public APIs.
10
+ """
@@ -0,0 +1,195 @@
1
+ """Shared base logic for both sync and async handlers.
2
+
3
+ This module provides HandlerBaseMixin which contains all the type extraction,
4
+ validation, and registration logic that is common between sync and async handlers.
5
+ """
6
+
7
+ import inspect
8
+ from typing import Any, get_args, get_origin
9
+
10
+ from .. import errors
11
+ from . import registry
12
+
13
+
14
+ def _validate_call_signature(
15
+ cls: type,
16
+ expected_request_type: type,
17
+ expected_response_type: type,
18
+ is_async: bool = False,
19
+ ) -> None:
20
+ """Validate that the handler's __call__ method has the correct signature.
21
+
22
+ Args:
23
+ cls: The handler class to validate
24
+ expected_request_type: The expected request parameter type
25
+ expected_response_type: The expected return type
26
+ is_async: Whether to expect async def __call__ or sync def __call__
27
+
28
+ Raises:
29
+ TypeError: If the signature doesn't match expectations
30
+ """
31
+ if "__call__" not in cls.__dict__:
32
+ raise errors.InvalidHandlerSignatureError(cls, "must implement __call__ method")
33
+
34
+ call_method = cls.__dict__["__call__"]
35
+
36
+ # Check if it's async when it should be (or vice versa)
37
+ if is_async and not inspect.iscoroutinefunction(call_method):
38
+ raise errors.InvalidHandlerSignatureError(
39
+ cls, "__call__ must be async (use 'async def __call__')"
40
+ )
41
+ elif not is_async and inspect.iscoroutinefunction(call_method):
42
+ raise errors.InvalidHandlerSignatureError(
43
+ cls, "__call__ must be sync (remove 'async' from 'def __call__')"
44
+ )
45
+
46
+ sig = inspect.signature(call_method)
47
+
48
+ # Validate parameters
49
+ params = list(sig.parameters.values())
50
+ if len(params) != 2: # self, request
51
+ raise errors.InvalidHandlerSignatureError(
52
+ cls,
53
+ f"__call__ must accept exactly one parameter (besides self), got {len(params) - 1}",
54
+ )
55
+
56
+ request_param = params[1] # Skip 'self'
57
+ if request_param.annotation == inspect.Parameter.empty:
58
+ raise errors.InvalidHandlerSignatureError(
59
+ cls, "__call__ request parameter must have type annotation"
60
+ )
61
+
62
+ if request_param.annotation != expected_request_type:
63
+ param_name = (
64
+ request_param.annotation.__name__
65
+ if hasattr(request_param.annotation, "__name__")
66
+ else str(request_param.annotation)
67
+ )
68
+ expected_name = expected_request_type.__name__
69
+ raise errors.InvalidHandlerSignatureError(
70
+ cls,
71
+ f"__call__ parameter must be of type {expected_name}, got {param_name}",
72
+ )
73
+
74
+ # Validate return type
75
+ if sig.return_annotation == inspect.Signature.empty:
76
+ raise errors.InvalidHandlerSignatureError(cls, "__call__ must have return type annotation")
77
+
78
+ if sig.return_annotation != expected_response_type:
79
+ raise errors.ResponseTypeMismatchError(cls, expected_response_type, sig.return_annotation)
80
+
81
+
82
+ class HandlerBaseMixin[RequestT]:
83
+ """Mixin providing shared logic for both sync and async handlers.
84
+
85
+ This mixin contains all the type extraction, validation, and registration
86
+ logic that is common between synchronous and asynchronous handlers.
87
+
88
+ Type Parameters:
89
+ RequestT: The type of request this handler processes.
90
+
91
+ Attributes:
92
+ _request_type: Class-level attribute storing the request type.
93
+ _response_type: Class-level attribute storing the inferred response type.
94
+ """
95
+
96
+ _request_type: type | None = None
97
+ _response_type: type | None = None
98
+ _is_async: bool = False # Set by subclass
99
+
100
+ def __init_subclass__(cls, **kwargs: Any) -> None:
101
+ """Extract request type and validate handler signature.
102
+
103
+ This hook is automatically called when a new Handler subclass is defined.
104
+ It extracts the request type from Handler[RequestType], looks up the
105
+ corresponding response type, validates the __call__ signature, and
106
+ registers the handler.
107
+
108
+ Args:
109
+ **kwargs: Additional keyword arguments passed to parent __init_subclass__.
110
+
111
+ Raises:
112
+ InvalidRequestTypeError: If the request type doesn't inherit from Request.
113
+ InvalidHandlerSignatureError: If __call__ signature is invalid.
114
+ ResponseTypeMismatchError: If return type doesn't match expected response.
115
+ """
116
+ super().__init_subclass__(**kwargs)
117
+
118
+ cls._request_type = None
119
+ cls._response_type = None
120
+
121
+ # Extract request type from Handler[RequestType]
122
+ # We need to find the right base class (could be Handler or AsyncHandler)
123
+ orig_bases = getattr(cls, "__orig_bases__", ())
124
+ for base in orig_bases:
125
+ origin = get_origin(base)
126
+ # Check if this is a Handler-like class (has HandlerBaseMixin in its mro)
127
+ if origin and any(hasattr(b, "_is_async") for b in getattr(origin, "__mro__", [])):
128
+ args = get_args(base)
129
+ if args:
130
+ cls._request_type = args[0]
131
+ break
132
+
133
+ # Look up response type from request registry
134
+ if cls._request_type is not None:
135
+ if registry.has_response_type(cls._request_type):
136
+ cls._response_type = registry.get_response_type(cls._request_type)
137
+
138
+ # Validate the __call__ signature
139
+ assert cls._response_type is not None, (
140
+ "Response type should not be None after check"
141
+ )
142
+ _validate_call_signature(
143
+ cls, cls._request_type, cls._response_type, is_async=cls._is_async
144
+ )
145
+
146
+ # Register handler
147
+ registry.register_handler(cls._request_type, cls)
148
+ else:
149
+ # Only raise if this isn't a base Handler class
150
+ if cls.__name__ not in ("Handler", "HandlerBaseMixin"):
151
+ raise errors.InvalidRequestTypeError(cls._request_type)
152
+
153
+ @classmethod
154
+ def get_request_type(cls) -> type | None:
155
+ """Get the request type this handler handles.
156
+
157
+ Returns:
158
+ The request type class that this handler is designed to process,
159
+ or None if no request type was specified.
160
+ """
161
+ return cls._request_type
162
+
163
+ @classmethod
164
+ def get_response_type(cls) -> type | None:
165
+ """Get the response type this handler returns.
166
+
167
+ The response type is automatically inferred from the request's
168
+ Request[ResponseT] declaration.
169
+
170
+ Returns:
171
+ The response type class that this handler will return,
172
+ or None if no response type was registered.
173
+ """
174
+ return cls._response_type
175
+
176
+ @classmethod
177
+ def get_handler_for_request(cls, request_type: type) -> Any:
178
+ """Get the handler class registered for a given request type.
179
+
180
+ This is a class-level utility method for looking up which handler
181
+ class is registered to handle a specific request type.
182
+
183
+ Args:
184
+ request_type: The request type class to look up.
185
+
186
+ Returns:
187
+ The handler class that processes the given request type.
188
+
189
+ Raises:
190
+ HandlerNotFoundError: If no handler is registered for the request type.
191
+ """
192
+ if not registry.has_handler(request_type):
193
+ available = registry.get_all_handler_request_types()
194
+ raise errors.HandlerNotFoundError(request_type, available)
195
+ return registry.get_handler_class(request_type)
@@ -0,0 +1,128 @@
1
+ """Base mixin for mediator implementations (sync and async)."""
2
+
3
+ from collections.abc import Callable, Sequence
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from .. import errors
7
+ from . import registry
8
+
9
+ if TYPE_CHECKING:
10
+ from ..request import Request
11
+ from ..service import ServiceProvider
12
+
13
+
14
+ class MediatorMixin:
15
+ """Mixin providing shared logic for both sync and async mediators.
16
+
17
+ This mixin contains the common initialization and request processing logic
18
+ that is shared between the synchronous Mediator and asynchronous Mediator.
19
+
20
+ The actual send() method is implemented differently in each variant:
21
+ - Synchronous: def send(...) -> ResponseT
22
+ - Asynchronous: async def send(...) -> ResponseT
23
+
24
+ Attributes:
25
+ _services: The services instance used to obtain handler and behavior instances.
26
+ """
27
+
28
+ _services: "ServiceProvider"
29
+
30
+ def __init__(self, services: "ServiceProvider") -> None:
31
+ """Initialize mediator with services for obtaining handler and behavior instances.
32
+
33
+ Args:
34
+ services: Any object implementing the ServiceProvider protocol.
35
+ This can be a ServiceProvider from Services.provider(),
36
+ a DependencyInjectorServiceProvider, or your own custom implementation.
37
+
38
+ Examples:
39
+ ```python
40
+ from pymediate import Mediator
41
+ from pymediate.service import Services
42
+
43
+ services = Services()
44
+ services.add(CreateUserHandler())
45
+ provider = services.provider()
46
+
47
+ mediator = Mediator(provider)
48
+ ```
49
+
50
+ With dependency injection:
51
+ ```python
52
+ from pymediate.providers import DependencyInjectorServiceProvider
53
+
54
+ container = AppContainer()
55
+ provider = DependencyInjectorServiceProvider(container)
56
+ mediator = Mediator(provider)
57
+ ```
58
+ """
59
+ self._services = services
60
+
61
+ def _resolve_handler(self, request: "Request[Any]") -> Any:
62
+ """Resolve the handler for a request.
63
+
64
+ Args:
65
+ request: The request instance to process
66
+
67
+ Returns:
68
+ Handler instance for the request
69
+
70
+ Raises:
71
+ HandlerNotFoundError: If no handler is registered for the request type
72
+ """
73
+ # Look up handler type from registry
74
+ request_type = type(request)
75
+ handler_class = registry.get_handler_class(request_type)
76
+ if handler_class is None:
77
+ raise errors.HandlerNotFoundError(request_type, [])
78
+
79
+ # Get handler instance
80
+ return self._services.get(handler_class)
81
+
82
+ def _resolve_behaviors(
83
+ self, request: "Request[Any]", pipeline_behavior_type: type
84
+ ) -> list[Any]:
85
+ """Resolve applicable behaviors for a request.
86
+
87
+ Args:
88
+ request: The request instance to process
89
+ pipeline_behavior_type: The PipelineBehavior type (sync or async variant)
90
+
91
+ Returns:
92
+ List of applicable behavior instances
93
+ """
94
+ # Get all registered pipeline behaviors
95
+ all_behaviors: Sequence[Any] = self._services.get_all(pipeline_behavior_type)
96
+
97
+ # Filter behaviors to only those that apply to this request
98
+ return [behavior for behavior in all_behaviors if type(behavior).should_apply(request)]
99
+
100
+ def _get_dispatch(
101
+ self,
102
+ request: "Request[Any]",
103
+ handler: Any,
104
+ behaviors: list[Any],
105
+ pipeline_class: type,
106
+ ) -> Callable[[], Any]:
107
+ """Get a dispatch callable for executing the request through behaviors and handler.
108
+
109
+ This method creates a callable that, when invoked, will execute the request
110
+ through the pipeline of behaviors and the handler. The callable can be called
111
+ synchronously or asynchronously depending on the pipeline class.
112
+
113
+ Args:
114
+ request: The request instance to process
115
+ handler: The handler instance
116
+ behaviors: List of applicable behaviors
117
+ pipeline_class: The Pipeline class (sync or async variant)
118
+
119
+ Returns:
120
+ Callable that executes the pipeline
121
+ """
122
+ # Fast path: if no behaviors, return handler directly
123
+ if not behaviors:
124
+ return lambda: handler(request)
125
+
126
+ # Construct and return pipeline callable
127
+ pipeline: Any = pipeline_class(behaviors, handler)
128
+ return lambda: pipeline(request)