weenspace-queue 0.1.0__tar.gz → 0.1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weenspace-queue
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: WeenSpace queue client for RabbitMQ and AWS SQS, supporting both synchronous and asynchronous operations.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -13,11 +13,15 @@ Classifier: Intended Audience :: Developers
13
13
  Classifier: License :: OSI Approved :: MIT License
14
14
  Classifier: Programming Language :: Python :: 3
15
15
  Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Programming Language :: Python :: 3.15
16
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
18
  Classifier: Topic :: System :: Networking
18
- Requires-Dist: packaging (>=25.0,<26.0)
19
+ Provides-Extra: aws
20
+ Requires-Dist: boto3 (>=1.43.100,<2.0.0) ; extra == "aws"
21
+ Requires-Dist: packaging (>=26.3,<27.0)
19
22
  Requires-Dist: python-qpid-proton (>=0.40.0,<0.41.0)
20
- Requires-Dist: typing-extensions (>=4.15.0,<5.0.0)
23
+ Requires-Dist: rabbitmq-amqp-python-client (>=1.0.1,<2.0.0)
24
+ Requires-Dist: typing-extensions (>=4.16.0,<5.0.0)
21
25
  Project-URL: Homepage, https://github.com/weenspace/weenspace-queue
22
26
  Project-URL: Repository, https://github.com/weenspace/weenspace-queue
23
27
  Description-Content-Type: text/markdown
@@ -49,86 +53,59 @@ pip install weenspace-queue
49
53
 
50
54
  ## 🚀 Quick Start
51
55
 
52
- ### Basic Publisher/Consumer
56
+ ### Unified Publisher/Consumer
53
57
 
54
58
  ```python
55
- from weenspace_queue import (
56
- Environment,
57
- Connection,
58
- Publisher,
59
- Consumer,
60
- Message,
61
- ClassicQueueSpecification,
62
- ExchangeSpecification,
63
- )
64
-
65
- # Create environment and connection
66
- environment = Environment(uri="amqp://guest:guest@localhost:5672/")
67
- connection = environment.connection()
68
-
69
- # Declare queue with dead letter support
70
- management = connection.management()
71
- queue_spec = ClassicQueueSpecification(
72
- name="my-queue",
73
- is_durable=True,
74
- dead_letter_exchange="dlx",
75
- dead_letter_routing_key="dlx-key",
76
- max_priority=10, # Enable priority
77
- )
78
- management.declare_queue(queue_spec)
59
+ from weenspace_queue import Message, QueueClient, QueueSpecification
79
60
 
80
- # Publish message
81
- publisher = connection.publisher("/queues/my-queue")
82
- publisher.publish(Message(body=b"Hello WeenSpace!"))
61
+ client = QueueClient("rabbitmq", uri="amqp://guest:guest@localhost:5672/")
62
+ queue = client.declare_queue(QueueSpecification(name="my-queue"))
63
+ client.publish(queue, Message(body=b"Hello WeenSpace!"))
83
64
 
84
- # Consume messages
85
- def on_message(message):
65
+ def on_message(message: Message) -> None:
86
66
  print(f"Received: {message.body}")
87
67
  message.accept()
88
68
 
89
- consumer = connection.consumer("/queues/my-queue", handler=on_message)
69
+ client.consume(queue, handler=on_message, prefetch=10)
90
70
  ```
91
71
 
72
+ Install AWS support with `pip install "weenspace-queue[aws]"`.
73
+
92
74
  ### Async Support
93
75
 
94
76
  ```python
95
77
  import asyncio
96
- from weenspace_queue.asyncio import AsyncEnvironment
78
+ from weenspace_queue import AsyncQueueClient, Message, QueueSpecification
97
79
 
98
80
  async def main():
99
- async with AsyncEnvironment(uri="amqp://localhost:5672/") as env:
100
- async with env.connection() as conn:
101
- publisher = await conn.publisher("/queues/my-queue")
102
- await publisher.publish(Message(body=b"Async message!"))
81
+ async with AsyncQueueClient("rabbitmq", uri="amqp://localhost:5672/") as client:
82
+ queue = await client.declare_queue(QueueSpecification(name="my-queue"))
83
+ await client.publish(queue, Message(body=b"Async message!"))
103
84
 
104
85
  asyncio.run(main())
105
86
  ```
106
87
 
107
- ### OAuth2 Authentication
88
+ ### RabbitMQ authentication
108
89
 
109
90
  ```python
110
- from weenspace_queue import Environment, OAuth2Options
91
+ from weenspace_queue import QueueClient
111
92
 
112
- oauth = OAuth2Options(token="your_jwt_token")
113
- environment = Environment(
93
+ client = QueueClient(
94
+ "rabbitmq",
114
95
  uri="amqp://localhost:5672/",
115
- oauth2_options=oauth
96
+ oauth2_options=your_oauth_options
116
97
  )
117
98
  ```
118
99
 
119
100
  ### TLS/SSL Connection
120
101
 
