rocketmq-sdk 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.
- rocketmq/__init__.py +75 -0
- rocketmq/amqp.py +222 -0
- rocketmq/client.py +435 -0
- rocketmq/error_codes.py +31 -0
- rocketmq/error_parser.py +215 -0
- rocketmq/errors.py +126 -0
- rocketmq/proto.py +188 -0
- rocketmq/py.typed +0 -0
- rocketmq/schema.py +89 -0
- rocketmq/serializer.py +66 -0
- rocketmq_sdk-0.1.0.dist-info/METADATA +94 -0
- rocketmq_sdk-0.1.0.dist-info/RECORD +14 -0
- rocketmq_sdk-0.1.0.dist-info/WHEEL +4 -0
- rocketmq_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
rocketmq/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RocketMQ Python SDK — schema-aware AMQP client.
|
|
3
|
+
|
|
4
|
+
Pydantic + aio-pika wrapper that mirrors the ``rocketmq.js`` API.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from rocketmq import connect
|
|
9
|
+
from rocketmq.schema import BaseSchema
|
|
10
|
+
|
|
11
|
+
class Order(BaseSchema):
|
|
12
|
+
id: str
|
|
13
|
+
customer_id: str
|
|
14
|
+
qty: int
|
|
15
|
+
|
|
16
|
+
async def main():
|
|
17
|
+
mq = await connect(url="amqp://localhost")
|
|
18
|
+
orders = await mq.queue("orders", Order)
|
|
19
|
+
await orders.send(Order(id="1", customer_id="c1", qty=5))
|
|
20
|
+
await orders.consume(lambda msg: print(msg))
|
|
21
|
+
await mq.close()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "0.1.0"
|
|
25
|
+
|
|
26
|
+
# Error codes
|
|
27
|
+
# Core API
|
|
28
|
+
from rocketmq.client import QueueHandle, RocketMQ, connect
|
|
29
|
+
from rocketmq.error_codes import BrokerErrorCode
|
|
30
|
+
|
|
31
|
+
# Errors
|
|
32
|
+
from rocketmq.errors import (
|
|
33
|
+
ConnectionError_,
|
|
34
|
+
ConsumeError,
|
|
35
|
+
PublishError,
|
|
36
|
+
QueueError,
|
|
37
|
+
RocketMQError,
|
|
38
|
+
SchemaError,
|
|
39
|
+
SchemaValidationError,
|
|
40
|
+
SerializationError,
|
|
41
|
+
TimeoutError_,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Schema
|
|
45
|
+
from rocketmq.schema import BaseSchema, Proto, ProtoType
|
|
46
|
+
|
|
47
|
+
# Serializer
|
|
48
|
+
from rocketmq.serializer import JsonSerializer, Serializer
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
# Schema
|
|
53
|
+
"BaseSchema",
|
|
54
|
+
# Error codes
|
|
55
|
+
"BrokerErrorCode",
|
|
56
|
+
"ConnectionError_",
|
|
57
|
+
"ConsumeError",
|
|
58
|
+
"JsonSerializer",
|
|
59
|
+
"Proto",
|
|
60
|
+
"ProtoType",
|
|
61
|
+
"PublishError",
|
|
62
|
+
"QueueError",
|
|
63
|
+
"QueueHandle",
|
|
64
|
+
"RocketMQ",
|
|
65
|
+
# Errors
|
|
66
|
+
"RocketMQError",
|
|
67
|
+
"SchemaError",
|
|
68
|
+
"SchemaValidationError",
|
|
69
|
+
"SerializationError",
|
|
70
|
+
# Serializer
|
|
71
|
+
"Serializer",
|
|
72
|
+
"TimeoutError_",
|
|
73
|
+
# Core
|
|
74
|
+
"connect",
|
|
75
|
+
]
|
rocketmq/amqp.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Thin wrapper around aio-pika's connection and channel.
|
|
3
|
+
|
|
4
|
+
Exposes only the operations the SDK needs without leaking the full
|
|
5
|
+
aio-pika surface. No schema or validation logic lives here.
|
|
6
|
+
|
|
7
|
+
Usage::
|
|
8
|
+
|
|
9
|
+
conn = await AmqpConnection.connect("amqp://localhost")
|
|
10
|
+
ch = await conn.create_channel()
|
|
11
|
+
await ch.assert_queue("orders")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from collections.abc import Callable, Coroutine
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Any, final
|
|
19
|
+
|
|
20
|
+
import aio_pika
|
|
21
|
+
from aio_pika.abc import AbstractChannel, AbstractIncomingMessage
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True, slots=True)
|
|
25
|
+
class AssertQueueReply:
|
|
26
|
+
"""Result of declaring a queue."""
|
|
27
|
+
|
|
28
|
+
queue: str
|
|
29
|
+
message_count: int
|
|
30
|
+
consumer_count: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class AssertExchangeReply:
|
|
35
|
+
"""Result of declaring an exchange."""
|
|
36
|
+
|
|
37
|
+
exchange: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
ConsumeHandler = Callable[[AbstractIncomingMessage], Coroutine[Any, Any, None]]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@final
|
|
44
|
+
class AmqpChannel:
|
|
45
|
+
"""Wraps an aio-pika channel with the operations the SDK needs.
|
|
46
|
+
|
|
47
|
+
No schema or validation logic. Just queue/exchange/publish/consume.
|
|
48
|
+
|
|
49
|
+
Usage::
|
|
50
|
+
|
|
51
|
+
ch = await conn.create_channel()
|
|
52
|
+
await ch.assert_queue("orders", arguments={"x-schema": proto_str})
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, channel: AbstractChannel) -> None:
|
|
56
|
+
self._ch = channel
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def raw(self) -> AbstractChannel:
|
|
60
|
+
"""Exposes the raw aio-pika channel for event listeners."""
|
|
61
|
+
return self._ch
|
|
62
|
+
|
|
63
|
+
async def assert_queue(
|
|
64
|
+
self,
|
|
65
|
+
name: str,
|
|
66
|
+
*,
|
|
67
|
+
durable: bool = False,
|
|
68
|
+
arguments: dict[str, Any] | None = None,
|
|
69
|
+
) -> AssertQueueReply:
|
|
70
|
+
"""Declares a queue on the broker.
|
|
71
|
+
|
|
72
|
+
Usage::
|
|
73
|
+
|
|
74
|
+
reply = await ch.assert_queue("orders", durable=True)
|
|
75
|
+
"""
|
|
76
|
+
queue = await self._ch.declare_queue(
|
|
77
|
+
name,
|
|
78
|
+
durable=durable,
|
|
79
|
+
arguments=arguments,
|
|
80
|
+
)
|
|
81
|
+
return AssertQueueReply(
|
|
82
|
+
queue=queue.name,
|
|
83
|
+
message_count=queue.declaration_result.message_count or 0,
|
|
84
|
+
consumer_count=queue.declaration_result.consumer_count or 0,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async def assert_exchange(
|
|
88
|
+
self,
|
|
89
|
+
name: str,
|
|
90
|
+
exchange_type: str = "direct",
|
|
91
|
+
*,
|
|
92
|
+
durable: bool = False,
|
|
93
|
+
) -> AssertExchangeReply:
|
|
94
|
+
"""Declares an exchange on the broker."""
|
|
95
|
+
exchange = await self._ch.declare_exchange(
|
|
96
|
+
name,
|
|
97
|
+
type=aio_pika.ExchangeType(exchange_type),
|
|
98
|
+
durable=durable,
|
|
99
|
+
)
|
|
100
|
+
return AssertExchangeReply(exchange=exchange.name)
|
|
101
|
+
|
|
102
|
+
async def bind_queue(self, queue: str, exchange: str, routing_key: str) -> None:
|
|
103
|
+
"""Binds a queue to an exchange with a routing key."""
|
|
104
|
+
queue_obj = await self._ch.get_queue(queue)
|
|
105
|
+
_ = await queue_obj.bind(exchange, routing_key)
|
|
106
|
+
|
|
107
|
+
async def publish(
|
|
108
|
+
self,
|
|
109
|
+
exchange: str,
|
|
110
|
+
routing_key: str,
|
|
111
|
+
body: bytes,
|
|
112
|
+
*,
|
|
113
|
+
content_type: str = "application/json",
|
|
114
|
+
persistent: bool = True,
|
|
115
|
+
headers: dict[str, Any] | None = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Publishes a message to an exchange with a routing key."""
|
|
118
|
+
delivery_mode = (
|
|
119
|
+
aio_pika.DeliveryMode.PERSISTENT if persistent else aio_pika.DeliveryMode.NOT_PERSISTENT
|
|
120
|
+
)
|
|
121
|
+
exchange_obj = await self._ch.get_exchange(exchange)
|
|
122
|
+
message = aio_pika.Message(
|
|
123
|
+
body=body,
|
|
124
|
+
content_type=content_type,
|
|
125
|
+
delivery_mode=delivery_mode,
|
|
126
|
+
headers=headers,
|
|
127
|
+
)
|
|
128
|
+
_ = await exchange_obj.publish(message, routing_key=routing_key)
|
|
129
|
+
|
|
130
|
+
async def send_to_queue(
|
|
131
|
+
self,
|
|
132
|
+
queue: str,
|
|
133
|
+
body: bytes,
|
|
134
|
+
*,
|
|
135
|
+
content_type: str = "application/json",
|
|
136
|
+
persistent: bool = True,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Publishes a message directly to a queue via the default exchange.
|
|
139
|
+
|
|
140
|
+
Usage::
|
|
141
|
+
|
|
142
|
+
await ch.send_to_queue("orders", b'{"id": 1}')
|
|
143
|
+
"""
|
|
144
|
+
delivery_mode = (
|
|
145
|
+
aio_pika.DeliveryMode.PERSISTENT if persistent else aio_pika.DeliveryMode.NOT_PERSISTENT
|
|
146
|
+
)
|
|
147
|
+
# WHY default exchange: publishing to "" with routing_key=queue_name
|
|
148
|
+
# is the AMQP standard way to send directly to a named queue.
|
|
149
|
+
exchange = await self._ch.get_exchange("")
|
|
150
|
+
message = aio_pika.Message(
|
|
151
|
+
body=body,
|
|
152
|
+
content_type=content_type,
|
|
153
|
+
delivery_mode=delivery_mode,
|
|
154
|
+
)
|
|
155
|
+
_ = await exchange.publish(message, routing_key=queue)
|
|
156
|
+
|
|
157
|
+
async def consume(
|
|
158
|
+
self,
|
|
159
|
+
queue: str,
|
|
160
|
+
handler: ConsumeHandler,
|
|
161
|
+
*,
|
|
162
|
+
no_ack: bool = True,
|
|
163
|
+
arguments: dict[str, Any] | None = None,
|
|
164
|
+
) -> str:
|
|
165
|
+
"""Subscribes to a queue and returns the consumer tag.
|
|
166
|
+
|
|
167
|
+
Usage::
|
|
168
|
+
|
|
169
|
+
tag = await ch.consume("orders", on_message, no_ack=True)
|
|
170
|
+
tag = await ch.consume("orders", on_message, arguments={"x-consumer-schema": proto})
|
|
171
|
+
"""
|
|
172
|
+
queue_obj = await self._ch.get_queue(queue)
|
|
173
|
+
tag = await queue_obj.consume(handler, no_ack=no_ack, arguments=arguments)
|
|
174
|
+
return tag
|
|
175
|
+
|
|
176
|
+
async def ack(self, message: AbstractIncomingMessage) -> None:
|
|
177
|
+
"""Acknowledges a message."""
|
|
178
|
+
await message.ack()
|
|
179
|
+
|
|
180
|
+
async def nack(self, message: AbstractIncomingMessage, *, requeue: bool = True) -> None:
|
|
181
|
+
"""Negative-acknowledges a message."""
|
|
182
|
+
await message.nack(requeue=requeue)
|
|
183
|
+
|
|
184
|
+
async def set_qos(self, prefetch_count: int) -> None:
|
|
185
|
+
"""Sets prefetch count (QoS) on the channel."""
|
|
186
|
+
_ = await self._ch.set_qos(prefetch_count=prefetch_count)
|
|
187
|
+
|
|
188
|
+
async def close(self) -> None:
|
|
189
|
+
"""Closes the channel."""
|
|
190
|
+
await self._ch.close()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@final
|
|
194
|
+
class AmqpConnection:
|
|
195
|
+
"""Manages the AMQP connection lifecycle.
|
|
196
|
+
|
|
197
|
+
Wraps aio-pika's ``connect_robust`` and exposes a single
|
|
198
|
+
``create_channel()`` method.
|
|
199
|
+
|
|
200
|
+
Usage::
|
|
201
|
+
|
|
202
|
+
conn = await AmqpConnection.connect("amqp://localhost")
|
|
203
|
+
ch = await conn.create_channel()
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def __init__(self, connection: aio_pika.abc.AbstractConnection) -> None:
|
|
207
|
+
self._conn = connection
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
async def connect(url: str) -> AmqpConnection:
|
|
211
|
+
"""Opens a new AMQP connection to the given URL."""
|
|
212
|
+
conn = await aio_pika.connect_robust(url)
|
|
213
|
+
return AmqpConnection(conn)
|
|
214
|
+
|
|
215
|
+
async def create_channel(self) -> AmqpChannel:
|
|
216
|
+
"""Creates a new channel on this connection."""
|
|
217
|
+
ch = await self._conn.channel()
|
|
218
|
+
return AmqpChannel(ch)
|
|
219
|
+
|
|
220
|
+
async def close(self) -> None:
|
|
221
|
+
"""Closes the underlying TCP connection."""
|
|
222
|
+
await self._conn.close()
|