acemq_amqp.interceptors

The seam around publishing and handling, for the work that surrounds them.

Every organisation has something every message needs and no library can guess: a tenant identifier, a trace context, an authorisation token, a schema version, a correlation identifier in the logging context, a timer, a size limit. Without a seam these end up copied into every call site, where one of them is eventually forgotten and nobody finds out until the message that needed it is the one that went without it.

An interceptor is one function that is handed the message and the rest of the work::

async def timed(context: ConsumeContext, handle: ConsumeNext) -> Ack:
    started = time.monotonic()
    try:
        return await handle(context)
    finally:
        log.info("%s took %.3fs", context.queue, time.monotonic() - started)

mq.intercept(timed)

That shape rather than the pair of before-and-after hooks the Java library uses, because Python already has the construct: try/finally runs the way out in the reverse of the way in, nests correctly without anybody having to reverse a list, and makes an interceptor that opens something and closes it one function instead of two halves that have to agree. It composes the way ASGI middleware does, which is the arrangement a Python programmer already knows.

Interceptors run in the order they were registered: the first registered is the outermost, so it sees the message first on the way in and last on the way out. That matters as soon as one of them reads what another wrote.

Refusing is raising. A publish interceptor that raises stops the publish and the caller sees the exception, which is the whole point of being able to intercept rather than only observe: a message that must not go out can be stopped in one place rather than in every publisher. A consume interceptor that raises is treated exactly as a handler that raised — retried, and then dead-lettered — because the alternative is acknowledging a message nothing processed.

What a handler answered is not what happened to the message. A handler asking for another attempt when there are none left is dead-lettered, and an interceptor that wants to record how a delivery really ended has to wait for the consumer to say — ConsumeContext.when_settled() is where it waits, and ~acemq_amqp.ack.Settlement is what it is told.

Everything here is built out of the public API and needs no private access. An interceptor sees the envelope, the payload, the destination and the body, and may change any of them; that is the same rule the patterns in acemq_amqp.patterns follow, and it is what keeps this a seam rather than a privileged back door.

  1# Copyright 2026 AceMQ.
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     https://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""The seam around publishing and handling, for the work that surrounds them.
 16
 17Every organisation has something every message needs and no library can guess: a
 18tenant identifier, a trace context, an authorisation token, a schema version, a
 19correlation identifier in the logging context, a timer, a size limit. Without a
 20seam these end up copied into every call site, where one of them is eventually
 21forgotten and nobody finds out until the message that needed it is the one that
 22went without it.
 23
 24An interceptor is one function that is handed the message and the rest of the
 25work::
 26
 27    async def timed(context: ConsumeContext, handle: ConsumeNext) -> Ack:
 28        started = time.monotonic()
 29        try:
 30            return await handle(context)
 31        finally:
 32            log.info("%s took %.3fs", context.queue, time.monotonic() - started)
 33
 34    mq.intercept(timed)
 35
 36That shape rather than the pair of before-and-after hooks the Java library uses,
 37because Python already has the construct: ``try``/``finally`` runs the way out
 38in the reverse of the way in, nests correctly without anybody having to reverse
 39a list, and makes an interceptor that opens something and closes it one function
 40instead of two halves that have to agree. It composes the way ASGI middleware
 41does, which is the arrangement a Python programmer already knows.
 42
 43Interceptors run in the order they were registered: the first registered is the
 44outermost, so it sees the message first on the way in and last on the way out.
 45That matters as soon as one of them reads what another wrote.
 46
 47Refusing is raising. A publish interceptor that raises stops the publish and the
 48caller sees the exception, which is the whole point of being able to intercept
 49rather than only observe: a message that must not go out can be stopped in one
 50place rather than in every publisher. A consume interceptor that raises is
 51treated exactly as a handler that raised — retried, and then dead-lettered —
 52because the alternative is acknowledging a message nothing processed.
 53
 54What a handler answered is not what happened to the message. A handler asking
 55for another attempt when there are none left is dead-lettered, and an
 56interceptor that wants to record how a delivery really ended has to wait for the
 57consumer to say — :meth:`ConsumeContext.when_settled` is where it waits, and
 58:class:`~acemq_amqp.ack.Settlement` is what it is told.
 59
 60Everything here is built out of the public API and needs no private access. An
 61interceptor sees the envelope, the payload, the destination and the body, and
 62may change any of them; that is the same rule the patterns in
 63:mod:`acemq_amqp.patterns` follow, and it is what keeps this a seam rather than
 64a privileged back door.
 65"""
 66
 67from __future__ import annotations
 68
 69import logging
 70from collections.abc import Awaitable, Callable, Sequence
 71from dataclasses import dataclass, field
 72from typing import Any, TypeAlias
 73
 74from .ack import Ack, Settlement
 75from .envelope import Envelope
 76from .transport import PublishResult
 77
 78log = logging.getLogger("acemq")
 79
 80#: Told what the consumer did with a delivery, once it had decided and before it
 81#: had done it. Registered with :meth:`ConsumeContext.when_settled`.
 82SettlementListener: TypeAlias = Callable[[Settlement], None]
 83
 84
 85@dataclass(slots=True)
 86class PublishContext:
 87    """A message on its way out, before it is encoded.
 88
 89    Mutable on purpose, and mutable rather than rebuilt-and-returned: an
 90    interceptor that only wants to add one header should not have to reconstruct
 91    the rest, and one that reads what an earlier interceptor wrote needs to be
 92    looking at the same object rather than at a copy of an earlier version.
 93
 94    :param exchange: where it is going. Changing it redirects the message
 95    :param routing_key: what it is published under
 96    :param envelope: the metadata. Reserved header names are still refused when
 97        the envelope is rendered, whoever wrote them
 98    :param payload: what is about to be encoded, before the codec sees it, so an
 99        interceptor can change the message and not only its metadata