121
102
  ```python
122
- from weenspace_queue import Environment, SslConfiguration
103
+ from weenspace_queue import QueueClient
123
104
 
124
- ssl_config = SslConfiguration(
125
- ca_cert="/path/to/ca.pem",
126
- client_cert="/path/to/client.pem",
127
- client_key="/path/to/client.key"
128
- )
129
- environment = Environment(
105
+ client = QueueClient(
106
+ "rabbitmq",
130
107
  uri="amqps://localhost:5671/",
131
- ssl_configuration=ssl_config
108
+ ssl_context=your_ssl_context
132
109
  )
133
110
  ```
134
111
 
@@ -143,7 +120,7 @@ See [examples](./examples) folder for more detailed usage examples.
143
120
  # from python_rabbitmq import RabbitMQ
144
121
 
145
122
  # New (weenspace-queue)
146
- from weenspace_queue import Environment, Connection
123
+ from weenspace_queue import QueueClient
147
124
  ```
148
125
 
149
126
  ## 📋 Requirements
@@ -25,86 +25,59 @@ pip install weenspace-queue
25
25
 
26
26
  ## 🚀 Quick Start
27
27
 
28
- ### Basic Publisher/Consumer
28
+ ### Unified Publisher/Consumer
29
29
 
30
30
  ```python
31
- from weenspace_queue import (
32
- Environment,
33
- Connection,
34
- Publisher,
35
- Consumer,
36
- Message,
37
- ClassicQueueSpecification,
38
- ExchangeSpecification,
39
- )
40
-
41
- # Create environment and connection
42
- environment = Environment(uri="amqp://guest:guest@localhost:5672/")
43
- connection = environment.connection()
44
-
45
- # Declare queue with dead letter support
46
- management = connection.management()
47
- queue_spec = ClassicQueueSpecification(
48
- name="my-queue",
49
- is_durable=True,
50
- dead_letter_exchange="dlx",
51
- dead_letter_routing_key="dlx-key",
52
- max_priority=10, # Enable priority
53
- )
54
- management.declare_queue(queue_spec)
31
+ from weenspace_queue import Message, QueueClient, QueueSpecification
55
32
 
56
- # Publish message
57
- publisher = connection.publisher("/queues/my-queue")
58
- publisher.publish(Message(body=b"Hello WeenSpace!"))
33
+ client = QueueClient("rabbitmq", uri="amqp://guest:guest@localhost:5672/")
34
+ queue = client.declare_queue(QueueSpecification(name="my-queue"))
35
+ client.publish(queue, Message(body=b"Hello WeenSpace!"))
59
36
 
60
- # Consume messages
61
- def on_message(message):
37
+ def on_message(message: Message) -> None:
62
38
  print(f"Received: {message.body}")
63
39
  message.accept()
64
40
 
65
- consumer = connection.consumer("/queues/my-queue", handler=on_message)
41
+ client.consume(queue, handler=on_message, prefetch=10)
66
42
  ```
67
43
 
44
+ Install AWS support with `pip install "weenspace-queue[aws]"`.
45
+
68
46
  ### Async Support
69
47
 
70
48
  ```python
71
49
  import asyncio
72
- from weenspace_queue.asyncio import AsyncEnvironment
50
+ from weenspace_queue import AsyncQueueClient, Message, QueueSpecification
73
51
 
74
52
  async def main():
75
- async with AsyncEnvironment(uri="amqp://localhost:5672/") as env:
76
- async with env.connection() as conn:
77
- publisher = await conn.publisher("/queues/my-queue")
78
- await publisher.publish(Message(body=b"Async message!"))
53
+ async with AsyncQueueClient("rabbitmq", uri="amqp://localhost:5672/") as client:
54
+ queue = await client.declare_queue(QueueSpecification(name="my-queue"))
55
+ await client.publish(queue, Message(body=b"Async message!"))
79
56
 
80
57
  asyncio.run(main())
81
58
  ```
82
59
 
83
- ### OAuth2 Authentication
60
+ ### RabbitMQ authentication
84
61
 
85
62
  ```python
86
- from weenspace_queue import Environment, OAuth2Options
63
+ from weenspace_queue import QueueClient
87
64
 
