Skip to content

Commit

Permalink
Merge pull request #43 from SamWarden/dev
Browse files Browse the repository at this point in the history
Publish Didiator v0.2.0
  • Loading branch information
SamWarden authored Dec 27, 2022
2 parents 8477269 + 258c81b commit 3eb13e2
Show file tree
Hide file tree
Showing 33 changed files with 712 additions and 169 deletions.
102 changes: 88 additions & 14 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Didiator
``didiator`` is an asynchronous library that implements the Mediator pattern and
uses the `DI <https://www.adriangb.com/di/>`_ library to help you to inject dependencies to called handlers

This library is inspired by the `MediatR <https://github.com/jbogard/MediatR>`_ used in C#
and follows CQRS principles
This library is inspired by the `MediatR <https://github.com/jbogard/MediatR>`_ used in C#,
follows CQRS principles and implements event publishing

Installation
============
Expand All @@ -22,7 +22,7 @@ It will install ``didiator`` with its optional DI dependency that is necessary t
Examples
========

You can find examples in `this folder <https://github.com/SamWarden/didiator/tree/dev/examples>`_
You can find more examples in `this folder <https://github.com/SamWarden/didiator/tree/dev/examples>`_

Create Commands and Queries with handlers for them
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -53,7 +53,7 @@ You can use functions as handlers
user_id: int
async def handle_get_user_by_id(query: GetUserById, user_repo: UserRepo) -> User:
user = await self._user_repo.get_user_by_id(user)
user = await user_repo.get_user_by_id(query.user_id)
return user
Create DiBuilder
Expand All @@ -71,21 +71,26 @@ Create DiBuilder
di_builder = DiBuilder(Container(), AsyncExecutor(), di_scopes)
di_builder.bind(bind_by_type(Dependent(UserRepoImpl, scope="request"), UserRepo))
Create Mediator and register handlers to it
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Create Mediator
~~~~~~~~~~~~~~~

Create dispatchers with their middlewares and use them to initialize the ``MediatorImpl``

``cls_scope`` is a scope that will be used to bind class Command/Query handlers initialized during request handling

.. code-block:: python
dispatchers_middlewares = (LoggingMiddleware(), DiMiddleware(di_builder, cls_scope="request"))
command_dispatcher = CommandDispatcherImpl(middlewares=dispatchers_middlewares)
query_dispatcher = QueryDispatcherImpl(middlewares=dispatchers_middlewares)
middlewares = (LoggingMiddleware(), DiMiddleware(di_builder, cls_scope="request"))
command_dispatcher = CommandDispatcherImpl(middlewares=middlewares)
query_dispatcher = QueryDispatcherImpl(middlewares=middlewares)
mediator = MediatorImpl(command_dispatcher, query_dispatcher)
Register handlers
~~~~~~~~~~~~~~~~~

.. code-block:: python
# CreateUserHandler is not initialized during registration
mediator.register_command_handler(CreateUser, CreateUserHandler)
mediator.register_query_handler(GetUserById, handle_get_user_by_id)
Expand All @@ -102,12 +107,77 @@ Use ``mediator.send(...)`` for commands and ``mediator.query(...)`` for queries
async with di_builder.enter_scope("request") as di_state:
scoped_mediator = mediator.bind(di_state=di_state)
# It will call CreateUserHandler(...).__call__(...) and inject UserRepoImpl to it
# It will call CreateUserHandler(UserRepoImpl()).__call__(command)
# UserRepoImpl() created and injected automatically
user_id = await scoped_mediator.send(CreateUser(1, "Jon"))
# It will call handle_get_user_by_id(query, user_repo)
# UserRepoImpl created earlier will be reused in this scope
user = await scoped_mediator.query(GetUserById(user_id))
print("User:", user)
# Session of UserRepoImpl will be closed after exiting the "request" scope
Events publishing
~~~~~~~~~~~~~~~~~

You can register and publish events using ``Mediator`` and its ``EventObserver``.
Unlike dispatchers, ``EventObserver`` publishes events to multiple event handlers subscribed to it
and doesn't return their result.
All middlewares also work with ``EventObserver``, as in in the case with Dispatchers.

