Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
import pytest
from injection import Module

from cq import CQ, Bus, CommandBus, EventBus, QueryBus
from cq._core.dispatchers.bus import SimpleBus
from cq import Bus, CommandBus, EventBus, QueryBus, Router
from cq._core.routing.dispatchers.bus import SimpleBus
from cq.ext.injection import InjectionAdapter
from tests.helpers.history import HistoryMiddleware


@pytest.fixture(scope="function")
def cq(injection_module: Module) -> CQ:
return CQ(InjectionAdapter(injection_module)).register_defaults()
def router(injection_module: Module) -> Router:
return Router(InjectionAdapter(injection_module)).register_defaults()


@pytest.fixture(scope="function")
Expand All @@ -21,20 +21,20 @@ def bus() -> Bus[Any, Any]:

@pytest.fixture(scope="function", autouse=True)
def ensure_test_dependencies(
cq: CQ,
router: Router,
history: HistoryMiddleware,
injection_module: Module,
) -> None:
injection_module.injectable(
lambda: cq.new_command_bus().add_middlewares(history),
lambda: router.new_command_bus().add_middlewares(history),
on=CommandBus,
)
injection_module.injectable(
lambda: cq.new_event_bus().add_middlewares(history),
lambda: router.new_event_bus().add_middlewares(history),
on=EventBus,
)
injection_module.injectable(
lambda: cq.new_query_bus().add_middlewares(history),
lambda: router.new_query_bus().add_middlewares(history),
on=QueryBus,
)

Expand Down
49 changes: 26 additions & 23 deletions cq/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
from ._core.cq import CQ
from ._core.di import DIAdapter
from ._core.di import NoDI as _NoDI
from ._core.dispatchers.abc import Dispatcher
from ._core.dispatchers.bus import Bus
from ._core.dispatchers.pipe import ContextPipeline, Pipe
from ._core.message import (
AnyCommandBus,
Command,
Expand All @@ -14,14 +8,20 @@
QueryBus,
)
from ._core.middleware import Middleware, MiddlewareResult, resolve_handler_source
from ._core.pipetools import ContextCommandPipeline as _ContextCommandPipeline
from ._core.pump import Pump
from ._core.queues.abc import Consumer, Delivery, Producer, Queue
from ._core.queues.memory import MemoryQueue
from ._core.queuing.pump import Pump
from ._core.queuing.queues.abc import Consumer, Delivery, Producer, Queue
from ._core.queuing.queues.memory import MemoryQueue
from ._core.related_events import AnyIORelatedEvents, RelatedEvents
from ._core.routing.command_pipeline import (
ContextCommandPipeline as _ContextCommandPipeline,
)
from ._core.routing.di import DIAdapter
from ._core.routing.dispatchers.abc import Dispatcher
from ._core.routing.dispatchers.bus import Bus
from ._core.routing.dispatchers.pipe import ContextPipeline, Pipe
from ._core.routing.router import Router

__all__ = (
"CQ",
"AnyCommandBus",
"AnyIORelatedEvents",
"Bus",
Expand All @@ -45,7 +45,8 @@
"QueryBus",
"Queue",
"RelatedEvents",
"__cq__",
"Router",
"__router__",
"command_handler",
"event_handler",
"new_command_bus",
Expand All @@ -55,28 +56,30 @@
"resolve_handler_source",
)


try:
from .ext.injection import InjectionAdapter as _InjectionAdapter
from .ext.injection import InjectionAdapter

except ImportError: # pragma: no cover
__cq__ = CQ(_NoDI())
__router__ = Router()

else:
__cq__ = CQ(_InjectionAdapter())
__router__ = Router(InjectionAdapter())
del InjectionAdapter

__cq__.register_defaults()
__router__.register_defaults()

command_handler = __cq__.command_handler
event_handler = __cq__.event_handler
query_handler = __cq__.query_handler
command_handler = __router__.command_handler
event_handler = __router__.event_handler
query_handler = __router__.query_handler