100    :param persistent: whether the broker is asked to write it to disk
101    :param mandatory: whether reaching no queue is an error rather than a
102        silence
103    :param reply_to: where an answer should be sent, written to AMQP's own
104        ``reply-to`` property. Empty for anything that is not a question
105    """
106
107    exchange: str
108    routing_key: str
109    envelope: Envelope
110    payload: Any
111    persistent: bool = True
112    mandatory: bool = False
113    reply_to: str = ""
114
115    def set_header(self, name: str, value: Any) -> None:
116        """Adds an application header to the message being published.
117
118        A convenience over rebuilding the envelope, which is what the three
119        commonest interceptors — a tenant, a trace context, a schema version —
120        all want to do and nothing else.
121        """
122        self.envelope = self.envelope.with_(headers={**self.envelope.headers, name: value})
123
124
125@dataclass(slots=True)
126class ConsumeContext:
127    """A message that has arrived and been decoded, before the handler sees it.
128
129    :param queue: what it was read from
130    :param envelope: the metadata, as it will reach the handler. Changing it
131        changes what the handler is given
132    :param payload: the decoded body, likewise
133    :param body: the undecoded bytes, for an interceptor that wants to see what
134        actually arrived
135    :param content_type: what the sender said the body was, or ``None``
136    :param routing_key: the key it arrived under
137    :param redelivered: the broker saying it has handed this one over before
138    :param state: somewhere for an interceptor to leave something for a later
139        one, or for its own way out. Empty, and this library never reads it
140    :param reports_settlement: whether whatever is driving this delivery will
141        say how it was settled. True when a consumer is, and false when an
142        interceptor chain is being run by something else — a test, or a caller
143        composing the chain by hand
144    :param reply_to: AMQP's own ``reply-to`` property as it arrived, empty when
145        the sender set none
146    """
147
148    queue: str
149    envelope: Envelope
150    payload: Any
151    body: bytes
152    content_type: str | None
153    routing_key: str
154    redelivered: bool
155    state: dict[str, Any] = field(default_factory=dict)
156    reports_settlement: bool = False
157    reply_to: str = ""
158    _settlement_listeners: list[SettlementListener] = field(
159        default_factory=list, repr=False
160    )
161
162    def when_settled(self, listener: SettlementListener) -> bool:
163        """Asks to be told how this delivery was settled.
164
165        What the handler answered is not what happened to the message: a
166        handler asking for a retry with no attempts left is dead-lettered, and
167        an interceptor that read only the :class:`~acemq_amqp.ack.Ack` would
168        report a retry that never happened. That gap is exactly what somebody
169        querying a trace backend for dead letters falls into, so the consumer
170        reports its decision here and the tracing adapter waits for it.
171
172        The listener is called once, on the consumer's task, after the decision
173        and before it is carried out. It should not block and must not raise;
174        one that does is logged and otherwise ignored, because a delivery still
175        has to be settled when whoever was watching it breaks.
176
177        :param listener: called with the :class:`~acemq_amqp.ack.Settlement`
178        :returns: whether anything will ever call it. ``False`` when this
179            delivery is not being driven by a consumer, in which case the
180            caller should not wait for an answer that is not coming
181        """
182        if not self.reports_settlement:
183            return False
184        self._settlement_listeners.append(listener)
185        return True
186
187    def settled(self, settlement: Settlement) -> None:
188        """Tells the listeners what happened. Called by the consumer.
189
190        :param settlement: what the consumer decided to do with the delivery
191        """
192        for listener in self._settlement_listeners:
193            try:
194                listener(settlement)
195            except Exception:
196                # The delivery still has to be settled. A listener is an
197                # observer, and an observer that breaks must not take the
198                # message with it.
199                log.exception(
200                    "acemq: a settlement listener failed for a delivery from %s", self.queue
201                )
202
203
204#: The rest of a publish: the next interceptor, or the publish itself.
205PublishNext: TypeAlias = Callable[[PublishContext], Awaitable[PublishResult]]
206
207#: The rest of a delivery: the next interceptor, or the handler itself.
208ConsumeNext: TypeAlias = Callable[[ConsumeContext], Awaitable[Ack]]
209
210#: Runs around every publish on a connection.
211#:
212#: Call ``send`` to carry on and return what it returns; do not call it to
213#: refuse, and raise to make the refusal the caller's problem rather than a
214#: message that quietly went nowhere.
215PublishInterceptor: TypeAlias = Callable[
216    [PublishContext, PublishNext], Awaitable[PublishResult]
217]
218
219#: Runs around every handler on a connection.
220#:
221#: Call ``handle`` to carry on. Raising means the handler never runs and the
222#: delivery is treated as a failed one, which is the honest outcome: the
223#: alternative is acknowledging something nothing processed.
224ConsumeInterceptor: TypeAlias = Callable[[ConsumeContext, ConsumeNext], Awaitable[Ack]]
225
226
227def publish_chain(
228    interceptors: Sequence[PublishInterceptor], publish: PublishNext
229) -> PublishNext:
230    """Folds interceptors around a publish, first registered outermost.
231
232    Built backwards because that is what nesting is: the last interceptor wraps
233    the publish, the one before wraps that, and the first ends up on the
234    outside, seeing the message before anybody and its result after everybody.
235
236    :param interceptors: what to wrap it in, in registration order
237    :param publish: the innermost work, which actually sends the message
238    :returns: the whole thing as one callable, or ``publish`` when there is
239        nothing to wrap it in
240    """
241    composed = publish
242    for interceptor in reversed(interceptors):
243        composed = _publish_step(interceptor, composed)
244    return composed
245
246
247def consume_chain(
248    interceptors: Sequence[ConsumeInterceptor], handle: ConsumeNext
249) -> ConsumeNext:
250    """Folds interceptors around a handler, first registered outermost.
251
252    :param interceptors: what to wrap it in, in registration order
253    :param handle: the innermost work, which runs the handler
254    :returns: the whole thing as one callable, or ``handle`` when there is
255        nothing to wrap it in
256    """
257    composed = handle
258    for interceptor in reversed(interceptors):
259        composed = _consume_step(interceptor, composed)
260    return composed
261
262
263def _publish_step(interceptor: PublishInterceptor, rest: PublishNext) -> PublishNext:
264    """One link, bound in a function of its own.
265
266    Not a closure written inline in the loop above: a closure made in a loop
267    captures the variable and not its value, so every link would end up calling
268    the last interceptor. It is the oldest bug in Python and it would show up
269    here as interceptors that all appear to run and only one of which does.
270    """
271
272    async def step(context: PublishContext) -> PublishResult:
273        return await interceptor(context, rest)
274
275    return step
276
277
278def _consume_step(interceptor: ConsumeInterceptor, rest: ConsumeNext) -> ConsumeNext:
279    async def step(context: ConsumeContext) -> Ack:
280        return await interceptor(context, rest)
281
282    return step
log = <Logger acemq (WARNING)>
SettlementListener: TypeAlias = Callable[[acemq_amqp.Settlement], None]
@dataclass(slots=True)
class PublishContext:
 86@dataclass(slots=True)
 87class PublishContext:
 88    """A message on its way out, before it is encoded.
 89
 90    Mutable on purpose, and mutable rather than rebuilt-and-returned: an
 91    interceptor that only wants to add one header should not have to reconstruct
 92    the rest, and one that reads what an earlier interceptor wrote needs to be
 93    looking at the same object rather than at a copy of an earlier version.
 94
 95    :param exchange: where it is going. Changing it redirects the message
 96    :param routing_key: what it is published under
 97    :param envelope: the metadata. Reserved header names are still refused when
 98        the envelope is rendered, whoever wrote them
 99    :param payload: what is about to be encoded, before the codec sees it, so an
