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 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()