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 3.0.2

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 3.0.2
File Size Uploaded
omnixys_outbox-3.0.2.tar.gz 63.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for omnixys-outbox 3.0.2
File Interpreter ABI Platform
omnixys_outbox-3.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 70.3 kB

Release files / omnixys_outbox-3.0.2.tar.gz

Download URL omnixys_outbox-3.0.2.tar.gz
Size 63.1 kB
Tags Source
SHA-256 checksum
How to use checksums
2d81ff5f5ada8bf2bede5ea6553cef43f97f8714d5cae8077cbec28caa33230d
BLAKE2b-256 checksum
How to use checksums
5c9da4134645d5718b6072ebee3e45baa3dc9deb988aa827490759ffeea0bb78
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / omnixys_outbox-3.0.2-py3-none-any.whl

Download URL omnixys_outbox-3.0.2-py3-none-any.whl
Size 7.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b49aa4e19ac3a385cfd93b02ff7655a8bf850b34de7c9f7e088148eb70bf8b76
BLAKE2b-256 checksum
How to use checksums
15f2759bfcc78676fad275da3cc562c4df328eede37892de5515d17771975a86
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","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

4.0.0

2 release files

This release

3.0.2 This release

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