rabbitmq-amqp-python-client 0.4.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of rabbitmq-amqp-python-client might be problematic. Click here for more details.
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/PKG-INFO +1 -1
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/pyproject.toml +1 -1
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/__init__.py +6 -2
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/amqp_consumer_handler.py +6 -3
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/asyncio/connection.py +2 -2
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/asyncio/consumer.py +16 -4
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/connection.py +2 -2
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/consumer.py +71 -13
- rabbitmq_amqp_python_client-0.5.0/rabbitmq_amqp_python_client/delivery_context.py +219 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/entities.py +118 -19
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/exceptions.py +9 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/options.py +25 -2
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_data.py +1 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_reactor.py +1 -1
- rabbitmq_amqp_python_client-0.4.0/rabbitmq_amqp_python_client/delivery_context.py +0 -114
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/LICENSE +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/README.md +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/address_helper.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/asyncio/__init__.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/asyncio/environment.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/asyncio/management.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/asyncio/publisher.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/common.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/environment.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/management.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/publisher.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/py.typed +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/__init__.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_common.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_condition.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_delivery.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_endpoints.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_events.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_exceptions.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_handler.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_handlers.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_io.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_message.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_selectable.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_tracing.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_transport.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_url.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_utils.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/_wrapper.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/handlers.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/py.typed +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/reactor.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/tracing.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/qpid/proton/utils.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/queues.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/ssl_configuration.py +0 -0
- {rabbitmq_amqp_python_client-0.4.0 → rabbitmq_amqp_python_client-0.5.0}/rabbitmq_amqp_python_client/utils.py +0 -0
|
@@ -13,8 +13,9 @@ from .common import ExchangeType, QueueType
|
|
|
13
13
|
from .connection import Connection
|
|
14
14
|
from .consumer import Consumer
|
|
15
15
|
from .entities import (
|
|
16
|
+
AbcConsumerOptions,
|
|
16
17
|
ConsumerOptions,
|
|
17
|
-
|
|
18
|
+
ConsumerSettleStrategy,
|
|
18
19
|
ExchangeCustomSpecification,
|
|
19
20
|
ExchangeSpecification,
|
|
20
21
|
ExchangeToExchangeBindingSpecification,
|
|
@@ -29,6 +30,7 @@ from .entities import (
|
|
|
29
30
|
from .environment import Environment
|
|
30
31
|
from .exceptions import (
|
|
31
32
|
ArgumentOutOfRangeException,
|
|
33
|
+
InvalidOperationException,
|
|
32
34
|
ValidationCodeException,
|
|
33
35
|
)
|
|
34
36
|
from .management import Management
|
|
@@ -72,8 +74,8 @@ __all__ = [
|
|
|
72
74
|
"ExchangeSpecification",
|
|
73
75
|
"QuorumQueueSpecification",
|
|
74
76
|
"ClassicQueueSpecification",
|
|
77
|
+
"ConsumerSettleStrategy",
|
|
75
78
|
"StreamSpecification",
|
|
76
|
-
"DirectReplyToConsumerOptions",
|
|
77
79
|
"ExchangeToQueueBindingSpecification",
|
|
78
80
|
"ExchangeToExchangeBindingSpecification",
|
|
79
81
|
"QueueType",
|
|
@@ -99,6 +101,7 @@ __all__ = [
|
|
|
99
101
|
"ConnectionClosed",
|
|
100
102
|
"StreamConsumerOptions",
|
|
101
103
|
"StreamFilterOptions",
|
|
104
|
+
"AbcConsumerOptions",
|
|
102
105
|
"ConsumerOptions",
|
|
103
106
|
"MessageProperties",
|
|
104
107
|
"OffsetSpecification",
|
|
@@ -113,4 +116,5 @@ __all__ = [
|
|
|
113
116
|
"AsyncPublisher",
|
|
114
117
|
"AsyncManagement",
|
|
115
118
|
"AsyncEnvironment",
|
|
119
|
+
"InvalidOperationException",
|
|
116
120
|
]
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
from .delivery_context import
|
|
1
|
+
from .delivery_context import (
|
|
2
|
+
AbcDeliveryContext,
|
|
3
|
+
DeliveryContext,
|
|
4
|
+
)
|
|
2
5
|
from .qpid.proton._events import Event
|
|
3
6
|
from .qpid.proton.handlers import MessagingHandler
|
|
4
7
|
|
|
5
8
|
"""
|
|
6
9
|
AMQPMessagingHandler extends the QPID MessagingHandler.
|
|
7
10
|
It is an helper to set the default values needed for manually accepting and settling messages.
|
|
8
|
-
self.delivery_context is an instance of DeliveryContext, which is used to accept, reject,
|
|
11
|
+
self.delivery_context is an instance of IDeliveryContext (defaults to DeliveryContext), which is used to accept, reject,
|
|
9
12
|
requeue or requeue with annotations a message.
|
|
10
13
|
It is not mandatory to use this class, but it is a good practice to use it.
|
|
11
14
|
"""
|
|
@@ -20,7 +23,7 @@ class AMQPMessagingHandler(MessagingHandler): # type: ignore
|
|
|
20
23
|
different methods of the delivery_context what to do with the message
|
|
21
24
|
"""
|
|
22
25
|
super().__init__(auto_accept=auto_accept, auto_settle=auto_settle)
|
|
23
|
-
self.delivery_context:
|
|
26
|
+
self.delivery_context: AbcDeliveryContext = DeliveryContext()
|
|
24
27
|
self._offset = 0
|
|
25
28
|
|
|
26
29
|
def on_amqp_message(self, event: Event) -> None:
|
|
@@ -8,7 +8,7 @@ from ..address_helper import validate_address
|
|
|
8
8
|
from ..connection import Connection
|
|
9
9
|
from ..consumer import Consumer
|
|
10
10
|
from ..entities import (
|
|
11
|
-
|
|
11
|
+
AbcConsumerOptions,
|
|
12
12
|
OAuth2Options,
|
|
13
13
|
RecoveryConfiguration,
|
|
14
14
|
)
|
|
@@ -228,7 +228,7 @@ class AsyncConnection:
|
|
|
228
228
|
self,
|
|
229
229
|
destination: str,
|
|
230
230
|
message_handler: Optional[MessagingHandler] = None,
|
|
231
|
-
consumer_options: Optional[
|
|
231
|
+
consumer_options: Optional[AbcConsumerOptions] = None,
|
|
232
232
|
credit: Optional[int] = None,
|
|
233
233
|
) -> AsyncConsumer:
|
|
234
234
|
"""
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
5
|
import threading
|
|
6
|
+
import warnings
|
|
6
7
|
from types import TracebackType
|
|
7
8
|
from typing import (
|
|
8
9
|
Any,
|
|
@@ -15,7 +16,7 @@ from typing import (
|
|
|
15
16
|
|
|
16
17
|
from ..amqp_consumer_handler import AMQPMessagingHandler
|
|
17
18
|
from ..consumer import Consumer
|
|
18
|
-
from ..entities import
|
|
19
|
+
from ..entities import AbcConsumerOptions
|
|
19
20
|
from ..qpid.proton._exceptions import Timeout
|
|
20
21
|
from ..qpid.proton._message import Message
|
|
21
22
|
from ..qpid.proton.utils import BlockingConnection
|
|
@@ -39,7 +40,7 @@ class AsyncConsumer:
|
|
|
39
40
|
_conn (BlockingConnection): The shared blocking connection
|
|
40
41
|
_addr (str): The address to consume from
|
|
41
42
|
_handler (Optional[AMQPMessagingHandler]): Optional message handling callback
|
|
42
|
-
_stream_options (Optional[
|
|
43
|
+
_stream_options (Optional[AbcConsumerOptions]): Configuration for stream consumption
|
|
43
44
|
_credit (Optional[int]): Flow control credit value
|
|
44
45
|
_connection_lock (asyncio.Lock): Lock for coordinating access to the shared connection
|
|
45
46
|
_remove_callback (Optional[Callable[[AsyncConsumer], None]]): Callback on close
|
|
@@ -56,7 +57,7 @@ class AsyncConsumer:
|
|
|
56
57
|
conn: BlockingConnection,
|
|
57
58
|
addr: str,
|
|
58
59
|
handler: Optional[AMQPMessagingHandler] = None,
|
|
59
|
-
stream_options: Optional[
|
|
60
|
+
stream_options: Optional[AbcConsumerOptions] = None,
|
|
60
61
|
credit: Optional[int] = None,
|
|
61
62
|
*,
|
|
62
63
|
connection_lock: asyncio.Lock,
|
|
@@ -69,7 +70,7 @@ class AsyncConsumer:
|
|
|
69
70
|
conn (BlockingConnection): The blocking connection to use
|
|
70
71
|
addr (str): The address to consume from
|
|
71
72
|
handler (Optional[AMQPMessagingHandler]): Optional message handler for processing received messages
|
|
72
|
-
stream_options (Optional[
|
|
73
|
+
stream_options (Optional[AbcConsumerOptions]): Optional configuration for stream-based consumption
|
|
73
74
|
credit (Optional[int]): Optional credit value for flow control
|
|
74
75
|
connection_lock (asyncio.Lock): Lock for coordinating access to the shared connection.
|
|
75
76
|
Must be created by the caller (AsyncConnection).
|
|
@@ -124,6 +125,11 @@ class AsyncConsumer:
|
|
|
124
125
|
"""
|
|
125
126
|
Consume a message from the queue.
|
|
126
127
|
|
|
128
|
+
.. deprecated::
|
|
129
|
+
Use the ``message_handler`` parameter when creating the consumer via
|
|
130
|
+
:meth:`AsyncConnection.consumer` instead. The message handler processes
|
|
131
|
+
messages as they arrive without polling.
|
|
132
|
+
|
|
127
133
|
Args:
|
|
128
134
|
timeout: The time to wait for a message.
|
|
129
135
|
None: Defaults to 60s
|
|
@@ -139,6 +145,12 @@ class AsyncConsumer:
|
|
|
139
145
|
The return type might be None if no message is available and timeout occurs,
|
|
140
146
|
but this is handled by the cast to Message.
|
|
141
147
|
"""
|
|
148
|
+
warnings.warn(
|
|
149
|
+
"consume() is deprecated; pass message_handler when creating the consumer "
|
|
150
|
+
"via connection.consumer() instead.",
|
|
151
|
+
DeprecationWarning,
|
|
152
|
+
stacklevel=2,
|
|
153
|
+
)
|
|
142
154
|
if not self._opened or self._consumer is None:
|
|
143
155
|
raise RuntimeError(
|
|
144
156
|
"Consumer is not opened. Call open() or use async context manager."
|
|
@@ -16,7 +16,7 @@ from packaging import version
|
|
|
16
16
|
from .address_helper import validate_address
|
|
17
17
|
from .consumer import Consumer
|
|
18
18
|
from .entities import (
|
|
19
|
-
|
|
19
|
+
AbcConsumerOptions,
|
|
20
20
|
OAuth2Options,
|
|
21
21
|
RecoveryConfiguration,
|
|
22
22
|
)
|
|
@@ -381,7 +381,7 @@ class Connection:
|
|
|
381
381
|
self,
|
|
382
382
|
destination: Optional[str] = None,
|
|
383
383
|
message_handler: Optional[MessagingHandler] = None,
|
|
384
|
-
consumer_options: Optional[
|
|
384
|
+
consumer_options: Optional[AbcConsumerOptions] = None,
|
|
385
385
|
credit: Optional[int] = None,
|
|
386
386
|
) -> Consumer:
|
|
387
387
|
"""
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import warnings
|
|
2
3
|
from typing import Literal, Optional, Union, cast
|
|
3
4
|
|
|
4
5
|
from .amqp_consumer_handler import AMQPMessagingHandler
|
|
5
6
|
from .entities import (
|
|
7
|
+
AbcConsumerOptions,
|
|
6
8
|
ConsumerOptions,
|
|
7
|
-
DirectReplyToConsumerOptions,
|
|
8
9
|
StreamConsumerOptions,
|
|
9
10
|
)
|
|
10
11
|
from .options import (
|
|
12
|
+
DynamicReceiverOption,
|
|
13
|
+
ReceiverOptionPreSettled,
|
|
11
14
|
ReceiverOptionUnsettled,
|
|
12
15
|
ReceiverOptionUnsettledWithFilters,
|
|
13
16
|
)
|
|
@@ -42,7 +45,7 @@ class Consumer:
|
|
|
42
45
|
conn: BlockingConnection,
|
|
43
46
|
addr: Optional[str] = None,
|
|
44
47
|
handler: Optional[AMQPMessagingHandler] = None,
|
|
45
|
-
consumer_options: Optional[
|
|
48
|
+
consumer_options: Optional[AbcConsumerOptions] = None,
|
|
46
49
|
credit: Optional[int] = None,
|
|
47
50
|
):
|
|
48
51
|
"""
|
|
@@ -73,16 +76,22 @@ class Consumer:
|
|
|
73
76
|
addr = ""
|
|
74
77
|
if self._addr is not None:
|
|
75
78
|
addr = self._addr
|
|
79
|
+
credit = 10
|
|
80
|
+
if self._credit is not None:
|
|
81
|
+
credit = self._credit
|
|
76
82
|
|
|
77
83
|
self._conn = conn
|
|
84
|
+
self._receiver = self._create_receiver(addr)
|
|
78
85
|
if self._consumer_options is None:
|
|
79
86
|
logger.debug("creating new receiver without stream")
|
|
87
|
+
|
|
80
88
|
self._receiver = self._conn.create_receiver(
|
|
81
89
|
addr,
|
|
82
90
|
options=ReceiverOptionUnsettled(addr),
|
|
83
91
|
handler=self._handler,
|
|
92
|
+
credit=self._credit,
|
|
84
93
|
)
|
|
85
|
-
|
|
94
|
+
elif isinstance(self._consumer_options, StreamConsumerOptions):
|
|
86
95
|
logger.debug("creating new stream receiver")
|
|
87
96
|
self._consumer_options.offset(self._handler.offset - 1) # type: ignore
|
|
88
97
|
self._receiver = self._conn.create_receiver(
|
|
@@ -92,6 +101,10 @@ class Consumer:
|
|
|
92
101
|
),
|
|
93
102
|
handler=self._handler,
|
|
94
103
|
)
|
|
104
|
+
elif isinstance(self._consumer_options, ConsumerOptions):
|
|
105
|
+
self._receiver = self._create_receiver_from_cq_consumer_options(
|
|
106
|
+
addr, credit
|
|
107
|
+
)
|
|
95
108
|
|
|
96
109
|
def _set_consumers_list(self, consumers: []) -> None: # type: ignore
|
|
97
110
|
self._consumers = consumers
|
|
@@ -100,6 +113,11 @@ class Consumer:
|
|
|
100
113
|
"""
|
|
101
114
|
Consume a message from the queue.
|
|
102
115
|
|
|
116
|
+
.. deprecated::
|
|
117
|
+
Use the ``message_handler`` parameter when creating the consumer via
|
|
118
|
+
:meth:`Connection.consumer` instead. The message handler processes
|
|
119
|
+
messages as they arrive without polling.
|
|
120
|
+
|
|
103
121
|
Args:
|
|
104
122
|
timeout: The time to wait for a message.
|
|
105
123
|
None: Defaults to 60s
|
|
@@ -112,9 +130,17 @@ class Consumer:
|
|
|
112
130
|
The return type might be None if no message is available and timeout occurs,
|
|
113
131
|
but this is handled by the cast to Message.
|
|
114
132
|
"""
|
|
133
|
+
warnings.warn(
|
|
134
|
+
"consume() is deprecated; pass message_handler when creating the consumer "
|
|
135
|
+
"via connection.consumer() instead.",
|
|
136
|
+
DeprecationWarning,
|
|
137
|
+
stacklevel=2,
|
|
138
|
+
)
|
|
115
139
|
if self._receiver is not None:
|
|
116
|
-
|
|
117
|
-
|
|
140
|
+
message_original = self._receiver.receive(timeout=timeout)
|
|
141
|
+
message = cast(Message, message_original)
|
|
142
|
+
return message
|
|
143
|
+
raise Exception("Receiver is not initialized")
|
|
118
144
|
|
|
119
145
|
def close(self) -> None:
|
|
120
146
|
"""
|
|
@@ -163,6 +189,8 @@ class Consumer:
|
|
|
163
189
|
else:
|
|
164
190
|
logger.debug("Creating the receiver, without options")
|
|
165
191
|
|
|
192
|
+
# we assume the default is for quorum and classic queues with
|
|
193
|
+
# Unsettled mode
|
|
166
194
|
if self._consumer_options is None:
|
|
167
195
|
return self._conn.create_receiver(
|
|
168
196
|
addr,
|
|
@@ -171,14 +199,6 @@ class Consumer:
|
|
|
171
199
|
credit=credit,
|
|
172
200
|
)
|
|
173
201
|
|
|
174
|
-
if isinstance(self._consumer_options, DirectReplyToConsumerOptions):
|
|
175
|
-
logger.debug("Creating dynamic receiver for direct reply-to")
|
|
176
|
-
dynamic_receiver = self._conn.create_dynamic_receiver(
|
|
177
|
-
credit, handler=self._handler
|
|
178
|
-
)
|
|
179
|
-
dynamic_receiver.credit = credit
|
|
180
|
-
return dynamic_receiver
|
|
181
|
-
|
|
182
202
|
if isinstance(self._consumer_options, StreamConsumerOptions):
|
|
183
203
|
return self._conn.create_receiver(
|
|
184
204
|
addr,
|
|
@@ -188,10 +208,48 @@ class Consumer:
|
|
|
188
208
|
handler=self._handler,
|
|
189
209
|
)
|
|
190
210
|
|
|
211
|
+
if isinstance(self._consumer_options, ConsumerOptions):
|
|
212
|
+
return self._create_receiver_from_cq_consumer_options(addr, credit)
|
|
213
|
+
|
|
191
214
|
raise Exception(
|
|
192
215
|
"Receiver is not initialized. No valid consumer options provided."
|
|
193
216
|
)
|
|
194
217
|
|
|
218
|
+
def _create_receiver_from_cq_consumer_options(
|
|
219
|
+
self, addr: Optional[str], credit: int
|
|
220
|
+
) -> BlockingReceiver:
|
|
221
|
+
|
|
222
|
+
if self._consumer_options is None:
|
|
223
|
+
raise Exception(
|
|
224
|
+
"Consumer options must be provided for CQ receiver creation."
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if self._consumer_options.pre_settled():
|
|
228
|
+
logger.debug("Creating receiver with pre-settled deliveries")
|
|
229
|
+
return self._conn.create_receiver(
|
|
230
|
+
addr,
|
|
231
|
+
options=ReceiverOptionPreSettled(addr),
|
|
232
|
+
handler=self._handler,
|
|
233
|
+
credit=credit,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if self._consumer_options.direct_reply_to():
|
|
237
|
+
logger.debug("Creating receiver for direct-reply-to queue")
|
|
238
|
+
return self._conn.create_receiver(
|
|
239
|
+
addr,
|
|
240
|
+
options=DynamicReceiverOption(),
|
|
241
|
+
handler=self._handler,
|
|
242
|
+
credit=credit,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
logger.debug("Creating receiver for CQ queue with default unsettled deliveries")
|
|
246
|
+
return self._conn.create_receiver(
|
|
247
|
+
addr,
|
|
248
|
+
options=ReceiverOptionUnsettled(addr),
|
|
249
|
+
handler=self._handler,
|
|
250
|
+
credit=credit,
|
|
251
|
+
)
|
|
252
|
+
|
|
195
253
|
@property
|
|
196
254
|
def address(self) -> Optional[str]:
|
|
197
255
|
if self._receiver is not None:
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
ArgumentOutOfRangeException,
|
|
6
|
+
InvalidOperationException,
|
|
7
|
+
)
|
|
8
|
+
from .qpid.proton._data import PythonAMQPData
|
|
9
|
+
from .qpid.proton._delivery import Delivery
|
|
10
|
+
from .qpid.proton._events import Event
|
|
11
|
+
from .utils import validate_annotations
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
DeliveryContext is a class that is used to accept, reject, requeue or requeue with annotations a message.
|
|
15
|
+
It is an helper to set the default values needed for manually accepting and settling messages.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AbcDeliveryContext(ABC):
|
|
20
|
+
"""
|
|
21
|
+
Abstract interface for delivery context operations.
|
|
22
|
+
Provides methods to accept, reject, requeue or requeue with annotations a message.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def accept(self, event: Event) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Accept the message (AMQP 1.0 <code>accepted</code> outcome).
|
|
29
|
+
|
|
30
|
+
This means the message has been processed and the broker can delete it.
|
|
31
|
+
"""
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def discard(self, event: Event) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Reject the message (AMQP 1.0 <code>rejected</code> outcome).
|
|
38
|
+
This means the message cannot be processed because it is invalid, the broker can drop it
|
|
39
|
+
or dead-letter it if it is configured.
|
|
40
|
+
"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def discard_with_annotations(
|
|
45
|
+
self, event: Event, annotations: Dict[str, "PythonAMQPData"]
|
|
46
|
+
) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Discard the message with annotations to combine with the existing message annotations.
|
|
49
|
+
This means the message cannot be processed because it is invalid, the broker can drop it
|
|
50
|
+
or dead-letter it if it is configured.
|
|
51
|
+
Application-specific annotation keys must start with the <code>x-opt-</code> prefix.
|
|
52
|
+
Annotation keys the broker understands starts with <code>x-</code>, but not with <code>x-opt-
|
|
53
|
+
This maps to the AMQP 1.0
|
|
54
|
+
modified{delivery-failed = true, undeliverable-here = true}</code> outcome.
|
|
55
|
+
<param name="annotations"> annotations message annotations to combine with existing ones </param>
|
|
56
|
+
<a
|
|
57
|
+
href="https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified">AMQP
|
|
58
|
+
1.0 <code>modified</code> outcome</a>
|
|
59
|
+
The annotations can be used only with Quorum queues, see https://www.rabbitmq.com/docs/amqp#modified-outcome
|
|
60
|
+
"""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def requeue(self, event: Event) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Requeue the message (AMQP 1.0 <code>released</code> outcome).
|
|
67
|
+
This means the message has not been processed and the broker can requeue it and deliver it
|
|
68
|
+
to the same or a different consumer.
|
|
69
|
+
"""
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def requeue_with_annotations(
|
|
74
|
+
self, event: Event, annotations: Dict[str, "PythonAMQPData"]
|
|
75
|
+
) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Requeue the message with annotations to combine with the existing message annotations.
|
|
78
|
+
This means the message has not been processed and the broker can requeue it and deliver it
|
|
79
|
+
to the same or a different consumer.
|
|
80
|
+
Application-specific annotation keys must start with the <code>x-opt-</code> prefix.
|
|
81
|
+
Annotation keys the broker understands starts with <code>x-</code>, but not with <code>x-opt-
|
|
82
|
+
</code>.
|
|
83
|
+
This maps to the AMQP 1.0 <code>
|
|
84
|
+
modified{delivery-failed = false, undeliverable-here = false}</code> outcome.
|
|
85
|
+
<param name="annotations"> annotations message annotations to combine with existing ones </param>
|
|
86
|
+
<a
|
|
87
|
+
href="https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified">AMQP
|
|
88
|
+
1.0 <code>modified</code> outcome</a>
|
|
89
|
+
The annotations can be used only with Quorum queues, see https://www.rabbitmq.com/docs/amqp#modified-outcome
|
|
90
|
+
"""
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DeliveryContext(AbcDeliveryContext):
|
|
95
|
+
"""
|
|
96
|
+
Default implementation of IDeliveryContext.
|
|
97
|
+
Used to accept, reject, requeue or requeue with annotations a message.
|
|
98
|
+
It is an helper to set the default values needed for manually accepting and settling messages.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def accept(self, event: Event) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Accept the message (AMQP 1.0 <code>accepted</code> outcome).
|
|
104
|
+
|
|
105
|
+
This means the message has been processed and the broker can delete it.
|
|
106
|
+
"""
|
|
107
|
+
dlv = event.delivery
|
|
108
|
+
if dlv.settled:
|
|
109
|
+
# like the other clients, it has to raise an exception if trying to accept an already settled delivery
|
|
110
|
+
raise InvalidOperationException(
|
|
111
|
+
"auto-settle on, message is already disposed"
|
|
112
|
+
)
|
|
113
|
+
dlv.update(Delivery.ACCEPTED)
|
|
114
|
+
dlv.settle()
|
|
115
|
+
|
|
116
|
+
def discard(self, event: Event) -> None:
|
|
117
|
+
"""
|
|
118
|
+
Reject the message (AMQP 1.0 <code>rejected</code> outcome).
|
|
119
|
+
This means the message cannot be processed because it is invalid, the broker can drop it
|
|
120
|
+
or dead-letter it if it is configured.
|
|
121
|
+
"""
|
|
122
|
+
dlv = event.delivery
|
|
123
|
+
if dlv.settled:
|
|
124
|
+
# like the other clients, it has to raise an exception if trying to discard an already settled delivery
|
|
125
|
+
raise InvalidOperationException(
|
|
126
|
+
"auto-settle on, message is already disposed"
|
|
127
|
+
)
|
|
128
|
+
dlv.update(Delivery.REJECTED)
|
|
129
|
+
dlv.settle()
|
|
130
|
+
|
|
131
|
+
def discard_with_annotations(
|
|
132
|
+
self, event: Event, annotations: Dict[str, "PythonAMQPData"]
|
|
133
|
+
) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Discard the message with annotations to combine with the existing message annotations.
|
|
136
|
+
This means the message cannot be processed because it is invalid, the broker can drop it
|
|
137
|
+
or dead-letter it if it is configured.
|
|
138
|
+
Application-specific annotation keys must start with the <code>x-opt-</code> prefix.
|
|
139
|
+
Annotation keys the broker understands starts with <code>x-</code>, but not with <code>x-opt-
|
|
140
|
+
This maps to the AMQP 1.0
|
|
141
|
+
modified{delivery-failed = true, undeliverable-here = true}</code> outcome.
|
|
142
|
+
<param name="annotations"> annotations message annotations to combine with existing ones </param>
|
|
143
|
+
<a
|
|
144
|
+
href="https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified">AMQP
|
|
145
|
+
1.0 <code>modified</code> outcome</a>
|
|
146
|
+
The annotations can be used only with Quorum queues, see https://www.rabbitmq.com/docs/amqp#modified-outcome
|
|
147
|
+
"""
|
|
148
|
+
dlv = event.delivery
|
|
149
|
+
if dlv.settled:
|
|
150
|
+
# like the other clients, it has to raise an exception if trying to discard an already settled delivery
|
|
151
|
+
raise InvalidOperationException(
|
|
152
|
+
"auto-settle on, message is already disposed"
|
|
153
|
+
)
|
|
154
|
+
dlv.local.annotations = annotations
|
|
155
|
+
dlv.local.failed = True
|
|
156
|
+
dlv.local.undeliverable = True
|
|
157
|
+
|
|
158
|
+
validated = validate_annotations(annotations.keys())
|
|
159
|
+
|
|
160
|
+
if validated is False:
|
|
161
|
+
raise ArgumentOutOfRangeException(
|
|
162
|
+
"Message annotation key must start with 'x-'"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
dlv.update(Delivery.MODIFIED)
|
|
166
|
+
dlv.settle()
|
|
167
|
+
|
|
168
|
+
def requeue(self, event: Event) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Requeue the message (AMQP 1.0 <code>released</code> outcome).
|
|
171
|
+
This means the message has not been processed and the broker can requeue it and deliver it
|
|
172
|
+
to the same or a different consumer.
|
|
173
|
+
"""
|
|
174
|
+
dlv = event.delivery
|
|
175
|
+
if dlv.settled:
|
|
176
|
+
# like the other clients, it has to raise an exception if trying to requeue an already settled delivery
|
|
177
|
+
raise InvalidOperationException(
|
|
178
|
+
"auto-settle on, message is already disposed"
|
|
179
|
+
)
|
|
180
|
+
dlv.update(Delivery.RELEASED)
|
|
181
|
+
dlv.settle()
|
|
182
|
+
|
|
183
|
+
def requeue_with_annotations(
|
|
184
|
+
self, event: Event, annotations: Dict[str, "PythonAMQPData"]
|
|
185
|
+
) -> None:
|
|
186
|
+
"""
|
|
187
|
+
Requeue the message with annotations to combine with the existing message annotations.
|
|
188
|
+
This means the message has not been processed and the broker can requeue it and deliver it
|
|
189
|
+
to the same or a different consumer.
|
|
190
|
+
Application-specific annotation keys must start with the <code>x-opt-</code> prefix.
|
|
191
|
+
Annotation keys the broker understands starts with <code>x-</code>, but not with <code>x-opt-
|
|
192
|
+
</code>.
|
|
193
|
+
This maps to the AMQP 1.0 <code>
|
|
194
|
+
modified{delivery-failed = false, undeliverable-here = false}</code> outcome.
|
|
195
|
+
<param name="annotations"> annotations message annotations to combine with existing ones </param>
|
|
196
|
+
<a
|
|
197
|
+
href="https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified">AMQP
|
|
198
|
+
1.0 <code>modified</code> outcome</a>
|
|
199
|
+
The annotations can be used only with Quorum queues, see https://www.rabbitmq.com/docs/amqp#modified-outcome
|
|
200
|
+
"""
|
|
201
|
+
dlv = event.delivery
|
|
202
|
+
if dlv.settled:
|
|
203
|
+
# like the other clients, it has to raise an exception if trying to requeue an already settled delivery
|
|
204
|
+
raise InvalidOperationException(
|
|
205
|
+
"auto-settle on, message is already disposed"
|
|
206
|
+
)
|
|
207
|
+
dlv.local.annotations = annotations
|
|
208
|
+
dlv.local.failed = False
|
|
209
|
+
dlv.local.undeliverable = False
|
|
210
|
+
|
|
211
|
+
validated = validate_annotations(annotations.keys())
|
|
212
|
+
|
|
213
|
+
if validated is False:
|
|
214
|
+
raise ArgumentOutOfRangeException(
|
|
215
|
+
"Message annotation key must start with 'x-'"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
dlv.update(Delivery.MODIFIED)
|
|
219
|
+
dlv.settle()
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
1
2
|
from dataclasses import dataclass, field
|
|
2
3
|
from datetime import datetime, timedelta
|
|
3
|
-
from enum import Enum
|
|
4
|
+
from enum import Enum, auto
|
|
4
5
|
from typing import Any, Dict, Optional, Union
|
|
5
6
|
|
|
6
7
|
from .common import ExchangeType, QueueType
|
|
7
8
|
from .exceptions import ValidationCodeException
|
|
8
|
-
from .qpid.proton._data import Described, symbol
|
|
9
|
+
from .qpid.proton._data import Described, symbol, timestamp
|
|
9
10
|
|
|
10
11
|
SQL_FILTER = "sql-filter"
|
|
11
12
|
AMQP_SQL_FILTER = "amqp:sql-filter"
|
|
@@ -109,11 +110,13 @@ class OffsetSpecification(Enum):
|
|
|
109
110
|
first: Start from the first message in the stream
|
|
110
111
|
next: Start from the next message to arrive
|
|
111
112
|
last: Start from the last message in the stream
|
|
113
|
+
timestamp: Start from a specific timestamp
|
|
112
114
|
"""
|
|
113
115
|
|
|
114
116
|
first = ("first",)
|
|
115
117
|
next = ("next",)
|
|
116
118
|
last = ("last",)
|
|
119
|
+
timestamp = ("timestamp",)
|
|
117
120
|
|
|
118
121
|
|
|
119
122
|
@dataclass
|
|
@@ -153,16 +156,27 @@ class ExchangeToExchangeBindingSpecification:
|
|
|
153
156
|
binding_key: Optional[str] = None
|
|
154
157
|
|
|
155
158
|
|
|
156
|
-
class
|
|
159
|
+
class AbcConsumerOptions(ABC):
|
|
160
|
+
"""
|
|
161
|
+
Abstract base class for consumer configuration options.
|
|
162
|
+
This class defines the interface for consumer options, including validation
|
|
163
|
+
and filter set retrieval.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
@abstractmethod
|
|
157
167
|
def validate(self, versions: Dict[str, bool]) -> None:
|
|
158
168
|
raise NotImplementedError("Subclasses should implement this method")
|
|
159
169
|
|
|
170
|
+
@abstractmethod
|
|
160
171
|
def filter_set(self) -> Dict[symbol, Described]:
|
|
161
172
|
raise NotImplementedError("Subclasses should implement this method")
|
|
162
173
|
|
|
163
174
|
def direct_reply_to(self) -> bool:
|
|
164
175
|
return False
|
|
165
176
|
|
|
177
|
+
def pre_settled(self) -> bool:
|
|
178
|
+
return False
|
|
179
|
+
|
|
166
180
|
|
|
167
181
|
@dataclass
|
|
168
182
|
class MessageProperties:
|
|
@@ -198,13 +212,12 @@ class MessageProperties:
|
|
|
198
212
|
reply_to_group_id: Optional[str] = None
|
|
199
213
|
|
|
200
214
|
|
|
201
|
-
"""
|
|
202
|
-
StreamFilterOptions defines the filtering options for a stream consumer.
|
|
203
|
-
for values and match_unfiltered see: https://www.rabbitmq.com/blog/2023/10/16/stream-filtering
|
|
204
|
-
"""
|
|
205
|
-
|
|
206
|
-
|
|
207
215
|
class StreamFilterOptions:
|
|
216
|
+
"""
|
|
217
|
+
StreamFilterOptions defines the filtering options for a stream consumer.
|
|
218
|
+
for values and match_unfiltered see: https://www.rabbitmq.com/blog/2023/10/16/stream-filtering
|
|
219
|
+
"""
|
|
220
|
+
|
|
208
221
|
values: Optional[list[str]] = None
|
|
209
222
|
match_unfiltered: bool = False
|
|
210
223
|
application_properties: Optional[dict[str, Any]] = None
|
|
@@ -226,7 +239,7 @@ class StreamFilterOptions:
|
|
|
226
239
|
self.sql = sql
|
|
227
240
|
|
|
228
241
|
|
|
229
|
-
class StreamConsumerOptions(
|
|
242
|
+
class StreamConsumerOptions(AbcConsumerOptions):
|
|
230
243
|
"""
|
|
231
244
|
Configuration options for stream queues.
|
|
232
245
|
|
|
@@ -243,7 +256,9 @@ class StreamConsumerOptions(ConsumerOptions):
|
|
|
243
256
|
|
|
244
257
|
def __init__(
|
|
245
258
|
self,
|
|
246
|
-
offset_specification: Optional[
|
|
259
|
+
offset_specification: Optional[
|
|
260
|
+
Union[OffsetSpecification, int, datetime]
|
|
261
|
+
] = None,
|
|
247
262
|
filter_options: Optional[StreamFilterOptions] = None,
|
|
248
263
|
):
|
|
249
264
|
|
|
@@ -275,7 +290,9 @@ class StreamConsumerOptions(ConsumerOptions):
|
|
|
275
290
|
if filter_options.sql is not None and filter_options.sql != "":
|
|
276
291
|
self._filter_sql(filter_options.sql)
|
|
277
292
|
|
|
278
|
-
def _offset(
|
|
293
|
+
def _offset(
|
|
294
|
+
self, offset_specification: Union[OffsetSpecification, int, datetime]
|
|
295
|
+
) -> None:
|
|
279
296
|
"""
|
|
280
297
|
Set the offset specification for the stream.
|
|
281
298
|
|
|
@@ -287,11 +304,20 @@ class StreamConsumerOptions(ConsumerOptions):
|
|
|
287
304
|
self._filter_set[symbol(STREAM_OFFSET_SPEC)] = Described(
|
|
288
305
|
symbol(STREAM_OFFSET_SPEC), offset_specification
|
|
289
306
|
)
|
|
290
|
-
|
|
307
|
+
if isinstance(offset_specification, OffsetSpecification):
|
|
291
308
|
self._filter_set[symbol(STREAM_OFFSET_SPEC)] = Described(
|
|
292
309
|
symbol(STREAM_OFFSET_SPEC), offset_specification.name
|
|
293
310
|
)
|
|
294
311
|
|
|
312
|
+
if isinstance(offset_specification, datetime):
|
|
313
|
+
# convert datetime to milliseconds since epoch
|
|
314
|
+
milliseconds_since_epoch = int(offset_specification.timestamp() * 1000)
|
|
315
|
+
# Create a timestamp instance
|
|
316
|
+
ts = timestamp(milliseconds_since_epoch)
|
|
317
|
+
self._filter_set[symbol(STREAM_OFFSET_SPEC)] = Described(
|
|
318
|
+
symbol(STREAM_OFFSET_SPEC), ts
|
|
319
|
+
)
|
|
320
|
+
|
|
295
321
|
def _filter_values(self, filters: list[str]) -> None:
|
|
296
322
|
"""
|
|
297
323
|
Set the filter values for the stream.
|
|
@@ -403,19 +429,92 @@ class StreamConsumerOptions(ConsumerOptions):
|
|
|
403
429
|
)
|
|
404
430
|
|
|
405
431
|
|
|
406
|
-
class
|
|
432
|
+
class ConsumerSettleStrategy(Enum):
|
|
433
|
+
"""
|
|
434
|
+
ConsumerSettleStrategy defines the settle strategy for Classic and Quorum queue consumers.
|
|
435
|
+
Uniform with the AMQP 1.0 clients interface (e.g. .NET client SettleStrategy).
|
|
436
|
+
|
|
437
|
+
Attributes:
|
|
438
|
+
ExplicitSettle: Consumer uses explicit settle (default). Message settle mode
|
|
439
|
+
is the default one (explicit settle).
|
|
440
|
+
DirectReplyTo: Enables Direct Reply-To consumer behavior.
|
|
441
|
+
Feature in RabbitMQ, allowing for simplified request-reply messaging patterns.
|
|
442
|
+
Docs: https://www.rabbitmq.com/docs/direct-reply-to#usage-amqp
|
|
443
|
+
PreSettled: Deliveries are pre-settled (at-most-once semantics).
|
|
444
|
+
When enabled, messages are automatically settled when received,
|
|
445
|
+
meaning they cannot be redelivered if processing fails.
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
ExplicitSettle = auto()
|
|
449
|
+
DirectReplyTo = auto()
|
|
450
|
+
PreSettled = auto()
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@dataclass
|
|
454
|
+
class ConsumerOptions(AbcConsumerOptions):
|
|
455
|
+
"""
|
|
456
|
+
Configuration options for FIFO (Classic and Quorum) queue consumers.
|
|
457
|
+
Not valid for Stream queues.
|
|
458
|
+
|
|
459
|
+
This class provides options for consuming from FIFO queues, including
|
|
460
|
+
support for pre-settled deliveries. Uses a single settle_strategy to
|
|
461
|
+
align with the uniform AMQP 1.0 clients interface.
|
|
462
|
+
|
|
463
|
+
Attributes:
|
|
464
|
+
see ConsumerSettleStrategy enum for available strategies.
|
|
465
|
+
"""
|
|
466
|
+
|
|
467
|
+
def __init__(
|
|
468
|
+
self,
|
|
469
|
+
settle_strategy: ConsumerSettleStrategy = ConsumerSettleStrategy.ExplicitSettle,
|
|
470
|
+
):
|
|
471
|
+
super().__init__()
|
|
472
|
+
self._settle_strategy = settle_strategy
|
|
407
473
|
|
|
408
474
|
def validate(self, versions: Dict[str, bool]) -> None:
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
475
|
+
"""
|
|
476
|
+
Validates Classic/Quorum consumer options against supported RabbitMQ server versions.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
versions: Dictionary mapping version strings to boolean indicating support.
|
|
480
|
+
|
|
481
|
+
Raises:
|
|
482
|
+
ValidationCodeException: If a feature requires a higher RabbitMQ version.
|
|
483
|
+
"""
|
|
484
|
+
# Classic/Quorum queues and pre-settled deliveries are supported in RabbitMQ 4.x
|
|
485
|
+
# No specific version validation needed at this time
|
|
486
|
+
if self._settle_strategy == ConsumerSettleStrategy.DirectReplyTo:
|
|
487
|
+
if not versions.get("4.2.0", False):
|
|
488
|
+
raise ValidationCodeException(
|
|
489
|
+
"Direct Reply-To requires RabbitMQ 4.2.0 or higher"
|
|
490
|
+
)
|
|
413
491
|
|
|
414
492
|
def filter_set(self) -> Dict[symbol, Described]:
|
|
493
|
+
"""
|
|
494
|
+
Get the filter set configuration (empty for FIFO consumers).
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
Dict[symbol, Described]: Empty dictionary as FIFO consumers don't use filters.
|
|
498
|
+
"""
|
|
415
499
|
return {}
|
|
416
500
|
|
|
417
501
|
def direct_reply_to(self) -> bool:
|
|
418
|
-
|
|
502
|
+
"""
|
|
503
|
+
Indicates if this is a direct reply-to consumer.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
bool: True if DirectReplyTo settle strategy is used, False otherwise.
|
|
507
|
+
"""
|
|
508
|
+
return self._settle_strategy is ConsumerSettleStrategy.DirectReplyTo
|
|
509
|
+
|
|
510
|
+
def pre_settled(self) -> bool:
|
|
511
|
+
"""
|
|
512
|
+
Indicates if deliveries are pre-settled.
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
bool: True if deliveries are pre-settled, False otherwise.
|
|
516
|
+
"""
|
|
517
|
+
return self._settle_strategy is ConsumerSettleStrategy.PreSettled
|
|
419
518
|
|
|
420
519
|
|
|
421
520
|
@dataclass
|
|
@@ -14,3 +14,12 @@ class ArgumentOutOfRangeException(BaseException):
|
|
|
14
14
|
|
|
15
15
|
def __str__(self) -> str:
|
|
16
16
|
return repr(self.msg)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InvalidOperationException(Exception):
|
|
20
|
+
# Constructor or Initializer
|
|
21
|
+
def __init__(self, msg: str):
|
|
22
|
+
self.msg = msg
|
|
23
|
+
|
|
24
|
+
def __str__(self) -> str:
|
|
25
|
+
return repr(self.msg)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
|
-
from .entities import
|
|
3
|
+
from .entities import AbcConsumerOptions
|
|
4
4
|
from .qpid.proton._data import ( # noqa: E402
|
|
5
5
|
Data,
|
|
6
6
|
PropertyDict,
|
|
@@ -89,7 +89,7 @@ class ReceiverOptionUnsettled(LinkOption): # type: ignore
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
class ReceiverOptionUnsettledWithFilters(Filter): # type: ignore
|
|
92
|
-
def __init__(self, addr: Optional[str], consumer_options:
|
|
92
|
+
def __init__(self, addr: Optional[str], consumer_options: AbcConsumerOptions):
|
|
93
93
|
super().__init__(consumer_options.filter_set())
|
|
94
94
|
self._addr = addr
|
|
95
95
|
|
|
@@ -103,3 +103,26 @@ class ReceiverOptionUnsettledWithFilters(Filter): # type: ignore
|
|
|
103
103
|
|
|
104
104
|
def test(self, link: Link) -> bool:
|
|
105
105
|
return bool(link.is_receiver)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ReceiverOptionPreSettled(LinkOption): # type: ignore
|
|
109
|
+
"""
|
|
110
|
+
Receiver option for pre-settled deliveries (at-most-once semantics).
|
|
111
|
+
|
|
112
|
+
When pre-settled is enabled, deliveries are automatically settled when received,
|
|
113
|
+
meaning they cannot be redelivered if processing fails. This uses RCV_FIRST
|
|
114
|
+
which causes the receiver to settle deliveries immediately upon receiving them.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, addr: Optional[str]):
|
|
118
|
+
self._addr = addr
|
|
119
|
+
|
|
120
|
+
def apply(self, link: Link) -> None:
|
|
121
|
+
link.target.address = self._addr
|
|
122
|
+
link.snd_settle_mode = Link.SND_SETTLED
|
|
123
|
+
link.rcv_settle_mode = Link.RCV_FIRST
|
|
124
|
+
link.properties = PropertyDict({symbol("paired"): True})
|
|
125
|
+
link.source.dynamic = False
|
|
126
|
+
|
|
127
|
+
def test(self, link: Link) -> bool:
|
|
128
|
+
return bool(link.is_receiver)
|
|
@@ -1039,7 +1039,7 @@ class Backoff(object):
|
|
|
1039
1039
|
|
|
1040
1040
|
|
|
1041
1041
|
def make_backoff_wrapper(
|
|
1042
|
-
backoff: Optional[Union[List[Union[float, int]], bool, Backoff]]
|
|
1042
|
+
backoff: Optional[Union[List[Union[float, int]], bool, Backoff]],
|
|
1043
1043
|
) -> Optional[Union[List[Union[float, int]], bool, Backoff]]:
|
|
1044
1044
|
"""
|
|
1045
1045
|
Make a wrapper for a backoff object:
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
from typing import Dict
|
|
2
|
-
|
|
3
|
-
from .exceptions import ArgumentOutOfRangeException
|
|
4
|
-
from .qpid.proton._data import PythonAMQPData
|
|
5
|
-
from .qpid.proton._delivery import Delivery
|
|
6
|
-
from .qpid.proton._events import Event
|
|
7
|
-
from .utils import validate_annotations
|
|
8
|
-
|
|
9
|
-
"""
|
|
10
|
-
DeliveryContext is a class that is used to accept, reject, requeue or requeue with annotations a message.
|
|
11
|
-
It is an helper to set the default values needed for manually accepting and settling messages.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class DeliveryContext:
|
|
16
|
-
"""
|
|
17
|
-
Accept the message (AMQP 1.0 <code>accepted</code> outcome).
|
|
18
|
-
|
|
19
|
-
This means the message has been processed and the broker can delete it.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
def accept(self, event: Event) -> None:
|
|
23
|
-
dlv = event.delivery
|
|
24
|
-
dlv.update(Delivery.ACCEPTED)
|
|
25
|
-
dlv.settle()
|
|
26
|
-
|
|
27
|
-
"""
|
|
28
|
-
Reject the message (AMQP 1.0 <code>rejected</code> outcome).
|
|
29
|
-
This means the message cannot be processed because it is invalid, the broker can drop it
|
|
30
|
-
or dead-letter it if it is configured.
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
def discard(self, event: Event) -> None:
|
|
34
|
-
dlv = event.delivery
|
|
35
|
-
dlv.update(Delivery.REJECTED)
|
|
36
|
-
dlv.settle()
|
|
37
|
-
|
|
38
|
-
"""
|
|
39
|
-
Discard the message with annotations to combine with the existing message annotations.
|
|
40
|
-
This means the message cannot be processed because it is invalid, the broker can drop it
|
|
41
|
-
or dead-letter it if it is configured.
|
|
42
|
-
Application-specific annotation keys must start with the <code>x-opt-</code> prefix.
|
|
43
|
-
Annotation keys the broker understands starts with <code>x-</code>, but not with <code>x-opt-
|
|
44
|
-
This maps to the AMQP 1.0
|
|
45
|
-
modified{delivery-failed = true, undeliverable-here = true}</code> outcome.
|
|
46
|
-
<param name="annotations"> annotations message annotations to combine with existing ones </param>
|
|
47
|
-
<a
|
|
48
|
-
href="https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified">AMQP
|
|
49
|
-
1.0 <code>modified</code> outcome</a>
|
|
50
|
-
The annotations can be used only with Quorum queues, see https://www.rabbitmq.com/docs/amqp#modified-outcome
|
|
51
|
-
"""
|
|
52
|
-
|
|
53
|
-
def discard_with_annotations(
|
|
54
|
-
self, event: Event, annotations: Dict[str, "PythonAMQPData"]
|
|
55
|
-
) -> None:
|
|
56
|
-
dlv = event.delivery
|
|
57
|
-
dlv.local.annotations = annotations
|
|
58
|
-
dlv.local.failed = True
|
|
59
|
-
dlv.local.undeliverable = True
|
|
60
|
-
|
|
61
|
-
validated = validate_annotations(annotations.keys())
|
|
62
|
-
|
|
63
|
-
if validated is False:
|
|
64
|
-
raise ArgumentOutOfRangeException(
|
|
65
|
-
"Message annotation key must start with 'x-'"
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
dlv.update(Delivery.MODIFIED)
|
|
69
|
-
dlv.settle()
|
|
70
|
-
|
|
71
|
-
"""
|
|
72
|
-
Requeue the message (AMQP 1.0 <code>released</code> outcome).
|
|
73
|
-
This means the message has not been processed and the broker can requeue it and deliver it
|
|
74
|
-
to the same or a different consumer.
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
def requeue(self, event: Event) -> None:
|
|
78
|
-
dlv = event.delivery
|
|
79
|
-
dlv.update(Delivery.RELEASED)
|
|
80
|
-
dlv.settle()
|
|
81
|
-
|
|
82
|
-
"""
|
|
83
|
-
Requeue the message with annotations to combine with the existing message annotations.
|
|
84
|
-
This means the message has not been processed and the broker can requeue it and deliver it
|
|
85
|
-
to the same or a different consumer.
|
|
86
|
-
Application-specific annotation keys must start with the <code>x-opt-</code> prefix.
|
|
87
|
-
Annotation keys the broker understands starts with <code>x-</code>, but not with <code>x-opt-
|
|
88
|
-
</code>.
|
|
89
|
-
This maps to the AMQP 1.0 <code>
|
|
90
|
-
modified{delivery-failed = false, undeliverable-here = false}</code> outcome.
|
|
91
|
-
<param name="annotations"> annotations message annotations to combine with existing ones </param>
|
|
92
|
-
<a
|
|
93
|
-
href="https://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-messaging-v1.0-os.html#type-modified">AMQP
|
|
94
|
-
1.0 <code>modified</code> outcome</a>
|
|
95
|
-
The annotations can be used only with Quorum queues, see https://www.rabbitmq.com/docs/amqp#modified-outcome
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
def requeue_with_annotations(
|
|
99
|
-
self, event: Event, annotations: Dict[str, "PythonAMQPData"]
|
|
100
|
-
) -> None:
|
|
101
|
-
dlv = event.delivery
|
|
102
|
-
dlv.local.annotations = annotations
|
|
103
|
-
dlv.local.failed = False
|
|
104
|
-
dlv.local.undeliverable = False
|
|
105
|
-
|
|
106
|
-
validated = validate_annotations(annotations.keys())
|
|
107
|
-
|
|
108
|
-
if validated is False:
|
|
109
|
-
raise ArgumentOutOfRangeException(
|
|
110
|
-
"Message annotation key must start with 'x-'"
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
dlv.update(Delivery.MODIFIED)
|
|
114
|
-
dlv.settle()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|