Define event and its handlers
-----------------------------

.. code-block:: python
class UserCreated(Event):
user_id: int
username: str
async def on_user_created1(event: UserCreated, logger: Logger) -> None:
logger.info("User created1: id=%s, username=%s", event.user_id, event.username)
async def on_user_created2(event: UserCreated, logger: Logger) -> None:
logger.info("User created2: id=%s, username=%s", event.user_id, event.username)
Create EventObserver and use it for Mediator
--------------------------------------------

.. code-block:: python
middlewares = (LoggingMiddleware(), DiMiddleware(di_builder, cls_scope="request"))
event_observer = EventObserver(middlewares=middlewares)
mediator = MediatorImpl(command_dispatcher, query_dispatcher, event_observer)
Register event handlers
-----------------------

You can register multiple event handlers for one event

.. code-block:: python
mediator.register_event_handler(UserCreated, on_user_created1)
mediator.register_event_handler(UserCreated, on_user_created2)
Publish event
-------------

Event handlers will be executed sequentially

.. code-block:: python
mediator.publish(UserCreated(1, "Jon"))
# User created1: id=1, username="Jon"
# User created2: id=1, username="Jon"
mediator.publish([UserCreated(2, "Sam"), UserCreated(3, "Nick")])
# User created1: id=2, username="Sam"
# User created2: id=2, username="Sam"
# User created1: id=3, username="Nick"
# User created2: id=3, username="Nick"
⚠️ **Attention: this is a beta version of** ``didiator`` **that depends on** ``DI``, **which is also in beta. Both of them can change their API!**

CQRS
Expand All @@ -116,17 +186,20 @@ CQRS
CQRS stands for "`Command Query Responsibility Segregation <https://www.martinfowler.com/bliki/CQRS.html>`_".
Its idea about splitting the responsibility of commands (writing) and queries (reading) into different models.

``didiator`` have segregated ``.send(command)`` and ``.query(query)`` methods in its ``Mediator`` and
``didiator`` have segregated ``.send(command)``, ``.query(query)`` and ``.publish(events)`` methods in its ``Mediator`` and
assumes that you will separate its handlers.
Use ``CommandMediator`` and ``QueryMediator`` protocols to explicitly define which method you need in ``YourController``
Use ``CommandMediator``, ``QueryMediator`` and ``EventMediator`` protocols to explicitly define which method you need in ``YourController``

.. code-block:: mermaid
graph LR;
YourController-- Command -->Mediator;
YourController-- Query -->Mediator;
Mediator-. Command .->CommandDispatcher-.->di1[DiMiddleware]-.->CommandHandler;
YourController-- Command -->Mediator;
Mediator-. Query .->QueryDispatcher-.->di2[DiMiddleware]-.->QueryHandler;
Mediator-. Command .->CommandDispatcher-.->di1[DiMiddleware]-.->CommandHandler;
CommandHandler-- Event -->Mediator;
Mediator-. Event .->EventObserver-.->di3[DiMiddleware]-.->EventHandler1;
EventObserver-.->di4[DiMiddleware]-.->EventHandler2;
``DiMiddleware`` initializes handlers and injects dependencies for them, you can just send a command with the data you need

Expand All @@ -136,6 +209,7 @@ Why ``didiator``?
- Easy dependency injection to your business logic
- Separating dependencies from your controllers. They can just parse external requests and interact with the ``Mediator``
- CQRS
- Event publishing
- Flexible configuration
- Middlewares support

Expand Down
17 changes: 14 additions & 3 deletions didiator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
from .dispatchers import CommandDispatcherImpl, QueryDispatcherImpl
from .interface.entities import Command, Query
from .interface import CommandDispatcher, EventHandler, EventObserver, QueryDispatcher
from .observers import EventObserverImpl
from .interface.entities import Command, Query, Event
from .interface.handlers import CommandHandler, QueryHandler
from .interface.mediator import Mediator
from .interface.mediator import Mediator, CommandMediator, QueryMediator, EventMediator
from .mediator import MediatorImpl