new_command_bus = __cq__.new_command_bus
new_event_bus = __cq__.new_event_bus
new_query_bus = __cq__.new_query_bus
new_command_bus = __router__.new_command_bus
new_event_bus = __router__.new_event_bus
new_query_bus = __router__.new_query_bus


class ContextCommandPipeline[C: Command](_ContextCommandPipeline[C]):
__slots__ = ()

def __init__(self, di: DIAdapter = __cq__.di) -> None:
def __init__(self, di: DIAdapter = __router__.di) -> None:
super().__init__(di)
2 changes: 1 addition & 1 deletion cq/_core/message.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Any

from cq._core.dispatchers.abc import Dispatcher
from cq._core.routing.dispatchers.abc import Dispatcher

Command = object
Event = object
Expand Down
2 changes: 1 addition & 1 deletion cq/_core/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from inspect import isasyncgenfunction
from typing import Any, Concatenate, Self, TypeGuard

from cq._core.handler import HandleFunction, HandlerType
from cq._core.routing.handler import HandleFunction, HandlerType
from cq.exceptions import MiddlewareError

type MiddlewareResult[T] = AsyncGenerator[None, T]
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion cq/_core/pump.py → cq/_core/queuing/pump.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import anyio

from cq._core.middleware import Middleware, MiddlewareGroup
from cq._core.queues.abc import Consumer
from cq._core.queuing.queues.abc import Consumer


@dataclass(repr=False, eq=False, frozen=True, slots=True)
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from anyio.abc import ObjectReceiveStream, ObjectSendStream

from cq._core.middleware import Middleware
from cq._core.pump import Pump
from cq._core.queues.abc import Delivery, Queue
from cq._core.queuing.pump import Pump
from cq._core.queuing.queues.abc import Delivery, Queue


class MemoryQueue[T](Queue[T]):
Expand All @@ -17,7 +17,7 @@ class MemoryQueue[T](Queue[T]):
__consumer: ObjectReceiveStream[T]
__producer: ObjectSendStream[T]

def __init__(self, maxsize: int = 0) -> None:
def __init__(self, maxsize: float = 0) -> None:
self.__producer, self.__consumer = anyio.create_memory_object_stream(maxsize)

async def __aenter__(self) -> Self:
Expand Down
Empty file added cq/_core/routing/__init__.py
Empty file.
10 changes: 5 additions & 5 deletions cq/_core/pipetools.py → cq/_core/routing/command_pipeline.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from typing import TYPE_CHECKING, Any, Self, overload

