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 +106 -0
- pymediate/_internal/__init__.py +10 -0
- pymediate/_internal/handler.py +195 -0
- pymediate/_internal/mediator.py +128 -0
- pymediate/_internal/registry.py +421 -0
- pymediate/aio/__init__.py +36 -0
- pymediate/aio/handler.py +111 -0
- pymediate/aio/mediator.py +196 -0
- pymediate/aio/pipeline.py +366 -0
- pymediate/errors.py +239 -0
- pymediate/handler.py +108 -0
- pymediate/mediator.py +170 -0
- pymediate/pipeline.py +363 -0
- pymediate/providers/__init__.py +9 -0
- pymediate/providers/dependency_injector.py +288 -0
- pymediate/py.typed +0 -0
- pymediate/request.py +85 -0
- pymediate/service.py +801 -0
- pymediate-0.1.0.dist-info/METADATA +238 -0
- pymediate-0.1.0.dist-info/RECORD +22 -0
- pymediate-0.1.0.dist-info/WHEEL +4 -0
- pymediate-0.1.0.dist-info/licenses/LICENSE +21 -0
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)
|