rabbitmq-amqp-python-client 0.6.0__tar.gz → 0.7.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.6.0 → rabbitmq_amqp_python_client-0.7.0}/PKG-INFO +11 -6
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/README.md +7 -1
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/pyproject.toml +19 -15
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/__init__.py +6 -0
- rabbitmq_amqp_python_client-0.7.0/rabbitmq_amqp_python_client/amqp_consumer_handler.py +94 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/connection.py +7 -6
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/consumer.py +8 -55
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/connection.py +14 -8
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/consumer.py +45 -39
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/delivery_context.py +21 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/entities.py +72 -4
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/environment.py +4 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/exceptions.py +15 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/management.py +24 -11
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/options.py +10 -10
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/publisher.py +35 -10
- rabbitmq_amqp_python_client-0.7.0/rabbitmq_amqp_python_client/qpid/proton/_flow_properties.py +142 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_handler.py +8 -1
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_handlers.py +10 -1
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_utils.py +0 -2
- rabbitmq_amqp_python_client-0.6.0/rabbitmq_amqp_python_client/amqp_consumer_handler.py +0 -39
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/LICENSE +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/address_helper.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/__init__.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/environment.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/management.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/publisher.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/common.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/py.typed +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/__init__.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_common.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_condition.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_data.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_delivery.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_endpoints.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_events.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_exceptions.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_io.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_message.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_reactor.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_selectable.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_tracing.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_transport.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_url.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_wrapper.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/handlers.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/py.typed +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/reactor.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/tracing.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/utils.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/queues.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/ssl_configuration.py +0 -0
- {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/utils.py +0 -0
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rabbitmq-amqp-python-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Python RabbitMQ client for AMQP 1.0 protocol
|
|
5
5
|
License: Apache-2.0 license
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Author: RabbitMQ team
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
9
|
Classifier: License :: Other/Proprietary License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
-
Requires-Dist: packaging (>=
|
|
16
|
+
Requires-Dist: packaging (>=26.0,<27.0)
|
|
18
17
|
Requires-Dist: python-qpid-proton (>=0.40.0,<0.41.0)
|
|
19
|
-
Requires-Dist: typing-extensions (>=4.
|
|
18
|
+
Requires-Dist: typing-extensions (>=4.15.0,<5.0.0)
|
|
20
19
|
Description-Content-Type: text/markdown
|
|
21
20
|
|
|
22
21
|
## RabbitMQ AMQP 1.0 Python Client
|
|
@@ -72,7 +71,7 @@ export LDFLAGS="-L/usr/lib/x86_64-linux-gnu"
|
|
|
72
71
|
pip install "python-qpid-proton>=0.39.0,<0.40.0" --no-binary python-qpid-proton --verbose --no-cache-dir
|
|
73
72
|
```
|
|
74
73
|
|
|
75
|
-
### Async Interface
|
|
74
|
+
### Async Interface
|
|
76
75
|
|
|
77
76
|
The client provides an async interface via the `rabbitmq_amqp_python_client.asyncio` module. The async classes act as facades that:
|
|
78
77
|
|
|
@@ -101,4 +100,10 @@ Additional async examples are available in the [examples](./examples) folder:
|
|
|
101
100
|
- TLS: [examples/tls/tls_example_async.py](./examples/tls/tls_example_async.py)
|
|
102
101
|
|
|
103
102
|
|
|
103
|
+
### Thread Safety
|
|
104
|
+
|
|
105
|
+
The TCP connection is not thread safe.
|
|
106
|
+
If you want to use the client in a multithreaded environment, you should create a connection per thread.
|
|
107
|
+
|
|
108
|
+
|
|
104
109
|
|
|
@@ -51,7 +51,7 @@ export LDFLAGS="-L/usr/lib/x86_64-linux-gnu"
|
|
|
51
51
|
pip install "python-qpid-proton>=0.39.0,<0.40.0" --no-binary python-qpid-proton --verbose --no-cache-dir
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
### Async Interface
|
|
54
|
+
### Async Interface
|
|
55
55
|
|
|
56
56
|
The client provides an async interface via the `rabbitmq_amqp_python_client.asyncio` module. The async classes act as facades that:
|
|
57
57
|
|
|
@@ -80,3 +80,9 @@ Additional async examples are available in the [examples](./examples) folder:
|
|
|
80
80
|
- TLS: [examples/tls/tls_example_async.py](./examples/tls/tls_example_async.py)
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
### Thread Safety
|
|
84
|
+
|
|
85
|
+
The TCP connection is not thread safe.
|
|
86
|
+
If you want to use the client in a multithreaded environment, you should create a connection per thread.
|
|
87
|
+
|
|
88
|
+
|
|
@@ -1,33 +1,37 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "rabbitmq-amqp-python-client"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "Python RabbitMQ client for AMQP 1.0 protocol"
|
|
5
5
|
authors = ["RabbitMQ team"]
|
|
6
6
|
license = "Apache-2.0 license"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
|
|
9
9
|
[tool.poetry.dependencies]
|
|
10
|
-
python = "^3.
|
|
10
|
+
python = "^3.10"
|
|
11
11
|
python-qpid-proton = "^0.40.0"
|
|
12
|
-
typing-extensions = "^4.
|
|
13
|
-
packaging = "^
|
|
12
|
+
typing-extensions = "^4.15.0"
|
|
13
|
+
packaging = "^26.0"
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
[tool.poetry.group.dev.dependencies]
|
|
17
|
-
pyjwt = "^2.
|
|
18
|
-
pika = "^1.
|
|
19
|
-
flake8 = "^7.
|
|
20
|
-
isort = "^
|
|
21
|
-
mypy = "^
|
|
22
|
-
pytest = "^
|
|
23
|
-
black = "^
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
pytest-asyncio = "^1.2.0"
|
|
17
|
+
pyjwt = "^2.12.1"
|
|
18
|
+
pika = "^1.4.0"
|
|
19
|
+
flake8 = "^7.3.0"
|
|
20
|
+
isort = "^8.0.1"
|
|
21
|
+
mypy = "^1.20.1"
|
|
22
|
+
pytest = "^9.0.3"
|
|
23
|
+
black = "^26.3.1"
|
|
24
|
+
requests = "^2.33.1"
|
|
25
|
+
pytest-asyncio = "^1.3.0"
|
|
27
26
|
|
|
28
27
|
[build-system]
|
|
29
28
|
requires = ["poetry-core"]
|
|
30
29
|
build-backend = "poetry.core.masonry.api"
|
|
31
30
|
|
|
31
|
+
[tool.black]
|
|
32
|
+
extend-exclude = "rabbitmq_amqp_python_client/qpid"
|
|
33
|
+
|
|
32
34
|
[tool.pytest.ini_options]
|
|
33
|
-
asyncio_mode = "auto"
|
|
35
|
+
asyncio_mode = "auto"
|
|
36
|
+
addopts = "-v"
|
|
37
|
+
console_output_style = "count"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from importlib import metadata
|
|
2
|
+
from typing import Optional
|
|
2
3
|
|
|
3
4
|
from .address_helper import AddressHelper
|
|
4
5
|
from .amqp_consumer_handler import AMQPMessagingHandler
|
|
@@ -23,12 +24,14 @@ from .entities import (
|
|
|
23
24
|
MessageProperties,
|
|
24
25
|
OAuth2Options,
|
|
25
26
|
OffsetSpecification,
|
|
27
|
+
QuorumConsumerOptions,
|
|
26
28
|
RecoveryConfiguration,
|
|
27
29
|
StreamConsumerOptions,
|
|
28
30
|
StreamFilterOptions,
|
|
29
31
|
)
|
|
30
32
|
from .environment import Environment
|
|
31
33
|
from .exceptions import (
|
|
34
|
+
AmqpMessageRejectedException,
|
|
32
35
|
ArgumentOutOfRangeException,
|
|
33
36
|
InvalidOperationException,
|
|
34
37
|
ValidationCodeException,
|
|
@@ -57,6 +60,7 @@ from .ssl_configuration import (
|
|
|
57
60
|
)
|
|
58
61
|
from .utils import Converter
|
|
59
62
|
|
|
63
|
+
__license__: Optional[str]
|
|
60
64
|
try:
|
|
61
65
|
__version__ = metadata.version(__package__)
|
|
62
66
|
__license__ = metadata.metadata(__package__)["license"]
|
|
@@ -91,6 +95,7 @@ __all__ = [
|
|
|
91
95
|
"AMQPMessagingHandler",
|
|
92
96
|
"ArgumentOutOfRangeException",
|
|
93
97
|
"ValidationCodeException",
|
|
98
|
+
"AmqpMessageRejectedException",
|
|
94
99
|
"PosixSslConfigurationContext",
|
|
95
100
|
"WinSslConfigurationContext",
|
|
96
101
|
"PosixClientCert",
|
|
@@ -103,6 +108,7 @@ __all__ = [
|
|
|
103
108
|
"StreamFilterOptions",
|
|
104
109
|
"AbcConsumerOptions",
|
|
105
110
|
"ConsumerOptions",
|
|
111
|
+
"QuorumConsumerOptions",
|
|
106
112
|
"MessageProperties",
|
|
107
113
|
"OffsetSpecification",
|
|
108
114
|
"OutcomeState",
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
from .delivery_context import (
|
|
4
|
+
AbcDeliveryContext,
|
|
5
|
+
DeliveryContext,
|
|
6
|
+
)
|
|
7
|
+
from .qpid.proton._endpoints import Link
|
|
8
|
+
from .qpid.proton._events import Event
|
|
9
|
+
from .qpid.proton._handler import Handler
|
|
10
|
+
from .qpid.proton.handlers import MessagingHandler
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
AMQPMessagingHandler extends the QPID MessagingHandler.
|
|
14
|
+
It is an helper to set the default values needed for manually accepting and settling messages.
|
|
15
|
+
self.delivery_context is an instance of IDeliveryContext (defaults to DeliveryContext), which is used to accept, reject,
|
|
16
|
+
requeue or requeue with annotations a message.
|
|
17
|
+
It is not mandatory to use this class, but it is a good practice to use it.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _DeferredFlowController(Handler):
|
|
22
|
+
"""
|
|
23
|
+
A flow controller that intentionally omits on_link_local_open.
|
|
24
|
+
|
|
25
|
+
The standard FlowController grants receiver credit in both on_link_local_open
|
|
26
|
+
and on_link_remote_open. Granting credit during on_link_local_open causes the
|
|
27
|
+
server to start delivering messages while BlockingLink.__init__() is still
|
|
28
|
+
running its wait() loop (i.e. before consumer.run() is ever called), because:
|
|
29
|
+
|
|
30
|
+
1. The FLOW frame carrying that credit is flushed to the server in the same
|
|
31
|
+
selector.select() call that performs the ATTACH handshake.
|
|
32
|
+
2. The server immediately sends stream messages back.
|
|
33
|
+
3. Those messages arrive in the same t.push() read as the ATTACH response
|
|
34
|
+
and are dispatched in the same process() iteration — inside wait() —
|
|
35
|
+
so on_amqp_message fires before consumer.run() returns.
|
|
36
|
+
|
|
37
|
+
By omitting on_link_local_open the FLOW frame is only queued (in
|
|
38
|
+
on_link_remote_open) after the remote ATTACH is received, at which point the
|
|
39
|
+
wait() loop has already exited. The queued FLOW is sent only when
|
|
40
|
+
consumer.run() drives the first process() cycle, ensuring messages start
|
|
41
|
+
arriving only after run() is called.
|
|
42
|
+
|
|
43
|
+
Credit replenishment (on_link_flow / on_delivery) works exactly as before.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, window: int = 10) -> None:
|
|
47
|
+
self._window = window
|
|
48
|
+
self._drained = 0
|
|
49
|
+
|
|
50
|
+
def on_link_remote_open(self, event: Event) -> None:
|
|
51
|
+
self._flow(event.link)
|
|
52
|
+
|
|
53
|
+
def on_link_flow(self, event: Event) -> None:
|
|
54
|
+
self._flow(event.link)
|
|
55
|
+
|
|
56
|
+
def on_delivery(self, event: Event) -> None:
|
|
57
|
+
self._flow(event.link)
|
|
58
|
+
|
|
59
|
+
def _flow(self, link: Union["Link", object]) -> None:
|
|
60
|
+
if link.is_receiver: # type: ignore[union-attr]
|
|
61
|
+
self._drained += link.drained() # type: ignore[union-attr]
|
|
62
|
+
if self._drained == 0:
|
|
63
|
+
delta = self._window - link.credit # type: ignore[union-attr]
|
|
64
|
+
link.flow(delta) # type: ignore[union-attr]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AMQPMessagingHandler(MessagingHandler):
|
|
68
|
+
|
|
69
|
+
def __init__(self, auto_accept: bool = False, auto_settle: bool = True):
|
|
70
|
+
"""
|
|
71
|
+
:param auto_accept: if True, the message is automatically accepted
|
|
72
|
+
by default is false, so the user has to manually accept the message and decide with the
|
|
73
|
+
different methods of the delivery_context what to do with the message
|
|
74
|
+
"""
|
|
75
|
+
# prefetch=0 disables the built-in FlowController so it cannot grant
|
|
76
|
+
# credit during link setup (which would make messages arrive before
|
|
77
|
+
# consumer.run() is called). _DeferredFlowController is added instead;
|
|
78
|
+
# it behaves identically except it does not implement on_link_local_open.
|
|
79
|
+
super().__init__(prefetch=0, auto_accept=auto_accept, auto_settle=auto_settle)
|
|
80
|
+
self.handlers.append(_DeferredFlowController())
|
|
81
|
+
self.delivery_context: AbcDeliveryContext = DeliveryContext()
|
|
82
|
+
self._offset = 0
|
|
83
|
+
|
|
84
|
+
def on_amqp_message(self, event: Event) -> None:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def on_message(self, event: Event) -> None:
|
|
88
|
+
if "x-stream-offset" in event.message.annotations:
|
|
89
|
+
self._offset = int(event.message.annotations["x-stream-offset"])
|
|
90
|
+
self.on_amqp_message(event)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def offset(self) -> int:
|
|
94
|
+
return self._offset
|
|
@@ -5,6 +5,7 @@ import logging
|
|
|
5
5
|
from typing import Callable, Optional, Union
|
|
6
6
|
|
|
7
7
|
from ..address_helper import validate_address
|
|
8
|
+
from ..amqp_consumer_handler import AMQPMessagingHandler
|
|
8
9
|
from ..connection import Connection
|
|
9
10
|
from ..consumer import Consumer
|
|
10
11
|
from ..entities import (
|
|
@@ -15,7 +16,6 @@ from ..entities import (
|
|
|
15
16
|
from ..exceptions import ArgumentOutOfRangeException
|
|
16
17
|
from ..management import Management
|
|
17
18
|
from ..publisher import Publisher
|
|
18
|
-
from ..qpid.proton._handlers import MessagingHandler
|
|
19
19
|
from ..ssl_configuration import (
|
|
20
20
|
PosixSslConfigurationContext,
|
|
21
21
|
WinSslConfigurationContext,
|
|
@@ -226,8 +226,8 @@ class AsyncConnection:
|
|
|
226
226
|
|
|
227
227
|
async def consumer(
|
|
228
228
|
self,
|
|
229
|
-
destination: str,
|
|
230
|
-
message_handler: Optional[
|
|
229
|
+
destination: Optional[str] = None,
|
|
230
|
+
message_handler: Optional[AMQPMessagingHandler] = None,
|
|
231
231
|
consumer_options: Optional[AbcConsumerOptions] = None,
|
|
232
232
|
credit: Optional[int] = None,
|
|
233
233
|
) -> AsyncConsumer:
|
|
@@ -235,7 +235,8 @@ class AsyncConnection:
|
|
|
235
235
|
Create a new consumer instance.
|
|
236
236
|
|
|
237
237
|
Args:
|
|
238
|
-
destination: The address to consume from
|
|
238
|
+
destination: The address to consume from. Use ``None`` only with
|
|
239
|
+
options such as Direct Reply-To where the broker supplies the address.
|
|
239
240
|
message_handler: Optional handler for processing messages
|
|
240
241
|
consumer_options: Optional configuration for queue consumption. Each queue has its own consumer options.
|
|
241
242
|
credit: Optional credit value for flow control
|
|
@@ -247,7 +248,7 @@ class AsyncConnection:
|
|
|
247
248
|
RuntimeError: If consumer creation fails
|
|
248
249
|
ArgumentOutOfRangeException: If destination address format is invalid
|
|
249
250
|
"""
|
|
250
|
-
if not validate_address(destination):
|
|
251
|
+
if destination is not None and not validate_address(destination):
|
|
251
252
|
raise ArgumentOutOfRangeException(
|
|
252
253
|
"destination address must start with /queues or /exchanges"
|
|
253
254
|
)
|
|
@@ -264,7 +265,7 @@ class AsyncConnection:
|
|
|
264
265
|
async_consumer = AsyncConsumer(
|
|
265
266
|
self._connection._conn,
|
|
266
267
|
destination,
|
|
267
|
-
message_handler,
|
|
268
|
+
message_handler,
|
|
268
269
|
consumer_options,
|
|
269
270
|
credit,
|
|
270
271
|
connection_lock=self._connection_lock,
|
|
@@ -3,22 +3,13 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
5
|
import threading
|
|
6
|
-
import warnings
|
|
7
6
|
from types import TracebackType
|
|
8
|
-
from typing import
|
|
9
|
-
Any,
|
|
10
|
-
Callable,
|
|
11
|
-
Literal,
|
|
12
|
-
Optional,
|
|
13
|
-
Type,
|
|
14
|
-
Union,
|
|
15
|
-
)
|
|
7
|
+
from typing import Any, Callable, Optional, Type
|
|
16
8
|
|
|
17
9
|
from ..amqp_consumer_handler import AMQPMessagingHandler
|
|
18
10
|
from ..consumer import Consumer
|
|
19
11
|
from ..entities import AbcConsumerOptions
|
|
20
12
|
from ..qpid.proton._exceptions import Timeout
|
|
21
|
-
from ..qpid.proton._message import Message
|
|
22
13
|
from ..qpid.proton.utils import BlockingConnection
|
|
23
14
|
|
|
24
15
|
logger = logging.getLogger(__name__)
|
|
@@ -38,7 +29,7 @@ class AsyncConsumer:
|
|
|
38
29
|
Attributes:
|
|
39
30
|
_consumer (Optional[Consumer]): The underlying synchronous Consumer
|
|
40
31
|
_conn (BlockingConnection): The shared blocking connection
|
|
41
|
-
_addr (str): The address to consume from
|
|
32
|
+
_addr (Optional[str]): The address to consume from
|
|
42
33
|
_handler (Optional[AMQPMessagingHandler]): Optional message handling callback
|
|
43
34
|
_stream_options (Optional[AbcConsumerOptions]): Configuration for stream consumption
|
|
44
35
|
_credit (Optional[int]): Flow control credit value
|
|
@@ -55,7 +46,7 @@ class AsyncConsumer:
|
|
|
55
46
|
def __init__(
|
|
56
47
|
self,
|
|
57
48
|
conn: BlockingConnection,
|
|
58
|
-
addr: str,
|
|
49
|
+
addr: Optional[str] = None,
|
|
59
50
|
handler: Optional[AMQPMessagingHandler] = None,
|
|
60
51
|
stream_options: Optional[AbcConsumerOptions] = None,
|
|
61
52
|
credit: Optional[int] = None,
|
|
@@ -68,7 +59,7 @@ class AsyncConsumer:
|
|
|
68
59
|
|
|
69
60
|
Args:
|
|
70
61
|
conn (BlockingConnection): The blocking connection to use
|
|
71
|
-
addr (str): The address to consume from
|
|
62
|
+
addr (Optional[str]): The address to consume from; ``None`` for Direct Reply-To
|
|
72
63
|
handler (Optional[AMQPMessagingHandler]): Optional message handler for processing received messages
|
|
73
64
|
stream_options (Optional[AbcConsumerOptions]): Optional configuration for stream-based consumption
|
|
74
65
|
credit (Optional[int]): Optional credit value for flow control
|
|
@@ -119,46 +110,6 @@ class AsyncConsumer:
|
|
|
119
110
|
self._stop_event.clear()
|
|
120
111
|
logger.debug(f"AsyncConsumer opened for address: {self._addr}")
|
|
121
112
|
|
|
122
|
-
async def consume(
|
|
123
|
-
self, timeout: Union[None, Literal[False], float] = False
|
|
124
|
-
) -> Message:
|
|
125
|
-
"""
|
|
126
|
-
Consume a message from the queue.
|
|
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
|
-
|
|
133
|
-
Args:
|
|
134
|
-
timeout: The time to wait for a message.
|
|
135
|
-
None: Defaults to 60s
|
|
136
|
-
float: Wait for specified number of seconds
|
|
137
|
-
|
|
138
|
-
Returns:
|
|
139
|
-
Message: The received message
|
|
140
|
-
|
|
141
|
-
Raises:
|
|
142
|
-
RuntimeError: If consumer is not opened
|
|
143
|
-
|
|
144
|
-
Note:
|
|
145
|
-
The return type might be None if no message is available and timeout occurs,
|
|
146
|
-
but this is handled by the cast to Message.
|
|
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
|
-
)
|
|
154
|
-
if not self._opened or self._consumer is None:
|
|
155
|
-
raise RuntimeError(
|
|
156
|
-
"Consumer is not opened. Call open() or use async context manager."
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
async with self._connection_lock:
|
|
160
|
-
return await asyncio.to_thread(self._consumer.consume, timeout)
|
|
161
|
-
|
|
162
113
|
async def close(self) -> None:
|
|
163
114
|
"""
|
|
164
115
|
Close the consumer connection.
|
|
@@ -336,8 +287,10 @@ class AsyncConsumer:
|
|
|
336
287
|
await self.close()
|
|
337
288
|
|
|
338
289
|
@property
|
|
339
|
-
def address(self) -> str:
|
|
340
|
-
"""Get the current consumer address."""
|
|
290
|
+
def address(self) -> Optional[str]:
|
|
291
|
+
"""Get the current consumer address (from the link when the receiver is open)."""
|
|
292
|
+
if self._opened and self._consumer is not None:
|
|
293
|
+
return self._consumer.address
|
|
341
294
|
return self._addr
|
|
342
295
|
|
|
343
296
|
@property
|
|
@@ -4,16 +4,20 @@ import time
|
|
|
4
4
|
from datetime import timedelta
|
|
5
5
|
from typing import (
|
|
6
6
|
Annotated,
|
|
7
|
+
Any,
|
|
7
8
|
Callable,
|
|
9
|
+
Mapping,
|
|
8
10
|
Optional,
|
|
9
11
|
TypeVar,
|
|
10
12
|
Union,
|
|
13
|
+
cast,
|
|
11
14
|
)
|
|
12
15
|
|
|
13
16
|
import typing_extensions
|
|
14
17
|
from packaging import version
|
|
15
18
|
|
|
16
19
|
from .address_helper import validate_address
|
|
20
|
+
from .amqp_consumer_handler import AMQPMessagingHandler
|
|
17
21
|
from .consumer import Consumer
|
|
18
22
|
from .entities import (
|
|
19
23
|
AbcConsumerOptions,
|
|
@@ -27,7 +31,6 @@ from .exceptions import (
|
|
|
27
31
|
from .management import Management
|
|
28
32
|
from .publisher import Publisher
|
|
29
33
|
from .qpid.proton._exceptions import ConnectionException
|
|
30
|
-
from .qpid.proton._handlers import MessagingHandler
|
|
31
34
|
from .qpid.proton._transport import SSLDomain
|
|
32
35
|
from .qpid.proton.utils import BlockingConnection
|
|
33
36
|
from .ssl_configuration import (
|
|
@@ -94,7 +97,7 @@ class Connection:
|
|
|
94
97
|
] = ssl_context
|
|
95
98
|
self._managements: list[Management] = []
|
|
96
99
|
self._recovery_configuration: RecoveryConfiguration = recovery_configuration
|
|
97
|
-
self._ssl_domain = None
|
|
100
|
+
self._ssl_domain: Optional[SSLDomain] = None
|
|
98
101
|
self._connections = [] # type: ignore
|
|
99
102
|
self._index: int = -1
|
|
100
103
|
self._publishers: list[Publisher] = []
|
|
@@ -185,8 +188,9 @@ class Connection:
|
|
|
185
188
|
if remote_props is None:
|
|
186
189
|
raise ValidationCodeException("No remote properties received from server")
|
|
187
190
|
|
|
191
|
+
props = cast(Mapping[str, Any], remote_props)
|
|
188
192
|
# Check if server is RabbitMQ
|
|
189
|
-
product =
|
|
193
|
+
product = props.get("product")
|
|
190
194
|
if product != "RabbitMQ":
|
|
191
195
|
raise ValidationCodeException(
|
|
192
196
|
f"Connection to non-RabbitMQ server detected. "
|
|
@@ -194,7 +198,7 @@ class Connection:
|
|
|
194
198
|
)
|
|
195
199
|
|
|
196
200
|
# Check server version is >= 4.0.0
|
|
197
|
-
server_version =
|
|
201
|
+
server_version = props.get("version")
|
|
198
202
|
if server_version is None:
|
|
199
203
|
raise ValidationCodeException("Server version not provided")
|
|
200
204
|
|
|
@@ -232,7 +236,8 @@ class Connection:
|
|
|
232
236
|
if remote_props is None:
|
|
233
237
|
raise ValidationCodeException("No remote properties received from server")
|
|
234
238
|
|
|
235
|
-
|
|
239
|
+
props = cast(Mapping[str, Any], remote_props)
|
|
240
|
+
server_version = props.get("version")
|
|
236
241
|
if server_version is None:
|
|
237
242
|
raise ValidationCodeException("Server version not provided")
|
|
238
243
|
|
|
@@ -298,7 +303,7 @@ class Connection:
|
|
|
298
303
|
|
|
299
304
|
self._ssl_domain.set_credentials(
|
|
300
305
|
client_cert,
|
|
301
|
-
client_key,
|
|
306
|
+
client_key or "",
|
|
302
307
|
password,
|
|
303
308
|
)
|
|
304
309
|
|
|
@@ -380,7 +385,7 @@ class Connection:
|
|
|
380
385
|
def consumer(
|
|
381
386
|
self,
|
|
382
387
|
destination: Optional[str] = None,
|
|
383
|
-
message_handler: Optional[
|
|
388
|
+
message_handler: Optional[AMQPMessagingHandler] = None,
|
|
384
389
|
consumer_options: Optional[AbcConsumerOptions] = None,
|
|
385
390
|
credit: Optional[int] = None,
|
|
386
391
|
) -> Consumer:
|
|
@@ -411,6 +416,7 @@ class Connection:
|
|
|
411
416
|
"4.0.0": self._is_server_version_gte("4.0.0"),
|
|
412
417
|
"4.1.0": self._is_server_version_gte("4.1.0"),
|
|
413
418
|
"4.2.0": self._is_server_version_gte("4.2.0"),
|
|
419
|
+
"4.3.0": self._is_server_version_gte("4.3.0"),
|
|
414
420
|
}
|
|
415
421
|
)
|
|
416
422
|
consumer = Consumer(
|
|
@@ -419,7 +425,7 @@ class Connection:
|
|
|
419
425
|
self._consumers.append(consumer)
|
|
420
426
|
return consumer
|
|
421
427
|
|
|
422
|
-
def _on_disconnection(self) -> None:
|
|
428
|
+
def _on_disconnection(self, *_args: Any) -> None:
|
|
423
429
|
|
|
424
430
|
logger.debug("_on_disconnection: disconnection detected")
|
|
425
431
|
if self in self._connections:
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
import
|
|
3
|
-
from typing import Literal, Optional, Union, cast
|
|
2
|
+
from typing import Optional
|
|
4
3
|
|
|
5
4
|
from .amqp_consumer_handler import AMQPMessagingHandler
|
|
6
5
|
from .entities import (
|
|
7
6
|
AbcConsumerOptions,
|
|
8
7
|
ConsumerOptions,
|
|
8
|
+
QuorumConsumerOptions,
|
|
9
9
|
StreamConsumerOptions,
|
|
10
10
|
)
|
|
11
11
|
from .options import (
|
|
@@ -14,7 +14,7 @@ from .options import (
|
|
|
14
14
|
ReceiverOptionUnsettled,
|
|
15
15
|
ReceiverOptionUnsettledWithFilters,
|
|
16
16
|
)
|
|
17
|
-
from .qpid.proton.
|
|
17
|
+
from .qpid.proton._flow_properties import SAC_CALLBACK_KEY
|
|
18
18
|
from .qpid.proton.utils import (
|
|
19
19
|
BlockingConnection,
|
|
20
20
|
BlockingReceiver,
|
|
@@ -70,6 +70,7 @@ class Consumer:
|
|
|
70
70
|
def _open(self) -> None:
|
|
71
71
|
if self._receiver is None:
|
|
72
72
|
logger.debug("Creating Receiver")
|
|
73
|
+
self._register_sac_callback()
|
|
73
74
|
self._receiver = self._create_receiver(self._addr)
|
|
74
75
|
|
|
75
76
|
def _update_connection(self, conn: BlockingConnection) -> None:
|
|
@@ -101,7 +102,10 @@ class Consumer:
|
|
|
101
102
|
),
|
|
102
103
|
handler=self._handler,
|
|
103
104
|
)
|
|
104
|
-
elif isinstance(
|
|
105
|
+
elif isinstance(
|
|
106
|
+
self._consumer_options, (QuorumConsumerOptions, ConsumerOptions)
|
|
107
|
+
):
|
|
108
|
+
self._register_sac_callback()
|
|
105
109
|
self._receiver = self._create_receiver_from_cq_consumer_options(
|
|
106
110
|
addr, credit
|
|
107
111
|
)
|
|
@@ -109,39 +113,6 @@ class Consumer:
|
|
|
109
113
|
def _set_consumers_list(self, consumers: []) -> None: # type: ignore
|
|
110
114
|
self._consumers = consumers
|
|
111
115
|
|
|
112
|
-
def consume(self, timeout: Union[None, Literal[False], float] = False) -> Message:
|
|
113
|
-
"""
|
|
114
|
-
Consume a message from the queue.
|
|
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
|
-
|
|
121
|
-
Args:
|
|
122
|
-
timeout: The time to wait for a message.
|
|
123
|
-
None: Defaults to 60s
|
|
124
|
-
float: Wait for specified number of seconds
|
|
125
|
-
|
|
126
|
-
Returns:
|
|
127
|
-
Message: The received message
|
|
128
|
-
|
|
129
|
-
Note:
|
|
130
|
-
The return type might be None if no message is available and timeout occurs,
|
|
131
|
-
but this is handled by the cast to Message.
|
|
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
|
-
)
|
|
139
|
-
if self._receiver is not None:
|
|
140
|
-
message_original = self._receiver.receive(timeout=timeout)
|
|
141
|
-
message = cast(Message, message_original)
|
|
142
|
-
return message
|
|
143
|
-
raise Exception("Receiver is not initialized")
|
|
144
|
-
|
|
145
116
|
def close(self) -> None:
|
|
146
117
|
"""
|
|
147
118
|
Close the consumer connection.
|
|
@@ -149,11 +120,43 @@ class Consumer:
|
|
|
149
120
|
Closes the receiver if it exists and cleans up resources.
|
|
150
121
|
"""
|
|
151
122
|
logger.debug("Closing the receiver")
|
|
123
|
+
self._unregister_sac_callback()
|
|
152
124
|
if self._receiver is not None:
|
|
153
|
-
self._receiver.close()
|
|
125
|
+
self._receiver.close() # type: ignore[no-untyped-call]
|
|
154
126
|
if self in self._consumers:
|
|
155
127
|
self._consumers.remove(self)
|
|
156
128
|
|
|
129
|
+
def _register_sac_callback(self) -> None:
|
|
130
|
+
"""Register the SAC callback on the transport for QuorumConsumerOptions."""
|
|
131
|
+
if not isinstance(self._consumer_options, QuorumConsumerOptions):
|
|
132
|
+
return
|
|
133
|
+
cb = self._consumer_options.sac_state_handler
|
|
134
|
+
if cb is None:
|
|
135
|
+
return
|
|
136
|
+
try:
|
|
137
|
+
conn = self._conn.conn
|
|
138
|
+
if conn is None:
|
|
139
|
+
return
|
|
140
|
+
transport = conn.transport
|
|
141
|
+
if transport is not None:
|
|
142
|
+
setattr(transport, SAC_CALLBACK_KEY, cb)
|
|
143
|
+
logger.debug("Registered SAC callback on transport")
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
logger.debug("Could not register SAC callback: %s", exc)
|
|
146
|
+
|
|
147
|
+
def _unregister_sac_callback(self) -> None:
|
|
148
|
+
"""Remove the SAC callback from the transport, if one was registered."""
|
|
149
|
+
try:
|
|
150
|
+
conn = self._conn.conn
|
|
151
|
+
if conn is None:
|
|
152
|
+
return
|
|
153
|
+
transport = conn.transport
|
|
154
|
+
if transport is not None and hasattr(transport, SAC_CALLBACK_KEY):
|
|
155
|
+
delattr(transport, SAC_CALLBACK_KEY)
|
|
156
|
+
logger.debug("Unregistered SAC callback from transport")
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
logger.debug("Could not unregister SAC callback: %s", exc)
|
|
159
|
+
|
|
157
160
|
def run(self) -> None:
|
|
158
161
|
"""
|
|
159
162
|
Run the consumer in continuous mode.
|
|
@@ -208,6 +211,9 @@ class Consumer:
|
|
|
208
211
|
handler=self._handler,
|
|
209
212
|
)
|
|
210
213
|
|
|
214
|
+
if isinstance(self._consumer_options, QuorumConsumerOptions):
|
|
215
|
+
return self._create_receiver_from_cq_consumer_options(addr, credit)
|
|
216
|
+
|
|
211
217
|
if isinstance(self._consumer_options, ConsumerOptions):
|
|
212
218
|
return self._create_receiver_from_cq_consumer_options(addr, credit)
|
|
213
219
|
|
|
@@ -253,7 +259,7 @@ class Consumer:
|
|
|
253
259
|
@property
|
|
254
260
|
def address(self) -> Optional[str]:
|
|
255
261
|
if self._receiver is not None:
|
|
256
|
-
return
|
|
262
|
+
return self._receiver.link.remote_source.address
|
|
257
263
|
else:
|
|
258
264
|
raise Exception("Receiver is not initialized")
|
|
259
265
|
|