from cq import Dispatcher
from cq._core.common.typing import Decorator
from cq._core.di import DIAdapter
from cq._core.dispatchers.lazy import LazyDispatcher
from cq._core.dispatchers.pipe import (
from cq._core.message import Command, CommandBus, Query, QueryBus
from cq._core.routing.di import DIAdapter
from cq._core.routing.dispatchers.abc import Dispatcher
from cq._core.routing.dispatchers.lazy import LazyDispatcher
from cq._core.routing.dispatchers.pipe import (
ContextPipeline,
ConvertMethod,
ConvertMethodAsync,
ConvertMethodSync,
)
from cq._core.message import Command, CommandBus, Query, QueryBus


class ContextCommandPipeline[C: Command](ContextPipeline[C]):
Expand Down
File renamed without changes.
Empty file.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
import anyio
from anyio.abc import TaskGroup

from cq._core.dispatchers.abc import BaseDispatcher, Dispatcher
from cq._core.handler import (
from cq._core.middleware import Middleware
from cq._core.routing.dispatchers.abc import BaseDispatcher, Dispatcher
from cq._core.routing.handler import (
HandleFunction,
HandlerFactory,
HandlerRegistry,
MultipleHandlerRegistry,
SingleHandlerRegistry,
)
from cq._core.middleware import Middleware

type Listener[T] = Callable[[T], Awaitable[Any]]

Expand Down Expand Up @@ -82,7 +82,7 @@ async def dispatch(self, message: I, /) -> O:
async with anyio.create_task_group() as task_group:
self._trigger_listeners(message, task_group)

for handler in self._handlers_from(type(message)):
for handler in self._handlers_from(message.__class__):
return await self._invoke(handler, message, handler.fail_silently)

return NotImplemented
Expand All @@ -98,7 +98,7 @@ async def dispatch(self, message: I, /) -> None:
async with anyio.create_task_group() as task_group:
self._trigger_listeners(message, task_group)

for handler in self._handlers_from(type(message)):
for handler in self._handlers_from(message.__class__):
task_group.start_soon(
self._invoke,
handler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
from types import GenericAlias
from typing import TypeAliasType

from cq._core.di import DIAdapter
from cq._core.dispatchers.abc import Dispatcher
from cq._core.routing.di import DIAdapter
from cq._core.routing.dispatchers.abc import Dispatcher


class LazyDispatcher[I, O](Dispatcher[I, O]):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
)

from cq._core.common.typing import Decorator, Method
from cq._core.dispatchers.abc import BaseDispatcher, Dispatcher
from cq._core.middleware import Middleware, MiddlewareGroup
from cq._core.routing.dispatchers.abc import BaseDispatcher, Dispatcher

type ConvertAsync[**P, I, O] = Callable[Concatenate[O, P], Awaitable[I]]
type ConvertSync[**P, I, O] = Callable[Concatenate[O, P], I]
Expand Down
2 changes: 1 addition & 1 deletion cq/_core/handler.py → cq/_core/routing/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from type_analyzer import MatchingTypesConfig, iter_matching_types, matching_types

from cq._core.common.typing import Decorator
from cq._core.di import DIAdapter, NoDI
from cq._core.routing.di import DIAdapter, NoDI

type HandlerType[**P, T] = type[Handler[P, T]]
type HandlerFactory[**P, T] = Callable[..., Awaitable[Handler[P, T]]]
Expand Down
14 changes: 7 additions & 7 deletions cq/_core/cq.py → cq/_core/routing/router.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
from collections.abc import KeysView
from typing import Any, Self

from cq._core.di import DIAdapter
from cq._core.dispatchers.bus import Bus, SimpleBus, TaskBus
from cq._core.handler import (
from cq._core.message import Command, Event, Query
from cq._core.routing.di import DIAdapter, NoDI
from cq._core.routing.dispatchers.bus import Bus, SimpleBus, TaskBus
from cq._core.routing.handler import (
HandlerDecorator,
HandlerRegistry,
MultipleHandlerRegistry,
SingleHandlerRegistry,
)
from cq._core.message import Command, Event, Query


class CQ:
class Router:
__slots__ = ("__command_registry", "__di", "__event_registry", "__query_registry")

__command_registry: HandlerRegistry[Command, Any]
__di: DIAdapter
__event_registry: HandlerRegistry[Event, Any]
__query_registry: HandlerRegistry[Query, Any]

def __init__(self, di: DIAdapter, /) -> None:
self.__di = di
def __init__(self, di: DIAdapter | None = None, /) -> None:
self.__di = di or NoDI()
self.__command_registry = SingleHandlerRegistry()
self.__event_registry = MultipleHandlerRegistry()
self.__query_registry = SingleHandlerRegistry()
Expand Down
2 changes: 1 addition & 1 deletion cq/ext/injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from injection import Module, adefine_scope, mod
from injection.exceptions import ScopeAlreadyDefinedError

from cq._core.di import DIAdapter
from cq._core.message import Command, CommandBus, EventBus, QueryBus
from cq._core.middleware import Middleware, MiddlewareResult
from cq._core.related_events import AnyIORelatedEvents, RelatedEvents
from cq._core.routing.di import DIAdapter

__all__ = ("CQScope", "InjectionAdapter", "InjectionScopeMiddleware")

Expand Down
32 changes: 16 additions & 16 deletions docs/di.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,41 @@

**python-cq** does not depend on any specific dependency injection container. Instead, it talks to DI through the `DIAdapter` protocol. Implement this protocol once and you can use the library with any container you already have in your project.

## The `CQ` class
## The `Router` class

`CQ` ties together the handler registries and the DI adapter. The module-level decorators (`command_handler`, `event_handler`, `query_handler`) and bus factories (`new_command_bus`, `new_event_bus`, `new_query_bus`) all derive from a default `CQ` instance built at import time.
`Router` ties together the handler registries and the DI adapter. The module-level decorators (`command_handler`, `event_handler`, `query_handler`) and bus factories (`new_command_bus`, `new_event_bus`, `new_query_bus`) all derive from a default `Router` instance built at import time.

You create your own `CQ` instance to wire the library against a custom `DIAdapter`:
You create your own `Router` instance to wire the library against a custom `DIAdapter`:

```python
from cq import CQ, ContextCommandPipeline
from cq import Router, ContextCommandPipeline

cq = CQ(my_di_adapter).register_defaults()
router = Router(my_di_adapter).register_defaults()

command_handler = cq.command_handler
event_handler = cq.event_handler
query_handler = cq.query_handler
command_handler = router.command_handler
event_handler = router.event_handler
query_handler = router.query_handler

new_command_bus = cq.new_command_bus
new_event_bus = cq.new_event_bus
new_query_bus = cq.new_query_bus
new_command_bus = router.new_command_bus
new_event_bus = router.new_event_bus
new_query_bus = router.new_query_bus
```

When you build a `ContextCommandPipeline` against a non-default `CQ`, pass its DI adapter explicitly so the pipeline dispatches through the right buses:
When you build a `ContextCommandPipeline` against a non-default `Router`, pass its DI adapter explicitly so the pipeline dispatches through the right buses:

```python
ContextCommandPipeline(cq.di)
ContextCommandPipeline(router.di)
```

If you use the default `CQ`, `ContextCommandPipeline()` (with no argument) is enough.
If you use the default `Router`, `ContextCommandPipeline()` (with no argument) is enough.

## Implementing a `DIAdapter`

`DIAdapter` is a `Protocol` with four methods, three of them required:

```python
from collections.abc import Awaitable, Callable
from cq import CQ, Command, DIAdapter, CommandBus, EventBus, Middleware, QueryBus
from cq import Router, Command, DIAdapter, CommandBus, EventBus, Middleware, QueryBus
from typing import Any


Expand Down Expand Up @@ -90,7 +90,7 @@ class MyDIAdapter(DIAdapter):
...


cq = CQ(MyDIAdapter()).register_defaults()
router = Router(MyDIAdapter()).register_defaults()
```

The reference implementation for python-injection lives in `cq.ext.injection.InjectionAdapter`. It is a good starting point if you need to model your own adapter on a working example.
2 changes: 1 addition & 1 deletion docs/guides/pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class PaymentContext:
def _(self, result: MerchantNotifiedResult): ...
```

`ContextCommandPipeline()` uses the default `CQ` instance. If you manage your own `CQ` (see [Custom DI adapter](../di.md)), pass its DI adapter explicitly: `ContextCommandPipeline(cq.di)`.
`ContextCommandPipeline()` uses the default `Router` instance. If you manage your own `Router` (see [Custom DI adapter](../di.md)), pass its DI adapter explicitly: `ContextCommandPipeline(router.di)`.

## Steps

Expand Down
2 changes: 1 addition & 1 deletion tests/core/dispatchers/test_bus.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest

from cq import MiddlewareResult
from cq._core.dispatchers.bus import SimpleBus, TaskBus
from cq._core.routing.dispatchers.bus import SimpleBus, TaskBus


class TestSimpleBus:
Expand Down
Loading