88
- oauth = OAuth2Options(token="your_jwt_token")
89
- environment = Environment(
65
+ client = QueueClient(
66
+ "rabbitmq",
90
67
  uri="amqp://localhost:5672/",
91
- oauth2_options=oauth
68
+ oauth2_options=your_oauth_options
92
69
  )
93
70
  ```
94
71
 
95
72
  ### TLS/SSL Connection
96
73
 
97
74
  ```python
98
- from weenspace_queue import Environment, SslConfiguration
75
+ from weenspace_queue import QueueClient
99
76
 
100
- ssl_config = SslConfiguration(
101
- ca_cert="/path/to/ca.pem",
102
- client_cert="/path/to/client.pem",
103
- client_key="/path/to/client.key"
104
- )
105
- environment = Environment(
77
+ client = QueueClient(
78
+ "rabbitmq",
106
79
  uri="amqps://localhost:5671/",
107
- ssl_configuration=ssl_config
80
+ ssl_context=your_ssl_context
108
81
  )
109
82
  ```
110
83
 
@@ -119,7 +92,7 @@ See [examples](./examples) folder for more detailed usage examples.
119
92
  # from python_rabbitmq import RabbitMQ
120
93
 
121
94
  # New (weenspace-queue)
122
- from weenspace_queue import Environment, Connection
95
+ from weenspace_queue import QueueClient
123
96
  ```
124
97
 
125
98
  ## 📋 Requirements
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "weenspace-queue"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "WeenSpace queue client for RabbitMQ and AWS SQS, supporting both synchronous and asynchronous operations."
5
5
  authors = ["WeenSpace <weenspace@gmail.com>"]
6
6
  license = "MIT"
@@ -22,29 +22,30 @@ packages = [{include = "weenspace_queue"}]
22
22
  [tool.poetry.dependencies]
23
23
  python = "^3.14"
24
24
  python-qpid-proton = "^0.40.0"
25
- typing-extensions = "^4.15.0"
26
- packaging = "^25.0"
25
+ rabbitmq-amqp-python-client = "^1.0.1"
26
+ boto3 = { version = "^1.43.100", optional = true }
27
+ typing-extensions = "^4.16.0"
28
+ packaging = "^26.3"
29
+
30
+ [tool.poetry.extras]
31
+ aws = ["boto3"]
27
32
 
28
33
  [tool.poetry.group.dev.dependencies]
29
- pyjwt = "^2.13.0"
34
+ boto3 = "^1.43.100"
35
+ pyjwt = "^2.14.0"
30
36
  pika = "^1.4.4"
31
- flake8 = "^7.3.0"
32
- isort = "^6.0.0"
33
- mypy = "^1.19.0"
34
- pytest = "^9.0.0"
37
+ flake8 = "^7.4.1"
38
+ isort = "^9.0.1"
39
+ mypy = "^2.3.1"
40
+ pytest = "^9.1.1"
35
41
  black = "^26.5.1"
36
- requests = "^2.32.0"
37
- pytest-asyncio = "^1.3.0"
42
+ requests = "^2.34.2"
43
+ pytest-asyncio = "^1.4.0"
38
44
 
39
45
  [build-system]
40
- requires = ["poetry-core"]
46
+ requires = ["poetry-core>=2.5.0"]
41
47
  build-backend = "poetry.core.masonry.api"
42
48
 
43
- [dependency-groups]
44
- dev = [
45
- "boto3>=1.43.80",
46
- ]
47
-
48
49
  [tool.pytest.ini_options]
49
50
  asyncio_mode = "auto"
50
51
 
@@ -1,7 +1,4 @@
1
- from typing import Any, Callable, Dict, Type
2
-
3
- from .asyncio.aws_async import AwsAsyncEngine
4
- from .asyncio.rabbitmq_async import RabbitMqAsyncEngine
1
+ from typing import Any, Callable, Type
5
2
  from .base import (
6
3
  AsyncQueueEngine,
7
4
  Message,
@@ -16,26 +13,54 @@ from .constants import (
16
13
  Provider,
17
14
  QueueKind,
18
15
  )
19
- from .providers.aws import AwsEngine
20
- from .providers.rabbitmq import RabbitMqEngine
16
+
17
+
18
+ def __getattr__(name: str) -> Any:
19
+ """Lazily expose the low-level RabbitMQ compatibility API."""
20
+ try:
21
+ from rabbitmq_amqp_python_client import __dict__ as rabbitmq_namespace
22
+
23
+ value = rabbitmq_namespace.get(name)
24
+ if value is None and name == "DirectReplyToConsumerOptions":
25
+ value = rabbitmq_namespace["ConsumerOptions"]
26
+ if value is None:
27
+ raise KeyError(name)
28
+ except (ImportError, KeyError) as exc:
29
+ raise AttributeError(name) from exc
30
+ globals()[name] = value
31
+ return value
32
+
33
+
34
+ def _engine_class(provider: str) -> Type[QueueEngine]:
35
+ if provider == PROVIDER_AWS:
36
+ from .providers.aws import AwsEngine
37
+
38
+ return AwsEngine
39
+ if provider == PROVIDER_RABBITMQ:
40
+ from .providers.rabbitmq import RabbitMqEngine
41
+
42
+ return RabbitMqEngine
43
+ raise ValueError(f"Unsupported provider '{provider}'")
44
+
45
+
46
+ def _async_engine_class(provider: str) -> Type[AsyncQueueEngine]:
47
+ if provider == PROVIDER_AWS:
48
+ from .asyncio.aws_async import AwsAsyncEngine
49
+
50
+ return AwsAsyncEngine
51
+ if provider == PROVIDER_RABBITMQ:
52
+ from .asyncio.rabbitmq_async import RabbitMqAsyncEngine
53
+
54
+ return RabbitMqAsyncEngine
55
+ raise ValueError(f"Unsupported provider '{provider}'")
21
56
 
22
57
 
23
58
  class QueueClient:
24
59
  """Single client: pass provider name, then use the same publish/consume/topology methods."""
25
60
 
26
- _ENGINES: Dict[str, Type[QueueEngine]] = {
27
- PROVIDER_AWS: AwsEngine,
28
- PROVIDER_RABBITMQ: RabbitMqEngine,
29
- }
30
-
31
61
  def __init__(self, provider: str, **config: Any) -> None:
32
62
  prov_key = provider.lower().strip()
33
- engine_cls = self._ENGINES.get(prov_key)
34
- if engine_cls is None:
35
- supported = ", ".join(sorted(self._ENGINES))
36
- raise ValueError(
37
- f"Unsupported provider '{provider}'. Supported: {supported}"
38
- )
63
+ engine_cls = _engine_class(prov_key)
39
64
  self.provider = prov_key
40
65
  self.engine: QueueEngine = engine_cls(**config)
41
66
 
@@ -48,11 +73,17 @@ class QueueClient:
48
73
  def bind_pattern(self, queue_id: str, topic_id: str, pattern: str) -> None:
49
74
  self.engine.bind_pattern(queue_id, topic_id, pattern)
50
75
 
51
- def publish(self, destination: str, message: Message) -> None:
52
- self.engine.publish(destination, message)
76
+ def publish(self, destination: str, message: Message) -> Any:
77
+ return self.engine.publish(destination, message)
53
78
 
54
- def consume(self, queue_id: str, handler: Callable[[Message], None]) -> None:
55
- self.engine.consume(queue_id, handler)
79
+ def consume(
80
+ self,
81
+ queue_id: str,
82
+ handler: Callable[[Message], None],
83
+ *,
84
+ prefetch: int | None = None,
85
+ ) -> None:
86
+ return self.engine.consume(queue_id, handler, prefetch=prefetch)
56
87
 
57
88
  def stop(self) -> None:
58
89
  self.engine.stop()
@@ -70,19 +101,9 @@ class QueueClient:
70
101
  class AsyncQueueClient:
71
102
  """Async twin of QueueClient. Same method names, same provider argument."""
72
103
 
73
- _ENGINES: Dict[str, Type[AsyncQueueEngine]] = {
74
- PROVIDER_AWS: AwsAsyncEngine,
75
- PROVIDER_RABBITMQ: RabbitMqAsyncEngine,
76
- }
77
-
78
104
  def __init__(self, provider: str, **config: Any) -> None:
79
105
  prov_key = provider.lower().strip()
80
- engine_cls = self._ENGINES.get(prov_key)
81
- if engine_cls is None:
82
- supported = ", ".join(sorted(self._ENGINES))
83
- raise ValueError(
84
- f"Unsupported provider '{provider}'. Supported: {supported}"
85
- )
106
+ engine_cls = _async_engine_class(prov_key)
86
107
  self.provider = prov_key
87
108
  self.engine: AsyncQueueEngine = engine_cls(**config)
88
109
 
@@ -95,11 +116,17 @@ class AsyncQueueClient:
95
116
  async def bind_pattern(self, queue_id: str, topic_id: str, pattern: str) -> None:
96
117
  await self.engine.bind_pattern(queue_id, topic_id, pattern)
97
118
 
98
- async def publish(self, destination: str, message: Message) -> None:
99
- await self.engine.publish(destination, message)
100
-
101
- async def consume(self, queue_id: str, handler: Callable[[Message], None]) -> None:
102
- await self.engine.consume(queue_id, handler)
119
+ async def publish(self, destination: str, message: Message) -> Any:
120
+ return await self.engine.publish(destination, message)
121
+
122
+ async def consume(
123
+ self,
124
+ queue_id: str,
125
+ handler: Callable[[Message], None],
126
+ *,
127
+ prefetch: int | None = None,
128
+ ) -> None:
129
+ await self.engine.consume(queue_id, handler, prefetch=prefetch)
103
130
 
104
131
  async def stop(self) -> None:
105
132
  await self.engine.stop()
@@ -0,0 +1,23 @@
1
+ from typing import Any
2
+
3
+ __all__ = ["AwsAsyncEngine", "RabbitMqAsyncEngine"]
4
+
5
+
6
+ def __getattr__(name: str) -> Any:
7
+ try:
8
+ from rabbitmq_amqp_python_client.asyncio import __dict__ as rabbitmq_namespace
9
+
10
+ value = rabbitmq_namespace[name]
11
+ globals()[name] = value
12
+ return value
13
+ except (ImportError, KeyError):
14
+ pass
15
+ if name == "AwsAsyncEngine":
16
+ from .aws_async import AwsAsyncEngine
17
+
18
+ return AwsAsyncEngine
19
+ if name == "RabbitMqAsyncEngine":
20
+ from .rabbitmq_async import RabbitMqAsyncEngine
21
+
22
+ return RabbitMqAsyncEngine
23
+ raise AttributeError(name)
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ from typing import Any, Callable
5
+
4
6
  from weenspace_queue.base import (
5
7
  AsyncQueueEngine,
6
8
  Message,
@@ -25,11 +27,19 @@ class AwsAsyncEngine(AsyncQueueEngine):
25
27
  async def bind_pattern(self, queue_id: str, topic_id: str, pattern: str) -> None:
26
28
  await asyncio.to_thread(self._sync.bind_pattern, queue_id, topic_id, pattern)
27
29
 
28
- async def publish(self, destination: str, message: Message) -> None:
29
- await asyncio.to_thread(self._sync.publish, destination, message)
30
-
31
- async def consume(self, queue_id: str, handler: Callable[[Message], None]) -> None:
32
- await asyncio.to_thread(self._sync.consume, queue_id, handler)
30
+ async def publish(self, destination: str, message: Message) -> Any:
31
+ return await asyncio.to_thread(self._sync.publish, destination, message)
32
+
33
+ async def consume(
34
+ self,
35
+ queue_id: str,
36
+ handler: Callable[[Message], None],
37
+ *,
38
+ prefetch: int | None = None,
39
+ ) -> None:
40
+ await asyncio.to_thread(
41
+ self._sync.consume, queue_id, handler, prefetch=prefetch
42
+ )
33
43
 
34
44
  async def stop(self) -> None:
35
45
  self._sync.stop()
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, Callable, Optional
4
4
 
5
- from weenspace_queue import (
5
+ from rabbitmq_amqp_python_client import (
6
6
  ClassicQueueSpecification,
7
7
  ExchangeSpecification,
8
8
  ExchangeToQueueBindingSpecification,
@@ -11,10 +11,10 @@ from weenspace_queue import (
11
11
  QuorumQueueSpecification,
12
12
  StreamSpecification,
13
13
  )
14
- from weenspace_queue.asyncio import AsyncEnvironment
15
- from weenspace_queue.delivery_context import DeliveryContext
16
- from weenspace_queue.amqp_consumer_handler import AMQPMessagingHandler
17
- from weenspace_queue.qpid.proton._events import Event
14
+ from rabbitmq_amqp_python_client.asyncio import AsyncEnvironment
15
+ from rabbitmq_amqp_python_client.delivery_context import DeliveryContext
16
+ from rabbitmq_amqp_python_client.amqp_consumer_handler import AMQPMessagingHandler
17
+ from rabbitmq_amqp_python_client.qpid.proton._events import Event
18
18
 
19
19
  from weenspace_queue.base import (
20
20
  AsyncQueueEngine,
@@ -35,10 +35,10 @@ from weenspace_queue.utils import (
35
35
  )
36
36
 
37
37
  _EXCHANGE_KIND_MAP = {
38
- ExchangeKind.DIRECT: ExchangeType.DIRECT,
39
- ExchangeKind.TOPIC: ExchangeType.TOPIC,
40
- ExchangeKind.FANOUT: ExchangeType.FANOUT,
41
- ExchangeKind.HEADERS: ExchangeType.HEADERS,
38
+ ExchangeKind.DIRECT: ExchangeType.direct,
39
+ ExchangeKind.TOPIC: ExchangeType.topic,
40
+ ExchangeKind.FANOUT: ExchangeType.fanout,
41
+ ExchangeKind.HEADERS: ExchangeType.headers,
42
42
  }
43
43
 
44
44
 
@@ -62,6 +62,9 @@ class _CallbackHandler(AMQPMessagingHandler):
62
62
  def requeue(evt: Event = event) -> None:
63
63
  context.requeue(evt)
64
64
 
65
+ def modified(evt: Event = event) -> None:
66
+ context.discard_with_annotations(evt, {})
67
+
65
68
  self._handler(
66
69
  Message(
67
70
  body=encode_body(proton_msg.body),
@@ -70,6 +73,7 @@ class _CallbackHandler(AMQPMessagingHandler):
70
73
  accept=accept,
71
74
  reject=reject,
72
75
  requeue=requeue,
76
+ modified=modified,
73
77
  )
74
78
  )
75
79
 
@@ -88,6 +92,7 @@ class RabbitMqAsyncEngine(AsyncQueueEngine):
88
92
  self._conn: Any = None
89
93
  self._mgmt: Any = None
90
94
  self._consumer: Any = None
95
+ self._publishers: dict[str, Any] = {}
91
96
 
92
97
  async def _ensure(self) -> None:
93
98
  if self._conn is not None:
@@ -95,6 +100,20 @@ class RabbitMqAsyncEngine(AsyncQueueEngine):
95
100
  self._conn = await self._env.connection()
96
101
  await self._conn.dial()
97
102
  self._mgmt = await self._conn.management()
103
+ self._fix_quorum_delivery_limit()
104
+
105
+ def _fix_quorum_delivery_limit(self) -> None:
106
+ management = getattr(self._mgmt, "_management", self._mgmt)
107
+ declare_queue = management._declare_queue
108
+
109
+ def declare_queue_with_rabbitmq_key(spec: Any) -> Any:
110
+ body = declare_queue(spec)
111
+ arguments = body.get("arguments", {})
112
+ if "x-deliver-limit" in arguments:
113
+ arguments["x-delivery-limit"] = arguments.pop("x-deliver-limit")
114
+ return body
115
+
116
+ management._declare_queue = declare_queue_with_rabbitmq_key
98
117
 
99
118
  async def declare_queue(self, spec: QueueSpecification) -> str:
100
119
  await self._ensure()
@@ -141,25 +160,32 @@ class RabbitMqAsyncEngine(AsyncQueueEngine):
141
160
  )
142
161
  )
143
162
 
144
- async def publish(self, destination: str, message: Message) -> None:
163
+ async def publish(self, destination: str, message: Message) -> Any:
145
164
  await self._ensure()
146
165
  address = self._publish_address(destination, message.routing_key)
147
- publisher = await self._conn.publisher(address)
148
- try:
149
- proton_msg = ProtonMessage(body=encode_body(message.body))
150
- proton_msg.inferred = True
151
- if message.routing_key:
152
- proton_msg.subject = message.routing_key
153
- await publisher.publish(proton_msg)
154
- finally:
155
- await publisher.close()
156
-
157
- async def consume(self, queue_id: str, handler: Callable[[Message], None]) -> None:
166
+ publisher = self._publishers.get(address)
167
+ if publisher is None:
168
+ publisher = await self._conn.publisher(address)
169
+ self._publishers[address] = publisher
170
+ proton_msg = ProtonMessage(body=encode_body(message.body))
171
+ proton_msg.inferred = True
172
+ if message.routing_key:
173
+ proton_msg.subject = message.routing_key
174
+ return await publisher.publish(proton_msg)
175
+
176
+ async def consume(
177
+ self,
178
+ queue_id: str,
179
+ handler: Callable[[Message], None],
180
+ *,
181
+ prefetch: Optional[int] = None,
182
+ ) -> None:
158
183
  await self._ensure()
159
184
  destination = rabbitmq_queue_address(queue_id)
160
- self._consumer = await self._conn.consumer(
161
- destination, message_handler=_CallbackHandler(handler)
162
- )
185
+ consumer_kwargs: dict[str, Any] = {"message_handler": _CallbackHandler(handler)}
186
+ if prefetch is not None:
187
+ consumer_kwargs["credit"] = prefetch
188
+ self._consumer = await self._conn.consumer(destination, **consumer_kwargs)
163
189
  await self._consumer.run()
164
190
 
165
191
  async def stop(self) -> None:
@@ -169,6 +195,9 @@ class RabbitMqAsyncEngine(AsyncQueueEngine):
169
195
  async def close(self) -> None:
170
196
  await self.stop()
171
197
  if self._conn is not None:
198
+ for publisher in self._publishers.values():
199
+ await publisher.close()
200
+ self._publishers.clear()
172
201
  await self._conn.close()
173
202
  await self._env.close()
174
203
 
@@ -17,6 +17,7 @@ class Message:
17
17
  accept: Optional[Callable[[], None]] = None
18
18
  reject: Optional[Callable[[], None]] = None
19
19
  requeue: Optional[Callable[[], None]] = None
20
+ modified: Optional[Callable[[], None]] = None
20
21
 
21
22
 
22
23
  @dataclass
@@ -55,11 +56,17 @@ class QueueEngine(ABC):
55
56
  pass
56
57
 
57
58
  @abstractmethod
58
- def publish(self, destination: str, message: Message) -> None:
59
+ def publish(self, destination: str, message: Message) -> Any:
59
60
  pass
60
61
 
61
62
  @abstractmethod
62
- def consume(self, queue_id: str, handler: Callable[[Message], None]) -> None:
63
+ def consume(
64
+ self,
65
+ queue_id: str,
66
+ handler: Callable[[Message], None],
67
+ *,
68
+ prefetch: Optional[int] = None,
69
+ ) -> None:
63
70
  pass
64
71
 
65
72
  @abstractmethod
@@ -87,11 +94,17 @@ class AsyncQueueEngine(ABC):
87
94
  pass
88
95
 
89
96
  @abstractmethod
90
- async def publish(self, destination: str, message: Message) -> None:
97
+ async def publish(self, destination: str, message: Message) -> Any:
91
98
  pass
92
99
 
93
100
  @abstractmethod
94
- async def consume(self, queue_id: str, handler: Callable[[Message], None]) -> None:
101
+ async def consume(
102
+ self,
103
+ queue_id: str,
104
+ handler: Callable[[Message], None],
105
+ *,
106
+ prefetch: Optional[int] = None,
107
+ ) -> None:
95
108
  pass
96
109
 
97
110
  @abstractmethod
@@ -0,0 +1,15 @@
1
+ from typing import Any
2
+
3
+ __all__ = ["AwsEngine", "RabbitMqEngine"]
4
+
5
+
6
+ def __getattr__(name: str) -> Any:
7
+ if name == "AwsEngine":
8
+ from .aws import AwsEngine
9
+
10
+ return AwsEngine
11
+ if name == "RabbitMqEngine":
12
+ from .rabbitmq import RabbitMqEngine
13
+
14
+ return RabbitMqEngine
15
+ raise AttributeError(name)
@@ -20,6 +20,7 @@ from weenspace_queue.constants import (
20
20
  SQS_FIFO_SUFFIX,
21
21
  SQS_POLICY_ATTRIBUTE,
22
22
  SQS_QUEUE_ARN_ATTRIBUTE,
23
+ SQS_ARN_MARKER,
23
24
  SQS_RECEIPT_HANDLE_ATTR,
24
25
  SQS_REDRIVE_POLICY_ATTRIBUTE,
25
26
  SQS_RETENTION_ATTRIBUTE,
@@ -118,7 +119,7 @@ class AwsEngine(QueueEngine):
118
119
  self.sns.subscribe(**subscribe_kwargs)
119
120
  self._allow_sns_to_sqs(queue_url, queue_arn, topic_id)
120
121
 
121
- def publish(self, destination: str, message: Message) -> None:
122
+ def publish(self, destination: str, message: Message) -> Dict[str, Any]:
122
123
  body_str = decode_body(message.body)
123
124
  msg_attrs = dict(inject_aws_routing_attrs(message.routing_key))
124
125
  extra_attrs = message.attributes or {}
@@ -143,8 +144,7 @@ class AwsEngine(QueueEngine):
143
144
  dedup = extra_attrs.get("message_deduplication_id")
144
145
  if dedup:
145
146
  publish_kwargs["MessageDeduplicationId"] = str(dedup)
146
- self.sns.publish(**publish_kwargs)
147
- return
147
+ return self.sns.publish(**publish_kwargs)
148
148
 
149
149
  queue_url = self._as_queue_url(destination)
150
150
  send_kwargs: Dict[str, Any] = {
@@ -160,15 +160,22 @@ class AwsEngine(QueueEngine):
160
160
  dedup = extra_attrs.get("message_deduplication_id")
161
161
  if dedup:
162
162
  send_kwargs["MessageDeduplicationId"] = str(dedup)
163
- self.sqs.send_message(**send_kwargs)
163
+ return self.sqs.send_message(**send_kwargs)
164
164
 
165
- def consume(self, queue_id: str, handler: Callable[[Message], None]) -> None:
165
+ def consume(
166
+ self,
167
+ queue_id: str,
168
+ handler: Callable[[Message], None],
169
+ *,
170
+ prefetch: int | None = None,
171
+ ) -> None:
166
172
  queue_url = self._as_queue_url(queue_id)
167
173
  self._running = True
174
+ max_messages = self._max_messages if prefetch is None else max(1, min(prefetch, 10))
168
175
  while self._running:
169
176
  response = self.sqs.receive_message(
170
177
  QueueUrl=queue_url,
171
- MaxNumberOfMessages=self._max_messages,
178
+ MaxNumberOfMessages=max_messages,
172
179
  WaitTimeSeconds=self._wait_time_seconds,
173
180
  MessageAttributeNames=["All"],
174
181
  AttributeNames=["All"],
@@ -203,6 +210,11 @@ class AwsEngine(QueueEngine):
203
210
  QueueUrl=queue_url, ReceiptHandle=handle, VisibilityTimeout=0
204
211
  )
205
212
 
213
+ def modified(handle: str = receipt) -> None:
214
+ self.sqs.change_message_visibility(
215
+ QueueUrl=queue_url, ReceiptHandle=handle, VisibilityTimeout=0
216
+ )
217
+
206
218
  return Message(
207
219
  body=body,
208
220
  routing_key=routing_key,
@@ -210,6 +222,7 @@ class AwsEngine(QueueEngine):
210
222
  accept=accept,
211
223
  reject=reject,
212
224
  requeue=requeue,
225
+ modified=modified,
213
226
  )
214
227
 
215
228
  def _as_queue_url(self, queue_id: str) -> str:
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, Callable, Optional
4
4
 
5
- from weenspace_queue import (
5
+ from rabbitmq_amqp_python_client import (
6
6
  AMQPMessagingHandler,
7
7
  ClassicQueueSpecification,
8
8
  Environment,
@@ -14,7 +14,7 @@ from weenspace_queue import (
14
14
  QuorumQueueSpecification,
15
15
  StreamSpecification,
16
16
  )
17
- from weenspace_queue.delivery_context import DeliveryContext
17
+ from rabbitmq_amqp_python_client.delivery_context import DeliveryContext
18
18
 
19
19
  from weenspace_queue.base import Message, QueueEngine, QueueSpecification, TopicSpecification
20
20
  from weenspace_queue.constants import (
@@ -30,10 +30,10 @@ from weenspace_queue.utils import (
30
30
  )
31
31
 
32
32
  _EXCHANGE_KIND_MAP = {
33
- ExchangeKind.DIRECT: ExchangeType.DIRECT,
34
- ExchangeKind.TOPIC: ExchangeType.TOPIC,
35
- ExchangeKind.FANOUT: ExchangeType.FANOUT,
36
- ExchangeKind.HEADERS: ExchangeType.HEADERS,
33
+ ExchangeKind.DIRECT: ExchangeType.direct,
34
+ ExchangeKind.TOPIC: ExchangeType.topic,
35
+ ExchangeKind.FANOUT: ExchangeType.fanout,
36
+ ExchangeKind.HEADERS: ExchangeType.headers,
37
37
  }
38
38
 
39
39
 
@@ -64,6 +64,9 @@ class _CallbackHandler(AMQPMessagingHandler):
64
64
  def requeue(evt: Event = event) -> None:
65
65
  context.requeue(evt)
66
66
 
67
+ def modified(evt: Event = event) -> None:
68
+ context.discard_with_annotations(evt, {})
69
+
67
70
  self._handler(
68
71
  Message(
69
72
  body=encode_body(proton_msg.body),
@@ -72,6 +75,7 @@ class _CallbackHandler(AMQPMessagingHandler):
72
75
  accept=accept,
73
76
  reject=reject,
74
77
  requeue=requeue,
78
+ modified=modified,
75
79
  )
76
80
  )
77
81
 
@@ -90,7 +94,21 @@ class RabbitMqEngine(QueueEngine):
90
94
  self._conn = self._env.connection()
91
95
  self._conn.dial()
92
96
  self._mgmt = self._conn.management()
97
+ self._fix_quorum_delivery_limit()
93
98
  self._consumer: Optional[Any] = None
99
+ self._publishers: dict[str, Any] = {}
100
+
101
+ def _fix_quorum_delivery_limit(self) -> None:
102
+ declare_queue = self._mgmt._declare_queue
103
+
104
+ def declare_queue_with_rabbitmq_key(spec: Any) -> Any:
105
+ body = declare_queue(spec)
106
+ arguments = body.get("arguments", {})
107
+ if "x-deliver-limit" in arguments:
108
+ arguments["x-delivery-limit"] = arguments.pop("x-deliver-limit")
109
+ return body
110
+
111
+ self._mgmt._declare_queue = declare_queue_with_rabbitmq_key
94
112
 
95
113
  def declare_queue(self, spec: QueueSpecification) -> str:
96
114
  if spec.kind == QueueKind.STREAM:
@@ -156,35 +174,44 @@ class RabbitMqEngine(QueueEngine):
156
174
  )
157
175
  )
158
176
 
159
- def publish(self, destination: str, message: Message) -> None:
177
+ def publish(self, destination: str, message: Message) -> Any:
160
178
  address = self._publish_address(destination, message.routing_key)
161
- publisher = self._conn.publisher(address)
162
- try:
163
- proton_msg = ProtonMessage(body=encode_body(message.body))
164
- proton_msg.inferred = True
165
- if message.routing_key:
166
- proton_msg.subject = message.routing_key
167
- correlation_id = (message.attributes or {}).get("correlation_id")
168
- if correlation_id is not None:
169
- proton_msg.correlation_id = correlation_id
170
- reply_to = (message.attributes or {}).get("reply_to")
171
- if reply_to:
172
- proton_msg.reply_to = reply_to
173
- if message.attributes:
174
- proton_msg.properties = {
175
- key: value
176
- for key, value in message.attributes.items()
177
- if key not in {"correlation_id", "reply_to"}
178
- and isinstance(value, (str, int, float, bool))
179
- }
180
- publisher.publish(proton_msg)
181
- finally:
182
- publisher.close()
183
-
184
- def consume(self, queue_id: str, handler: Callable[[Message], None]) -> None:
179
+ publisher = self._publishers.get(address)
180
+ if publisher is None:
181
+ publisher = self._conn.publisher(address)
182
+ self._publishers[address] = publisher
183
+ proton_msg = ProtonMessage(body=encode_body(message.body))
184
+ proton_msg.inferred = True
185
+ if message.routing_key:
186
+ proton_msg.subject = message.routing_key
187
+ correlation_id = (message.attributes or {}).get("correlation_id")
188
+ if correlation_id is not None:
189
+ proton_msg.correlation_id = correlation_id
190
+ reply_to = (message.attributes or {}).get("reply_to")
191
+ if reply_to:
192
+ proton_msg.reply_to = reply_to
193
+ if message.attributes:
194
+ proton_msg.properties = {
195
+ key: value
196
+ for key, value in message.attributes.items()
197
+ if key not in {"correlation_id", "reply_to"}
198
+ and isinstance(value, (str, int, float, bool))
199
+ }
200
+ return publisher.publish(proton_msg)
201
+
202
+ def consume(
203
+ self,
204
+ queue_id: str,
205
+ handler: Callable[[Message], None],
206
+ *,
207
+ prefetch: Optional[int] = None,
208
+ ) -> None:
185
209
  destination = rabbitmq_queue_address(queue_id)
186
210
  wrapped = _CallbackHandler(handler)
187
- self._consumer = self._conn.consumer(destination, message_handler=wrapped)
211
+ consumer_kwargs: dict[str, Any] = {"message_handler": wrapped}
212
+ if prefetch is not None:
213
+ consumer_kwargs["credit"] = prefetch
214
+ self._consumer = self._conn.consumer(destination, **consumer_kwargs)
188
215
  self._consumer.run()
189
216
 
190
217
  def stop(self) -> None:
@@ -193,6 +220,9 @@ class RabbitMqEngine(QueueEngine):
193
220
 
194
221
  def close(self) -> None:
195
222
  self.stop()
223
+ for publisher in self._publishers.values():
224
+ publisher.close()
225
+ self._publishers.clear()
196
226
  self._conn.close()
197
227
 
198
228
  def _publish_address(self, destination: str, routing_key: str) -> str:
@@ -0,0 +1,3 @@
1
+ """Compatibility exports for the RabbitMQ TLS configuration API."""
2
+
3
+ from rabbitmq_amqp_python_client.ssl_configuration import * # noqa: F401,F403
@@ -24,6 +24,14 @@ from .constants import (
24
24
  )
25
25
 
26
26
 
27
+ def __getattr__(name: str) -> Any:
28
+ if name == "Converter":
29
+ from rabbitmq_amqp_python_client.utils import Converter
30
+
31
+ return Converter
32
+ raise AttributeError(name)
33
+
34
+
27
35
  def encode_body(body: Any) -> bytes:
28
36
  if isinstance(body, bytes):
29
37
  return body
@@ -1,4 +0,0 @@
1
- from .aws_async import AwsAsyncEngine
2
- from .rabbitmq_async import RabbitMqAsyncEngine
3
-
4
- __all__ = ["AwsAsyncEngine", "RabbitMqAsyncEngine"]
@@ -1,4 +0,0 @@
1
- from .aws import AwsEngine
2
- from .rabbitmq import RabbitMqEngine
3
-
4
- __all__ = ["AwsEngine", "RabbitMqEngine"]
File without changes