Source code for alchemiq.faststream.publisher
"""FastStream ``Publisher`` adapter for the alchemiq outbox relay."""
from __future__ import annotations
from typing import Any
from alchemiq.outbox.message import OutboxMessage
from alchemiq.outbox.publisher import TransientPublishError
_DEFAULT_TRANSIENT: tuple[type[Exception], ...] = (ConnectionError, OSError, TimeoutError)
[docs]
class FastStreamPublisher:
"""Publish ``OutboxMessage`` objects to any FastStream broker.
Implements the :class:`.Publisher` protocol so it can be wired directly into a
:class:`.Relay`. Works with any FastStream broker (RabbitMQ, Kafka, NATS, Redis)
or any object that exposes the same duck-typed ``publish`` signature::
async def publish(message, destination, *, headers, correlation_id) -> None: ...
The caller owns the broker's connection lifecycle (``connect`` / ``start`` /
``stop`` / ``close``). ``correlation_id`` is set to ``str(message.id)`` - the
dedup key consumers use under the at-least-once delivery contract.
Connection-class errors (the ``transient_errors`` tuple) are re-raised as
:class:`.TransientPublishError` so the relay backs off without burning retry
attempts. Pass broker-specific connection exception classes in
``transient_errors`` without importing them in this module.
E.g.::
from faststream.rabbit import RabbitBroker
from alchemiq.faststream import FastStreamPublisher
from alchemiq import Relay
_broker = RabbitBroker()
relay = Relay(FastStreamPublisher(_broker), batch_size=10)
# custom transient errors:
publisher = FastStreamPublisher(_broker, transient_errors=(BrokerGone,))
:param broker: a started FastStream broker or duck-typed equivalent.
:param transient_errors: exception classes that indicate a transient connection
failure and should be wrapped in :class:`.TransientPublishError`;
defaults to ``(ConnectionError, OSError, TimeoutError)``.
.. seealso:: :class:`.Relay` - drives the outbox drain loop using this publisher.
"""
def __init__(
self,
broker: Any,
*,
transient_errors: tuple[type[Exception], ...] = _DEFAULT_TRANSIENT,
) -> None:
self._broker = broker
self._transient_errors = transient_errors
[docs]
async def publish(self, message: OutboxMessage) -> None:
"""Publish *message* to its ``topic`` via the broker.
Sets ``correlation_id`` to ``str(message.id)`` and forwards all
``alchemiq.*`` metadata headers present on the message. Re-raises
connection-class exceptions as :class:`.TransientPublishError` so the
relay backs off without burning retry attempts.
:param message: the ``OutboxMessage`` to deliver.
:raises TransientPublishError: on connection-class errors listed in
``transient_errors``.
"""
try:
await self._broker.publish(
message.payload,
message.topic,
headers=self._headers(message),
correlation_id=str(message.id),
)
except self._transient_errors as e:
raise TransientPublishError(str(e)) from e
@staticmethod
def _headers(message: OutboxMessage) -> dict[str, Any]:
headers = dict(message.headers or {})
for key, value in (
("alchemiq.aggregate_type", message.aggregate_type),
("alchemiq.aggregate_id", message.aggregate_id),
("alchemiq.event_type", message.event_type),
):
if value is not None:
headers[key] = value
return headers