Skip to content
Open
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
75 changes: 75 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Tests

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]

jobs:
lint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"

- name: Run ruff check
run: |
ruff check src tests examples

- name: Run ruff format check
run: |
ruff format --check src tests examples

- name: Run pyright
if: matrix.python-version == '3.12'
run: |
pyright src tests

- name: Check minimum Python version (vermin)
run: |
vermin --target=3.10- --violations --eval-annotations --backport typing_extensions --exclude=venv --exclude=build --exclude=.git --exclude=.venv src examples tests

test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"

- name: Run tests
run: |
pytest --cov=src --cov-report=xml ./tests/unit

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
28 changes: 7 additions & 21 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
repos:
- hooks:
- id: check-toml
- id: check-docstring-first
- id: check-ast
- exclude: (^tests/mock/|^tests/integration/|^tests/fixtures)
id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-added-large-files
- args:
- --pytest-test-first
Expand All @@ -21,25 +19,6 @@ repos:
- id: add-trailing-comma
repo: https://github.com/asottile/add-trailing-comma
rev: v3.1.0
- hooks:
- args:
- --autofix
- --indent
- '2'
files: ^.*\.yaml$
id: pretty-format-yaml
- args:
- --autofix
- --indent
- '2'
id: pretty-format-toml
repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.12.0
- hooks:
- id: toml-sort
- id: toml-sort-fix
repo: https://github.com/pappasam/toml-sort
rev: v0.23.1
- hooks:
- id: pycln
name: pycln
Expand All @@ -61,6 +40,13 @@ repos:
hooks:
- id: pyright
types: [python]
- repo: https://github.com/netromdk/vermin
rev: v1.6.0
hooks:
- id: vermin
args: [--target=3.10-, --violations, --eval-annotations, --backport typing_extensions, --exclude=venv, --exclude=build, --exclude=.git, --exclude=.venv, src, examples, tests]
language: python
additional_dependencies: [vermin]
- repo: local
hooks:
- id: pytest-unit
Expand Down
66 changes: 64 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
<h1>Python CQRS</h1>
<h3>Event-Driven Architecture Framework for Distributed Systems</h3>
<p>
<a href="https://pypi.org/project/python-cqrs/">
<img src="https://img.shields.io/pypi/pyversions/python-cqrs?logo=python&logoColor=white" alt="Python Versions">
</a>
<a href="https://pypi.org/project/python-cqrs/">
<img src="https://img.shields.io/pypi/v/python-cqrs?label=pypi&logo=pypi" alt="PyPI version">
</a>
Expand All @@ -18,6 +21,9 @@
<a href="https://pepy.tech/projects/python-cqrs">
<img src="https://pepy.tech/badge/python-cqrs/month" alt="Downloads per month">
</a>
<a href="https://codecov.io/gh/vadikko2/python-cqrs">
<img src="https://img.shields.io/codecov/c/github/vadikko2/python-cqrs?logo=codecov&logoColor=white" alt="Coverage">
</a>
<a href="https://mkdocs.python-cqrs.dev/">
<img src="https://img.shields.io/badge/docs-mkdocs-blue?logo=readthedocs" alt="Documentation">
</a>
Expand Down Expand Up @@ -52,7 +58,8 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en
11. Parallel event processing with configurable concurrency limits;
12. Chain of Responsibility pattern support with `CORRequestHandler` for processing requests through multiple handlers in sequence;
13. Orchestrated Saga pattern support for managing distributed transactions with automatic compensation and recovery mechanisms;
14. Built-in Mermaid diagram generation, enabling automatic generation of Sequence and Class diagrams for documentation and visualization.
14. Built-in Mermaid diagram generation, enabling automatic generation of Sequence and Class diagrams for documentation and visualization;
15. Flexible Request and Response types support - use Pydantic-based or Dataclass-based implementations, with the ability to mix and match types based on your needs.

## Request Handlers

Expand Down Expand Up @@ -236,6 +243,61 @@ class_diagram = generator.class_diagram()

Complete example: [CoR Mermaid Diagrams](https://github.com/vadikko2/cqrs/blob/master/examples/cor_mermaid.py)

## Request and Response Types

The library supports both Pydantic-based (`PydanticRequest`/`PydanticResponse`, aliased as `Request`/`Response`) and Dataclass-based (`DCRequest`/`DCResponse`) implementations. You can also implement custom classes by implementing the `IRequest`/`IResponse` interfaces directly.

```python
import dataclasses

# Pydantic-based (default)
class CreateUserCommand(cqrs.Request):
username: str
email: str

class UserResponse(cqrs.Response):
user_id: str
username: str

# Dataclass-based
@dataclasses.dataclass
class CreateProductCommand(cqrs.DCRequest):
name: str
price: float

@dataclasses.dataclass
class ProductResponse(cqrs.DCResponse):
product_id: str
name: str

# Custom implementation
class CustomRequest(cqrs.IRequest):
def __init__(self, user_id: str, action: str):
self.user_id = user_id
self.action = action

def to_dict(self) -> dict:
return {"user_id": self.user_id, "action": self.action}

@classmethod
def from_dict(cls, **kwargs) -> "CustomRequest":
return cls(user_id=kwargs["user_id"], action=kwargs["action"])

class CustomResponse(cqrs.IResponse):
def __init__(self, result: str, status: int):
self.result = result
self.status = status

def to_dict(self) -> dict:
return {"result": self.result, "status": self.status}

@classmethod
def from_dict(cls, **kwargs) -> "CustomResponse":
return cls(result=kwargs["result"], status=kwargs["status"])
```

A complete example can be found in [request_response_types.py](https://github.com/vadikko2/cqrs/blob/master/examples/request_response_types.py)

## Saga Pattern

The package implements the Orchestrated Saga pattern for managing distributed transactions across multiple services or operations.
Expand Down Expand Up @@ -871,7 +933,7 @@ async def process_files_stream(
async for result in mediator.stream(command):
sse_data = {
"type": "progress",
"data": result.model_dump(),
"data": result.to_dict(),
}
yield f"data: {json.dumps(sse_data)}\n\n"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@
from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from typing import Generic, Optional, Self, TypeVar
import sys
from typing import Generic, Optional, TypeVar

if sys.version_info >= (3, 11):
from typing import Self # novm
else:
from typing_extensions import Self

import uvicorn

Expand Down
2 changes: 1 addition & 1 deletion examples/kafka_event_consuming.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,6 @@ async def hello_world_event_handler(
)
print(
f"1. Run kafka infrastructure with: `docker compose -f ./docker-compose-dev.yml up -d`\n"
f"2. Send to kafka topic `hello_world` event: {orjson.dumps(ev.model_dump(mode='json')).decode()}",
f"2. Send to kafka topic `hello_world` event: {orjson.dumps(ev.to_dict()).decode()}",
)
asyncio.run(app.run())
Loading
Loading