100        interceptor can change the message and not only its metadata
101    :param persistent: whether the broker is asked to write it to disk
102    :param mandatory: whether reaching no queue is an error rather than a
103        silence
104    :param reply_to: where an answer should be sent, written to AMQP's own
105        ``reply-to`` property. Empty for anything that is not a question
106    """
107
108    exchange: str
109    routing_key: str
110    envelope: Envelope
111    payload: Any
112    persistent: bool = True
113    mandatory: bool = False
114    reply_to: str = ""
115
116    def set_header(self, name: str, value: Any) -> None:
117        """Adds an application header to the message being published.
118
119        A convenience over rebuilding the envelope, which is what the three
120        commonest interceptors — a tenant, a trace context, a schema version —
121        all want to do and nothing else.
122        """
123        self.envelope = self.envelope.with_(headers={**self.envelope.headers, name: value})

A message on its way out, before it is encoded.

Mutable on purpose, and mutable rather than rebuilt-and-returned: an interceptor that only wants to add one header should not have to reconstruct the rest, and one that reads what an earlier interceptor wrote needs to be looking at the same object rather than at a copy of an earlier version.

Parameters
  • exchange: where it is going. Changing it redirects the message
  • routing_key: what it is published under
  • envelope: the metadata. Reserved header names are still refused when the envelope is rendered, whoever wrote them
  • payload: what is about to be encoded, before the codec sees it, so an interceptor can change the message and not only its metadata
  • persistent: whether the broker is asked to write it to disk
  • mandatory: whether reaching no queue is an error rather than a silence
  • reply_to: where an answer should be sent, written to AMQP's own reply-to property. Empty for anything that is not a question
PublishContext( exchange: str, routing_key: str, envelope: acemq_amqp.Envelope, payload: Any, persistent: bool = True, mandatory: bool = False, reply_to: str = '')
exchange: str
routing_key: str
payload: Any
persistent: bool
mandatory: bool
reply_to: str
def set_header(self, name: str, value: Any) -> None:
116    def set_header(self, name: str, value: Any) -> None:
117        """Adds an application header to the message being published.
118
119        A convenience over rebuilding the envelope, which is what the three
120        commonest interceptors — a tenant, a trace context, a schema version —
121        all want to do and nothing else.
122        """
123        self.envelope = self.envelope.with_(headers={**self.envelope.headers, name: value})

Adds an application header to the message being published.

A convenience over rebuilding the envelope, which is what the three commonest interceptors — a tenant, a trace context, a schema version — all want to do and nothing else.

@dataclass(slots=True)
class ConsumeContext:
126@dataclass(slots=True)
127class ConsumeContext:
128    """A message that has arrived and been decoded, before the handler sees it.
129
130    :param queue: what it was read from
131    :param envelope: the metadata, as it will reach the handler. Changing it
132        changes what the handler is given
133    :param payload: the decoded body, likewise
134    :param body: the undecoded bytes, for an interceptor that wants to see what
135        actually arrived
136    :param content_type: what the sender said the body was, or ``None``
137    :param routing_key: the key it arrived under
138    :param redelivered: the broker saying it has handed this one over before
139    :param state: somewhere for an interceptor to leave something for a later
140        one, or for its own way out. Empty, and this library never reads it
141    :param reports_settlement: whether whatever is driving this delivery will
142        say how it was settled. True when a consumer is, and false when an
143        interceptor chain is being run by something else — a test, or a caller
144        composing the chain by hand
145    :param reply_to: AMQP's own ``reply-to`` property as it arrived, empty when
146        the sender set none
147    """
148
149    queue: str
150    envelope: Envelope
151    payload: Any
152    body: bytes
153    content_type: str | None
154    routing_key: str
155    redelivered: bool
156    state: dict[str, Any] = field(default_factory=dict)
157    reports_settlement: bool = False
158    reply_to: str = ""
159    _settlement_listeners: list[SettlementListener] = field(
160        default_factory=list, repr=False
161    )
162
163    def when_settled(self, listener: SettlementListener) -> bool:
164        """Asks to be told how this delivery was settled.
165
166        What the handler answered is not what happened to the message: a
167        handler asking for a retry with no attempts left is dead-lettered, and
168        an interceptor that read only the :class:`~acemq_amqp.ack.Ack` would
169        report a retry that never happened. That gap is exactly what somebody
170        querying a trace backend for dead letters falls into, so the consumer
171        reports its decision here and the tracing adapter waits for it.
172
173        The listener is called once, on the consumer's task, after the decision
174        and before it is carried out. It should not block and must not raise;
175        one that does is logged and otherwise ignored, because a delivery still
176        has to be settled when whoever was watching it breaks.
177
178        :param listener: called with the :class:`~acemq_amqp.ack.Settlement`
179        :returns: whether anything will ever call it. ``False`` when this
180            delivery is not being driven by a consumer, in which case the
181            caller should not wait for an answer that is not coming
182        """
183        if not self.reports_settlement:
184            return False
185        self._settlement_listeners.append(listener)
186        return True
187
188    def settled(self, settlement: Settlement) -> None:
189        """Tells the listeners what happened. Called by the consumer.
190
191        :param settlement: what the consumer decided to do with the delivery
192        """
193        for listener in self._settlement_listeners:
194            try:
195                listener(settlement)
196            except Exception:
197                # The delivery still has to be settled. A listener is an
198                # observer, and an observer that breaks must not take the
199                # message with it.
200                log.exception(
201                    "acemq: a settlement listener failed for a delivery from %s", self.queue
202                )

A message that has arrived and been decoded, before the handler sees it.

Parameters
  • queue: what it was read from
  • envelope: the metadata, as it will reach the handler. Changing it changes what the handler is given
  • payload: the decoded body, likewise
  • body: the undecoded bytes, for an interceptor that wants to see what actually arrived
  • content_type: what the sender said the body was, or None
  • routing_key: the key it arrived under
  • redelivered: the broker saying it has handed this one over before
  • state: somewhere for an interceptor to leave something for a later one, or for its own way out. Empty, and this library never reads it
  • reports_settlement: whether whatever is driving this delivery will say how it was settled. True when a consumer is, and false when an interceptor chain is being run by something else — a test, or a caller composing the chain by hand
  • reply_to: AMQP's own reply-to property as it arrived, empty when the sender set none
ConsumeContext( queue: str, envelope: acemq_amqp.Envelope, payload: Any, body: bytes, content_type: str | None, routing_key: str, redelivered: bool, state: dict[str, typing.Any] = <factory>, reports_settlement: bool = False, reply_to: str = '', _settlement_listeners: list[Callable[[acemq_amqp.Settlement], None]] = <factory>)
queue: str
payload: Any
body: bytes
content_type: str | None
routing_key: str
redelivered: bool
state: dict[str, typing.Any]
reports_settlement: bool
reply_to: str
def when_settled(self, listener: Callable[[acemq_amqp.Settlement], None]) -> bool:
163    def when_settled(self, listener: SettlementListener) -> bool:
164        """Asks to be told how this delivery was settled.
165
166        What the handler answered is not what happened to the message: a
167        handler asking for a retry with no attempts left is dead-lettered, and
168        an interceptor that read only the :class:`~acemq_amqp.ack.Ack` would
169        report a retry that never happened. That gap is exactly what somebody
170        querying a trace backend for dead letters falls into, so the consumer
171        reports its decision here and the tracing adapter waits for it.
172
173        The listener is called once, on the consumer's task, after the decision
174        and before it is carried out. It should not block and must not raise;
175        one that does is logged and otherwise ignored, because a delivery still
176        has to be settled when whoever was watching it breaks.
177
178        :param listener: called with the :class:`~acemq_amqp.ack.Settlement`
179        :returns: whether anything will ever call it. ``False`` when this
180            delivery is not being driven by a consumer, in which case the
181            caller should not wait for an answer that is not coming
182        """
183        if not self.reports_settlement:
184            return False
185        self._settlement_listeners.append(listener)
186        return True

Asks to be told how this delivery was settled.

What the handler answered is not what happened to the message: a handler asking for a retry with no attempts left is dead-lettered, and an interceptor that read only the ~acemq_amqp.ack.Ack would report a retry that never happened. That gap is exactly what somebody querying a trace backend for dead letters falls into, so the consumer reports its decision here and the tracing adapter waits for it.

The listener is called once, on the consumer's task, after the decision and before it is carried out. It should not block and must not raise; one that does is logged and otherwise ignored, because a delivery still has to be settled when whoever was watching it breaks.

Parameters
  • listener: called with the ~acemq_amqp.ack.Settlement :returns: whether anything will ever call it. False when this delivery is not being driven by a consumer, in which case the caller should not wait for an answer that is not coming
def settled(self, settlement: acemq_amqp.Settlement) -> None:
188    def settled(self, settlement: Settlement) -> None:
189        """Tells the listeners what happened. Called by the consumer.
190
191        :param settlement: what the consumer decided to do with the delivery
192        """
193        for listener in self._settlement_listeners:
194            try:
195                listener(settlement)
196            except Exception:
197                # The delivery still has to be settled. A listener is an
198                # observer, and an observer that breaks must not take the
199                # message with it.
200                log.exception(
201                    "acemq: a settlement listener failed for a delivery from %s", self.queue
202                )

Tells the listeners what happened. Called by the consumer.

Parameters
  • settlement: what the consumer decided to do with the delivery
PublishNext: TypeAlias = Callable[[PublishContext], Awaitable[acemq_amqp.PublishResult]]
ConsumeNext: TypeAlias = Callable[[ConsumeContext], Awaitable[acemq_amqp.Ack]]
PublishInterceptor: TypeAlias = Callable[[PublishContext, Callable[[PublishContext], Awaitable[acemq_amqp.PublishResult]]], Awaitable[acemq_amqp.PublishResult]]
ConsumeInterceptor: TypeAlias = Callable[[ConsumeContext, Callable[[ConsumeContext], Awaitable[acemq_amqp.Ack]]], Awaitable[acemq_amqp.Ack]]
def publish_chain( interceptors: Sequence[Callable[[PublishContext, Callable[[PublishContext], Awaitable[acemq_amqp.PublishResult]]], Awaitable[acemq_amqp.PublishResult]]], publish: Callable[[PublishContext], Awaitable[acemq_amqp.PublishResult]]) -> Callable[[PublishContext], Awaitable[acemq_amqp.PublishResult]]:
228def publish_chain(
229    interceptors: Sequence[PublishInterceptor], publish: PublishNext
230) -> PublishNext:
231    """Folds interceptors around a publish, first registered outermost.
232
233    Built backwards because that is what nesting is: the last interceptor wraps
234    the publish, the one before wraps that, and the first ends up on the
235    outside, seeing the message before anybody and its result after everybody.
236
237    :param interceptors: what to wrap it in, in registration order
238    :param publish: the innermost work, which actually sends the message
239    :returns: the whole thing as one callable, or ``publish`` when there is
240        nothing to wrap it in
241    """
242    composed = publish
243    for interceptor in reversed(interceptors):
244        composed = _publish_step(interceptor, composed)
245    return composed

Folds interceptors around a publish, first registered outermost.

Built backwards because that is what nesting is: the last interceptor wraps the publish, the one before wraps that, and the first ends up on the outside, seeing the message before anybody and its result after everybody.

Parameters
  • interceptors: what to wrap it in, in registration order
  • publish: the innermost work, which actually sends the message :returns: the whole thing as one callable, or publish when there is nothing to wrap it in
def consume_chain( interceptors: Sequence[Callable[[ConsumeContext, Callable[[ConsumeContext], Awaitable[acemq_amqp.Ack]]], Awaitable[acemq_amqp.Ack]]], handle: Callable[[ConsumeContext], Awaitable[acemq_amqp.Ack]]) -> Callable[[ConsumeContext], Awaitable[acemq_amqp.Ack]]:
248def consume_chain(
249    interceptors: Sequence[ConsumeInterceptor], handle: ConsumeNext
250) -> ConsumeNext:
251    """Folds interceptors around a handler, first registered outermost.
252
253    :param interceptors: what to wrap it in, in registration order
254    :param handle: the innermost work, which runs the handler
255    :returns: the whole thing as one callable, or ``handle`` when there is
256        nothing to wrap it in
257    """
258    composed = handle
259    for interceptor in reversed(interceptors):
260        composed = _consume_step(interceptor, composed)
261    return composed

Folds interceptors around a handler, first registered outermost.

Parameters
  • interceptors: what to wrap it in, in registration order
  • handle: the innermost work, which runs the handler :returns: the whole thing as one callable, or handle when there is nothing to wrap it in