__version__ = "0.1.1"
__version__ = "0.2.0"

__all__ = (
"__version__",
"MediatorImpl",
"Mediator",
"CommandMediator",
"QueryMediator",
"EventMediator",
"Command",
"CommandHandler",
"CommandDispatcher",
"CommandDispatcherImpl",
"Query",
"QueryHandler",
"QueryDispatcher",
"QueryDispatcherImpl",
"Event",
"EventHandler",
"EventObserver",
"EventObserverImpl",
)
4 changes: 2 additions & 2 deletions didiator/dispatchers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from .command import CommandDispatcherImpl
from .request import RequestDispatcherImpl
from .request import DispatcherImpl
from .query import QueryDispatcherImpl

__all__ = (
"CommandDispatcherImpl",
"RequestDispatcherImpl",
"DispatcherImpl",
"QueryDispatcherImpl",
)
10 changes: 6 additions & 4 deletions didiator/dispatchers/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
from didiator.interface.dispatchers.command import CommandDispatcher
from didiator.interface.handlers.request import HandlerType
from didiator.interface.exceptions import CommandHandlerNotFound, HandlerNotFound
from didiator.dispatchers.request import RequestDispatcherImpl
from didiator.dispatchers.request import DispatcherImpl

CRes = TypeVar("CRes")
C = TypeVar("C", bound=Command[Any])


class CommandDispatcherImpl(RequestDispatcherImpl, CommandDispatcher):
class CommandDispatcherImpl(DispatcherImpl, CommandDispatcher):
def register_handler(self, command: Type[C], handler: HandlerType[C, CRes]) -> None:
super()._register_handler(command, handler)

async def send(self, command: Command[CRes], *args: Any, **kwargs: Any) -> CRes:
try:
return await self._handle(command, *args, **kwargs)
except HandlerNotFound:
raise CommandHandlerNotFound(f"Command handler for {type(command).__name__} command is not registered", command)
except HandlerNotFound as err:
raise CommandHandlerNotFound(
f"Command handler for {type(command).__name__} command is not registered", command,
) from err
10 changes: 6 additions & 4 deletions didiator/dispatchers/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
from didiator.interface.dispatchers.query import QueryDispatcher
from didiator.interface.entities.query import Query
from didiator.interface.exceptions import HandlerNotFound, QueryHandlerNotFound
from didiator.dispatchers.request import RequestDispatcherImpl
from didiator.dispatchers.request import DispatcherImpl

QRes = TypeVar("QRes")
Q = TypeVar("Q", bound=Query[Any])


class QueryDispatcherImpl(RequestDispatcherImpl, QueryDispatcher):
class QueryDispatcherImpl(DispatcherImpl, QueryDispatcher):
def register_handler(self, query: Type[Q], handler: HandlerType[Q, QRes]) -> None:
super()._register_handler(query, handler)

async def query(self, query: Query[QRes], *args: Any, **kwargs: Any) -> QRes:
try:
return await self._handle(query, *args, **kwargs)
except HandlerNotFound:
raise QueryHandlerNotFound(f"Query handler for {type(query).__name__} query is not registered", query)
except HandlerNotFound as err:
raise QueryHandlerNotFound(
f"Query handler for {type(query).__name__} query is not registered", query,
) from err
39 changes: 19 additions & 20 deletions didiator/dispatchers/request.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,51 @@
import abc
import functools
from typing import Any, Awaitable, Callable, Sequence, Type, TypeVar

from didiator.interface.entities.request import Request
from didiator.interface.exceptions import HandlerNotFound
from didiator.interface.handlers import HandlerType
from didiator.middlewares.base import Middleware
from didiator.interface.dispatchers.request import Dispatcher, MiddlewareType
from didiator.middlewares.base import Middleware, MiddlewareType, wrap_middleware
from didiator.interface.dispatchers.request import Dispatcher

RRes = TypeVar("RRes")
R = TypeVar("R", bound=Request[Any])
Middlewares = Sequence[MiddlewareType[Request[Any], Any]]

