rabbitmq-amqp-python-client 0.1.0b3__tar.gz → 0.2.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.
- rabbitmq_amqp_python_client-0.2.0/PKG-INFO +65 -0
- rabbitmq_amqp_python_client-0.2.0/README.md +43 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/pyproject.toml +5 -2
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/__init__.py +10 -2
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/connection.py +96 -6
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/consumer.py +4 -4
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/entities.py +176 -11
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/management.py +5 -5
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/options.py +3 -3
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/publisher.py +8 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_endpoints.py +1 -1
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_exceptions.py +8 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_message.py +21 -16
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_reactor.py +6 -1
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_transport.py +1 -1
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_utils.py +5 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/ssl_configuration.py +2 -2
- rabbitmq_amqp_python_client-0.2.0/rabbitmq_amqp_python_client/utils.py +38 -0
- rabbitmq_amqp_python_client-0.1.0b3/PKG-INFO +0 -183
- rabbitmq_amqp_python_client-0.1.0b3/README.md +0 -163
- rabbitmq_amqp_python_client-0.1.0b3/rabbitmq_amqp_python_client/utils.py +0 -9
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/LICENSE +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/address_helper.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/amqp_consumer_handler.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/common.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/delivery_context.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/environment.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/exceptions.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/py.typed +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/__init__.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_common.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_condition.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_data.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_delivery.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_events.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_handler.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_handlers.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_io.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_selectable.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_tracing.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_url.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_wrapper.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/handlers.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/py.typed +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/reactor.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/tracing.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/utils.py +0 -0
- {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/queues.py +0 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rabbitmq-amqp-python-client
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python RabbitMQ client for AMQP 1.0 protocol
|
|
5
|
+
License: Apache-2.0 license
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: RabbitMQ team
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: License :: Other/Proprietary License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Dist: packaging (>=23.0,<24.0)
|
|
18
|
+
Requires-Dist: python-qpid-proton (>=0.39.0,<0.40.0)
|
|
19
|
+
Requires-Dist: typing-extensions (>=4.13.0,<5.0.0)
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
## RabbitMQ AMQP 1.0 Python Client
|
|
23
|
+
This library is meant to be used with RabbitMQ `4.x`. Suitable for testing in pre-production environments.
|
|
24
|
+
|
|
25
|
+
The client is distributed via [`PIP`](https://pypi.org/project/rabbitmq-amqp-python-client/):
|
|
26
|
+
```bash
|
|
27
|
+
pip install rabbitmq-amqp-python-client
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Getting Started
|
|
31
|
+
|
|
32
|
+
Inside the [examples](./examples) folder you can find a set of examples that show how to use the client.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Documentation
|
|
36
|
+
|
|
37
|
+
[Client Guide](https://www.rabbitmq.com/client-libraries/amqp-client-libraries) select the python section.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Build
|
|
41
|
+
|
|
42
|
+
- `make rabbitmq-server`: run the RabbitMQ server in a docker container
|
|
43
|
+
- `poetry build`: build the source project
|
|
44
|
+
- `poetry install`: resolves and install dependencies
|
|
45
|
+
- `make test`: run the tests
|
|
46
|
+
|
|
47
|
+
### Note for MAC users:
|
|
48
|
+
|
|
49
|
+
To run TLS you need to:
|
|
50
|
+
``` bash
|
|
51
|
+
- pip uninstall python-qpid-proton
|
|
52
|
+
- brew install swig
|
|
53
|
+
- brew install pkg-config
|
|
54
|
+
- export CFLAGS="-I/usr/local/opt/openssl/include"; pip install python-qpid-proton --verbose --no-cache-dir
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Read more about the issue [here](https://stackoverflow.com/questions/44979947/python-qpid-proton-for-mac-using-amqps)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
## RabbitMQ AMQP 1.0 Python Client
|
|
2
|
+
This library is meant to be used with RabbitMQ `4.x`. Suitable for testing in pre-production environments.
|
|
3
|
+
|
|
4
|
+
The client is distributed via [`PIP`](https://pypi.org/project/rabbitmq-amqp-python-client/):
|
|
5
|
+
```bash
|
|
6
|
+
pip install rabbitmq-amqp-python-client
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
### Getting Started
|
|
10
|
+
|
|
11
|
+
Inside the [examples](./examples) folder you can find a set of examples that show how to use the client.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Documentation
|
|
15
|
+
|
|
16
|
+
[Client Guide](https://www.rabbitmq.com/client-libraries/amqp-client-libraries) select the python section.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Build
|
|
20
|
+
|
|
21
|
+
- `make rabbitmq-server`: run the RabbitMQ server in a docker container
|
|
22
|
+
- `poetry build`: build the source project
|
|
23
|
+
- `poetry install`: resolves and install dependencies
|
|
24
|
+
- `make test`: run the tests
|
|
25
|
+
|
|
26
|
+
### Note for MAC users:
|
|
27
|
+
|
|
28
|
+
To run TLS you need to:
|
|
29
|
+
``` bash
|
|
30
|
+
- pip uninstall python-qpid-proton
|
|
31
|
+
- brew install swig
|
|
32
|
+
- brew install pkg-config
|
|
33
|
+
- export CFLAGS="-I/usr/local/opt/openssl/include"; pip install python-qpid-proton --verbose --no-cache-dir
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Read more about the issue [here](https://stackoverflow.com/questions/44979947/python-qpid-proton-for-mac-using-amqps)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "rabbitmq-amqp-python-client"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Python RabbitMQ client for AMQP 1.0 protocol"
|
|
5
5
|
authors = ["RabbitMQ team"]
|
|
6
6
|
license = "Apache-2.0 license"
|
|
@@ -9,10 +9,13 @@ readme = "README.md"
|
|
|
9
9
|
[tool.poetry.dependencies]
|
|
10
10
|
python = "^3.9"
|
|
11
11
|
python-qpid-proton = "^0.39.0"
|
|
12
|
-
pyjwt = "^2.10.1"
|
|
13
12
|
typing-extensions = "^4.13.0"
|
|
13
|
+
packaging = "^23.0"
|
|
14
|
+
|
|
14
15
|
|
|
15
16
|
[tool.poetry.group.dev.dependencies]
|
|
17
|
+
pyjwt = "^2.10.1"
|
|
18
|
+
pika = "^1.3.2"
|
|
16
19
|
flake8 = "^7.1.1"
|
|
17
20
|
isort = "^5.9.3"
|
|
18
21
|
mypy = "^0.910"
|
|
@@ -6,14 +6,17 @@ from .common import ExchangeType, QueueType
|
|
|
6
6
|
from .connection import Connection
|
|
7
7
|
from .consumer import Consumer
|
|
8
8
|
from .entities import (
|
|
9
|
+
ConsumerOptions,
|
|
9
10
|
ExchangeCustomSpecification,
|
|
10
11
|
ExchangeSpecification,
|
|
11
12
|
ExchangeToExchangeBindingSpecification,
|
|
12
13
|
ExchangeToQueueBindingSpecification,
|
|
14
|
+
MessageProperties,
|
|
13
15
|
OAuth2Options,
|
|
14
16
|
OffsetSpecification,
|
|
15
17
|
RecoveryConfiguration,
|
|
16
|
-
|
|
18
|
+
StreamConsumerOptions,
|
|
19
|
+
StreamFilterOptions,
|
|
17
20
|
)
|
|
18
21
|
from .environment import Environment
|
|
19
22
|
from .exceptions import (
|
|
@@ -42,6 +45,7 @@ from .ssl_configuration import (
|
|
|
42
45
|
WinClientCert,
|
|
43
46
|
WinSslConfigurationContext,
|
|
44
47
|
)
|
|
48
|
+
from .utils import Converter
|
|
45
49
|
|
|
46
50
|
try:
|
|
47
51
|
__version__ = metadata.version(__package__)
|
|
@@ -84,11 +88,15 @@ __all__ = [
|
|
|
84
88
|
"CurrentUserStore",
|
|
85
89
|
"PKCS12Store",
|
|
86
90
|
"ConnectionClosed",
|
|
87
|
-
"
|
|
91
|
+
"StreamConsumerOptions",
|
|
92
|
+
"StreamFilterOptions",
|
|
93
|
+
"ConsumerOptions",
|
|
94
|
+
"MessageProperties",
|
|
88
95
|
"OffsetSpecification",
|
|
89
96
|
"OutcomeState",
|
|
90
97
|
"Environment",
|
|
91
98
|
"ExchangeCustomSpecification",
|
|
92
99
|
"RecoveryConfiguration",
|
|
93
100
|
"OAuth2Options",
|
|
101
|
+
"Converter",
|
|
94
102
|
]
|
|
@@ -11,13 +11,14 @@ from typing import (
|
|
|
11
11
|
)
|
|
12
12
|
|
|
13
13
|
import typing_extensions
|
|
14
|
+
from packaging import version
|
|
14
15
|
|
|
15
16
|
from .address_helper import validate_address
|
|
16
17
|
from .consumer import Consumer
|
|
17
18
|
from .entities import (
|
|
19
|
+
ConsumerOptions,
|
|
18
20
|
OAuth2Options,
|
|
19
21
|
RecoveryConfiguration,
|
|
20
|
-
StreamOptions,
|
|
21
22
|
)
|
|
22
23
|
from .exceptions import (
|
|
23
24
|
ArgumentOutOfRangeException,
|
|
@@ -166,6 +167,87 @@ class Connection:
|
|
|
166
167
|
password=self._password,
|
|
167
168
|
)
|
|
168
169
|
|
|
170
|
+
self._validate_server_properties()
|
|
171
|
+
|
|
172
|
+
def _validate_server_properties(self) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Validate the server properties returned in the connection handshake.
|
|
175
|
+
|
|
176
|
+
Checks that the server is RabbitMQ and the version is >= 4.0.0.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
ValidationCodeException: If server is not RabbitMQ or version < 4.0.0
|
|
180
|
+
"""
|
|
181
|
+
if self._conn is None or self._conn.conn is None:
|
|
182
|
+
raise ValidationCodeException("Connection not established")
|
|
183
|
+
|
|
184
|
+
remote_props = self._conn.conn.remote_properties
|
|
185
|
+
if remote_props is None:
|
|
186
|
+
raise ValidationCodeException("No remote properties received from server")
|
|
187
|
+
|
|
188
|
+
# Check if server is RabbitMQ
|
|
189
|
+
product = remote_props.get("product")
|
|
190
|
+
if product != "RabbitMQ":
|
|
191
|
+
raise ValidationCodeException(
|
|
192
|
+
f"Connection to non-RabbitMQ server detected. "
|
|
193
|
+
f"Expected 'RabbitMQ', got '{product}'"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Check server version is >= 4.0.0
|
|
197
|
+
server_version = remote_props.get("version")
|
|
198
|
+
if server_version is None:
|
|
199
|
+
raise ValidationCodeException("Server version not provided")
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
if version.parse(str(server_version)) < version.parse("4.0.0"):
|
|
203
|
+
raise ValidationCodeException(
|
|
204
|
+
f"The AMQP client library requires RabbitMQ 4.0.0 or higher. "
|
|
205
|
+
f"Server version: {server_version}"
|
|
206
|
+
)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
raise ValidationCodeException(
|
|
209
|
+
f"Failed to parse server version '{server_version}': {e}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
logger.debug(f"Connected to RabbitMQ server version {server_version}")
|
|
213
|
+
|
|
214
|
+
def _is_server_version_gte(self, target_version: str) -> bool:
|
|
215
|
+
"""
|
|
216
|
+
Check if the server version is greater than or equal to version.
|
|
217
|
+
|
|
218
|
+
This is an internal method that can be used to conditionally enable
|
|
219
|
+
features that require RabbitMQ version or higher.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
bool: True if server version >= version, False otherwise
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
ValidationCodeException: If connection is not established or
|
|
226
|
+
remote properties are not available
|
|
227
|
+
"""
|
|
228
|
+
if self._conn is None or self._conn.conn is None:
|
|
229
|
+
raise ValidationCodeException("Connection not established")
|
|
230
|
+
|
|
231
|
+
remote_props = self._conn.conn.remote_properties
|
|
232
|
+
if remote_props is None:
|
|
233
|
+
raise ValidationCodeException("No remote properties received from server")
|
|
234
|
+
|
|
235
|
+
server_version = remote_props.get("version")
|
|
236
|
+
if server_version is None:
|
|
237
|
+
raise ValidationCodeException("Server version not provided")
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
srv = version.parse(str(server_version))
|
|
241
|
+
trg = version.parse(target_version)
|
|
242
|
+
# compare the version even if it contains pre-release or build metadata
|
|
243
|
+
return (
|
|
244
|
+
version.parse("{}.{}.{}".format(srv.major, srv.minor, srv.micro)) >= trg
|
|
245
|
+
)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
raise ValidationCodeException(
|
|
248
|
+
f"Failed to parse server version '{server_version}': {e}"
|
|
249
|
+
)
|
|
250
|
+
|
|
169
251
|
def dial(self) -> None:
|
|
170
252
|
"""
|
|
171
253
|
Establish a connection to the AMQP server.
|
|
@@ -286,7 +368,7 @@ class Connection:
|
|
|
286
368
|
ArgumentOutOfRangeException: If destination address format is invalid
|
|
287
369
|
"""
|
|
288
370
|
if destination != "":
|
|
289
|
-
if validate_address(destination)
|
|
371
|
+
if not validate_address(destination):
|
|
290
372
|
raise ArgumentOutOfRangeException(
|
|
291
373
|
"destination address must start with /queues or /exchanges"
|
|
292
374
|
)
|
|
@@ -299,7 +381,7 @@ class Connection:
|
|
|
299
381
|
self,
|
|
300
382
|
destination: str,
|
|
301
383
|
message_handler: Optional[MessagingHandler] = None,
|
|
302
|
-
|
|
384
|
+
consumer_options: Optional[ConsumerOptions] = None,
|
|
303
385
|
credit: Optional[int] = None,
|
|
304
386
|
) -> Consumer:
|
|
305
387
|
"""
|
|
@@ -308,7 +390,7 @@ class Connection:
|
|
|
308
390
|
Args:
|
|
309
391
|
destination: The address to consume from
|
|
310
392
|
message_handler: Optional handler for processing messages
|
|
311
|
-
|
|
393
|
+
consumer_options: Optional configuration for queue consumption. Each queue has its own consumer options.
|
|
312
394
|
credit: Optional credit value for flow control
|
|
313
395
|
|
|
314
396
|
Returns:
|
|
@@ -317,12 +399,20 @@ class Connection:
|
|
|
317
399
|
Raises:
|
|
318
400
|
ArgumentOutOfRangeException: If destination address format is invalid
|
|
319
401
|
"""
|
|
320
|
-
if validate_address(destination)
|
|
402
|
+
if not validate_address(destination):
|
|
321
403
|
raise ArgumentOutOfRangeException(
|
|
322
404
|
"destination address must start with /queues or /exchanges"
|
|
323
405
|
)
|
|
406
|
+
if consumer_options is not None:
|
|
407
|
+
consumer_options.validate(
|
|
408
|
+
{
|
|
409
|
+
"4.0.0": self._is_server_version_gte("4.0.0"),
|
|
410
|
+
"4.1.0": self._is_server_version_gte("4.1.0"),
|
|
411
|
+
"4.2.0": self._is_server_version_gte("4.2.0"),
|
|
412
|
+
}
|
|
413
|
+
)
|
|
324
414
|
consumer = Consumer(
|
|
325
|
-
self._conn, destination, message_handler,
|
|
415
|
+
self._conn, destination, message_handler, consumer_options, credit
|
|
326
416
|
)
|
|
327
417
|
self._consumers.append(consumer)
|
|
328
418
|
return consumer
|
|
@@ -2,7 +2,7 @@ import logging
|
|
|
2
2
|
from typing import Literal, Optional, Union, cast
|
|
3
3
|
|
|
4
4
|
from .amqp_consumer_handler import AMQPMessagingHandler
|
|
5
|
-
from .entities import
|
|
5
|
+
from .entities import ConsumerOptions
|
|
6
6
|
from .options import (
|
|
7
7
|
ReceiverOptionUnsettled,
|
|
8
8
|
ReceiverOptionUnsettledWithFilters,
|
|
@@ -29,7 +29,7 @@ class Consumer:
|
|
|
29
29
|
_conn (BlockingConnection): The underlying connection to RabbitMQ
|
|
30
30
|
_addr (str): The address to consume from
|
|
31
31
|
_handler (Optional[MessagingHandler]): Optional message handling callback
|
|
32
|
-
_stream_options (Optional[
|
|
32
|
+
_stream_options (Optional[StreamConsumerOptions]): Configuration for stream consumption
|
|
33
33
|
_credit (Optional[int]): Flow control credit value
|
|
34
34
|
"""
|
|
35
35
|
|
|
@@ -38,7 +38,7 @@ class Consumer:
|
|
|
38
38
|
conn: BlockingConnection,
|
|
39
39
|
addr: str,
|
|
40
40
|
handler: Optional[AMQPMessagingHandler] = None,
|
|
41
|
-
stream_options: Optional[
|
|
41
|
+
stream_options: Optional[ConsumerOptions] = None,
|
|
42
42
|
credit: Optional[int] = None,
|
|
43
43
|
):
|
|
44
44
|
"""
|
|
@@ -62,7 +62,7 @@ class Consumer:
|
|
|
62
62
|
|
|
63
63
|
def _open(self) -> None:
|
|
64
64
|
if self._receiver is None:
|
|
65
|
-
logger.debug("Creating
|
|
65
|
+
logger.debug("Creating Receiver")
|
|
66
66
|
self._receiver = self._create_receiver(self._addr)
|
|
67
67
|
|
|
68
68
|
def _update_connection(self, conn: BlockingConnection) -> None:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
|
-
from datetime import timedelta
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Any, Dict, Optional, Union
|
|
5
5
|
|
|
@@ -7,9 +7,13 @@ from .common import ExchangeType, QueueType
|
|
|
7
7
|
from .exceptions import ValidationCodeException
|
|
8
8
|
from .qpid.proton._data import Described, symbol
|
|
9
9
|
|
|
10
|
+
SQL_FILTER = "sql-filter"
|
|
11
|
+
AMQP_SQL_FILTER = "amqp:sql-filter"
|
|
10
12
|
STREAM_FILTER_SPEC = "rabbitmq:stream-filter"
|
|
11
13
|
STREAM_OFFSET_SPEC = "rabbitmq:stream-offset-spec"
|
|
12
14
|
STREAM_FILTER_MATCH_UNFILTERED = "rabbitmq:stream-match-unfiltered"
|
|
15
|
+
AMQP_PROPERTIES_FILTER = "amqp:properties-filter"
|
|
16
|
+
AMQP_APPLICATION_PROPERTIES_FILTER = "amqp:application-properties-filter"
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
@dataclass
|
|
@@ -149,7 +153,77 @@ class ExchangeToExchangeBindingSpecification:
|
|
|
149
153
|
binding_key: Optional[str] = None
|
|
150
154
|
|
|
151
155
|
|
|
152
|
-
class
|
|
156
|
+
class ConsumerOptions:
|
|
157
|
+
def validate(self, versions: Dict[str, bool]) -> None:
|
|
158
|
+
raise NotImplementedError("Subclasses should implement this method")
|
|
159
|
+
|
|
160
|
+
def filter_set(self) -> Dict[symbol, Described]:
|
|
161
|
+
raise NotImplementedError("Subclasses should implement this method")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class MessageProperties:
|
|
166
|
+
"""
|
|
167
|
+
Properties for an AMQP message.
|
|
168
|
+
|
|
169
|
+
Attributes:
|
|
170
|
+
message_id: Uniquely identifies a message within the system (int, UUID, bytes, or str).
|
|
171
|
+
user_id: Identity of the user responsible for producing the message.
|
|
172
|
+
subject: Summary information about the message content and purpose.
|
|
173
|
+
reply_to: Address of the node to send replies to.
|
|
174
|
+
correlation_id: Client-specific id for marking or identifying messages (int, UUID, bytes, or str).
|
|
175
|
+
content_type: RFC-2046 MIME type for the message's body.
|
|
176
|
+
content_encoding: Modifier to the content-type.
|
|
177
|
+
absolute_expiry_time: Absolute time when the message expires.
|
|
178
|
+
creation_time: Absolute time when the message was created.
|
|
179
|
+
group_id: Group the message belongs to.
|
|
180
|
+
group_sequence: Relative position of this message within its group.
|
|
181
|
+
reply_to_group_id: Id for sending replies to a specific group.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
message_id: Optional[Union[int, str, bytes]] = None
|
|
185
|
+
user_id: Optional[bytes] = None
|
|
186
|
+
subject: Optional[str] = None
|
|
187
|
+
reply_to: Optional[str] = None
|
|
188
|
+
correlation_id: Optional[Union[int, str, bytes]] = None
|
|
189
|
+
content_type: Optional[str] = None
|
|
190
|
+
content_encoding: Optional[str] = None
|
|
191
|
+
absolute_expiry_time: Optional[datetime] = None
|
|
192
|
+
creation_time: Optional[datetime] = None
|
|
193
|
+
group_id: Optional[str] = None
|
|
194
|
+
group_sequence: Optional[int] = None
|
|
195
|
+
reply_to_group_id: Optional[str] = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
StreamFilterOptions defines the filtering options for a stream consumer.
|
|
200
|
+
for values and match_unfiltered see: https://www.rabbitmq.com/blog/2023/10/16/stream-filtering
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class StreamFilterOptions:
|
|
205
|
+
values: Optional[list[str]] = None
|
|
206
|
+
match_unfiltered: bool = False
|
|
207
|
+
application_properties: Optional[dict[str, Any]] = None
|
|
208
|
+
message_properties: Optional[MessageProperties] = None
|
|
209
|
+
sql: str = ""
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
values: Optional[list[str]] = None,
|
|
214
|
+
match_unfiltered: bool = False,
|
|
215
|
+
application_properties: Optional[dict[str, Any]] = None,
|
|
216
|
+
message_properties: Optional[MessageProperties] = None,
|
|
217
|
+
sql: str = "",
|
|
218
|
+
):
|
|
219
|
+
self.values = values
|
|
220
|
+
self.match_unfiltered = match_unfiltered
|
|
221
|
+
self.application_properties = application_properties
|
|
222
|
+
self.message_properties = message_properties
|
|
223
|
+
self.sql = sql
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class StreamConsumerOptions(ConsumerOptions):
|
|
153
227
|
"""
|
|
154
228
|
Configuration options for stream queues.
|
|
155
229
|
|
|
@@ -161,29 +235,42 @@ class StreamOptions:
|
|
|
161
235
|
Args:
|
|
162
236
|
offset_specification: Either an OffsetSpecification enum value or
|
|
163
237
|
an integer offset
|
|
164
|
-
|
|
238
|
+
filter_options: Filter options for the stream consumer. See StreamFilterOptions
|
|
165
239
|
"""
|
|
166
240
|
|
|
167
241
|
def __init__(
|
|
168
242
|
self,
|
|
169
243
|
offset_specification: Optional[Union[OffsetSpecification, int]] = None,
|
|
170
|
-
|
|
171
|
-
filter_match_unfiltered: bool = False,
|
|
244
|
+
filter_options: Optional[StreamFilterOptions] = None,
|
|
172
245
|
):
|
|
173
246
|
|
|
174
|
-
|
|
247
|
+
self._filter_set: Dict[symbol, Described] = {}
|
|
248
|
+
self._filter_option = filter_options
|
|
249
|
+
|
|
250
|
+
if offset_specification is None and filter_options is None:
|
|
175
251
|
raise ValidationCodeException(
|
|
176
252
|
"At least one between offset_specification and filters must be set when setting up filtering"
|
|
177
253
|
)
|
|
178
|
-
self._filter_set: Dict[symbol, Described] = {}
|
|
179
254
|
if offset_specification is not None:
|
|
180
255
|
self._offset(offset_specification)
|
|
181
256
|
|
|
182
|
-
if
|
|
183
|
-
|
|
257
|
+
if filter_options is None:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
if filter_options.values is not None:
|
|
261
|
+
self._filter_values(filter_options.values)
|
|
262
|
+
|
|
263
|
+
if filter_options.match_unfiltered:
|
|
264
|
+
self._filter_match_unfiltered(filter_options.match_unfiltered)
|
|
184
265
|
|
|
185
|
-
if
|
|
186
|
-
self.
|
|
266
|
+
if filter_options.message_properties is not None:
|
|
267
|
+
self._filter_message_properties(filter_options.message_properties)
|
|
268
|
+
|
|
269
|
+
if filter_options.application_properties is not None:
|
|
270
|
+
self._filter_application_properties(filter_options.application_properties)
|
|
271
|
+
|
|
272
|
+
if filter_options.sql is not None and filter_options.sql != "":
|
|
273
|
+
self._filter_sql(filter_options.sql)
|
|
187
274
|
|
|
188
275
|
def _offset(self, offset_specification: Union[OffsetSpecification, int]) -> None:
|
|
189
276
|
"""
|
|
@@ -225,6 +312,49 @@ class StreamOptions:
|
|
|
225
312
|
symbol(STREAM_FILTER_MATCH_UNFILTERED), filter_match_unfiltered
|
|
226
313
|
)
|
|
227
314
|
|
|
315
|
+
def _filter_message_properties(
|
|
316
|
+
self, message_properties: Optional[MessageProperties]
|
|
317
|
+
) -> None:
|
|
318
|
+
"""
|
|
319
|
+
Set AMQP message properties for filtering.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
message_properties: MessageProperties object containing AMQP message properties
|
|
323
|
+
"""
|
|
324
|
+
if message_properties is not None:
|
|
325
|
+
# dictionary of symbols and described
|
|
326
|
+
filter_prop: Dict[symbol, Any] = {}
|
|
327
|
+
|
|
328
|
+
for key, value in message_properties.__dict__.items():
|
|
329
|
+
if value is not None:
|
|
330
|
+
# replace _ with - for the key
|
|
331
|
+
filter_prop[symbol(key.replace("_", "-"))] = value
|
|
332
|
+
|
|
333
|
+
if len(filter_prop) > 0:
|
|
334
|
+
self._filter_set[symbol(AMQP_PROPERTIES_FILTER)] = Described(
|
|
335
|
+
symbol(AMQP_PROPERTIES_FILTER), filter_prop
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _filter_application_properties(
|
|
339
|
+
self, application_properties: Optional[dict[str, Any]]
|
|
340
|
+
) -> None:
|
|
341
|
+
if application_properties is not None:
|
|
342
|
+
app_prop = application_properties.copy()
|
|
343
|
+
|
|
344
|
+
if len(app_prop) > 0:
|
|
345
|
+
self._filter_set[symbol(AMQP_APPLICATION_PROPERTIES_FILTER)] = (
|
|
346
|
+
Described(symbol(AMQP_APPLICATION_PROPERTIES_FILTER), app_prop)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def _filter_sql(self, sql: str) -> None:
|
|
350
|
+
"""
|
|
351
|
+
Set SQL filter for the stream.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
sql: SQL string to apply as a filter
|
|
355
|
+
"""
|
|
356
|
+
self._filter_set[symbol(SQL_FILTER)] = Described(symbol(AMQP_SQL_FILTER), sql)
|
|
357
|
+
|
|
228
358
|
def filter_set(self) -> Dict[symbol, Described]:
|
|
229
359
|
"""
|
|
230
360
|
Get the current filter set configuration.
|
|
@@ -234,6 +364,41 @@ class StreamOptions:
|
|
|
234
364
|
"""
|
|
235
365
|
return self._filter_set
|
|
236
366
|
|
|
367
|
+
def validate(self, versions: Dict[str, bool]) -> None:
|
|
368
|
+
"""
|
|
369
|
+
Validates stream filter options against supported RabbitMQ server versions.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
versions: Dictionary mapping version strings to boolean indicating support.
|
|
373
|
+
|
|
374
|
+
Raises:
|
|
375
|
+
ValidationCodeException: If a filter option requires a higher RabbitMQ version.
|
|
376
|
+
"""
|
|
377
|
+
if self._filter_option is None:
|
|
378
|
+
return
|
|
379
|
+
if self._filter_option.values and not versions.get("4.1.0", False):
|
|
380
|
+
raise ValidationCodeException(
|
|
381
|
+
"Stream filter by values requires RabbitMQ 4.1.0 or higher"
|
|
382
|
+
)
|
|
383
|
+
if self._filter_option.match_unfiltered and not versions.get("4.1.0", False):
|
|
384
|
+
raise ValidationCodeException(
|
|
385
|
+
"Stream filter by match_unfiltered requires RabbitMQ 4.1.0 or higher"
|
|
386
|
+
)
|
|
387
|
+
if self._filter_option.sql and not versions.get("4.2.0", False):
|
|
388
|
+
raise ValidationCodeException(
|
|
389
|
+
"Stream filter by SQL requires RabbitMQ 4.2.0 or higher"
|
|
390
|
+
)
|
|
391
|
+
if self._filter_option.message_properties and not versions.get("4.1.0", False):
|
|
392
|
+
raise ValidationCodeException(
|
|
393
|
+
"Stream filter by message_properties requires RabbitMQ 4.1.0 or higher"
|
|
394
|
+
)
|
|
395
|
+
if self._filter_option.application_properties and not versions.get(
|
|
396
|
+
"4.1.0", False
|
|
397
|
+
):
|
|
398
|
+
raise ValidationCodeException(
|
|
399
|
+
"Stream filter by application_properties requires RabbitMQ 4.1.0 or higher"
|
|
400
|
+
)
|
|
401
|
+
|
|
237
402
|
|
|
238
403
|
@dataclass
|
|
239
404
|
class RecoveryConfiguration:
|
|
@@ -125,18 +125,20 @@ class Management:
|
|
|
125
125
|
|
|
126
126
|
def _request(
|
|
127
127
|
self,
|
|
128
|
-
|
|
128
|
+
msg_id: str,
|
|
129
129
|
body: Any,
|
|
130
130
|
path: str,
|
|
131
131
|
method: str,
|
|
132
132
|
expected_response_codes: list[int],
|
|
133
133
|
) -> Message:
|
|
134
134
|
amq_message = Message(
|
|
135
|
-
id=
|
|
135
|
+
id=msg_id,
|
|
136
136
|
body=body,
|
|
137
|
+
inferred=False,
|
|
137
138
|
reply_to="$me",
|
|
138
139
|
address=path,
|
|
139
140
|
subject=method,
|
|
141
|
+
durable=False,
|
|
140
142
|
)
|
|
141
143
|
|
|
142
144
|
if self._sender is not None:
|
|
@@ -169,9 +171,7 @@ class Management:
|
|
|
169
171
|
ValidationCodeException: If exchange already exists or other validation fails
|
|
170
172
|
"""
|
|
171
173
|
logger.debug("declare_exchange operation called")
|
|
172
|
-
body: dict[str, Any] = {}
|
|
173
|
-
body["auto_delete"] = exchange_specification.is_auto_delete
|
|
174
|
-
body["durable"] = exchange_specification.is_durable
|
|
174
|
+
body: dict[str, Any] = {"durable": exchange_specification.is_durable}
|
|
175
175
|
if isinstance(exchange_specification, ExchangeSpecification):
|
|
176
176
|
body["type"] = exchange_specification.exchange_type.value
|
|
177
177
|
elif isinstance(exchange_specification, ExchangeCustomSpecification):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from .entities import
|
|
1
|
+
from .entities import ConsumerOptions
|
|
2
2
|
from .qpid.proton._data import ( # noqa: E402
|
|
3
3
|
PropertyDict,
|
|
4
4
|
symbol,
|
|
@@ -68,8 +68,8 @@ class ReceiverOptionUnsettled(LinkOption): # type: ignore
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
class ReceiverOptionUnsettledWithFilters(Filter): # type: ignore
|
|
71
|
-
def __init__(self, addr: str,
|
|
72
|
-
super().__init__(
|
|
71
|
+
def __init__(self, addr: str, consumer_options: ConsumerOptions):
|
|
72
|
+
super().__init__(consumer_options.filter_set())
|
|
73
73
|
self._addr = addr
|
|
74
74
|
|
|
75
75
|
def apply(self, link: Link) -> None:
|
|
@@ -80,6 +80,14 @@ class Publisher:
|
|
|
80
80
|
"address specified in both message and publisher"
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
+
if not isinstance(message.body, (bytes, type(None))):
|
|
84
|
+
raise ArgumentOutOfRangeException(
|
|
85
|
+
"Message body must be of type bytes or None"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if not message.inferred:
|
|
89
|
+
raise ArgumentOutOfRangeException("Message inferred must be True")
|
|
90
|
+
|
|
83
91
|
if self._addr != "":
|
|
84
92
|
if self._sender is not None:
|
|
85
93
|
return self._sender.send(message)
|