Skip to content
Closed
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
6 changes: 3 additions & 3 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [ "3.10", "3.11", "3.12" ]
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ]

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v3
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand All @@ -28,6 +28,6 @@ jobs:
- name: Build package
run: python -m build
- name: Publish package
if: success() && github.event_name == 'release' && matrix.python-version == '3.12'
if: success() && github.event_name == 'release' && matrix.python-version == '3.14'
run: |
twine upload dist/* --username __token__ --password ${{ secrets.PYPI_API_TOKEN }}
26 changes: 22 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,19 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
include:
- python-version: "3.9"
pyright-version: "3.9"
- python-version: "3.10"
pyright-version: "3.10"
- python-version: "3.11"
pyright-version: "3.11"
- python-version: "3.12"
pyright-version: "3.12"
- python-version: "3.13"
pyright-version: "3.13"
- python-version: "3.14"
pyright-version: "3.14"

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -62,19 +74,25 @@ jobs:

- name: Run pyright
run: |
pyright --pythonversion ${{ matrix.python-version }} src tests examples
pyright --pythonversion ${{ matrix.pyright-version }} src

- 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
vermin --target=3.9- --violations --eval-annotations --backport typing_extensions --exclude=venv --exclude=build --exclude=.git --exclude=.venv src examples tests

test:
name: test (py ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12"]
include:
- python-version: "3.9"
- python-version: "3.10"
- python-version: "3.11"
- python-version: "3.12"
- python-version: "3.13"
- python-version: "3.14"

steps:
- uses: actions/checkout@v4
Expand Down
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ repos:
hooks:
- id: pytest-unit
name: unit tests
entry: pytest -c ./tests/pytest-config.ini ./tests/unit
entry: env PYTHONPATH=src ./venv/bin/python -m pytest -c ./tests/pytest-config.ini ./tests/unit
language: system
types: [python]
pass_filenames: false
always_run: true
- id: pytest-integration
name: integration tests
entry: pytest -c ./tests/pytest-config.ini ./tests/integration
entry: env PYTHONPATH=src ./venv/bin/python -m pytest -c ./tests/pytest-config.ini ./tests/integration
language: system
types: [python]
pass_filenames: false
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<h1>Python CQRS</h1>
<p><strong>Event-Driven Architecture Framework for Distributed Systems</strong></p>
<p>
<strong>Python 3.10+</strong> · Full documentation: <a href="https://mkdocs.python-cqrs.dev/">mkdocs.python-cqrs.dev</a>
<strong>Python 3.9+</strong> · Full documentation: <a href="https://mkdocs.python-cqrs.dev/">mkdocs.python-cqrs.dev</a>
</p>
<p>
<a href="https://pypi.org/project/python-cqrs/">
Expand Down Expand Up @@ -88,7 +88,7 @@ project ([documentation](https://akhundmurad.github.io/diator/)) with several en

## Installation

**Python 3.10+** is required.
**Python 3.9+** is required. CI runs on Python **3.9-3.14**.

```bash
pip install python-cqrs
Expand Down
1 change: 0 additions & 1 deletion docker-compose-dev.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3'
services:
mysql_dev:
image: mysql:8.3.0
Expand Down
10 changes: 5 additions & 5 deletions examples/cor_mermaid.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class PaymentResult(cqrs.Response):
"""Payment processing result."""

success: bool
transaction_id: str | None = None
transaction_id: typing.Optional[str] = None
message: str = ""


Expand Down Expand Up @@ -109,7 +109,7 @@ def __init__(self) -> None:
def events(self) -> typing.List[Event]:
return self._events.copy()

async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
async def handle(self, request: ProcessPaymentCommand) -> typing.Optional[PaymentResult]:
"""Process credit card payment."""
if request.payment_method == "credit_card":
transaction_id = f"cc_{request.user_id}_{int(request.amount * 100)}"
Expand Down Expand Up @@ -143,7 +143,7 @@ def __init__(self) -> None:
def events(self) -> typing.List[Event]:
return self._events.copy()

async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
async def handle(self, request: ProcessPaymentCommand) -> typing.Optional[PaymentResult]:
"""Process PayPal payment."""
if request.payment_method == "paypal":
transaction_id = f"pp_{request.user_id}_{int(request.amount * 100)}"
Expand Down Expand Up @@ -177,7 +177,7 @@ def __init__(self) -> None:
def events(self) -> typing.List[Event]:
return self._events.copy()

async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
async def handle(self, request: ProcessPaymentCommand) -> typing.Optional[PaymentResult]:
"""Process bank transfer payment."""
if request.payment_method == "bank_transfer":
transaction_id = f"bt_{request.user_id}_{int(request.amount * 100)}"
Expand Down Expand Up @@ -207,7 +207,7 @@ class DefaultPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResu
def events(self) -> typing.List[Event]:
return []

async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
async def handle(self, request: ProcessPaymentCommand) -> typing.Optional[PaymentResult]:
"""Handle unsupported payment methods."""
return PaymentResult(
success=False,
Expand Down
7 changes: 4 additions & 3 deletions examples/cor_request_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@

import asyncio
import logging
import typing

import di
from di import dependent
Expand Down Expand Up @@ -96,7 +97,7 @@ class SourceAHandler(CORRequestHandler[FetchDataCommand, FetchDataResult]):
def events(self) -> list[cqrs.Event]:
return []

async def handle(self, request: FetchDataCommand) -> FetchDataResult | None:
async def handle(self, request: FetchDataCommand) -> typing.Optional[FetchDataResult]:
if request.source == "a":
logger.info("COR chain: SourceAHandler handled source=a")
HANDLER_SOURCE.append("chain")
Expand All @@ -109,7 +110,7 @@ class SourceBHandler(CORRequestHandler[FetchDataCommand, FetchDataResult]):
def events(self) -> list[cqrs.Event]:
return []

async def handle(self, request: FetchDataCommand) -> FetchDataResult | None:
async def handle(self, request: FetchDataCommand) -> typing.Optional[FetchDataResult]:
if request.source == "b":
logger.info("COR chain: SourceBHandler handled source=b")
HANDLER_SOURCE.append("chain")
Expand All @@ -124,7 +125,7 @@ class DefaultChainHandler(CORRequestHandler[FetchDataCommand, FetchDataResult]):
def events(self) -> list[cqrs.Event]:
return []

async def handle(self, request: FetchDataCommand) -> FetchDataResult | None:
async def handle(self, request: FetchDataCommand) -> typing.Optional[FetchDataResult]:
if request.source == "error":
logger.info("COR chain: DefaultChainHandler raising ConnectionError for source=error")
raise ConnectionError("Downstream service unavailable")
Expand Down
10 changes: 5 additions & 5 deletions examples/cor_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class ProcessPaymentCommand(cqrs.Request):

class PaymentResult(cqrs.Response):
success: bool
transaction_id: str | None = None
transaction_id: typing.Optional[str] = None
message: str = ""


Expand All @@ -101,7 +101,7 @@ def __init__(self) -> None:
super().__init__()
self._events: typing.List[cqrs.Event] = []

async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
async def handle(self, request: ProcessPaymentCommand) -> typing.Optional[PaymentResult]:
if request.payment_method == "credit_card":
transaction_id = f"cc_{request.user_id}_{int(request.amount * 100)}"
TRANSACTIONS["credit_card"].append(transaction_id)
Expand Down Expand Up @@ -134,7 +134,7 @@ def __init__(self) -> None:
super().__init__()
self._events: typing.List[cqrs.Event] = []

async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
async def handle(self, request: ProcessPaymentCommand) -> typing.Optional[PaymentResult]:
if request.payment_method == "paypal":
transaction_id = f"pp_{request.user_id}_{int(request.amount * 100)}"
TRANSACTIONS["paypal"].append(transaction_id)
Expand Down Expand Up @@ -167,7 +167,7 @@ def __init__(self) -> None:
super().__init__()
self._events: typing.List[cqrs.Event] = []

async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
async def handle(self, request: ProcessPaymentCommand) -> typing.Optional[PaymentResult]:
if request.payment_method == "bank_transfer":
transaction_id = f"bt_{request.user_id}_{int(request.amount * 100)}"
TRANSACTIONS["bank_transfer"].append(transaction_id)
Expand Down Expand Up @@ -198,7 +198,7 @@ class DefaultPaymentHandler(CORRequestHandler[ProcessPaymentCommand, PaymentResu
def events(self) -> typing.List[cqrs.Event]:
return []

async def handle(self, request: ProcessPaymentCommand) -> PaymentResult | None:
async def handle(self, request: ProcessPaymentCommand) -> typing.Optional[PaymentResult]:
# Default handler always handles the request (end of chain)
print(
f"Default: Unsupported payment method '{request.payment_method}' for user {request.user_id}",
Expand Down
4 changes: 2 additions & 2 deletions examples/kafka_event_consuming.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async def empty_message_decoder(
[kafka.KafkaMessage],
typing.Awaitable[types.DecodedMessage],
],
) -> types.DecodedMessage | None:
) -> typing.Optional[types.DecodedMessage]:
"""
Decode a kafka message and return it if it is not empty.
"""
Expand Down Expand Up @@ -158,7 +158,7 @@ def mediator_factory() -> cqrs.EventMediator:
decoder=empty_message_decoder,
)
async def hello_world_event_handler(
body: cqrs.NotificationEvent[HelloWorldPayload] | deserializers.DeserializeJsonError | None,
body: typing.Union[cqrs.NotificationEvent[HelloWorldPayload], typing.Optional[deserializers.DeserializeJsonError]],
msg: kafka.KafkaMessage,
mediator: cqrs.EventMediator = faststream.Depends(mediator_factory),
):
Expand Down
7 changes: 4 additions & 3 deletions examples/saga.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
import asyncio
import dataclasses
import logging
import typing
import uuid

import di
Expand Down Expand Up @@ -119,9 +120,9 @@ class OrderContext(SagaContext):
shipping_address: str

# These fields are populated by steps during execution
inventory_reservation_id: str | None = None
payment_id: str | None = None
shipment_id: str | None = None
inventory_reservation_id: typing.Optional[str] = None
payment_id: typing.Optional[str] = None
shipment_id: typing.Optional[str] = None


# ============================================================================
Expand Down
3 changes: 2 additions & 1 deletion examples/saga_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import asyncio
import dataclasses
import logging
import typing
import uuid

import di
Expand Down Expand Up @@ -104,7 +105,7 @@ class OrderContext(SagaContext):
amount: float

# This field is populated by step during execution
reservation_id: str | None = None
reservation_id: typing.Optional[str] = None


# ============================================================================
Expand Down
8 changes: 4 additions & 4 deletions examples/saga_fastapi_sse.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ class OrderContext(SagaContext):
total_amount: float
shipping_address: str

inventory_reservation_id: str | None = None
payment_id: str | None = None
shipment_id: str | None = None
inventory_reservation_id: typing.Optional[str] = None
payment_id: typing.Optional[str] = None
shipment_id: typing.Optional[str] = None


class ProcessOrderRequest(pydantic.BaseModel):
Expand Down Expand Up @@ -448,7 +448,7 @@ def mediator_factory() -> cqrs.SagaMediator:
)


def serialize_response(response: Response | None) -> dict[str, typing.Any]:
def serialize_response(response: typing.Optional[Response]) -> dict[str, typing.Any]:
if response is None:
return {}
return response.to_dict()
Expand Down
6 changes: 3 additions & 3 deletions examples/saga_mermaid.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ class OrderContext(SagaContext):
total_amount: float
shipping_address: str

inventory_reservation_id: str | None = None
payment_id: str | None = None
shipment_id: str | None = None
inventory_reservation_id: typing.Optional[str] = None
payment_id: typing.Optional[str] = None
shipment_id: typing.Optional[str] = None


class ReserveInventoryResponse(Response):
Expand Down
6 changes: 3 additions & 3 deletions examples/saga_recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ class OrderContext(SagaContext):
shipping_address: str

# These fields are populated by steps during execution
inventory_reservation_id: str | None = None
payment_id: str | None = None
shipment_id: str | None = None
inventory_reservation_id: typing.Optional[str] = None
payment_id: typing.Optional[str] = None
shipment_id: typing.Optional[str] = None


# ============================================================================
Expand Down
8 changes: 4 additions & 4 deletions examples/saga_recovery_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ class OrderContext(SagaContext):
total_amount: float
shipping_address: str

inventory_reservation_id: str | None = None
payment_id: str | None = None
shipment_id: str | None = None
inventory_reservation_id: typing.Optional[str] = None
payment_id: typing.Optional[str] = None
shipment_id: typing.Optional[str] = None


# ============================================================================
Expand Down Expand Up @@ -536,7 +536,7 @@ async def recovery_loop(
storage: ISagaStorage,
*,
interval_seconds: float = RECOVERY_INTERVAL_SECONDS,
max_iterations: int | None = None,
max_iterations: typing.Optional[int] = None,
) -> None:
"""
Run the recovery scheduler loop.
Expand Down
11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ classifiers = [
"Development Status :: 4 - Beta",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12"
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14"
]
dependencies = [
"dataclass-wizard==0.*",
Expand All @@ -22,15 +26,14 @@ dependencies = [
"orjson==3.*",
"pydantic==2.*",
"sqlalchemy[asyncio]==2.0.*",
"python-dotenv==1.*",
"retry-async==0.1.*",
"python-dotenv>=0.21,<2",
"typing-extensions>=4.0"
]
description = "Event-Driven Architecture Framework for Distributed Systems"
maintainers = [{name = "Vadim Kozyrevskiy", email = "vadikko2@mail.ru"}]
name = "python-cqrs"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.9,<3.15"
version = "4.10.1"

[project.optional-dependencies]
Expand Down
Loading
Loading