DEFAULT_MIDDLEWARES: tuple[MiddlewareType, ...] = (Middleware(),)
DEFAULT_MIDDLEWARES: tuple[MiddlewareType[Request[Any], Any], ...] = (Middleware(),)


class RequestDispatcherImpl(Dispatcher, abc.ABC):
def __init__(self, middlewares: Sequence[MiddlewareType] = ()) -> None:
self._handlers: dict[Type[Request[Any]], HandlerType[Any, Any]] = {}
self._middlewares: Sequence[MiddlewareType] = middlewares
class DispatcherImpl(Dispatcher, abc.ABC):
def __init__(self, middlewares: Middlewares = ()) -> None:
self._handlers: dict[Type[Request[Any]], HandlerType[Request[Any], Any]] = {}
self._middlewares: Middlewares = middlewares

def _register_handler(self, request: Type[R], handler: HandlerType[R, RRes]) -> None:
self._handlers[request] = handler

@property
def handlers(self) -> dict[Type[Request[Any]], HandlerType]:
def handlers(self) -> dict[Type[Request[Any]], HandlerType[Request[Any], Any]]:
return self._handlers

@property
def middlewares(self) -> tuple[MiddlewareType, ...]:
def middlewares(self) -> tuple[MiddlewareType[Request[Any], Any], ...]:
return tuple(self._middlewares)

async def _handle(self, request: Request[RRes], *args: Any, **kwargs: Any) -> RRes:
try:
handler = self._handlers[type(request)]
except KeyError:
raise HandlerNotFound(f"Request handler for {type(request).__name__} request is not registered", request)
except KeyError as err:
raise HandlerNotFound(
f"Request handler for {type(request).__name__} request is not registered", request,
) from err

# Handler has to be wrapped with at least one middleware to initialize the handler if it is necessary
middlewares = self._middlewares if self._middlewares else DEFAULT_MIDDLEWARES
middlewares: Middlewares = self._middlewares if self._middlewares else DEFAULT_MIDDLEWARES
wrapped_handler: Callable[..., Awaitable[RRes]] = self._wrap_middleware(middlewares, handler)
return await wrapped_handler(request, *args, **kwargs)

@staticmethod
def _wrap_middleware(
middlewares: Sequence[MiddlewareType[R, RRes]],
handler: HandlerType[R, RRes],
) -> Callable[..., Awaitable[RRes]]:
for middleware in reversed(middlewares):
handler = functools.partial(middleware, handler)

return handler
middlewares: Sequence[MiddlewareType[R, Any]],
handler: HandlerType[R, Any],
) -> Callable[..., Awaitable[Any]]:
return wrap_middleware(middlewares, handler)
23 changes: 14 additions & 9 deletions didiator/interface/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
from .dispatchers.command import CommandDispatcher
from .dispatchers.request import Dispatcher
from .dispatchers.query import QueryDispatcher
from .mediator import CommandMediator, Mediator, QueryMediator
from .entities import Command, Query, Request
from .handlers import CommandHandler, Handler, QueryHandler

from .observers.event import EventObserver, Listener
from .mediator import CommandMediator, EventMediator, Mediator, QueryMediator
from .entities import Command, Query, Request, Event
from .handlers import CommandHandler, EventHandler, Handler, QueryHandler

__all__ = (
"Mediator",
"CommandMediator",
"QueryMediator",
"Dispatcher",
"CommandDispatcher",
"QueryDispatcher",
"EventMediator",
"Request",
"Command",
"Query",
"Handler",
"Dispatcher",
"Command",
"CommandHandler",
"CommandDispatcher",
"Query",
"QueryHandler",
"QueryDispatcher",
"Event",
"EventHandler",
"Listener",
"EventObserver",
)
2 changes: 1 addition & 1 deletion didiator/interface/dispatchers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from .query import QueryDispatcher

__all__ = (
"CommandDispatcher",
"Dispatcher",
"CommandDispatcher",
"QueryDispatcher",
)
Loading

0 comments on commit 3eb13e2

Please sign in to comment.