cangling-broker 0.1.11__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.
- cangling_broker-0.1.11/PKG-INFO +91 -0
- cangling_broker-0.1.11/README.md +72 -0
- cangling_broker-0.1.11/cangling_broker/__init__.py +12 -0
- cangling_broker-0.1.11/cangling_broker/client.py +354 -0
- cangling_broker-0.1.11/cangling_broker/compat.py +37 -0
- cangling_broker-0.1.11/cangling_broker/models.py +61 -0
- cangling_broker-0.1.11/cangling_broker/proto/__init__.py +1 -0
- cangling_broker-0.1.11/cangling_broker/proto/queue_pb2.py +79 -0
- cangling_broker-0.1.11/cangling_broker/proto/queue_pb2_grpc.py +364 -0
- cangling_broker-0.1.11/cangling_broker.egg-info/PKG-INFO +91 -0
- cangling_broker-0.1.11/cangling_broker.egg-info/SOURCES.txt +14 -0
- cangling_broker-0.1.11/cangling_broker.egg-info/dependency_links.txt +1 -0
- cangling_broker-0.1.11/cangling_broker.egg-info/requires.txt +7 -0
- cangling_broker-0.1.11/cangling_broker.egg-info/top_level.txt +1 -0
- cangling_broker-0.1.11/pyproject.toml +29 -0
- cangling_broker-0.1.11/setup.cfg +4 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cangling-broker
|
|
3
|
+
Version: 0.1.11
|
|
4
|
+
Summary: Python producer and consumer for cangling-broker
|
|
5
|
+
Author-email: zhangjianshe <zhangjianshe@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/zhangjianshe/cangling-broker
|
|
8
|
+
Project-URL: Repository, https://github.com/zhangjianshe/cangling-broker
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: grpcio>=1.83.0
|
|
14
|
+
Requires-Dist: protobuf>=5.28.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: grpcio-tools>=1.83.0; extra == "dev"
|
|
17
|
+
Requires-Dist: build; extra == "dev"
|
|
18
|
+
Requires-Dist: twine; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# cangling-broker (Python)
|
|
21
|
+
|
|
22
|
+
Producer and consumer for cangling-broker. Same contract as the Java client: `AcceptMessages` to publish, `Subscribe` to consume, optional `Register` metadata, `CL_BROKER_AUTH_TOKEN` on every RPC.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install cangling-broker
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
From this tree:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e python
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Use
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from cangling_broker import SatwayClient, SubscribeOptions
|
|
40
|
+
|
|
41
|
+
with SatwayClient.connect("127.0.0.1:7500", "change-me") as client:
|
|
42
|
+
client.send("cangling-test", "hello")
|
|
43
|
+
with client.subscribe(
|
|
44
|
+
SubscribeOptions(topic="cangling-test", name="worker-1"),
|
|
45
|
+
lambda message: print(message.id, message.payload),
|
|
46
|
+
):
|
|
47
|
+
...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`SatwayClient.connect(broker)` also reads `CL_BROKER_AUTH_TOKEN` from the environment.
|
|
51
|
+
|
|
52
|
+
Batch-set topic delivery (`single` = one consumer, `broadcast` = every live stream):
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from cangling_broker import TopicConfig
|
|
56
|
+
|
|
57
|
+
client.configure_topics([
|
|
58
|
+
TopicConfig("jobs", "single"),
|
|
59
|
+
TopicConfig("alerts", "broadcast"),
|
|
60
|
+
])
|
|
61
|
+
print(client.list_topics())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Existing Kafka senders can keep ``send(topic, value)`` / ``flush()``:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# from kafka import KafkaProducer
|
|
68
|
+
from cangling_broker import KafkaProducer
|
|
69
|
+
|
|
70
|
+
producer = KafkaProducer(bootstrap_servers="127.0.0.1:7500")
|
|
71
|
+
producer.send(topic, msg)
|
|
72
|
+
producer.flush()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# consume
|
|
77
|
+
python python/examples/consumer.py --broker 127.0.0.1:7500 --topic cangling-test --name py-s0 --token change-me
|
|
78
|
+
|
|
79
|
+
# produce
|
|
80
|
+
python python/examples/producer.py --broker 127.0.0.1:7500 --topic cangling-test --text hello --count 1 --token change-me
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Publish to PyPI
|
|
84
|
+
|
|
85
|
+
CI on tag `v*` builds the package and uploads with `pypa/gh-action-pypi-publish`. Set repository secret `PYPI_API_TOKEN`.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cd python
|
|
89
|
+
python generate_proto.py
|
|
90
|
+
python -m build
|
|
91
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# cangling-broker (Python)
|
|
2
|
+
|
|
3
|
+
Producer and consumer for cangling-broker. Same contract as the Java client: `AcceptMessages` to publish, `Subscribe` to consume, optional `Register` metadata, `CL_BROKER_AUTH_TOKEN` on every RPC.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install cangling-broker
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
From this tree:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e python
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Use
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from cangling_broker import SatwayClient, SubscribeOptions
|
|
21
|
+
|
|
22
|
+
with SatwayClient.connect("127.0.0.1:7500", "change-me") as client:
|
|
23
|
+
client.send("cangling-test", "hello")
|
|
24
|
+
with client.subscribe(
|
|
25
|
+
SubscribeOptions(topic="cangling-test", name="worker-1"),
|
|
26
|
+
lambda message: print(message.id, message.payload),
|
|
27
|
+
):
|
|
28
|
+
...
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`SatwayClient.connect(broker)` also reads `CL_BROKER_AUTH_TOKEN` from the environment.
|
|
32
|
+
|
|
33
|
+
Batch-set topic delivery (`single` = one consumer, `broadcast` = every live stream):
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from cangling_broker import TopicConfig
|
|
37
|
+
|
|
38
|
+
client.configure_topics([
|
|
39
|
+
TopicConfig("jobs", "single"),
|
|
40
|
+
TopicConfig("alerts", "broadcast"),
|
|
41
|
+
])
|
|
42
|
+
print(client.list_topics())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Existing Kafka senders can keep ``send(topic, value)`` / ``flush()``:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# from kafka import KafkaProducer
|
|
49
|
+
from cangling_broker import KafkaProducer
|
|
50
|
+
|
|
51
|
+
producer = KafkaProducer(bootstrap_servers="127.0.0.1:7500")
|
|
52
|
+
producer.send(topic, msg)
|
|
53
|
+
producer.flush()
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# consume
|
|
58
|
+
python python/examples/consumer.py --broker 127.0.0.1:7500 --topic cangling-test --name py-s0 --token change-me
|
|
59
|
+
|
|
60
|
+
# produce
|
|
61
|
+
python python/examples/producer.py --broker 127.0.0.1:7500 --topic cangling-test --text hello --count 1 --token change-me
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Publish to PyPI
|
|
65
|
+
|
|
66
|
+
CI on tag `v*` builds the package and uploads with `pypa/gh-action-pypi-publish`. Set repository secret `PYPI_API_TOKEN`.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cd python
|
|
70
|
+
python generate_proto.py
|
|
71
|
+
python -m build
|
|
72
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .client import SatwayClient
|
|
2
|
+
from .compat import KafkaProducer
|
|
3
|
+
from .models import SatwayMessage, SendResult, SubscribeOptions, TopicConfig
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"KafkaProducer",
|
|
7
|
+
"SatwayClient",
|
|
8
|
+
"SatwayMessage",
|
|
9
|
+
"SendResult",
|
|
10
|
+
"SubscribeOptions",
|
|
11
|
+
"TopicConfig",
|
|
12
|
+
]
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
import grpc
|
|
10
|
+
|
|
11
|
+
from .models import SatwayMessage, SendResult, SubscribeOptions, TopicConfig, auth_token_from_env
|
|
12
|
+
from .proto import queue_pb2, queue_pb2_grpc
|
|
13
|
+
|
|
14
|
+
LOG = logging.getLogger("cangling_broker")
|
|
15
|
+
|
|
16
|
+
INITIAL_BACKOFF_SECS = 0.2
|
|
17
|
+
MAX_BACKOFF_SECS = 5.0
|
|
18
|
+
RPC_DEADLINE_SECS = 15
|
|
19
|
+
RETRYABLE = {
|
|
20
|
+
grpc.StatusCode.UNAVAILABLE,
|
|
21
|
+
grpc.StatusCode.DEADLINE_EXCEEDED,
|
|
22
|
+
grpc.StatusCode.ABORTED,
|
|
23
|
+
grpc.StatusCode.UNKNOWN,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Handler = Callable[[SatwayMessage], None]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _broker_target(broker: str) -> str:
|
|
30
|
+
broker = broker.strip()
|
|
31
|
+
if broker.startswith("http://"):
|
|
32
|
+
return broker[len("http://") :]
|
|
33
|
+
if broker.startswith("https://"):
|
|
34
|
+
return broker[len("https://") :]
|
|
35
|
+
return broker
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _auth_metadata(token: str | None) -> list[tuple[str, str]] | None:
|
|
39
|
+
token = (token or "").strip()
|
|
40
|
+
if not token:
|
|
41
|
+
return None
|
|
42
|
+
if not token.lower().startswith("bearer "):
|
|
43
|
+
token = "Bearer " + token
|
|
44
|
+
return [("authorization", token)]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SatwayClient:
|
|
48
|
+
"""Broker client. Owns the gRPC channel and retries unary RPCs.
|
|
49
|
+
|
|
50
|
+
Each :meth:`subscribe` stream reopens on the same ``consumer_id`` after a drop.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, channel: grpc.Channel, metadata: list[tuple[str, str]] | None):
|
|
54
|
+
self._channel = channel
|
|
55
|
+
self._stub = queue_pb2_grpc.MessageQueueStub(channel)
|
|
56
|
+
self._metadata = metadata
|
|
57
|
+
self._open = True
|
|
58
|
+
self._consumers: list[Consumer] = []
|
|
59
|
+
self._lock = threading.Lock()
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def connect(cls, broker: str, token: str | None = None) -> SatwayClient:
|
|
63
|
+
if not broker or not broker.strip():
|
|
64
|
+
raise ValueError("broker is required")
|
|
65
|
+
if token is None:
|
|
66
|
+
token = auth_token_from_env()
|
|
67
|
+
channel = grpc.insecure_channel(_broker_target(broker))
|
|
68
|
+
return cls(channel, _auth_metadata(token))
|
|
69
|
+
|
|
70
|
+
def send(
|
|
71
|
+
self,
|
|
72
|
+
topic: str,
|
|
73
|
+
payload: str | bytes,
|
|
74
|
+
idempotency_key: str = "",
|
|
75
|
+
attributes: dict[str, str] | None = None,
|
|
76
|
+
) -> SendResult:
|
|
77
|
+
if not topic or not topic.strip():
|
|
78
|
+
raise ValueError("topic is required")
|
|
79
|
+
if payload is None or payload == "" or payload == b"":
|
|
80
|
+
raise ValueError("payload is required")
|
|
81
|
+
body = payload.encode("utf-8") if isinstance(payload, str) else payload
|
|
82
|
+
key = idempotency_key.strip() if idempotency_key else str(uuid.uuid4())
|
|
83
|
+
attrs = attributes or {}
|
|
84
|
+
|
|
85
|
+
def once() -> SendResult:
|
|
86
|
+
request = queue_pb2.AcceptMessageRequest(
|
|
87
|
+
idempotency_key=key,
|
|
88
|
+
topic=topic,
|
|
89
|
+
payload=body,
|
|
90
|
+
attributes=attrs,
|
|
91
|
+
)
|
|
92
|
+
for response in self._stub.AcceptMessages(
|
|
93
|
+
iter([request]),
|
|
94
|
+
timeout=RPC_DEADLINE_SECS,
|
|
95
|
+
metadata=self._metadata,
|
|
96
|
+
):
|
|
97
|
+
return SendResult(response.message_id, response.duplicate)
|
|
98
|
+
raise RuntimeError("publish stream closed without a response")
|
|
99
|
+
|
|
100
|
+
return self._call_with_reconnect("publish", once)
|
|
101
|
+
|
|
102
|
+
def register(
|
|
103
|
+
self,
|
|
104
|
+
topic: str,
|
|
105
|
+
name: str = "",
|
|
106
|
+
attributes: dict[str, str] | None = None,
|
|
107
|
+
consumer_id: str = "",
|
|
108
|
+
) -> str:
|
|
109
|
+
def once() -> str:
|
|
110
|
+
return self._stub.Register(
|
|
111
|
+
queue_pb2.RegisterRequest(
|
|
112
|
+
topic=topic,
|
|
113
|
+
consumer_id=consumer_id or "",
|
|
114
|
+
name=name or "",
|
|
115
|
+
attributes=attributes or {},
|
|
116
|
+
),
|
|
117
|
+
timeout=RPC_DEADLINE_SECS,
|
|
118
|
+
metadata=self._metadata,
|
|
119
|
+
).consumer_id
|
|
120
|
+
|
|
121
|
+
return self._call_with_reconnect("register", once)
|
|
122
|
+
|
|
123
|
+
def configure_topics(self, topics: list[TopicConfig]) -> list[TopicConfig]:
|
|
124
|
+
if not topics:
|
|
125
|
+
raise ValueError("topics is required")
|
|
126
|
+
|
|
127
|
+
def once() -> list[TopicConfig]:
|
|
128
|
+
response = self._stub.ConfigureTopics(
|
|
129
|
+
queue_pb2.ConfigureTopicsRequest(
|
|
130
|
+
topics=[
|
|
131
|
+
queue_pb2.TopicConfig(topic=item.topic, delivery=item.delivery)
|
|
132
|
+
for item in topics
|
|
133
|
+
]
|
|
134
|
+
),
|
|
135
|
+
timeout=RPC_DEADLINE_SECS,
|
|
136
|
+
metadata=self._metadata,
|
|
137
|
+
)
|
|
138
|
+
return [TopicConfig(topic=item.topic, delivery=item.delivery) for item in response.topics]
|
|
139
|
+
|
|
140
|
+
return self._call_with_reconnect("configure_topics", once)
|
|
141
|
+
|
|
142
|
+
def list_topics(self) -> list[TopicConfig]:
|
|
143
|
+
def once() -> list[TopicConfig]:
|
|
144
|
+
response = self._stub.ListTopics(
|
|
145
|
+
queue_pb2.ListTopicsRequest(),
|
|
146
|
+
timeout=RPC_DEADLINE_SECS,
|
|
147
|
+
metadata=self._metadata,
|
|
148
|
+
)
|
|
149
|
+
return [TopicConfig(topic=item.topic, delivery=item.delivery) for item in response.topics]
|
|
150
|
+
|
|
151
|
+
return self._call_with_reconnect("list_topics", once)
|
|
152
|
+
|
|
153
|
+
def unregister(self, consumer_id: str) -> None:
|
|
154
|
+
if not consumer_id:
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
def once() -> None:
|
|
158
|
+
self._stub.Unregister(
|
|
159
|
+
queue_pb2.UnregisterRequest(consumer_id=consumer_id),
|
|
160
|
+
timeout=RPC_DEADLINE_SECS,
|
|
161
|
+
metadata=self._metadata,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
self._call_with_reconnect("unregister", once)
|
|
165
|
+
|
|
166
|
+
def subscribe(
|
|
167
|
+
self,
|
|
168
|
+
topic: str | SubscribeOptions,
|
|
169
|
+
handler: Handler,
|
|
170
|
+
*,
|
|
171
|
+
name: str = "",
|
|
172
|
+
consumer_id: str = "",
|
|
173
|
+
attributes: dict[str, str] | None = None,
|
|
174
|
+
) -> Consumer:
|
|
175
|
+
if handler is None:
|
|
176
|
+
raise ValueError("handler is required")
|
|
177
|
+
if isinstance(topic, SubscribeOptions):
|
|
178
|
+
options = topic
|
|
179
|
+
else:
|
|
180
|
+
options = SubscribeOptions(
|
|
181
|
+
topic=topic,
|
|
182
|
+
name=name,
|
|
183
|
+
consumer_id=consumer_id,
|
|
184
|
+
attributes=attributes or {},
|
|
185
|
+
)
|
|
186
|
+
cid = options.consumer_id
|
|
187
|
+
if options.name or options.attributes or options.consumer_id:
|
|
188
|
+
cid = self.register(
|
|
189
|
+
options.topic,
|
|
190
|
+
name=options.name,
|
|
191
|
+
attributes=dict(options.attributes),
|
|
192
|
+
consumer_id=options.consumer_id,
|
|
193
|
+
)
|
|
194
|
+
consumer = Consumer(self, options, cid, handler)
|
|
195
|
+
with self._lock:
|
|
196
|
+
self._consumers.append(consumer)
|
|
197
|
+
return consumer
|
|
198
|
+
|
|
199
|
+
def close(self) -> None:
|
|
200
|
+
with self._lock:
|
|
201
|
+
if not self._open:
|
|
202
|
+
return
|
|
203
|
+
self._open = False
|
|
204
|
+
consumers = list(self._consumers)
|
|
205
|
+
for consumer in consumers:
|
|
206
|
+
consumer.close()
|
|
207
|
+
self._channel.close()
|
|
208
|
+
|
|
209
|
+
def __enter__(self) -> SatwayClient:
|
|
210
|
+
return self
|
|
211
|
+
|
|
212
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
213
|
+
self.close()
|
|
214
|
+
|
|
215
|
+
def _is_open(self) -> bool:
|
|
216
|
+
return self._open
|
|
217
|
+
|
|
218
|
+
def _ack(self, message_id: str, lease: str, success: bool, error: str = "") -> None:
|
|
219
|
+
def once() -> None:
|
|
220
|
+
self._stub.AckMessage(
|
|
221
|
+
queue_pb2.AckMessageRequest(
|
|
222
|
+
message_id=message_id,
|
|
223
|
+
lease=lease,
|
|
224
|
+
success=success,
|
|
225
|
+
error=error or "",
|
|
226
|
+
),
|
|
227
|
+
timeout=RPC_DEADLINE_SECS,
|
|
228
|
+
metadata=self._metadata,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
self._call_with_reconnect("ack", once)
|
|
232
|
+
|
|
233
|
+
def _ensure_registered(self, options: SubscribeOptions, consumer_id: str) -> None:
|
|
234
|
+
if not consumer_id:
|
|
235
|
+
return
|
|
236
|
+
self.register(
|
|
237
|
+
options.topic,
|
|
238
|
+
name=options.name,
|
|
239
|
+
attributes=dict(options.attributes),
|
|
240
|
+
consumer_id=consumer_id,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def _subscribe_stream(self, topic: str, consumer_id: str):
|
|
244
|
+
return self._stub.Subscribe(
|
|
245
|
+
queue_pb2.SubscribeRequest(topic=topic, consumer_id=consumer_id or ""),
|
|
246
|
+
metadata=self._metadata,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _call_with_reconnect(self, op: str, call):
|
|
250
|
+
backoff = INITIAL_BACKOFF_SECS
|
|
251
|
+
while True:
|
|
252
|
+
if not self._open:
|
|
253
|
+
raise RuntimeError("client closed")
|
|
254
|
+
try:
|
|
255
|
+
return call()
|
|
256
|
+
except grpc.RpcError as error:
|
|
257
|
+
if not self._open:
|
|
258
|
+
raise RuntimeError("client closed") from error
|
|
259
|
+
if error.code() not in RETRYABLE:
|
|
260
|
+
raise RuntimeError(f"{op} failed: {error.code()}: {error.details()}") from error
|
|
261
|
+
LOG.warning("%s failed, reconnecting: %s", op, error.details() or error.code())
|
|
262
|
+
time.sleep(backoff)
|
|
263
|
+
backoff = min(backoff * 2, MAX_BACKOFF_SECS)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class Consumer:
|
|
267
|
+
def __init__(
|
|
268
|
+
self,
|
|
269
|
+
client: SatwayClient,
|
|
270
|
+
options: SubscribeOptions,
|
|
271
|
+
consumer_id: str,
|
|
272
|
+
handler: Handler,
|
|
273
|
+
):
|
|
274
|
+
self._client = client
|
|
275
|
+
self._closed = False
|
|
276
|
+
self.consumer_id = consumer_id
|
|
277
|
+
self._thread = threading.Thread(
|
|
278
|
+
target=self._run,
|
|
279
|
+
args=(options, handler),
|
|
280
|
+
name="cangling-subscribe",
|
|
281
|
+
daemon=True,
|
|
282
|
+
)
|
|
283
|
+
self._thread.start()
|
|
284
|
+
|
|
285
|
+
def close(self) -> None:
|
|
286
|
+
self._closed = True
|
|
287
|
+
|
|
288
|
+
def __enter__(self) -> Consumer:
|
|
289
|
+
return self
|
|
290
|
+
|
|
291
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
292
|
+
self.close()
|
|
293
|
+
|
|
294
|
+
def _running(self) -> bool:
|
|
295
|
+
return not self._closed and self._client._is_open()
|
|
296
|
+
|
|
297
|
+
def _run(self, options: SubscribeOptions, handler: Handler) -> None:
|
|
298
|
+
backoff = INITIAL_BACKOFF_SECS
|
|
299
|
+
while self._running():
|
|
300
|
+
try:
|
|
301
|
+
self._client._ensure_registered(options, self.consumer_id)
|
|
302
|
+
stream = self._client._subscribe_stream(options.topic, self.consumer_id)
|
|
303
|
+
backoff = INITIAL_BACKOFF_SECS
|
|
304
|
+
for incoming in stream:
|
|
305
|
+
if not self._running():
|
|
306
|
+
return
|
|
307
|
+
message = _to_message(incoming)
|
|
308
|
+
try:
|
|
309
|
+
handler(message)
|
|
310
|
+
self._client._ack(incoming.message_id, incoming.lease, True, "")
|
|
311
|
+
except Exception as error:
|
|
312
|
+
LOG.warning("handler failed", exc_info=error)
|
|
313
|
+
self._client._ack(
|
|
314
|
+
incoming.message_id,
|
|
315
|
+
incoming.lease,
|
|
316
|
+
False,
|
|
317
|
+
str(error) or "handler failed",
|
|
318
|
+
)
|
|
319
|
+
if self._running():
|
|
320
|
+
LOG.info("subscribe stream ended, reconnecting")
|
|
321
|
+
except grpc.RpcError as error:
|
|
322
|
+
if self._running():
|
|
323
|
+
LOG.warning("subscribe stream closed, reconnecting: %s", error.details() or error.code())
|
|
324
|
+
else:
|
|
325
|
+
return
|
|
326
|
+
except Exception as error:
|
|
327
|
+
if self._running():
|
|
328
|
+
LOG.warning("subscribe failed, reconnecting: %s", error)
|
|
329
|
+
else:
|
|
330
|
+
return
|
|
331
|
+
if self._running():
|
|
332
|
+
time.sleep(backoff)
|
|
333
|
+
backoff = min(backoff * 2, MAX_BACKOFF_SECS)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _to_message(incoming: queue_pb2.SatwayMessage) -> SatwayMessage:
|
|
337
|
+
raw = incoming.payload
|
|
338
|
+
try:
|
|
339
|
+
payload = raw.decode("utf-8")
|
|
340
|
+
encoding = "utf-8"
|
|
341
|
+
except UnicodeDecodeError:
|
|
342
|
+
import base64
|
|
343
|
+
|
|
344
|
+
payload = base64.b64encode(raw).decode("ascii")
|
|
345
|
+
encoding = "base64"
|
|
346
|
+
return SatwayMessage(
|
|
347
|
+
id=incoming.message_id,
|
|
348
|
+
topic=incoming.topic,
|
|
349
|
+
payload=payload,
|
|
350
|
+
payload_encoding=encoding,
|
|
351
|
+
attributes=dict(incoming.attributes),
|
|
352
|
+
created_at=incoming.created_at,
|
|
353
|
+
lease=incoming.lease,
|
|
354
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""kafka-python shaped helpers so existing senders can target this broker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .client import SatwayClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class KafkaProducer:
|
|
9
|
+
"""Stand-in for ``kafka.KafkaProducer``.
|
|
10
|
+
|
|
11
|
+
Keep calling ``send(topic, value)`` and ``flush()``. ``bootstrap_servers``
|
|
12
|
+
is the cangling-broker (``host:port``). Extra Kafka kwargs are ignored.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
bootstrap_servers="",
|
|
18
|
+
token=None,
|
|
19
|
+
value_serializer=None,
|
|
20
|
+
**_ignored,
|
|
21
|
+
):
|
|
22
|
+
if isinstance(bootstrap_servers, (list, tuple)):
|
|
23
|
+
if not bootstrap_servers:
|
|
24
|
+
raise ValueError("bootstrap_servers is required")
|
|
25
|
+
bootstrap_servers = bootstrap_servers[0]
|
|
26
|
+
self._client = SatwayClient.connect(str(bootstrap_servers), token)
|
|
27
|
+
self._value_serializer = value_serializer
|
|
28
|
+
|
|
29
|
+
def send(self, topic, value=None, key=None, headers=None, partition=None, timestamp_ms=None):
|
|
30
|
+
payload = self._value_serializer(value) if self._value_serializer is not None else value
|
|
31
|
+
return self._client.send(topic, payload)
|
|
32
|
+
|
|
33
|
+
def flush(self, timeout=None):
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def close(self, timeout=None):
|
|
37
|
+
self._client.close()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
import os
|
|
5
|
+
from typing import Mapping
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def auth_token_from_env() -> str:
|
|
9
|
+
return (os.environ.get("CL_BROKER_AUTH_TOKEN") or "").strip()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class SendResult:
|
|
14
|
+
message_id: str
|
|
15
|
+
duplicate: bool = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class SatwayMessage:
|
|
20
|
+
id: str
|
|
21
|
+
topic: str
|
|
22
|
+
payload: str
|
|
23
|
+
payload_encoding: str = "utf-8"
|
|
24
|
+
attributes: Mapping[str, str] = field(default_factory=dict)
|
|
25
|
+
created_at: str = ""
|
|
26
|
+
lease: str = ""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class TopicConfig:
|
|
31
|
+
topic: str
|
|
32
|
+
delivery: str = "single"
|
|
33
|
+
|
|
34
|
+
def __post_init__(self) -> None:
|
|
35
|
+
if not self.topic or not self.topic.strip():
|
|
36
|
+
raise ValueError("topic is required")
|
|
37
|
+
delivery = (self.delivery or "single").strip().lower()
|
|
38
|
+
if delivery not in {"single", "broadcast", "queue", "competing", "fanout", "pubsub"}:
|
|
39
|
+
raise ValueError("delivery must be single or broadcast")
|
|
40
|
+
if delivery in {"queue", "competing"}:
|
|
41
|
+
delivery = "single"
|
|
42
|
+
if delivery in {"fanout", "pubsub"}:
|
|
43
|
+
delivery = "broadcast"
|
|
44
|
+
object.__setattr__(self, "topic", self.topic.strip())
|
|
45
|
+
object.__setattr__(self, "delivery", delivery)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class SubscribeOptions:
|
|
50
|
+
topic: str
|
|
51
|
+
consumer_id: str = ""
|
|
52
|
+
name: str = ""
|
|
53
|
+
attributes: Mapping[str, str] = field(default_factory=dict)
|
|
54
|
+
|
|
55
|
+
def __post_init__(self) -> None:
|
|
56
|
+
if not self.topic or not self.topic.strip():
|
|
57
|
+
raise ValueError("topic is required")
|
|
58
|
+
object.__setattr__(self, "topic", self.topic.strip())
|
|
59
|
+
object.__setattr__(self, "consumer_id", self.consumer_id or "")
|
|
60
|
+
object.__setattr__(self, "name", self.name or "")
|
|
61
|
+
object.__setattr__(self, "attributes", dict(self.attributes or {}))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Generated gRPC stubs live in this package.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# NO CHECKED-IN PROTOBUF GENCODE
|
|
4
|
+
# source: queue.proto
|
|
5
|
+
# Protobuf Python Version: 7.35.1
|
|
6
|
+
"""Generated protocol buffer code."""
|
|
7
|
+
from google.protobuf import descriptor as _descriptor
|
|
8
|
+
from google.protobuf import descriptor_pool as _descriptor_pool
|
|
9
|
+
from google.protobuf import runtime_version as _runtime_version
|
|
10
|
+
from google.protobuf import symbol_database as _symbol_database
|
|
11
|
+
from google.protobuf.internal import builder as _builder
|
|
12
|
+
_runtime_version.ValidateProtobufRuntimeVersion(
|
|
13
|
+
_runtime_version.Domain.PUBLIC,
|
|
14
|
+
7,
|
|
15
|
+
35,
|
|
16
|
+
1,
|
|
17
|
+
'',
|
|
18
|
+
'queue.proto'
|
|
19
|
+
)
|
|
20
|
+
# @@protoc_insertion_point(imports)
|
|
21
|
+
|
|
22
|
+
_sym_db = _symbol_database.Default()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0bqueue.proto\x12\rdispatcher.v1\"\xcb\x01\n\x14\x41\x63\x63\x65ptMessageRequest\x12\x17\n\x0fidempotency_key\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12\x0f\n\x07payload\x18\x03 \x01(\x0c\x12G\n\nattributes\x18\x04 \x03(\x0b\x32\x33.dispatcher.v1.AcceptMessageRequest.AttributesEntry\x1a\x31\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\">\n\x15\x41\x63\x63\x65ptMessageResponse\x12\x12\n\nmessage_id\x18\x01 \x01(\t\x12\x11\n\tduplicate\x18\x02 \x01(\x08\"\xba\x01\n\x0fRegisterRequest\x12\r\n\x05topic\x18\x01 \x01(\t\x12\x13\n\x0b\x63onsumer_id\x18\x02 \x01(\t\x12\x0c\n\x04name\x18\x03 \x01(\t\x12\x42\n\nattributes\x18\x04 \x03(\x0b\x32..dispatcher.v1.RegisterRequest.AttributesEntry\x1a\x31\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\'\n\x10RegisterResponse\x12\x13\n\x0b\x63onsumer_id\x18\x01 \x01(\t\"(\n\x11UnregisterRequest\x12\x13\n\x0b\x63onsumer_id\x18\x01 \x01(\t\"\x14\n\x12UnregisterResponse\"6\n\x10SubscribeRequest\x12\r\n\x05topic\x18\x01 \x01(\t\x12\x13\n\x0b\x63onsumer_id\x18\x02 \x01(\t\"\xdb\x01\n\rSatwayMessage\x12\x12\n\nmessage_id\x18\x01 \x01(\t\x12\r\n\x05topic\x18\x02 \x01(\t\x12\x0f\n\x07payload\x18\x03 \x01(\x0c\x12@\n\nattributes\x18\x04 \x03(\x0b\x32,.dispatcher.v1.SatwayMessage.AttributesEntry\x12\x12\n\ncreated_at\x18\x05 \x01(\t\x12\r\n\x05lease\x18\x06 \x01(\t\x1a\x31\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"V\n\x11\x41\x63kMessageRequest\x12\x12\n\nmessage_id\x18\x01 \x01(\t\x12\r\n\x05lease\x18\x02 \x01(\t\x12\x0f\n\x07success\x18\x03 \x01(\x08\x12\r\n\x05\x65rror\x18\x04 \x01(\t\"&\n\x12\x41\x63kMessageResponse\x12\x10\n\x08\x61\x63\x63\x65pted\x18\x01 \x01(\x08\".\n\x0bTopicConfig\x12\r\n\x05topic\x18\x01 \x01(\t\x12\x10\n\x08\x64\x65livery\x18\x02 \x01(\t\"D\n\x16\x43onfigureTopicsRequest\x12*\n\x06topics\x18\x01 \x03(\x0b\x32\x1a.dispatcher.v1.TopicConfig\"E\n\x17\x43onfigureTopicsResponse\x12*\n\x06topics\x18\x01 \x03(\x0b\x32\x1a.dispatcher.v1.TopicConfig\"\x13\n\x11ListTopicsRequest\"@\n\x12ListTopicsResponse\x12*\n\x06topics\x18\x01 \x03(\x0b\x32\x1a.dispatcher.v1.TopicConfig2\xe5\x04\n\x0cMessageQueue\x12_\n\x0e\x41\x63\x63\x65ptMessages\x12#.dispatcher.v1.AcceptMessageRequest\x1a$.dispatcher.v1.AcceptMessageResponse(\x01\x30\x01\x12K\n\x08Register\x12\x1e.dispatcher.v1.RegisterRequest\x1a\x1f.dispatcher.v1.RegisterResponse\x12Q\n\nUnregister\x12 .dispatcher.v1.UnregisterRequest\x1a!.dispatcher.v1.UnregisterResponse\x12L\n\tSubscribe\x12\x1f.dispatcher.v1.SubscribeRequest\x1a\x1c.dispatcher.v1.SatwayMessage0\x01\x12Q\n\nAckMessage\x12 .dispatcher.v1.AckMessageRequest\x1a!.dispatcher.v1.AckMessageResponse\x12`\n\x0f\x43onfigureTopics\x12%.dispatcher.v1.ConfigureTopicsRequest\x1a&.dispatcher.v1.ConfigureTopicsResponse\x12Q\n\nListTopics\x12 .dispatcher.v1.ListTopicsRequest\x1a!.dispatcher.v1.ListTopicsResponseB&\n\x16\x63n.mapway.broker.protoB\nQueueProtoP\x01\x62\x06proto3')
|
|
28
|
+
|
|
29
|
+
_globals = globals()
|
|
30
|
+
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
|
31
|
+
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'queue_pb2', _globals)
|
|
32
|
+
if not _descriptor._USE_C_DESCRIPTORS:
|
|
33
|
+
_globals['DESCRIPTOR']._loaded_options = None
|
|
34
|
+
_globals['DESCRIPTOR']._serialized_options = b'\n\026cn.mapway.broker.protoB\nQueueProtoP\001'
|
|
35
|
+
_globals['_ACCEPTMESSAGEREQUEST_ATTRIBUTESENTRY']._loaded_options = None
|
|
36
|
+
_globals['_ACCEPTMESSAGEREQUEST_ATTRIBUTESENTRY']._serialized_options = b'8\001'
|
|
37
|
+
_globals['_REGISTERREQUEST_ATTRIBUTESENTRY']._loaded_options = None
|
|
38
|
+
_globals['_REGISTERREQUEST_ATTRIBUTESENTRY']._serialized_options = b'8\001'
|
|
39
|
+
_globals['_SATWAYMESSAGE_ATTRIBUTESENTRY']._loaded_options = None
|
|
40
|
+
_globals['_SATWAYMESSAGE_ATTRIBUTESENTRY']._serialized_options = b'8\001'
|
|
41
|
+
_globals['_ACCEPTMESSAGEREQUEST']._serialized_start=31
|
|
42
|
+
_globals['_ACCEPTMESSAGEREQUEST']._serialized_end=234
|
|
43
|
+
_globals['_ACCEPTMESSAGEREQUEST_ATTRIBUTESENTRY']._serialized_start=185
|
|
44
|
+
_globals['_ACCEPTMESSAGEREQUEST_ATTRIBUTESENTRY']._serialized_end=234
|
|
45
|
+
_globals['_ACCEPTMESSAGERESPONSE']._serialized_start=236
|
|
46
|
+
_globals['_ACCEPTMESSAGERESPONSE']._serialized_end=298
|
|
47
|
+
_globals['_REGISTERREQUEST']._serialized_start=301
|
|
48
|
+
_globals['_REGISTERREQUEST']._serialized_end=487
|
|
49
|
+
_globals['_REGISTERREQUEST_ATTRIBUTESENTRY']._serialized_start=185
|
|
50
|
+
_globals['_REGISTERREQUEST_ATTRIBUTESENTRY']._serialized_end=234
|
|
51
|
+
_globals['_REGISTERRESPONSE']._serialized_start=489
|
|
52
|
+
_globals['_REGISTERRESPONSE']._serialized_end=528
|
|
53
|
+
_globals['_UNREGISTERREQUEST']._serialized_start=530
|
|
54
|
+
_globals['_UNREGISTERREQUEST']._serialized_end=570
|
|
55
|
+
_globals['_UNREGISTERRESPONSE']._serialized_start=572
|
|
56
|
+
_globals['_UNREGISTERRESPONSE']._serialized_end=592
|
|
57
|
+
_globals['_SUBSCRIBEREQUEST']._serialized_start=594
|
|
58
|
+
_globals['_SUBSCRIBEREQUEST']._serialized_end=648
|
|
59
|
+
_globals['_SATWAYMESSAGE']._serialized_start=651
|
|
60
|
+
_globals['_SATWAYMESSAGE']._serialized_end=870
|
|
61
|
+
_globals['_SATWAYMESSAGE_ATTRIBUTESENTRY']._serialized_start=185
|
|
62
|
+
_globals['_SATWAYMESSAGE_ATTRIBUTESENTRY']._serialized_end=234
|
|
63
|
+
_globals['_ACKMESSAGEREQUEST']._serialized_start=872
|
|
64
|
+
_globals['_ACKMESSAGEREQUEST']._serialized_end=958
|
|
65
|
+
_globals['_ACKMESSAGERESPONSE']._serialized_start=960
|
|
66
|
+
_globals['_ACKMESSAGERESPONSE']._serialized_end=998
|
|
67
|
+
_globals['_TOPICCONFIG']._serialized_start=1000
|
|
68
|
+
_globals['_TOPICCONFIG']._serialized_end=1046
|
|
69
|
+
_globals['_CONFIGURETOPICSREQUEST']._serialized_start=1048
|
|
70
|
+
_globals['_CONFIGURETOPICSREQUEST']._serialized_end=1116
|
|
71
|
+
_globals['_CONFIGURETOPICSRESPONSE']._serialized_start=1118
|
|
72
|
+
_globals['_CONFIGURETOPICSRESPONSE']._serialized_end=1187
|
|
73
|
+
_globals['_LISTTOPICSREQUEST']._serialized_start=1189
|
|
74
|
+
_globals['_LISTTOPICSREQUEST']._serialized_end=1208
|
|
75
|
+
_globals['_LISTTOPICSRESPONSE']._serialized_start=1210
|
|
76
|
+
_globals['_LISTTOPICSRESPONSE']._serialized_end=1274
|
|
77
|
+
_globals['_MESSAGEQUEUE']._serialized_start=1277
|
|
78
|
+
_globals['_MESSAGEQUEUE']._serialized_end=1890
|
|
79
|
+
# @@protoc_insertion_point(module_scope)
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
|
2
|
+
"""Client and server classes corresponding to protobuf-defined services."""
|
|
3
|
+
import grpc
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
from . import queue_pb2 as queue__pb2
|
|
7
|
+
|
|
8
|
+
GRPC_GENERATED_VERSION = '1.83.0'
|
|
9
|
+
GRPC_VERSION = grpc.__version__
|
|
10
|
+
_version_not_supported = False
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from grpc._utilities import first_version_is_lower
|
|
14
|
+
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
|
15
|
+
except ImportError:
|
|
16
|
+
_version_not_supported = True
|
|
17
|
+
|
|
18
|
+
if _version_not_supported:
|
|
19
|
+
raise RuntimeError(
|
|
20
|
+
f'The grpc package installed is at version {GRPC_VERSION},'
|
|
21
|
+
+ ' but the generated code in queue_pb2_grpc.py depends on'
|
|
22
|
+
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
|
23
|
+
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
|
24
|
+
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MessageQueueStub:
|
|
29
|
+
"""Missing associated documentation comment in .proto file."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, channel):
|
|
32
|
+
"""Constructor.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
channel: A grpc.Channel.
|
|
36
|
+
"""
|
|
37
|
+
self.AcceptMessages = channel.stream_stream(
|
|
38
|
+
'/dispatcher.v1.MessageQueue/AcceptMessages',
|
|
39
|
+
request_serializer=queue__pb2.AcceptMessageRequest.SerializeToString,
|
|
40
|
+
response_deserializer=queue__pb2.AcceptMessageResponse.FromString,
|
|
41
|
+
_registered_method=True)
|
|
42
|
+
self.Register = channel.unary_unary(
|
|
43
|
+
'/dispatcher.v1.MessageQueue/Register',
|
|
44
|
+
request_serializer=queue__pb2.RegisterRequest.SerializeToString,
|
|
45
|
+
response_deserializer=queue__pb2.RegisterResponse.FromString,
|
|
46
|
+
_registered_method=True)
|
|
47
|
+
self.Unregister = channel.unary_unary(
|
|
48
|
+
'/dispatcher.v1.MessageQueue/Unregister',
|
|
49
|
+
request_serializer=queue__pb2.UnregisterRequest.SerializeToString,
|
|
50
|
+
response_deserializer=queue__pb2.UnregisterResponse.FromString,
|
|
51
|
+
_registered_method=True)
|
|
52
|
+
self.Subscribe = channel.unary_stream(
|
|
53
|
+
'/dispatcher.v1.MessageQueue/Subscribe',
|
|
54
|
+
request_serializer=queue__pb2.SubscribeRequest.SerializeToString,
|
|
55
|
+
response_deserializer=queue__pb2.SatwayMessage.FromString,
|
|
56
|
+
_registered_method=True)
|
|
57
|
+
self.AckMessage = channel.unary_unary(
|
|
58
|
+
'/dispatcher.v1.MessageQueue/AckMessage',
|
|
59
|
+
request_serializer=queue__pb2.AckMessageRequest.SerializeToString,
|
|
60
|
+
response_deserializer=queue__pb2.AckMessageResponse.FromString,
|
|
61
|
+
_registered_method=True)
|
|
62
|
+
self.ConfigureTopics = channel.unary_unary(
|
|
63
|
+
'/dispatcher.v1.MessageQueue/ConfigureTopics',
|
|
64
|
+
request_serializer=queue__pb2.ConfigureTopicsRequest.SerializeToString,
|
|
65
|
+
response_deserializer=queue__pb2.ConfigureTopicsResponse.FromString,
|
|
66
|
+
_registered_method=True)
|
|
67
|
+
self.ListTopics = channel.unary_unary(
|
|
68
|
+
'/dispatcher.v1.MessageQueue/ListTopics',
|
|
69
|
+
request_serializer=queue__pb2.ListTopicsRequest.SerializeToString,
|
|
70
|
+
response_deserializer=queue__pb2.ListTopicsResponse.FromString,
|
|
71
|
+
_registered_method=True)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MessageQueueServicer:
|
|
75
|
+
"""Missing associated documentation comment in .proto file."""
|
|
76
|
+
|
|
77
|
+
def AcceptMessages(self, request_iterator, context):
|
|
78
|
+
"""Client streams publishes; server streams one response per request after SQLite commit.
|
|
79
|
+
"""
|
|
80
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
81
|
+
context.set_details('Method not implemented!')
|
|
82
|
+
raise NotImplementedError('Method not implemented!')
|
|
83
|
+
|
|
84
|
+
def Register(self, request, context):
|
|
85
|
+
"""Optional metadata about a consumer. Delivery is on Subscribe, not this call.
|
|
86
|
+
"""
|
|
87
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
88
|
+
context.set_details('Method not implemented!')
|
|
89
|
+
raise NotImplementedError('Method not implemented!')
|
|
90
|
+
|
|
91
|
+
def Unregister(self, request, context):
|
|
92
|
+
"""Unregister drops stored consumer metadata.
|
|
93
|
+
"""
|
|
94
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
95
|
+
context.set_details('Method not implemented!')
|
|
96
|
+
raise NotImplementedError('Method not implemented!')
|
|
97
|
+
|
|
98
|
+
def Subscribe(self, request, context):
|
|
99
|
+
"""Open a consume stream. Multiple subscribers on one topic compete:
|
|
100
|
+
each message is sent to exactly one of them.
|
|
101
|
+
"""
|
|
102
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
103
|
+
context.set_details('Method not implemented!')
|
|
104
|
+
raise NotImplementedError('Method not implemented!')
|
|
105
|
+
|
|
106
|
+
def AckMessage(self, request, context):
|
|
107
|
+
"""Confirm or reject a message received from Subscribe.
|
|
108
|
+
"""
|
|
109
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
110
|
+
context.set_details('Method not implemented!')
|
|
111
|
+
raise NotImplementedError('Method not implemented!')
|
|
112
|
+
|
|
113
|
+
def ConfigureTopics(self, request, context):
|
|
114
|
+
"""Create or replace delivery mode for many topics at once.
|
|
115
|
+
delivery is "single" (competing consumers, default) or "broadcast" (every live stream).
|
|
116
|
+
"""
|
|
117
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
118
|
+
context.set_details('Method not implemented!')
|
|
119
|
+
raise NotImplementedError('Method not implemented!')
|
|
120
|
+
|
|
121
|
+
def ListTopics(self, request, context):
|
|
122
|
+
"""List stored topic delivery configs.
|
|
123
|
+
"""
|
|
124
|
+
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
|
125
|
+
context.set_details('Method not implemented!')
|
|
126
|
+
raise NotImplementedError('Method not implemented!')
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def add_MessageQueueServicer_to_server(servicer, server):
|
|
130
|
+
rpc_method_handlers = {
|
|
131
|
+
'AcceptMessages': grpc.stream_stream_rpc_method_handler(
|
|
132
|
+
servicer.AcceptMessages,
|
|
133
|
+
request_deserializer=queue__pb2.AcceptMessageRequest.FromString,
|
|
134
|
+
response_serializer=queue__pb2.AcceptMessageResponse.SerializeToString,
|
|
135
|
+
),
|
|
136
|
+
'Register': grpc.unary_unary_rpc_method_handler(
|
|
137
|
+
servicer.Register,
|
|
138
|
+
request_deserializer=queue__pb2.RegisterRequest.FromString,
|
|
139
|
+
response_serializer=queue__pb2.RegisterResponse.SerializeToString,
|
|
140
|
+
),
|
|
141
|
+
'Unregister': grpc.unary_unary_rpc_method_handler(
|
|
142
|
+
servicer.Unregister,
|
|
143
|
+
request_deserializer=queue__pb2.UnregisterRequest.FromString,
|
|
144
|
+
response_serializer=queue__pb2.UnregisterResponse.SerializeToString,
|
|
145
|
+
),
|
|
146
|
+
'Subscribe': grpc.unary_stream_rpc_method_handler(
|
|
147
|
+
servicer.Subscribe,
|
|
148
|
+
request_deserializer=queue__pb2.SubscribeRequest.FromString,
|
|
149
|
+
response_serializer=queue__pb2.SatwayMessage.SerializeToString,
|
|
150
|
+
),
|
|
151
|
+
'AckMessage': grpc.unary_unary_rpc_method_handler(
|
|
152
|
+
servicer.AckMessage,
|
|
153
|
+
request_deserializer=queue__pb2.AckMessageRequest.FromString,
|
|
154
|
+
response_serializer=queue__pb2.AckMessageResponse.SerializeToString,
|
|
155
|
+
),
|
|
156
|
+
'ConfigureTopics': grpc.unary_unary_rpc_method_handler(
|
|
157
|
+
servicer.ConfigureTopics,
|
|
158
|
+
request_deserializer=queue__pb2.ConfigureTopicsRequest.FromString,
|
|
159
|
+
response_serializer=queue__pb2.ConfigureTopicsResponse.SerializeToString,
|
|
160
|
+
),
|
|
161
|
+
'ListTopics': grpc.unary_unary_rpc_method_handler(
|
|
162
|
+
servicer.ListTopics,
|
|
163
|
+
request_deserializer=queue__pb2.ListTopicsRequest.FromString,
|
|
164
|
+
response_serializer=queue__pb2.ListTopicsResponse.SerializeToString,
|
|
165
|
+
),
|
|
166
|
+
}
|
|
167
|
+
generic_handler = grpc.method_handlers_generic_handler(
|
|
168
|
+
'dispatcher.v1.MessageQueue', rpc_method_handlers)
|
|
169
|
+
server.add_generic_rpc_handlers((generic_handler,))
|
|
170
|
+
server.add_registered_method_handlers('dispatcher.v1.MessageQueue', rpc_method_handlers)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# This class is part of an EXPERIMENTAL API.
|
|
174
|
+
class MessageQueue:
|
|
175
|
+
"""Missing associated documentation comment in .proto file."""
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def AcceptMessages(request_iterator,
|
|
179
|
+
target,
|
|
180
|
+
options=(),
|
|
181
|
+
channel_credentials=None,
|
|
182
|
+
call_credentials=None,
|
|
183
|
+
insecure=False,
|
|
184
|
+
compression=None,
|
|
185
|
+
wait_for_ready=None,
|
|
186
|
+
timeout=None,
|
|
187
|
+
metadata=None):
|
|
188
|
+
return grpc.experimental.stream_stream(
|
|
189
|
+
request_iterator,
|
|
190
|
+
target,
|
|
191
|
+
'/dispatcher.v1.MessageQueue/AcceptMessages',
|
|
192
|
+
queue__pb2.AcceptMessageRequest.SerializeToString,
|
|
193
|
+
queue__pb2.AcceptMessageResponse.FromString,
|
|
194
|
+
options,
|
|
195
|
+
channel_credentials,
|
|
196
|
+
insecure,
|
|
197
|
+
call_credentials,
|
|
198
|
+
compression,
|
|
199
|
+
wait_for_ready,
|
|
200
|
+
timeout,
|
|
201
|
+
metadata,
|
|
202
|
+
_registered_method=True)
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def Register(request,
|
|
206
|
+
target,
|
|
207
|
+
options=(),
|
|
208
|
+
channel_credentials=None,
|
|
209
|
+
call_credentials=None,
|
|
210
|
+
insecure=False,
|
|
211
|
+
compression=None,
|
|
212
|
+
wait_for_ready=None,
|
|
213
|
+
timeout=None,
|
|
214
|
+
metadata=None):
|
|
215
|
+
return grpc.experimental.unary_unary(
|
|
216
|
+
request,
|
|
217
|
+
target,
|
|
218
|
+
'/dispatcher.v1.MessageQueue/Register',
|
|
219
|
+
queue__pb2.RegisterRequest.SerializeToString,
|
|
220
|
+
queue__pb2.RegisterResponse.FromString,
|
|
221
|
+
options,
|
|
222
|
+
channel_credentials,
|
|
223
|
+
insecure,
|
|
224
|
+
call_credentials,
|
|
225
|
+
compression,
|
|
226
|
+
wait_for_ready,
|
|
227
|
+
timeout,
|
|
228
|
+
metadata,
|
|
229
|
+
_registered_method=True)
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def Unregister(request,
|
|
233
|
+
target,
|
|
234
|
+
options=(),
|
|
235
|
+
channel_credentials=None,
|
|
236
|
+
call_credentials=None,
|
|
237
|
+
insecure=False,
|
|
238
|
+
compression=None,
|
|
239
|
+
wait_for_ready=None,
|
|
240
|
+
timeout=None,
|
|
241
|
+
metadata=None):
|
|
242
|
+
return grpc.experimental.unary_unary(
|
|
243
|
+
request,
|
|
244
|
+
target,
|
|
245
|
+
'/dispatcher.v1.MessageQueue/Unregister',
|
|
246
|
+
queue__pb2.UnregisterRequest.SerializeToString,
|
|
247
|
+
queue__pb2.UnregisterResponse.FromString,
|
|
248
|
+
options,
|
|
249
|
+
channel_credentials,
|
|
250
|
+
insecure,
|
|
251
|
+
call_credentials,
|
|
252
|
+
compression,
|
|
253
|
+
wait_for_ready,
|
|
254
|
+
timeout,
|
|
255
|
+
metadata,
|
|
256
|
+
_registered_method=True)
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def Subscribe(request,
|
|
260
|
+
target,
|
|
261
|
+
options=(),
|
|
262
|
+
channel_credentials=None,
|
|
263
|
+
call_credentials=None,
|
|
264
|
+
insecure=False,
|
|
265
|
+
compression=None,
|
|
266
|
+
wait_for_ready=None,
|
|
267
|
+
timeout=None,
|
|
268
|
+
metadata=None):
|
|
269
|
+
return grpc.experimental.unary_stream(
|
|
270
|
+
request,
|
|
271
|
+
target,
|
|
272
|
+
'/dispatcher.v1.MessageQueue/Subscribe',
|
|
273
|
+
queue__pb2.SubscribeRequest.SerializeToString,
|
|
274
|
+
queue__pb2.SatwayMessage.FromString,
|
|
275
|
+
options,
|
|
276
|
+
channel_credentials,
|
|
277
|
+
insecure,
|
|
278
|
+
call_credentials,
|
|
279
|
+
compression,
|
|
280
|
+
wait_for_ready,
|
|
281
|
+
timeout,
|
|
282
|
+
metadata,
|
|
283
|
+
_registered_method=True)
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def AckMessage(request,
|
|
287
|
+
target,
|
|
288
|
+
options=(),
|
|
289
|
+
channel_credentials=None,
|
|
290
|
+
call_credentials=None,
|
|
291
|
+
insecure=False,
|
|
292
|
+
compression=None,
|
|
293
|
+
wait_for_ready=None,
|
|
294
|
+
timeout=None,
|
|
295
|
+
metadata=None):
|
|
296
|
+
return grpc.experimental.unary_unary(
|
|
297
|
+
request,
|
|
298
|
+
target,
|
|
299
|
+
'/dispatcher.v1.MessageQueue/AckMessage',
|
|
300
|
+
queue__pb2.AckMessageRequest.SerializeToString,
|
|
301
|
+
queue__pb2.AckMessageResponse.FromString,
|
|
302
|
+
options,
|
|
303
|
+
channel_credentials,
|
|
304
|
+
insecure,
|
|
305
|
+
call_credentials,
|
|
306
|
+
compression,
|
|
307
|
+
wait_for_ready,
|
|
308
|
+
timeout,
|
|
309
|
+
metadata,
|
|
310
|
+
_registered_method=True)
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def ConfigureTopics(request,
|
|
314
|
+
target,
|
|
315
|
+
options=(),
|
|
316
|
+
channel_credentials=None,
|
|
317
|
+
call_credentials=None,
|
|
318
|
+
insecure=False,
|
|
319
|
+
compression=None,
|
|
320
|
+
wait_for_ready=None,
|
|
321
|
+
timeout=None,
|
|
322
|
+
metadata=None):
|
|
323
|
+
return grpc.experimental.unary_unary(
|
|
324
|
+
request,
|
|
325
|
+
target,
|
|
326
|
+
'/dispatcher.v1.MessageQueue/ConfigureTopics',
|
|
327
|
+
queue__pb2.ConfigureTopicsRequest.SerializeToString,
|
|
328
|
+
queue__pb2.ConfigureTopicsResponse.FromString,
|
|
329
|
+
options,
|
|
330
|
+
channel_credentials,
|
|
331
|
+
insecure,
|
|
332
|
+
call_credentials,
|
|
333
|
+
compression,
|
|
334
|
+
wait_for_ready,
|
|
335
|
+
timeout,
|
|
336
|
+
metadata,
|
|
337
|
+
_registered_method=True)
|
|
338
|
+
|
|
339
|
+
@staticmethod
|
|
340
|
+
def ListTopics(request,
|
|
341
|
+
target,
|
|
342
|
+
options=(),
|
|
343
|
+
channel_credentials=None,
|
|
344
|
+
call_credentials=None,
|
|
345
|
+
insecure=False,
|
|
346
|
+
compression=None,
|
|
347
|
+
wait_for_ready=None,
|
|
348
|
+
timeout=None,
|
|
349
|
+
metadata=None):
|
|
350
|
+
return grpc.experimental.unary_unary(
|
|
351
|
+
request,
|
|
352
|
+
target,
|
|
353
|
+
'/dispatcher.v1.MessageQueue/ListTopics',
|
|
354
|
+
queue__pb2.ListTopicsRequest.SerializeToString,
|
|
355
|
+
queue__pb2.ListTopicsResponse.FromString,
|
|
356
|
+
options,
|
|
357
|
+
channel_credentials,
|
|
358
|
+
insecure,
|
|
359
|
+
call_credentials,
|
|
360
|
+
compression,
|
|
361
|
+
wait_for_ready,
|
|
362
|
+
timeout,
|
|
363
|
+
metadata,
|
|
364
|
+
_registered_method=True)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cangling-broker
|
|
3
|
+
Version: 0.1.11
|
|
4
|
+
Summary: Python producer and consumer for cangling-broker
|
|
5
|
+
Author-email: zhangjianshe <zhangjianshe@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/zhangjianshe/cangling-broker
|
|
8
|
+
Project-URL: Repository, https://github.com/zhangjianshe/cangling-broker
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: grpcio>=1.83.0
|
|
14
|
+
Requires-Dist: protobuf>=5.28.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: grpcio-tools>=1.83.0; extra == "dev"
|
|
17
|
+
Requires-Dist: build; extra == "dev"
|
|
18
|
+
Requires-Dist: twine; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# cangling-broker (Python)
|
|
21
|
+
|
|
22
|
+
Producer and consumer for cangling-broker. Same contract as the Java client: `AcceptMessages` to publish, `Subscribe` to consume, optional `Register` metadata, `CL_BROKER_AUTH_TOKEN` on every RPC.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install cangling-broker
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
From this tree:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e python
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Use
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from cangling_broker import SatwayClient, SubscribeOptions
|
|
40
|
+
|
|
41
|
+
with SatwayClient.connect("127.0.0.1:7500", "change-me") as client:
|
|
42
|
+
client.send("cangling-test", "hello")
|
|
43
|
+
with client.subscribe(
|
|
44
|
+
SubscribeOptions(topic="cangling-test", name="worker-1"),
|
|
45
|
+
lambda message: print(message.id, message.payload),
|
|
46
|
+
):
|
|
47
|
+
...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`SatwayClient.connect(broker)` also reads `CL_BROKER_AUTH_TOKEN` from the environment.
|
|
51
|
+
|
|
52
|
+
Batch-set topic delivery (`single` = one consumer, `broadcast` = every live stream):
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from cangling_broker import TopicConfig
|
|
56
|
+
|
|
57
|
+
client.configure_topics([
|
|
58
|
+
TopicConfig("jobs", "single"),
|
|
59
|
+
TopicConfig("alerts", "broadcast"),
|
|
60
|
+
])
|
|
61
|
+
print(client.list_topics())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Existing Kafka senders can keep ``send(topic, value)`` / ``flush()``:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# from kafka import KafkaProducer
|
|
68
|
+
from cangling_broker import KafkaProducer
|
|
69
|
+
|
|
70
|
+
producer = KafkaProducer(bootstrap_servers="127.0.0.1:7500")
|
|
71
|
+
producer.send(topic, msg)
|
|
72
|
+
producer.flush()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# consume
|
|
77
|
+
python python/examples/consumer.py --broker 127.0.0.1:7500 --topic cangling-test --name py-s0 --token change-me
|
|
78
|
+
|
|
79
|
+
# produce
|
|
80
|
+
python python/examples/producer.py --broker 127.0.0.1:7500 --topic cangling-test --text hello --count 1 --token change-me
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Publish to PyPI
|
|
84
|
+
|
|
85
|
+
CI on tag `v*` builds the package and uploads with `pypa/gh-action-pypi-publish`. Set repository secret `PYPI_API_TOKEN`.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cd python
|
|
89
|
+
python generate_proto.py
|
|
90
|
+
python -m build
|
|
91
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
cangling_broker/__init__.py
|
|
4
|
+
cangling_broker/client.py
|
|
5
|
+
cangling_broker/compat.py
|
|
6
|
+
cangling_broker/models.py
|
|
7
|
+
cangling_broker.egg-info/PKG-INFO
|
|
8
|
+
cangling_broker.egg-info/SOURCES.txt
|
|
9
|
+
cangling_broker.egg-info/dependency_links.txt
|
|
10
|
+
cangling_broker.egg-info/requires.txt
|
|
11
|
+
cangling_broker.egg-info/top_level.txt
|
|
12
|
+
cangling_broker/proto/__init__.py
|
|
13
|
+
cangling_broker/proto/queue_pb2.py
|
|
14
|
+
cangling_broker/proto/queue_pb2_grpc.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cangling_broker
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cangling-broker"
|
|
3
|
+
version = "0.1.11"
|
|
4
|
+
description = "Python producer and consumer for cangling-broker"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
authors = [{ name = "zhangjianshe", email = "zhangjianshe@gmail.com" }]
|
|
8
|
+
license = { text = "Apache-2.0" }
|
|
9
|
+
dependencies = [
|
|
10
|
+
"grpcio>=1.83.0",
|
|
11
|
+
"protobuf>=5.28.0",
|
|
12
|
+
]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
]
|
|
17
|
+
urls.Homepage = "https://github.com/zhangjianshe/cangling-broker"
|
|
18
|
+
urls.Repository = "https://github.com/zhangjianshe/cangling-broker"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = ["grpcio-tools>=1.83.0", "build", "twine"]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["setuptools", "wheel"]
|
|
25
|
+
build-backend = "setuptools.build_meta"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["."]
|
|
29
|
+
include = ["cangling_broker", "cangling_broker.*"]
|