omnixys-outbox
Transactional outbox for Omnixys services: reliably persist domain events in the same transaction as your aggregates, then publish them to Kafka (transactionally-safe, exactly-once-at-least-once) without losing events when the broker or process fails.
Installation
pip install omnixys-outbox
Features
- OutboxMessageModel — SQLAlchemy ORM model (
outbox_messagestable) withJSONBpayload, status/retry columns, and indexes for claiming and tracing. - OutboxRepository —
add_event(),claim_batch()(skips locked rows, marks claimed rowsprocessing),mark_processed(),mark_failed(),count_pending(). - OutboxProcessor — claims a batch, publishes each message through an
OutboxPublisher, and marks it processed or failed accordingly. - OutboxPublisher — a
Protocol(publish_raw) so any Kafka/HTTP publisher can be plugged in. - Dishka wiring —
OutboxProviderprovides a request-scoped repository and processor.
Quick start
Persist the event inside your business transaction:
from outbox import OutboxRepository
async def create_account(session, repository: OutboxRepository, account, request_id) -> None:
await repository.add_event(
aggregate_type="account",
aggregate_id=account.id,
event_type="account.created",
topic="omnixys.account.events",
payload={"id": str(account.id), "email": account.email},
correlation_id=request_id,
)
await session.commit() # event and aggregate are committed atomically
Publish pending events with a background worker:
from outbox import OutboxProcessor
async def worker_loop(processor: OutboxProcessor) -> None:
while True:
processed = await processor.process_batch(limit=100, max_retries=5)
await asyncio.sleep(0.1 if processed else 1)
process_batch claims up to limit messages with
retry_count < max_retries, publishes each, and returns how many were
processed successfully.
Publish protocol
Any object with an async publish_raw method can act as the publisher:
from outbox import OutboxPublisher
class KafkaPublisher(OutboxPublisher):
async def publish_raw(
self,
topic: str,
value: bytes,
key: str | None = None,
headers: list[tuple[str, bytes]] | None = None,
) -> None:
...
The processor publishes json.dumps(payload, default=str).encode("utf-8")
with key=str(aggregate_id) and headers x-event-type (and
x-correlation-id when present). Failed publishes do not raise: the message is
marked failed and its retry_count incremented so the next claim can retry
it.
Repository
from outbox import OutboxRepository
# claim a batch for processing (SELECT ... FOR UPDATE SKIP LOCKED)
messages = await repository.claim_batch(limit=100, max_retries=5)
# update after the processor publishes
await repository.mark_processed(message.id)
# or
await repository.mark_failed(message.id, "Publishing failed")
pending = await repository.count_pending()
claim_batch only selects rows with status pending/failed and
retry_count < max_retries, ordered by created_at ascending. Claimed rows
are set to processing; failed messages keep their error message (truncated to
4000 chars) and a bumped retry_count.
Statuses
OutboxMessageStatus is a StrEnum:
| Value | Meaning |
|---|---|
pending |
persisted, not yet claimed |
processing |
claimed, being published |
processed |
published successfully |
failed |
publish failed, eligible for retry |
Dependency injection
from dishka import Provider, Scope, make_async_container
from outbox import OutboxProcessor, OutboxPublisher, OutboxRepository
from outbox.container import OutboxProvider
class OuterProvider(Provider):
@provide(scope=Scope.REQUEST)
def session(self) -> AsyncSession:
...
@provide(scope=Scope.REQUEST)
def publisher(self) -> OutboxPublisher:
return KafkaPublisher(...)
container = make_async_container(OutboxProvider(), OuterProvider(...))
async with container(scope=Scope.REQUEST) as request:
repository = await request.get(OutboxRepository)
processor = await request.get(OutboxProcessor)
Development
uv sync
uv run pytest -q
uv run ruff check .
uv run mypy src/
License
GPL-3.0-or-later
Metadata
Release files for omnixys-outbox 4.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| omnixys_outbox-4.0.0.tar.gz | 46.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| omnixys_outbox-4.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 53.5 kB
Release files / omnixys_outbox-4.0.0.tar.gz
| Download URL | omnixys_outbox-4.0.0.tar.gz |
|---|---|
| Size | 46.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
27c420c1d0849bf4d5d708be1532fdb2f0142cb779a1c4b4c2df9d53543e529a
|
|
BLAKE2b-256 checksum How to use checksums |
47806ab1c04151079c08c26604b88e093103ba9859264d3471b816be4887e5e8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.8 {"installer":{"name":"uv","version":"0.12.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / omnixys_outbox-4.0.0-py3-none-any.whl
| Download URL | omnixys_outbox-4.0.0-py3-none-any.whl |
|---|---|
| Size | 7.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
726a5a7ea763169f0fe2d51176f61cde19abe8b5aa28c20997ccbeb25cb45ae0
|
|
BLAKE2b-256 checksum How to use checksums |
4e574a090d15d816566ded05db25c519dece16baae583a24db3b3893a88cb4e5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.8 {"installer":{"name":"uv","version":"0.12.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|