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.
- weenspace_queue/__init__.py +128 -0
- weenspace_queue/asyncio/__init__.py +4 -0
- weenspace_queue/asyncio/aws_async.py +38 -0
- weenspace_queue/asyncio/rabbitmq_async.py +182 -0
- weenspace_queue/base.py +103 -0
- weenspace_queue/constants.py +75 -0
- weenspace_queue/providers/__init__.py +4 -0
- weenspace_queue/providers/aws.py +273 -0
- weenspace_queue/providers/rabbitmq.py +205 -0
- weenspace_queue/utils.py +271 -0
- weenspace_queue-0.1.0.dist-info/METADATA +161 -0
- weenspace_queue-0.1.0.dist-info/RECORD +14 -0
- weenspace_queue-0.1.0.dist-info/WHEEL +4 -0
- weenspace_queue-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Callable, Dict, Optional
|
|
5
|
+
|
|
6
|
+
from weenspace_queue.base import Message, QueueEngine, QueueSpecification, TopicSpecification
|
|
7
|
+
from weenspace_queue.constants import (
|
|
8
|
+
ALLOW_SNS_SEND_SID_PREFIX,
|
|
9
|
+
AWS_SOURCE_ARN_CONDITION,
|
|
10
|
+
DEAD_LETTER_TARGET_ARN_KEY,
|
|
11
|
+
DEFAULT_AWS_REGION,
|
|
12
|
+
DEFAULT_SQS_MAX_MESSAGES,
|
|
13
|
+
DEFAULT_SQS_RETENTION_SECONDS,
|
|
14
|
+
DEFAULT_SQS_WAIT_TIME_SECONDS,
|
|
15
|
+
IAM_POLICY_VERSION,
|
|
16
|
+
MAX_RECEIVE_COUNT_KEY,
|
|
17
|
+
SNS_FILTER_POLICY_SCOPE_MESSAGE_ATTRIBUTES,
|
|
18
|
+
SNS_PROTOCOL_SQS,
|
|
19
|
+
SNS_SERVICE_PRINCIPAL,
|
|
20
|
+
SQS_FIFO_SUFFIX,
|
|
21
|
+
SQS_POLICY_ATTRIBUTE,
|
|
22
|
+
SQS_QUEUE_ARN_ATTRIBUTE,
|
|
23
|
+
SQS_RECEIPT_HANDLE_ATTR,
|
|
24
|
+
SQS_REDRIVE_POLICY_ATTRIBUTE,
|
|
25
|
+
SQS_RETENTION_ATTRIBUTE,
|
|
26
|
+
SQS_SEND_MESSAGE_ACTION,
|
|
27
|
+
SQS_VISIBILITY_ATTRIBUTE,
|
|
28
|
+
)
|
|
29
|
+
from weenspace_queue.utils import (
|
|
30
|
+
decode_body,
|
|
31
|
+
extract_sqs_body,
|
|
32
|
+
inject_aws_routing_attrs,
|
|
33
|
+
is_fifo_queue,
|
|
34
|
+
is_sns_destination,
|
|
35
|
+
optional_queue_name_from_url,
|
|
36
|
+
rmq_pattern_to_aws_sns,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AwsEngine(QueueEngine):
|
|
41
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
42
|
+
try:
|
|
43
|
+
import boto3
|
|
44
|
+
except ImportError as exc:
|
|
45
|
+
raise ImportError(
|
|
46
|
+
'AWS provider requires boto3. Install with: pip install "weenspace-mq[aws]"'
|
|
47
|
+
) from exc
|
|
48
|
+
|
|
49
|
+
self._running = False
|
|
50
|
+
self.session = boto3.Session(
|
|
51
|
+
region_name=kwargs.get("region_name", DEFAULT_AWS_REGION),
|
|
52
|
+
aws_access_key_id=kwargs.get("aws_access_key_id"),
|
|
53
|
+
aws_secret_access_key=kwargs.get("aws_secret_access_key"),
|
|
54
|
+
aws_session_token=kwargs.get("aws_session_token"),
|
|
55
|
+
profile_name=kwargs.get("profile_name"),
|
|
56
|
+
)
|
|
57
|
+
client_kwargs = {}
|
|
58
|
+
if kwargs.get("endpoint_url"):
|
|
59
|
+
client_kwargs["endpoint_url"] = kwargs["endpoint_url"]
|
|
60
|
+
self.sqs = self.session.client("sqs", **client_kwargs)
|
|
61
|
+
self.sns = self.session.client("sns", **client_kwargs)
|
|
62
|
+
self._wait_time_seconds = int(
|
|
63
|
+
kwargs.get("wait_time_seconds", DEFAULT_SQS_WAIT_TIME_SECONDS)
|
|
64
|
+
)
|
|
65
|
+
self._max_messages = int(
|
|
66
|
+
kwargs.get("max_number_of_messages", DEFAULT_SQS_MAX_MESSAGES)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def declare_queue(self, spec: QueueSpecification) -> str:
|
|
70
|
+
attributes: Dict[str, str] = {}
|
|
71
|
+
if spec.dead_letter_target:
|
|
72
|
+
attributes[SQS_REDRIVE_POLICY_ATTRIBUTE] = json.dumps(
|
|
73
|
+
{
|
|
74
|
+
DEAD_LETTER_TARGET_ARN_KEY: self._as_queue_arn(
|
|
75
|
+
spec.dead_letter_target
|
|
76
|
+
),
|
|
77
|
+
MAX_RECEIVE_COUNT_KEY: str(spec.max_receive_count),
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
if spec.is_durable:
|
|
81
|
+
attributes[SQS_RETENTION_ATTRIBUTE] = DEFAULT_SQS_RETENTION_SECONDS
|
|
82
|
+
if spec.visibility_timeout_seconds is not None:
|
|
83
|
+
attributes[SQS_VISIBILITY_ATTRIBUTE] = str(spec.visibility_timeout_seconds)
|
|
84
|
+
|
|
85
|
+
fifo = spec.name.endswith(SQS_FIFO_SUFFIX)
|
|
86
|
+
create_kwargs: Dict[str, Any] = {"QueueName": spec.name}
|
|
87
|
+
if attributes:
|
|
88
|
+
create_kwargs["Attributes"] = attributes
|
|
89
|
+
if fifo:
|
|
90
|
+
create_kwargs.setdefault("Attributes", {})["FifoQueue"] = "true"
|
|
91
|
+
create_kwargs["Attributes"]["ContentBasedDeduplication"] = "true"
|
|
92
|
+
|
|
93
|
+
response = self.sqs.create_queue(**create_kwargs)
|
|
94
|
+
return response["QueueUrl"]
|
|
95
|
+
|
|
96
|
+
def declare_topic(self, spec: TopicSpecification) -> str:
|
|
97
|
+
create_kwargs: Dict[str, Any] = {"Name": spec.name}
|
|
98
|
+
if spec.name.endswith(SQS_FIFO_SUFFIX):
|
|
99
|
+
create_kwargs["Attributes"] = {"FifoTopic": "true"}
|
|
100
|
+
response = self.sns.create_topic(**create_kwargs)
|
|
101
|
+
return response["TopicArn"]
|
|
102
|
+
|
|
103
|
+
def bind_pattern(self, queue_id: str, topic_id: str, pattern: str) -> None:
|
|
104
|
+
queue_url = self._as_queue_url(queue_id)
|
|
105
|
+
queue_arn = self._as_queue_arn(queue_url)
|
|
106
|
+
policy = rmq_pattern_to_aws_sns(pattern)
|
|
107
|
+
subscribe_kwargs: Dict[str, Any] = {
|
|
108
|
+
"TopicArn": topic_id,
|
|
109
|
+
"Protocol": SNS_PROTOCOL_SQS,
|
|
110
|
+
"Endpoint": queue_arn,
|
|
111
|
+
}
|
|
112
|
+
attributes: Dict[str, str] = {}
|
|
113
|
+
if policy:
|
|
114
|
+
attributes["FilterPolicy"] = json.dumps(policy)
|
|
115
|
+
attributes["FilterPolicyScope"] = SNS_FILTER_POLICY_SCOPE_MESSAGE_ATTRIBUTES
|
|
116
|
+
if attributes:
|
|
117
|
+
subscribe_kwargs["Attributes"] = attributes
|
|
118
|
+
self.sns.subscribe(**subscribe_kwargs)
|
|
119
|
+
self._allow_sns_to_sqs(queue_url, queue_arn, topic_id)
|
|
120
|
+
|
|
121
|
+
def publish(self, destination: str, message: Message) -> None:
|
|
122
|
+
body_str = decode_body(message.body)
|
|
123
|
+
msg_attrs = dict(inject_aws_routing_attrs(message.routing_key))
|
|
124
|
+
extra_attrs = message.attributes or {}
|
|
125
|
+
for key, value in extra_attrs.items():
|
|
126
|
+
if key in msg_attrs or not isinstance(value, (str, int)):
|
|
127
|
+
continue
|
|
128
|
+
msg_attrs[key] = {
|
|
129
|
+
"DataType": "String",
|
|
130
|
+
"StringValue": str(value),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if is_sns_destination(destination):
|
|
134
|
+
publish_kwargs: Dict[str, Any] = {
|
|
135
|
+
"TopicArn": destination,
|
|
136
|
+
"Message": body_str,
|
|
137
|
+
}
|
|
138
|
+
if msg_attrs:
|
|
139
|
+
publish_kwargs["MessageAttributes"] = msg_attrs
|
|
140
|
+
group_id = extra_attrs.get("message_group_id") or message.routing_key
|
|
141
|
+
if destination.endswith(SQS_FIFO_SUFFIX) and group_id:
|
|
142
|
+
publish_kwargs["MessageGroupId"] = str(group_id)
|
|
143
|
+
dedup = extra_attrs.get("message_deduplication_id")
|
|
144
|
+
if dedup:
|
|
145
|
+
publish_kwargs["MessageDeduplicationId"] = str(dedup)
|
|
146
|
+
self.sns.publish(**publish_kwargs)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
queue_url = self._as_queue_url(destination)
|
|
150
|
+
send_kwargs: Dict[str, Any] = {
|
|
151
|
+
"QueueUrl": queue_url,
|
|
152
|
+
"MessageBody": body_str,
|
|
153
|
+
}
|
|
154
|
+
if msg_attrs:
|
|
155
|
+
send_kwargs["MessageAttributes"] = msg_attrs
|
|
156
|
+
if is_fifo_queue(queue_url):
|
|
157
|
+
send_kwargs["MessageGroupId"] = str(
|
|
158
|
+
extra_attrs.get("message_group_id") or message.routing_key or "default"
|
|
159
|
+
)
|
|
160
|
+
dedup = extra_attrs.get("message_deduplication_id")
|
|
161
|
+
if dedup:
|
|
162
|
+
send_kwargs["MessageDeduplicationId"] = str(dedup)
|
|
163
|
+
self.sqs.send_message(**send_kwargs)
|
|
164
|
+
|
|
165
|
+
def consume(self, queue_id: str, handler: Callable[[Message], None]) -> None:
|
|
166
|
+
queue_url = self._as_queue_url(queue_id)
|
|
167
|
+
self._running = True
|
|
168
|
+
while self._running:
|
|
169
|
+
response = self.sqs.receive_message(
|
|
170
|
+
QueueUrl=queue_url,
|
|
171
|
+
MaxNumberOfMessages=self._max_messages,
|
|
172
|
+
WaitTimeSeconds=self._wait_time_seconds,
|
|
173
|
+
MessageAttributeNames=["All"],
|
|
174
|
+
AttributeNames=["All"],
|
|
175
|
+
)
|
|
176
|
+
for sqs_msg in response.get("Messages", []):
|
|
177
|
+
if not self._running:
|
|
178
|
+
break
|
|
179
|
+
handler(self._to_message(queue_url, sqs_msg))
|
|
180
|
+
|
|
181
|
+
def stop(self) -> None:
|
|
182
|
+
self._running = False
|
|
183
|
+
|
|
184
|
+
def close(self) -> None:
|
|
185
|
+
self.stop()
|
|
186
|
+
|
|
187
|
+
def _to_message(self, queue_url: str, sqs_msg: Dict[str, Any]) -> Message:
|
|
188
|
+
body, routing_key, attributes = extract_sqs_body(sqs_msg)
|
|
189
|
+
receipt = sqs_msg[SQS_RECEIPT_HANDLE_ATTR]
|
|
190
|
+
attributes[SQS_RECEIPT_HANDLE_ATTR] = receipt
|
|
191
|
+
|
|
192
|
+
def accept(handle: str = receipt) -> None:
|
|
193
|
+
self.sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=handle)
|
|
194
|
+
|
|
195
|
+
def requeue(handle: str = receipt) -> None:
|
|
196
|
+
self.sqs.change_message_visibility(
|
|
197
|
+
QueueUrl=queue_url, ReceiptHandle=handle, VisibilityTimeout=0
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def reject(handle: str = receipt) -> None:
|
|
201
|
+
# Let the redrive policy / visibility timeout move the message toward the DLQ.
|
|
202
|
+
self.sqs.change_message_visibility(
|
|
203
|
+
QueueUrl=queue_url, ReceiptHandle=handle, VisibilityTimeout=0
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return Message(
|
|
207
|
+
body=body,
|
|
208
|
+
routing_key=routing_key,
|
|
209
|
+
attributes=attributes,
|
|
210
|
+
accept=accept,
|
|
211
|
+
reject=reject,
|
|
212
|
+
requeue=requeue,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def _as_queue_url(self, queue_id: str) -> str:
|
|
216
|
+
if queue_id.startswith("https://") or queue_id.startswith("http://"):
|
|
217
|
+
return queue_id
|
|
218
|
+
if SQS_ARN_MARKER in queue_id:
|
|
219
|
+
name = queue_id.rsplit(":", 1)[-1]
|
|
220
|
+
return self.sqs.get_queue_url(QueueName=name)["QueueUrl"]
|
|
221
|
+
return self.sqs.get_queue_url(QueueName=queue_id)["QueueUrl"]
|
|
222
|
+
|
|
223
|
+
def _as_queue_arn(self, queue_id: str) -> str:
|
|
224
|
+
if SQS_ARN_MARKER in queue_id:
|
|
225
|
+
return queue_id
|
|
226
|
+
queue_url = self._as_queue_url(queue_id)
|
|
227
|
+
attrs = self.sqs.get_queue_attributes(
|
|
228
|
+
QueueUrl=queue_url, AttributeNames=[SQS_QUEUE_ARN_ATTRIBUTE]
|
|
229
|
+
)
|
|
230
|
+
return attrs["Attributes"][SQS_QUEUE_ARN_ATTRIBUTE]
|
|
231
|
+
|
|
232
|
+
def _allow_sns_to_sqs(self, queue_url: str, queue_arn: str, topic_arn: str) -> None:
|
|
233
|
+
try:
|
|
234
|
+
current = self.sqs.get_queue_attributes(
|
|
235
|
+
QueueUrl=queue_url, AttributeNames=[SQS_POLICY_ATTRIBUTE]
|
|
236
|
+
)
|
|
237
|
+
policy = json.loads(current.get("Attributes", {}).get(SQS_POLICY_ATTRIBUTE) or "{}")
|
|
238
|
+
except Exception:
|
|
239
|
+
policy = {}
|
|
240
|
+
if not policy:
|
|
241
|
+
policy = {"Version": IAM_POLICY_VERSION, "Statement": []}
|
|
242
|
+
statements = policy.setdefault("Statement", [])
|
|
243
|
+
sid = f"{ALLOW_SNS_SEND_SID_PREFIX}{abs(hash(topic_arn))}"
|
|
244
|
+
statement = {
|
|
245
|
+
"Sid": sid,
|
|
246
|
+
"Effect": "Allow",
|
|
247
|
+
"Principal": {"Service": SNS_SERVICE_PRINCIPAL},
|
|
248
|
+
"Action": SQS_SEND_MESSAGE_ACTION,
|
|
249
|
+
"Resource": queue_arn,
|
|
250
|
+
"Condition": {"ArnEquals": {AWS_SOURCE_ARN_CONDITION: topic_arn}},
|
|
251
|
+
}
|
|
252
|
+
statements = [
|
|
253
|
+
item
|
|
254
|
+
for item in statements
|
|
255
|
+
if not (
|
|
256
|
+
item.get("Action") == SQS_SEND_MESSAGE_ACTION
|
|
257
|
+
and item.get("Condition", {})
|
|
258
|
+
.get("ArnEquals", {})
|
|
259
|
+
.get(AWS_SOURCE_ARN_CONDITION)
|
|
260
|
+
== topic_arn
|
|
261
|
+
)
|
|
262
|
+
]
|
|
263
|
+
statements.append(statement)
|
|
264
|
+
policy["Statement"] = statements
|
|
265
|
+
self.sqs.set_queue_attributes(
|
|
266
|
+
QueueUrl=queue_url,
|
|
267
|
+
Attributes={SQS_POLICY_ATTRIBUTE: json.dumps(policy)},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
def resolve_queue_name(self, queue_id: str) -> Optional[str]:
|
|
271
|
+
if queue_id.startswith("https://"):
|
|
272
|
+
return optional_queue_name_from_url(queue_id)
|
|
273
|
+
return queue_id
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Optional
|
|
4
|
+
|
|
5
|
+
from weenspace_queue import (
|
|
6
|
+
AMQPMessagingHandler,
|
|
7
|
+
ClassicQueueSpecification,
|
|
8
|
+
Environment,
|
|
9
|
+
Event,
|
|
10
|
+
ExchangeSpecification,
|
|
11
|
+
ExchangeToQueueBindingSpecification,
|
|
12
|
+
ExchangeType,
|
|
13
|
+
Message as ProtonMessage,
|
|
14
|
+
QuorumQueueSpecification,
|
|
15
|
+
StreamSpecification,
|
|
16
|
+
)
|
|
17
|
+
from weenspace_queue.delivery_context import DeliveryContext
|
|
18
|
+
|
|
19
|
+
from weenspace_queue.base import Message, QueueEngine, QueueSpecification, TopicSpecification
|
|
20
|
+
from weenspace_queue.constants import (
|
|
21
|
+
DEFAULT_RABBITMQ_URI,
|
|
22
|
+
ExchangeKind,
|
|
23
|
+
QueueKind,
|
|
24
|
+
)
|
|
25
|
+
from weenspace_queue.utils import (
|
|
26
|
+
encode_body,
|
|
27
|
+
rabbitmq_exchange_address,
|
|
28
|
+
rabbitmq_queue_address,
|
|
29
|
+
rabbitmq_resource_name,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_EXCHANGE_KIND_MAP = {
|
|
33
|
+
ExchangeKind.DIRECT: ExchangeType.DIRECT,
|
|
34
|
+
ExchangeKind.TOPIC: ExchangeType.TOPIC,
|
|
35
|
+
ExchangeKind.FANOUT: ExchangeType.FANOUT,
|
|
36
|
+
ExchangeKind.HEADERS: ExchangeType.HEADERS,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _CallbackHandler(AMQPMessagingHandler):
|
|
41
|
+
def __init__(self, handler: Callable[[Message], None]) -> None:
|
|
42
|
+
super().__init__(auto_accept=False, auto_settle=True)
|
|
43
|
+
self._handler = handler
|
|
44
|
+
|
|
45
|
+
def on_amqp_message(self, event: Event) -> None:
|
|
46
|
+
proton_msg = event.message
|
|
47
|
+
routing_key = proton_msg.subject or ""
|
|
48
|
+
attributes = dict(proton_msg.properties or {})
|
|
49
|
+
if proton_msg.address:
|
|
50
|
+
attributes["address"] = proton_msg.address
|
|
51
|
+
if proton_msg.correlation_id is not None:
|
|
52
|
+
attributes["correlation_id"] = proton_msg.correlation_id
|
|
53
|
+
if proton_msg.reply_to:
|
|
54
|
+
attributes["reply_to"] = proton_msg.reply_to
|
|
55
|
+
|
|
56
|
+
context = DeliveryContext()
|
|
57
|
+
|
|
58
|
+
def accept(evt: Event = event) -> None:
|
|
59
|
+
context.accept(evt)
|
|
60
|
+
|
|
61
|
+
def reject(evt: Event = event) -> None:
|
|
62
|
+
context.discard(evt)
|
|
63
|
+
|
|
64
|
+
def requeue(evt: Event = event) -> None:
|
|
65
|
+
context.requeue(evt)
|
|
66
|
+
|
|
67
|
+
self._handler(
|
|
68
|
+
Message(
|
|
69
|
+
body=encode_body(proton_msg.body),
|
|
70
|
+
routing_key=routing_key,
|
|
71
|
+
attributes=attributes,
|
|
72
|
+
accept=accept,
|
|
73
|
+
reject=reject,
|
|
74
|
+
requeue=requeue,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class RabbitMqEngine(QueueEngine):
|
|
80
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
81
|
+
uri = kwargs.get("uri")
|
|
82
|
+
uris = kwargs.get("uris")
|
|
83
|
+
if uri is None and uris is None:
|
|
84
|
+
uri = DEFAULT_RABBITMQ_URI
|
|
85
|
+
env_kwargs: dict[str, Any] = {"uri": uri, "uris": uris}
|
|
86
|
+
for key in ("ssl_context", "oauth2_options", "recovery_configuration"):
|
|
87
|
+
if kwargs.get(key) is not None:
|
|
88
|
+
env_kwargs[key] = kwargs[key]
|
|
89
|
+
self._env = Environment(**env_kwargs)
|
|
90
|
+
self._conn = self._env.connection()
|
|
91
|
+
self._conn.dial()
|
|
92
|
+
self._mgmt = self._conn.management()
|
|
93
|
+
self._consumer: Optional[Any] = None
|
|
94
|
+
|
|
95
|
+
def declare_queue(self, spec: QueueSpecification) -> str:
|
|
96
|
+
if spec.kind == QueueKind.STREAM:
|
|
97
|
+
self._mgmt.declare_queue(
|
|
98
|
+
StreamSpecification(
|
|
99
|
+
name=spec.name,
|
|
100
|
+
**{
|
|
101
|
+
key: value
|
|
102
|
+
for key, value in spec.extra.items()
|
|
103
|
+
if key in StreamSpecification.__dataclass_fields__
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
return rabbitmq_queue_address(spec.name)
|
|
108
|
+
|
|
109
|
+
if spec.kind == QueueKind.QUORUM:
|
|
110
|
+
self._mgmt.declare_queue(
|
|
111
|
+
QuorumQueueSpecification(
|
|
112
|
+
name=spec.name,
|
|
113
|
+
dead_letter_exchange=spec.dead_letter_target,
|
|
114
|
+
dead_letter_routing_key=spec.dead_letter_routing_key,
|
|
115
|
+
deliver_limit=spec.max_receive_count,
|
|
116
|
+
**{
|
|
117
|
+
key: value
|
|
118
|
+
for key, value in spec.extra.items()
|
|
119
|
+
if key in QuorumQueueSpecification.__dataclass_fields__
|
|
120
|
+
},
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
return rabbitmq_queue_address(spec.name)
|
|
124
|
+
|
|
125
|
+
self._mgmt.declare_queue(
|
|
126
|
+
ClassicQueueSpecification(
|
|
127
|
+
name=spec.name,
|
|
128
|
+
is_durable=spec.is_durable,
|
|
129
|
+
dead_letter_exchange=spec.dead_letter_target,
|
|
130
|
+
dead_letter_routing_key=spec.dead_letter_routing_key,
|
|
131
|
+
**{
|
|
132
|
+
key: value
|
|
133
|
+
for key, value in spec.extra.items()
|
|
134
|
+
if key in ClassicQueueSpecification.__dataclass_fields__
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
return rabbitmq_queue_address(spec.name)
|
|
139
|
+
|
|
140
|
+
def declare_topic(self, spec: TopicSpecification) -> str:
|
|
141
|
+
self._mgmt.declare_exchange(
|
|
142
|
+
ExchangeSpecification(
|
|
143
|
+
name=spec.name,
|
|
144
|
+
exchange_type=_EXCHANGE_KIND_MAP.get(spec.kind, ExchangeType.TOPIC),
|
|
145
|
+
is_durable=spec.is_durable,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
return spec.name
|
|
149
|
+
|
|
150
|
+
def bind_pattern(self, queue_id: str, topic_id: str, pattern: str) -> None:
|
|
151
|
+
self._mgmt.bind(
|
|
152
|
+
ExchangeToQueueBindingSpecification(
|
|
153
|
+
source_exchange=rabbitmq_resource_name(topic_id),
|
|
154
|
+
destination_queue=rabbitmq_resource_name(queue_id),
|
|
155
|
+
binding_key=pattern,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def publish(self, destination: str, message: Message) -> None:
|
|
160
|
+
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:
|
|
185
|
+
destination = rabbitmq_queue_address(queue_id)
|
|
186
|
+
wrapped = _CallbackHandler(handler)
|
|
187
|
+
self._consumer = self._conn.consumer(destination, message_handler=wrapped)
|
|
188
|
+
self._consumer.run()
|
|
189
|
+
|
|
190
|
+
def stop(self) -> None:
|
|
191
|
+
if self._consumer is not None:
|
|
192
|
+
self._consumer.stop()
|
|
193
|
+
|
|
194
|
+
def close(self) -> None:
|
|
195
|
+
self.stop()
|
|
196
|
+
self._conn.close()
|
|
197
|
+
|
|
198
|
+
def _publish_address(self, destination: str, routing_key: str) -> str:
|
|
199
|
+
if destination.startswith("/queues/"):
|
|
200
|
+
return destination
|
|
201
|
+
if destination.startswith("/exchanges/"):
|
|
202
|
+
return rabbitmq_exchange_address(destination, routing_key)
|
|
203
|
+
if routing_key:
|
|
204
|
+
return rabbitmq_exchange_address(destination, routing_key)
|
|
205
|
+
return rabbitmq_queue_address(destination)
|