weenspace-queue 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,271 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from .constants import (
7
+ AWS_NUMBER_DATA_TYPE,
8
+ AWS_STRING_DATA_TYPE,
9
+ RABBITMQ_EXCHANGE_PREFIX,
10
+ RABBITMQ_QUEUE_PREFIX,
11
+ ROUTING_KEY_ATTR,
12
+ ROUTING_KEY_INDEX_PREFIX,
13
+ ROUTING_KEY_LENGTH_ATTR,
14
+ ROUTING_KEY_REVERSE_INDEX_PREFIX,
15
+ ROUTING_SEPARATOR,
16
+ SNS_ARN_MARKER,
17
+ SNS_NOTIFICATION_TYPE,
18
+ SQS_ARN_MARKER,
19
+ SQS_BODY_KEY,
20
+ SQS_FIFO_SUFFIX,
21
+ SQS_URL_MARKER,
22
+ WILDCARD_MULTI,
23
+ WILDCARD_SINGLE,
24
+ )
25
+
26
+
27
+ def encode_body(body: Any) -> bytes:
28
+ if isinstance(body, bytes):
29
+ return body
30
+ if body is None:
31
+ return b""
32
+ if isinstance(body, str):
33
+ return body.encode("utf-8")
34
+ return json.dumps(body).encode("utf-8")
35
+
36
+
37
+ def decode_body(body: Any) -> str:
38
+ if isinstance(body, bytes):
39
+ return body.decode("utf-8")
40
+ return str(body)
41
+
42
+
43
+ def split_routing(value: str) -> List[str]:
44
+ if value == "":
45
+ return []
46
+ return value.split(ROUTING_SEPARATOR)
47
+
48
+
49
+ def is_sns_destination(destination: str) -> bool:
50
+ return SNS_ARN_MARKER in destination
51
+
52
+
53
+ def is_sqs_destination(destination: str) -> bool:
54
+ return SQS_ARN_MARKER in destination or SQS_URL_MARKER in destination
55
+
56
+
57
+ def is_fifo_queue(name_or_url: str) -> bool:
58
+ return name_or_url.rstrip("/").endswith(SQS_FIFO_SUFFIX)
59
+
60
+
61
+ def rabbitmq_queue_address(queue_id: str) -> str:
62
+ if queue_id.startswith(RABBITMQ_QUEUE_PREFIX) or queue_id.startswith(
63
+ RABBITMQ_EXCHANGE_PREFIX
64
+ ):
65
+ return queue_id
66
+ return f"{RABBITMQ_QUEUE_PREFIX}{queue_id.lstrip('/')}"
67
+
68
+
69
+ def rabbitmq_exchange_address(topic_id: str, routing_key: str = "") -> str:
70
+ if topic_id.startswith(RABBITMQ_EXCHANGE_PREFIX) or topic_id.startswith(
71
+ RABBITMQ_QUEUE_PREFIX
72
+ ):
73
+ if routing_key and topic_id.count("/") == 2:
74
+ return f"{topic_id}/{routing_key}"
75
+ return topic_id
76
+ if routing_key:
77
+ return f"{RABBITMQ_EXCHANGE_PREFIX}{topic_id}/{routing_key}"
78
+ return f"{RABBITMQ_EXCHANGE_PREFIX}{topic_id}"
79
+
80
+
81
+ def rabbitmq_resource_name(resource_id: str) -> str:
82
+ if resource_id.startswith(RABBITMQ_QUEUE_PREFIX):
83
+ return resource_id[len(RABBITMQ_QUEUE_PREFIX) :]
84
+ if resource_id.startswith(RABBITMQ_EXCHANGE_PREFIX):
85
+ remainder = resource_id[len(RABBITMQ_EXCHANGE_PREFIX) :]
86
+ return remainder.split("/", 1)[0]
87
+ return resource_id
88
+
89
+
90
+ def inject_aws_routing_attrs(routing_key: str) -> Dict[str, Any]:
91
+ """Hydrate SNS/SQS MessageAttributes so wildcard filter policies can match."""
92
+ if not routing_key:
93
+ return {}
94
+ segments = split_routing(routing_key)
95
+ attributes: Dict[str, Any] = {
96
+ ROUTING_KEY_ATTR: {
97
+ "DataType": AWS_STRING_DATA_TYPE,
98
+ "StringValue": routing_key,
99
+ },
100
+ ROUTING_KEY_LENGTH_ATTR: {
101
+ "DataType": AWS_NUMBER_DATA_TYPE,
102
+ "StringValue": str(len(segments)),
103
+ },
104
+ }
105
+ for idx, value in enumerate(segments):
106
+ attributes[f"{ROUTING_KEY_INDEX_PREFIX}{idx}"] = {
107
+ "DataType": AWS_STRING_DATA_TYPE,
108
+ "StringValue": value,
109
+ }
110
+ attributes[f"{ROUTING_KEY_REVERSE_INDEX_PREFIX}{idx}"] = {
111
+ "DataType": AWS_STRING_DATA_TYPE,
112
+ "StringValue": segments[-(idx + 1)],
113
+ }
114
+ return attributes
115
+
116
+
117
+ def rmq_pattern_to_aws_sns(routing_pattern: str) -> Dict[str, Any]:
118
+ """
119
+ Translate RabbitMQ topic patterns into SNS Message Attribute filter policies.
120
+
121
+ Supported shapes:
122
+ - exact: order.placed
123
+ - single-word *: order.*.completed
124
+ - multi-word trailing #: global.orders.#
125
+ - multi-word leading #: #.delayed
126
+ - combined: eu.*.truck.#
127
+ - middle #: region.#.failed
128
+ """
129
+ pattern = routing_pattern.strip()
130
+ if pattern in ("", WILDCARD_MULTI):
131
+ return {}
132
+
133
+ segments = split_routing(pattern)
134
+ if WILDCARD_SINGLE not in segments and WILDCARD_MULTI not in segments:
135
+ return {
136
+ ROUTING_KEY_ATTR: [pattern],
137
+ ROUTING_KEY_LENGTH_ATTR: [{"numeric": ["=", len(segments)]}],
138
+ }
139
+
140
+ hash_indexes = [i for i, segment in enumerate(segments) if segment == WILDCARD_MULTI]
141
+ policy: Dict[str, Any] = {}
142
+
143
+ if not hash_indexes:
144
+ policy[ROUTING_KEY_LENGTH_ATTR] = [{"numeric": ["=", len(segments)]}]
145
+ _apply_segment_filters(policy, segments, reverse=False)
146
+ return policy
147
+
148
+ first_hash = hash_indexes[0]
149
+ last_hash = hash_indexes[-1]
150
+ prefix = segments[:first_hash]
151
+ suffix = segments[last_hash + 1 :]
152
+
153
+ _apply_segment_filters(policy, prefix, reverse=False)
154
+ _apply_segment_filters(policy, list(reversed(suffix)), reverse=True)
155
+ return policy
156
+
157
+
158
+ def _apply_segment_filters(
159
+ policy: Dict[str, Any], segments: List[str], reverse: bool
160
+ ) -> None:
161
+ prefix = ROUTING_KEY_REVERSE_INDEX_PREFIX if reverse else ROUTING_KEY_INDEX_PREFIX
162
+ for idx, segment in enumerate(segments):
163
+ key = f"{prefix}{idx}"
164
+ if segment == WILDCARD_MULTI:
165
+ continue
166
+ if segment == WILDCARD_SINGLE:
167
+ policy[key] = [{"exists": True}]
168
+ else:
169
+ policy[key] = [segment]
170
+
171
+
172
+ def rabbitmq_pattern_matches(pattern: str, routing_key: str) -> bool:
173
+ """Reference matcher for RabbitMQ topic semantics used in tests."""
174
+ return _match_segments(split_routing(pattern), split_routing(routing_key))
175
+
176
+
177
+ def _match_segments(pattern: List[str], key: List[str]) -> bool:
178
+ if not pattern:
179
+ return not key
180
+ head, tail = pattern[0], pattern[1:]
181
+ if head == WILDCARD_MULTI:
182
+ for consumed in range(len(key) + 1):
183
+ if _match_segments(tail, key[consumed:]):
184
+ return True
185
+ return False
186
+ if not key:
187
+ return False
188
+ if head in (WILDCARD_SINGLE, key[0]):
189
+ return _match_segments(tail, key[1:])
190
+ return False
191
+
192
+
193
+ def sns_filter_matches(policy: Dict[str, Any], routing_key: str) -> bool:
194
+ """Evaluate a translated SNS filter against injected routing attributes."""
195
+ if not policy:
196
+ return True
197
+ attributes = inject_aws_routing_attrs(routing_key)
198
+ for attr_name, conditions in policy.items():
199
+ actual = attributes.get(attr_name)
200
+ if actual is None:
201
+ return False
202
+ actual_value = actual["StringValue"]
203
+ if not _condition_matches(conditions, actual_value):
204
+ return False
205
+ return True
206
+
207
+
208
+ def _condition_matches(conditions: List[Any], actual_value: str) -> bool:
209
+ for condition in conditions:
210
+ if isinstance(condition, dict):
211
+ if "exists" in condition:
212
+ if condition["exists"]:
213
+ return True
214
+ continue
215
+ numeric = condition.get("numeric")
216
+ if numeric and numeric[0] == "=":
217
+ return str(actual_value) == str(numeric[1])
218
+ continue
219
+ if str(condition) == str(actual_value):
220
+ return True
221
+ return False
222
+
223
+
224
+ def unwrap_sqs_body(raw_body: str) -> tuple[bytes, str, Dict[str, Any]]:
225
+ """Unwrap SNS-to-SQS envelopes when present; otherwise treat as the payload."""
226
+ attributes: Dict[str, Any] = {}
227
+ routing_key = ""
228
+ body = raw_body
229
+ try:
230
+ parsed = json.loads(raw_body)
231
+ except (TypeError, json.JSONDecodeError):
232
+ return encode_body(raw_body), routing_key, attributes
233
+
234
+ if isinstance(parsed, dict) and parsed.get("Type") == SNS_NOTIFICATION_TYPE:
235
+ body = parsed.get("Message", "")
236
+ sns_attrs = parsed.get("MessageAttributes") or {}
237
+ routing_key = _routing_key_from_sns_attrs(sns_attrs)
238
+ attributes["sns"] = {key: value for key, value in parsed.items() if key != "Message"}
239
+ return encode_body(body), routing_key, attributes
240
+
241
+ return encode_body(raw_body), routing_key, attributes
242
+
243
+
244
+ def _routing_key_from_sns_attrs(sns_attrs: Dict[str, Any]) -> str:
245
+ routing = sns_attrs.get(ROUTING_KEY_ATTR) or {}
246
+ return str(routing.get("Value") or routing.get("StringValue") or "")
247
+
248
+
249
+ def routing_key_from_sqs_attributes(message: Dict[str, Any]) -> str:
250
+ attrs = message.get("MessageAttributes") or {}
251
+ routing = attrs.get(ROUTING_KEY_ATTR) or {}
252
+ return str(routing.get("StringValue") or routing.get("Value") or "")
253
+
254
+
255
+ def extract_sqs_body(message: Dict[str, Any]) -> tuple[bytes, str, Dict[str, Any]]:
256
+ raw = message.get(SQS_BODY_KEY, "")
257
+ body, routing_key, extra = unwrap_sqs_body(raw)
258
+ if not routing_key:
259
+ routing_key = routing_key_from_sqs_attributes(message)
260
+ extra["sqs"] = {
261
+ key: value
262
+ for key, value in message.items()
263
+ if key not in {SQS_BODY_KEY}
264
+ }
265
+ return body, routing_key, extra
266
+
267
+
268
+ def optional_queue_name_from_url(queue_url: str) -> Optional[str]:
269
+ if not queue_url:
270
+ return None
271
+ return queue_url.rstrip("/").split("/")[-1]
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: weenspace-queue
3
+ Version: 0.1.0
4
+ Summary: WeenSpace queue client for RabbitMQ and AWS SQS, supporting both synchronous and asynchronous operations.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: rabbitmq,amqp,messaging,event-driven,microservices,queue,weenspace
8
+ Author: WeenSpace
9
+ Author-email: weenspace@gmail.com
10
+ Requires-Python: >=3.14,<4.0
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Classifier: Topic :: System :: Networking
18
+ Requires-Dist: packaging (>=25.0,<26.0)
19
+ Requires-Dist: python-qpid-proton (>=0.40.0,<0.41.0)
20
+ Requires-Dist: typing-extensions (>=4.15.0,<5.0.0)
21
+ Project-URL: Homepage, https://github.com/weenspace/weenspace-queue
22
+ Project-URL: Repository, https://github.com/weenspace/weenspace-queue
23
+ Description-Content-Type: text/markdown
24
+
25
+ # WeenSpace RabbitMQ Client
26
+
27
+ [![PyPI version](https://badge.fury.io/py/weenspace-queue.svg)](https://pypi.org/project/weenspace-queue/)
28
+ [![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
29
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
30
+
31
+ A powerful Python RabbitMQ client for AMQP 1.0 protocol, designed for event-driven microservices architecture.
32
+
33
+ ## ✨ Features
34
+
35
+ - 🔐 **Authentication**: OAuth2, TLS/SSL, Basic Auth
36
+ - 📬 **Queue Types**: Classic, Quorum, Stream queues
37
+ - ☠️ **Dead Letter Queues**: Built-in DLQ support with configurable strategies
38
+ - ⚡ **Priority Queues**: Native priority queue support
39
+ - 🔄 **Auto Reconnection**: Configurable recovery with exponential backoff
40
+ - 🔀 **All Exchange Types**: Direct, Fanout, Topic, Headers
41
+ - 🚀 **Async Support**: Native asyncio integration
42
+ - 📊 **Streams**: RabbitMQ Streams with filtering support
43
+
44
+ ## 📦 Installation
45
+
46
+ ```bash
47
+ pip install weenspace-queue
48
+ ```
49
+
50
+ ## 🚀 Quick Start
51
+
52
+ ### Basic Publisher/Consumer
53
+
54
+ ```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)
79
+
80
+ # Publish message
81
+ publisher = connection.publisher("/queues/my-queue")
82
+ publisher.publish(Message(body=b"Hello WeenSpace!"))
83
+
84
+ # Consume messages
85
+ def on_message(message):
86
+ print(f"Received: {message.body}")
87
+ message.accept()
88
+
89
+ consumer = connection.consumer("/queues/my-queue", handler=on_message)
90
+ ```
91
+
92
+ ### Async Support
93
+
94
+ ```python
95
+ import asyncio
96
+ from weenspace_queue.asyncio import AsyncEnvironment
97
+
98
+ 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!"))
103
+
104
+ asyncio.run(main())
105
+ ```
106
+
107
+ ### OAuth2 Authentication
108
+
109
+ ```python
110
+ from weenspace_queue import Environment, OAuth2Options
111
+
112
+ oauth = OAuth2Options(token="your_jwt_token")
113
+ environment = Environment(
114
+ uri="amqp://localhost:5672/",
115
+ oauth2_options=oauth
116
+ )
117
+ ```
118
+
119
+ ### TLS/SSL Connection
120
+
121
+ ```python
122
+ from weenspace_queue import Environment, SslConfiguration
123
+
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(
130
+ uri="amqps://localhost:5671/",
131
+ ssl_configuration=ssl_config
132
+ )
133
+ ```
134
+
135
+ ## 📚 Documentation
136
+
137
+ See [examples](./examples) folder for more detailed usage examples.
138
+
139
+ ## 🔄 Migration from python-rabbitmq
140
+
141
+ ```python
142
+ # Old (python-rabbitmq)
143
+ # from python_rabbitmq import RabbitMQ
144
+
145
+ # New (weenspace-queue)
146
+ from weenspace_queue import Environment, Connection
147
+ ```
148
+
149
+ ## 📋 Requirements
150
+
151
+ - Python 3.13+
152
+ - RabbitMQ 4.x with AMQP 1.0 plugin enabled
153
+
154
+ ## 📄 License
155
+
156
+ MIT License - Based on the official [RabbitMQ AMQP Python Client](https://github.com/rabbitmq/rabbitmq-amqp-python-client)
157
+
158
+ ## 🙏 Credits
159
+
160
+ This library is based on the official RabbitMQ AMQP 1.0 Python client by the RabbitMQ team.
161
+
@@ -0,0 +1,14 @@
1
+ weenspace_queue/__init__.py,sha256=RGWo5dl9u-FfSlxT-dYiKg-ZPFu7ZBs3gd-SYOvuTaI,3964
2
+ weenspace_queue/asyncio/__init__.py,sha256=DO2yDMXX-WosrZfxUbilQvUsIZlK-4I8K5qREu--4FI,139
3
+ weenspace_queue/asyncio/aws_async.py,sha256=Jd-6tpF5PnLdqxTQ_9ScLkPihRl7NrcWFIPX_d4Da-k,1356
4
+ weenspace_queue/asyncio/rabbitmq_async.py,sha256=-FWzSHLWV7f1wWebpYhRh2Wm7u5mhIw4elpZn7X8zRU,6339
5
+ weenspace_queue/base.py,sha256=XTnA4afHlxgDr6fgZKEaox33AEaOYSZt5zRvNDDkSnQ,2602
6
+ weenspace_queue/constants.py,sha256=RGpCUeu7GEv8yX7DGkBjxAqPoeIBMtrGzIRGWPau3kM,2009
7
+ weenspace_queue/providers/__init__.py,sha256=mnVEFyRt1ttlNxiRUAUUV2yZl3djJhsKVHCRIsMo4cw,107
8
+ weenspace_queue/providers/aws.py,sha256=0pQgcIciVbR2b0Zj4l4bTin_vCeUsh6eMN7RNLZJw50,10671
9
+ weenspace_queue/providers/rabbitmq.py,sha256=0tDBx-oxBOHgy1UefmeUL0VvmWJPv-HCrYDL7qIeZKE,7390
10
+ weenspace_queue/utils.py,sha256=6CB1kSNxuiGxA7A6dEDIooc7YHCw7cGuIpwZmq4mG74,8809
11
+ weenspace_queue-0.1.0.dist-info/METADATA,sha256=d-trZDroRkRXlVba_1e441U_Z12JqE9P2c1bu16UiRg,4618
12
+ weenspace_queue-0.1.0.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
13
+ weenspace_queue-0.1.0.dist-info/licenses/LICENSE,sha256=aHFdqhTZFeswtMH5Fi6Owota-zZ_juV_37U2wuj_axU,1066
14
+ weenspace_queue-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WeenSpace
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.