Skip to main content

Python CQRS + Domain Events Framework inspired by CommandQuery.Framing

Project description

CQRS Framing

A Python framework for CQRS (Command Query Responsibility Segregation) and Domain Events, inspired by .NET's CommandQuery.Framing. Features built-in dependency injection using di-done-right.

Features

  • CQRS Pattern: Separate command and query handlers with type-based routing
  • Built-in Dependency Injection: Uses di-done-right for automatic dependency resolution
  • Sync & Async Support: Both synchronous and asynchronous handler execution
  • Pipeline Middleware: Cross-cutting concerns via middleware pattern
  • Domain Events: First-class domain event support with aggregate roots
  • Delegate-Style Events: Subscribe to events using += / -= syntax
  • Framework Agnostic: No dependency on FastAPI or other web frameworks
  • Type Safe: Full type hints and py.typed marker

Installation

pip install cqrs-framing

Quick Start

Handler Contract

All handlers must inherit from either Handler (sync) or AsyncHandler (async):

from cqrs_framing import Handler, AsyncHandler, CancellationToken

# Synchronous handler
class MySyncHandler(Handler[MyCommand, str]):
    def execute(self, message: MyCommand) -> str:
        return "result"

# Asynchronous handler
class MyAsyncHandler(AsyncHandler[MyCommand, str]):
    async def execute(self, message: MyCommand, cancellation_token: CancellationToken) -> str:
        return "result"

Why inheritance is required:

  • ✅ Clear contract discovery in IDEs (autocomplete, hints)
  • ✅ Type checker enforcement (mypy/Pylance catches errors)
  • ✅ Registration-time validation (fails fast if signature is wrong)
  • ✅ Prevents accidental direct invocation bypassing the broker

Define Messages and Handlers

from cqrs_framing import Message, AsyncHandler, CommandResponse, Response, CancellationToken
from dataclasses import dataclass

@dataclass
class CreateUser(Message):
    username: str
    email: str

class CreateUserHandler(AsyncHandler[CreateUser, CommandResponse[str]]):
    async def execute(self, message: CreateUser, cancellation_token: CancellationToken) -> CommandResponse[str]:
        # Your business logic here
        user_id = f"user-{message.username}"
        return Response.ok(user_id)

Register and Execute

from cqrs_framing import Broker, HandlerRegistry

# Setup
registry = HandlerRegistry()
broker = Broker(registry)

# Register handler (will be auto-instantiated by DI)
registry.register(CreateUser, CreateUserHandler)

# Execute command
result = await broker.handle_async(CreateUser(username="john", email="john@example.com"))

Async and sync semantics

  • Prefer Broker.handle_async(...) in async applications.
  • Broker.handle(...) is for synchronous handlers. If you provide a sync_pipeline, it requires creating an event loop internally; calling it from within an already-running event loop will raise a RuntimeError.
  • Event.fire(...) / EventHub.publish(...) are fire-and-forget. If you subscribe any async handlers, publish from an async context or use fire_async(...) / publish_async(...).

Handlers with Dependencies

class UserRepository:
    def save(self, user): ...

class CreateUserHandler(AsyncHandler[CreateUser, CommandResponse[str]]):
    def __init__(self, repository: UserRepository):
        self.repository = repository
    
    async def execute(self, message: CreateUser, cancellation_token) -> CommandResponse[str]:
        # Dependencies are auto-injected!
        user_id = f"user-{message.username}"
        self.repository.save({"id": user_id, "username": message.username})
        return Response.ok(user_id)

# Register service in DI container
registry.container.register_instance(UserRepository, UserRepository())

# Register handler (repository will be injected)
registry.register(CreateUser, CreateUserHandler)

Domain Events

from cqrs_framing import AggregateRoot, DomainEvent, EventHub
from dataclasses import dataclass

@dataclass
class UserCreated(DomainEvent):
    user_id: str
    email: str

class User(AggregateRoot):
    def __init__(self, user_id: str, email: str):
        super().__init__()
        self.user_id = user_id
        self.email = email
        self._raise(UserCreated(user_id=user_id, email=email))

# Subscribe to events
hub = EventHub()

def send_welcome_email(event: UserCreated):
    print(f"Sending welcome email to {event.email}")

hub[UserCreated] += send_welcome_email

# Publish events
user = User("123", "john@example.com")
for event in user.pending_events:
    hub.publish(event)

Requirements

  • Python >= 3.10

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cqrs_framing-0.1.1.tar.gz (14.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cqrs_framing-0.1.1-py3-none-any.whl (15.9 kB view details)

Uploaded Python 3

File details

Details for the file cqrs_framing-0.1.1.tar.gz.

File metadata

  • Download URL: cqrs_framing-0.1.1.tar.gz
  • Upload date:
  • Size: 14.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for cqrs_framing-0.1.1.tar.gz
Algorithm Hash digest
SHA256 18a9d220250ee63e7a5b3feb7d3d9aad70e08140090f21777bf193abc96e640a
MD5 f8dcb62b847e37a8ddfa5a4e9ff91add
BLAKE2b-256 ec2101c58eb6e161ded96892bc5b0718c6cb6b73d1c6fdf550c01576c8a0fa69

See more details on using hashes here.

File details

Details for the file cqrs_framing-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: cqrs_framing-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 15.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.14

File hashes

Hashes for cqrs_framing-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f201a011f1a796cce154067e24f18d2c61851b78ccd71cf81447289c48d5f34f
MD5 9e191d6f880afbcc5450afadfa5d37be
BLAKE2b-256 f0c365f3d38b261633b2a109b044625b67d30d995fdbdcfbdcb31509cab6e705

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page