Skip to main content

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_messages table) with JSONB payload, status/retry columns, and indexes for claiming and tracing.
  • OutboxRepository — add_event(), claim_batch() (skips locked rows, marks claimed rows processing), 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 — OutboxProvider provides 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)

Source distribution for omnixys-outbox 4.0.0
File Size Uploaded
omnixys_outbox-4.0.0.tar.gz 46.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for omnixys-outbox 4.0.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

4.0.0 This release

2 release files

3.0.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page