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.
Files changed (48) hide show
  1. rabbitmq_amqp_python_client-0.2.0/PKG-INFO +65 -0
  2. rabbitmq_amqp_python_client-0.2.0/README.md +43 -0
  3. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/pyproject.toml +5 -2
  4. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/__init__.py +10 -2
  5. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/connection.py +96 -6
  6. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/consumer.py +4 -4
  7. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/entities.py +176 -11
  8. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/management.py +5 -5
  9. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/options.py +3 -3
  10. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/publisher.py +8 -0
  11. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_endpoints.py +1 -1
  12. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_exceptions.py +8 -0
  13. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_message.py +21 -16
  14. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_reactor.py +6 -1
  15. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_transport.py +1 -1
  16. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_utils.py +5 -0
  17. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/ssl_configuration.py +2 -2
  18. rabbitmq_amqp_python_client-0.2.0/rabbitmq_amqp_python_client/utils.py +38 -0
  19. rabbitmq_amqp_python_client-0.1.0b3/PKG-INFO +0 -183
  20. rabbitmq_amqp_python_client-0.1.0b3/README.md +0 -163
  21. rabbitmq_amqp_python_client-0.1.0b3/rabbitmq_amqp_python_client/utils.py +0 -9
  22. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/LICENSE +0 -0
  23. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/address_helper.py +0 -0
  24. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/amqp_consumer_handler.py +0 -0
  25. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/common.py +0 -0
  26. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/delivery_context.py +0 -0
  27. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/environment.py +0 -0
  28. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/exceptions.py +0 -0
  29. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/py.typed +0 -0
  30. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/__init__.py +0 -0
  31. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_common.py +0 -0
  32. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_condition.py +0 -0
  33. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_data.py +0 -0
  34. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_delivery.py +0 -0
  35. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_events.py +0 -0
  36. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_handler.py +0 -0
  37. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_handlers.py +0 -0
  38. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_io.py +0 -0
  39. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_selectable.py +0 -0
  40. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_tracing.py +0 -0
  41. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_url.py +0 -0
  42. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/_wrapper.py +0 -0
  43. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/handlers.py +0 -0
  44. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/py.typed +0 -0
  45. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/reactor.py +0 -0
  46. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/tracing.py +0 -0
  47. {rabbitmq_amqp_python_client-0.1.0b3 → rabbitmq_amqp_python_client-0.2.0}/rabbitmq_amqp_python_client/qpid/proton/utils.py +0 -0
  48. {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.1.0-beta.3"
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
- StreamOptions,
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
- "StreamOptions",
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) is False:
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
- stream_filter_options: Optional[StreamOptions] = None,
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
- stream_filter_options: Optional configuration for stream consumption
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) is False:
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, stream_filter_options, credit
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 StreamOptions
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[StreamOptions]): Configuration for stream consumption
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[StreamOptions] = None,
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 Sender")
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 StreamOptions:
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
- filters: List of filter strings to apply to the stream
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
- filters: Optional[list[str]] = None,
171
- filter_match_unfiltered: bool = False,
244
+ filter_options: Optional[StreamFilterOptions] = None,
172
245
  ):
173
246
 
174
- if offset_specification is None and filters is None:
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 filters is not None:
183
- self._filter_values(filters)
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 filter_match_unfiltered is True:
186
- self._filter_match_unfiltered(filter_match_unfiltered)
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
- id: str,
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=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 StreamOptions
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, filter_options: StreamOptions):
72
- super().__init__(filter_options.filter_set())
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)
@@ -482,7 +482,7 @@ class Connection(Wrapper, Endpoint):
482
482
  return c and SymbolList(c)
483
483
 
484
484
  @property
485
- def remote_properties(self):
485
+ def remote_properties(self) -> Optional[Data]:
486
486
  """
487
487
  The properties specified by the remote peer for this connection.
488
488