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.

Files changed (53) hide show
  1. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/PKG-INFO +11 -6
  2. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/README.md +7 -1
  3. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/pyproject.toml +19 -15
  4. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/__init__.py +6 -0
  5. rabbitmq_amqp_python_client-0.7.0/rabbitmq_amqp_python_client/amqp_consumer_handler.py +94 -0
  6. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/connection.py +7 -6
  7. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/consumer.py +8 -55
  8. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/connection.py +14 -8
  9. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/consumer.py +45 -39
  10. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/delivery_context.py +21 -0
  11. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/entities.py +72 -4
  12. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/environment.py +4 -0
  13. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/exceptions.py +15 -0
  14. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/management.py +24 -11
  15. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/options.py +10 -10
  16. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/publisher.py +35 -10
  17. rabbitmq_amqp_python_client-0.7.0/rabbitmq_amqp_python_client/qpid/proton/_flow_properties.py +142 -0
  18. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_handler.py +8 -1
  19. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_handlers.py +10 -1
  20. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_utils.py +0 -2
  21. rabbitmq_amqp_python_client-0.6.0/rabbitmq_amqp_python_client/amqp_consumer_handler.py +0 -39
  22. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/LICENSE +0 -0
  23. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/address_helper.py +0 -0
  24. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/__init__.py +0 -0
  25. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/environment.py +0 -0
  26. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/management.py +0 -0
  27. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/asyncio/publisher.py +0 -0
  28. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/common.py +0 -0
  29. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/py.typed +0 -0
  30. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/__init__.py +0 -0
  31. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_common.py +0 -0
  32. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_condition.py +0 -0
  33. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_data.py +0 -0
  34. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_delivery.py +0 -0
  35. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_endpoints.py +0 -0
  36. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_events.py +0 -0
  37. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_exceptions.py +0 -0
  38. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_io.py +0 -0
  39. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_message.py +0 -0
  40. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_reactor.py +0 -0
  41. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_selectable.py +0 -0
  42. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_tracing.py +0 -0
  43. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_transport.py +0 -0
  44. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_url.py +0 -0
  45. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/_wrapper.py +0 -0
  46. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/handlers.py +0 -0
  47. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/py.typed +0 -0
  48. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/reactor.py +0 -0
  49. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/tracing.py +0 -0
  50. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/qpid/proton/utils.py +0 -0
  51. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/queues.py +0 -0
  52. {rabbitmq_amqp_python_client-0.6.0 → rabbitmq_amqp_python_client-0.7.0}/rabbitmq_amqp_python_client/ssl_configuration.py +0 -0
  53. {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.6.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.9,<4.0
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 (>=24.0,<25.0)
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.13.0,<5.0.0)
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 (Experimental)
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 (Experimental)
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.6.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.9"
10
+ python = "^3.10"
11
11
  python-qpid-proton = "^0.40.0"
12
- typing-extensions = "^4.13.0"
13
- packaging = "^24.0"
12
+ typing-extensions = "^4.15.0"
13
+ packaging = "^26.0"
14
14
 
15
15
 
16
16
  [tool.poetry.group.dev.dependencies]
17
- pyjwt = "^2.10.1"
18
- pika = "^1.3.2"
19
- flake8 = "^7.1.1"
20
- isort = "^5.9.3"
21
- mypy = "^0.910"
22
- pytest = "^8.3.4"
23
- black = "^24.3.0"
24
- python-qpid-proton = "^0.40.0"
25
- requests = "^2.31.0"
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[MessagingHandler] = None,
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, # pyright: ignore[reportArgumentType]
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 = remote_props.get("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 = remote_props.get("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
- server_version = remote_props.get("version")
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[MessagingHandler] = None,
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 warnings
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._message import Message
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(self._consumer_options, ConsumerOptions):
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 cast(Optional[str], self._receiver.link.remote_source.address)
262
+ return self._receiver.link.remote_source.address
257
263
  else:
258
264
  raise Exception("Receiver is not initialized")
259
265