notifyhub-client 0.1.0__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.
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: notifyhub-client
3
+ Version: 0.1.0
4
+ Summary: NotifyHub 多语言通知推送服务 - Python 客户端
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/huangwenfu750/notifyhub
7
+ Project-URL: Repository, https://github.com/huangwenfu750/notifyhub
8
+ Project-URL: Issues, https://github.com/huangwenfu750/notifyhub/issues
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: grpcio>=1.60
12
+ Provides-Extra: dev
13
+ Requires-Dist: grpcio-tools>=1.60; extra == "dev"
14
+
15
+ # NotifyHub Python SDK
16
+
17
+ > English: [README.en.md](README.en.md)
18
+
19
+ ```bash
20
+ pip install -e sdks/python # 本地安装(发布到 PyPI 前的用法)
21
+ ```
22
+
23
+ ```python
24
+ from notifyhub import NotifyClient, PublishRequest
25
+
26
+ with NotifyClient("localhost:9987", token="ntf_xxx") as client:
27
+ client.publish("alert", "部署完成", "v1.2.0 上线", params={"env": "prod"})
28
+ print(client.ping())
29
+ ```
30
+
31
+ 批量发布(一条双向流,单条失败不中断):
32
+
33
+ ```python
34
+ reqs = [
35
+ PublishRequest(topic="alert.db", title="t1", content="c1"),
36
+ PublishRequest(title="缺 topic 的非法请求"),
37
+ ]
38
+ for ack in client.publish_batch(reqs):
39
+ print(ack.accepted, ack.error) # True '' / False 'INVALID_ARGUMENT: topic 不能为空'
40
+ ```
41
+
42
+ 订阅主题:
43
+
44
+ ```python
45
+ sub = client.subscribe(["alert.*"], print)
46
+ ...
47
+ sub.cancel()
48
+ ```
49
+
50
+ 用代码注册推送平台:
51
+
52
+ ```python
53
+ client.upsert_platform(
54
+ name="ding-alert", type="dingtalk",
55
+ webhook="https://oapi.dingtalk.com/robot/send?access_token=xxx",
56
+ secret="SECxxx", topics=["alert"],
57
+ )
58
+ ```
59
+
60
+ 重新生成 stub(改了 proto 之后):
61
+
62
+ ```bash
63
+ ./scripts/gen-protos.sh python
64
+ ```
@@ -0,0 +1,50 @@
1
+ # NotifyHub Python SDK
2
+
3
+ > English: [README.en.md](README.en.md)
4
+
5
+ ```bash
6
+ pip install -e sdks/python # 本地安装(发布到 PyPI 前的用法)
7
+ ```
8
+
9
+ ```python
10
+ from notifyhub import NotifyClient, PublishRequest
11
+
12
+ with NotifyClient("localhost:9987", token="ntf_xxx") as client:
13
+ client.publish("alert", "部署完成", "v1.2.0 上线", params={"env": "prod"})
14
+ print(client.ping())
15
+ ```
16
+
17
+ 批量发布(一条双向流,单条失败不中断):
18
+
19
+ ```python
20
+ reqs = [
21
+ PublishRequest(topic="alert.db", title="t1", content="c1"),
22
+ PublishRequest(title="缺 topic 的非法请求"),
23
+ ]
24
+ for ack in client.publish_batch(reqs):
25
+ print(ack.accepted, ack.error) # True '' / False 'INVALID_ARGUMENT: topic 不能为空'
26
+ ```
27
+
28
+ 订阅主题:
29
+
30
+ ```python
31
+ sub = client.subscribe(["alert.*"], print)
32
+ ...
33
+ sub.cancel()
34
+ ```
35
+
36
+ 用代码注册推送平台:
37
+
38
+ ```python
39
+ client.upsert_platform(
40
+ name="ding-alert", type="dingtalk",
41
+ webhook="https://oapi.dingtalk.com/robot/send?access_token=xxx",
42
+ secret="SECxxx", topics=["alert"],
43
+ )
44
+ ```
45
+
46
+ 重新生成 stub(改了 proto 之后):
47
+
48
+ ```bash
49
+ ./scripts/gen-protos.sh python
50
+ ```
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "notifyhub-client"
7
+ version = "0.1.0"
8
+ description = "NotifyHub 多语言通知推送服务 - Python 客户端"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ dependencies = ["grpcio>=1.60"]
13
+
14
+ [project.urls]
15
+ Homepage = "https://github.com/huangwenfu750/notifyhub"
16
+ Repository = "https://github.com/huangwenfu750/notifyhub"
17
+ Issues = "https://github.com/huangwenfu750/notifyhub/issues"
18
+
19
+ [project.optional-dependencies]
20
+ dev = ["grpcio-tools>=1.60"]
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
24
+
25
+ [tool.setuptools.package-data]
26
+ notifyhub = ["notify/v1/*.pyi"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,38 @@
1
+ """NotifyHub Python 客户端(gRPC 薄封装)。
2
+
3
+ 用法::
4
+
5
+ from notifyhub import NotifyClient
6
+
7
+ client = NotifyClient("localhost:9987", token="xxx")
8
+ client.publish("alert", "部署完成", "v1.2.0 上线")
9
+ client.close()
10
+
11
+ 批量发布(双向流)::
12
+
13
+ from notifyhub import NotifyClient, PublishRequest
14
+
15
+ reqs = [PublishRequest(topic="a.b", title="t1"),
16
+ PublishRequest(title="非法:缺 topic")]
17
+ for ack in client.publish_batch(reqs):
18
+ print(ack.accepted, ack.error)
19
+ """
20
+
21
+ from .client import Event, NotifyClient, NotifyError, PublishAck, Subscription
22
+ from .notify.v1 import notify_pb2
23
+
24
+ Options = notify_pb2.Options
25
+ PublishRequest = notify_pb2.PublishRequest
26
+ PlatformConfig = notify_pb2.PlatformConfig
27
+
28
+ __all__ = [
29
+ "NotifyClient",
30
+ "NotifyError",
31
+ "PublishAck",
32
+ "Event",
33
+ "Subscription",
34
+ "PublishRequest",
35
+ "Options",
36
+ "PlatformConfig",
37
+ ]
38
+ __version__ = "0.1.0"
@@ -0,0 +1,227 @@
1
+ """NotifyClient: NotifyHub gRPC 客户端薄封装。
2
+
3
+ 发布、订阅、Admin(代码配置推送平台)三类操作;token 通过 metadata 注入。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Callable, Iterable, Optional
9
+
10
+ import grpc
11
+
12
+ from .notify.v1 import notify_pb2 as pb
13
+ from .notify.v1 import notify_pb2_grpc as pb_grpc
14
+
15
+
16
+ class NotifyError(Exception):
17
+ """服务端返回的错误(对应 gRPC Status)。"""
18
+
19
+ def __init__(self, code: grpc.StatusCode, details: str):
20
+ self.code = code
21
+ self.details = details
22
+ super().__init__(f"{code.name}: {details}")
23
+
24
+
25
+ class Event:
26
+ """订阅收到的事件。"""
27
+
28
+ def __init__(self, msg: pb.Event):
29
+ self.topic = msg.topic
30
+ self.title = msg.title
31
+ self.content = msg.content
32
+ self.params = dict(msg.params)
33
+ self.event_id = msg.event_id
34
+ self.timestamp = msg.timestamp
35
+
36
+ def __repr__(self) -> str: # pragma: no cover
37
+ return f"Event(topic={self.topic!r}, title={self.title!r}, event_id={self.event_id!r})"
38
+
39
+
40
+ class PublishAck:
41
+ """发布回执。"""
42
+
43
+ def __init__(self, ack: pb.PublishAck):
44
+ self.event_id = ack.event_id
45
+ self.accepted = ack.accepted
46
+ self.deduplicated = ack.deduplicated
47
+ self.matched_platforms = list(ack.matched_platforms)
48
+ self.error = ack.error
49
+
50
+ def __repr__(self) -> str: # pragma: no cover
51
+ return (f"PublishAck(accepted={self.accepted}, deduplicated={self.deduplicated}, "
52
+ f"matched={self.matched_platforms})")
53
+
54
+
55
+ class Subscription:
56
+ """订阅句柄:close() 主动退订。"""
57
+
58
+ def __init__(self, call: grpc.Future | object, close: Callable[[], None]):
59
+ self._call = call
60
+ self._close = close
61
+
62
+ def cancel(self) -> None:
63
+ self._close()
64
+
65
+ close = cancel
66
+
67
+
68
+ def _wrap_grpc_error(fn):
69
+ def inner(*args, **kwargs):
70
+ try:
71
+ return fn(*args, **kwargs)
72
+ except grpc.RpcError as e:
73
+ raise NotifyError(e.code(), e.details()) from e
74
+ return inner
75
+
76
+
77
+ class NotifyClient:
78
+ """与 NotifyHub 服务端的连接。线程安全,可在多线程间共享。"""
79
+
80
+ def __init__(self, target: str, token: Optional[str] = None):
81
+ """target 形如 "localhost:9987"。token 为服务端 auth.tokens 中的一项。"""
82
+ self._token = token
83
+ self._raw_channel = grpc.insecure_channel(target)
84
+ if token:
85
+ # 用拦截器把 x-api-token 注入每次调用
86
+ self._channel = grpc.intercept_channel(self._raw_channel, self._make_interceptor(token))
87
+ else:
88
+ self._channel = self._raw_channel
89
+ self._stub = pb_grpc.NotifyStub(self._channel)
90
+
91
+ @staticmethod
92
+ def _make_interceptor(token: str):
93
+ class _Invoker(grpc.UnaryUnaryClientInterceptor,
94
+ grpc.UnaryStreamClientInterceptor,
95
+ grpc.StreamUnaryClientInterceptor,
96
+ grpc.StreamStreamClientInterceptor):
97
+ def intercept_unary_unary(self, continuation, client_call_details, request):
98
+ return continuation(self._with_token(client_call_details), request)
99
+
100
+ def intercept_unary_stream(self, continuation, client_call_details, request):
101
+ return continuation(self._with_token(client_call_details), request)
102
+
103
+ def intercept_stream_unary(self, continuation, client_call_details, request_iterator):
104
+ return continuation(self._with_token(client_call_details), request_iterator)
105
+
106
+ def intercept_stream_stream(self, continuation, client_call_details, request_iterator):
107
+ return continuation(self._with_token(client_call_details), request_iterator)
108
+
109
+ @staticmethod
110
+ def _with_token(client_call_details):
111
+ md = list(client_call_details.metadata or [])
112
+ md.append(("x-api-token", token))
113
+ return client_call_details._replace(metadata=md)
114
+
115
+ return _Invoker()
116
+
117
+ # ---------- 发布 ----------
118
+
119
+ @_wrap_grpc_error
120
+ def publish(self, topic: str, title: str = "", content: str = "",
121
+ params: Optional[dict] = None,
122
+ platforms: Optional[Iterable[str]] = None,
123
+ dedup_key: str = "",
124
+ skip_subscribers: bool = False,
125
+ skip_platforms: bool = False,
126
+ timeout: float = 10.0) -> PublishAck:
127
+ """发布一条通知:按路由推送平台 + 广播给订阅者。"""
128
+ req = pb.PublishRequest(
129
+ topic=topic, title=title, content=content,
130
+ params=params or {},
131
+ platforms=list(platforms or []),
132
+ options=pb.Options(dedup_key=dedup_key,
133
+ skip_subscribers=skip_subscribers,
134
+ skip_platforms=skip_platforms),
135
+ )
136
+ return PublishAck(self._stub.Publish(req, timeout=timeout))
137
+
138
+ # ---------- 批量发布(双向流) ----------
139
+
140
+ @_wrap_grpc_error
141
+ def publish_batch(self, requests, timeout: float = 10.0) -> list:
142
+ """批量发布(双向流):逐条发送并收集回执。
143
+
144
+ requests 为 pb.PublishRequest 的可迭代对象;单条错误(topic 为空、平台不存在)
145
+ 不中断流,对应回执的 accepted=False 且带 error。
146
+ """
147
+ return [PublishAck(ack) for ack in self._stub.PublishStream(iter(requests), timeout=timeout)]
148
+
149
+ def publish_stream(self, requests):
150
+ """底层双向流:返回回执迭代器,由调用方自行消费(适合边生成边发送)。"""
151
+ return self._stub.PublishStream(iter(requests))
152
+
153
+ # ---------- 订阅 ----------
154
+
155
+ def subscribe(self, topics: Iterable[str], on_event: Callable[[Event], None],
156
+ on_error: Optional[Callable[[Exception], None]] = None) -> Subscription:
157
+ """订阅主题,回调在后台线程触发。"""
158
+ import threading
159
+
160
+ call = self._stub.Subscribe(pb.SubscribeRequest(topics=list(topics)))
161
+ stop = threading.Event()
162
+ finished = threading.Event()
163
+
164
+ def pump():
165
+ try:
166
+ for msg in call:
167
+ if stop.is_set():
168
+ break
169
+ on_event(Event(msg))
170
+ except grpc.RpcError as e:
171
+ if e.code() != grpc.StatusCode.CANCELLED and on_error:
172
+ on_error(e)
173
+ finally:
174
+ finished.set()
175
+
176
+ t = threading.Thread(target=pump, daemon=True, name="notifyhub-sub")
177
+ t.start()
178
+
179
+ def close():
180
+ stop.set()
181
+ call.cancel()
182
+
183
+ return Subscription(call, close)
184
+
185
+ # ---------- Admin ----------
186
+
187
+ @_wrap_grpc_error
188
+ def upsert_platform(self, name: str, type: str, webhook: str,
189
+ secret: str = "", topics: Optional[Iterable[str]] = None,
190
+ template: str = "", at_mobiles: Optional[Iterable[str]] = None,
191
+ extra: Optional[dict] = None,
192
+ rate_limit_qps: int = 0,
193
+ timeout: float = 10.0) -> dict:
194
+ """用代码注册/更新推送平台(运行时生效,重启后需重新注册或写入配置文件)。"""
195
+ cfg = pb.PlatformConfig(
196
+ name=name, type=type, webhook=webhook, secret=secret,
197
+ topics=list(topics or ["*"]), template=template,
198
+ at_mobiles=list(at_mobiles or []), extra=extra or {},
199
+ rate_limit_qps=rate_limit_qps)
200
+ out = self._stub.UpsertPlatform(cfg, timeout=timeout)
201
+ return {"name": out.name, "type": out.type, "topics": list(out.topics)}
202
+
203
+ @_wrap_grpc_error
204
+ def list_platforms(self, timeout: float = 10.0) -> list:
205
+ out = self._stub.ListPlatforms(pb.Empty(), timeout=timeout)
206
+ return [{"name": p.name, "type": p.type, "webhook": p.webhook,
207
+ "topics": list(p.topics)} for p in out.platforms]
208
+
209
+ @_wrap_grpc_error
210
+ def remove_platform(self, name: str, timeout: float = 10.0) -> None:
211
+ self._stub.RemovePlatform(pb.PlatformRef(name=name), timeout=timeout)
212
+
213
+ @_wrap_grpc_error
214
+ def ping(self, timeout: float = 5.0) -> dict:
215
+ out = self._stub.Ping(pb.Empty(), timeout=timeout)
216
+ return {"version": out.version, "uptime_seconds": out.uptime_seconds}
217
+
218
+ # ---------- 生命周期 ----------
219
+
220
+ def close(self):
221
+ self._raw_channel.close()
222
+
223
+ def __enter__(self):
224
+ return self
225
+
226
+ def __exit__(self, *exc):
227
+ self.close()
@@ -0,0 +1,71 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: notify/v1/notify.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
+ 'notify/v1/notify.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\x16notify/v1/notify.proto\x12\tnotify.v1\"\x07\n\x05\x45mpty\"/\n\x04Pong\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x16\n\x0euptime_seconds\x18\x02 \x01(\x03\"N\n\x07Options\x12\x11\n\tdedup_key\x18\x01 \x01(\t\x12\x18\n\x10skip_subscribers\x18\x02 \x01(\x08\x12\x16\n\x0eskip_platforms\x18\x03 \x01(\x08\"\xdd\x01\n\x0ePublishRequest\x12\r\n\x05topic\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\x12\x35\n\x06params\x18\x04 \x03(\x0b\x32%.notify.v1.PublishRequest.ParamsEntry\x12\x11\n\tplatforms\x18\x05 \x03(\t\x12#\n\x07options\x18\x06 \x01(\x0b\x32\x12.notify.v1.Options\x1a-\n\x0bParamsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"p\n\nPublishAck\x12\x10\n\x08\x65vent_id\x18\x01 \x01(\t\x12\x10\n\x08\x61\x63\x63\x65pted\x18\x02 \x01(\x08\x12\x14\n\x0c\x64\x65\x64uplicated\x18\x03 \x01(\x08\x12\x19\n\x11matched_platforms\x18\x04 \x03(\t\x12\r\n\x05\x65rror\x18\x05 \x01(\t\"\"\n\x10SubscribeRequest\x12\x0e\n\x06topics\x18\x01 \x03(\t\"\xca\x01\n\x05\x45vent\x12\r\n\x05topic\x18\x01 \x01(\t\x12\r\n\x05title\x18\x02 \x01(\t\x12\x0f\n\x07\x63ontent\x18\x03 \x01(\t\x12,\n\x06params\x18\x04 \x03(\x0b\x32\x1c.notify.v1.Event.ParamsEntry\x12\x10\n\x08platform\x18\x05 \x01(\t\x12\x10\n\x08\x65vent_id\x18\x06 \x01(\t\x12\x11\n\ttimestamp\x18\x07 \x01(\x03\x1a-\n\x0bParamsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xa5\x02\n\x0ePlatformConfig\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0f\n\x07webhook\x18\x03 \x01(\t\x12\x0e\n\x06secret\x18\x04 \x01(\t\x12\x0e\n\x06topics\x18\x05 \x03(\t\x12\x10\n\x08template\x18\x06 \x01(\t\x12\x12\n\nat_mobiles\x18\x07 \x03(\t\x12\x33\n\x05\x65xtra\x18\x08 \x03(\x0b\x32$.notify.v1.PlatformConfig.ExtraEntry\x12%\n\x05retry\x18\t \x01(\x0b\x32\x16.notify.v1.RetryPolicy\x12\x16\n\x0erate_limit_qps\x18\n \x01(\x05\x1a,\n\nExtraEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"7\n\x0bRetryPolicy\x12\x14\n\x0cmax_attempts\x18\x01 \x01(\x05\x12\x12\n\nbackoff_ms\x18\x02 \x01(\x03\"\x1b\n\x0bPlatformRef\x12\x0c\n\x04name\x18\x01 \x01(\t\"<\n\x0cPlatformList\x12,\n\tplatforms\x18\x01 \x03(\x0b\x32\x19.notify.v1.PlatformConfig2\xb5\x03\n\x06Notify\x12;\n\x07Publish\x12\x19.notify.v1.PublishRequest\x1a\x15.notify.v1.PublishAck\x12<\n\tSubscribe\x12\x1b.notify.v1.SubscribeRequest\x1a\x10.notify.v1.Event0\x01\x12\x45\n\rPublishStream\x12\x19.notify.v1.PublishRequest\x1a\x15.notify.v1.PublishAck(\x01\x30\x01\x12\x46\n\x0eUpsertPlatform\x12\x19.notify.v1.PlatformConfig\x1a\x19.notify.v1.PlatformConfig\x12:\n\rListPlatforms\x12\x10.notify.v1.Empty\x1a\x17.notify.v1.PlatformList\x12:\n\x0eRemovePlatform\x12\x16.notify.v1.PlatformRef\x1a\x10.notify.v1.Empty\x12)\n\x04Ping\x12\x10.notify.v1.Empty\x1a\x0f.notify.v1.PongBc\n\x0fio.notifyhub.v1B\x0bNotifyProtoP\x01ZAgithub.com/huangwenfu750/notifyhub/sdks/go/gen/notify/v1;notifyv1b\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'notify.v1.notify_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ _globals['DESCRIPTOR']._loaded_options = None
34
+ _globals['DESCRIPTOR']._serialized_options = b'\n\017io.notifyhub.v1B\013NotifyProtoP\001ZAgithub.com/huangwenfu750/notifyhub/sdks/go/gen/notify/v1;notifyv1'
35
+ _globals['_PUBLISHREQUEST_PARAMSENTRY']._loaded_options = None
36
+ _globals['_PUBLISHREQUEST_PARAMSENTRY']._serialized_options = b'8\001'
37
+ _globals['_EVENT_PARAMSENTRY']._loaded_options = None
38
+ _globals['_EVENT_PARAMSENTRY']._serialized_options = b'8\001'
39
+ _globals['_PLATFORMCONFIG_EXTRAENTRY']._loaded_options = None
40
+ _globals['_PLATFORMCONFIG_EXTRAENTRY']._serialized_options = b'8\001'
41
+ _globals['_EMPTY']._serialized_start=37
42
+ _globals['_EMPTY']._serialized_end=44
43
+ _globals['_PONG']._serialized_start=46
44
+ _globals['_PONG']._serialized_end=93
45
+ _globals['_OPTIONS']._serialized_start=95
46
+ _globals['_OPTIONS']._serialized_end=173
47
+ _globals['_PUBLISHREQUEST']._serialized_start=176
48
+ _globals['_PUBLISHREQUEST']._serialized_end=397
49
+ _globals['_PUBLISHREQUEST_PARAMSENTRY']._serialized_start=352
50
+ _globals['_PUBLISHREQUEST_PARAMSENTRY']._serialized_end=397
51
+ _globals['_PUBLISHACK']._serialized_start=399
52
+ _globals['_PUBLISHACK']._serialized_end=511
53
+ _globals['_SUBSCRIBEREQUEST']._serialized_start=513
54
+ _globals['_SUBSCRIBEREQUEST']._serialized_end=547
55
+ _globals['_EVENT']._serialized_start=550
56
+ _globals['_EVENT']._serialized_end=752
57
+ _globals['_EVENT_PARAMSENTRY']._serialized_start=352
58
+ _globals['_EVENT_PARAMSENTRY']._serialized_end=397
59
+ _globals['_PLATFORMCONFIG']._serialized_start=755
60
+ _globals['_PLATFORMCONFIG']._serialized_end=1048
61
+ _globals['_PLATFORMCONFIG_EXTRAENTRY']._serialized_start=1004
62
+ _globals['_PLATFORMCONFIG_EXTRAENTRY']._serialized_end=1048
63
+ _globals['_RETRYPOLICY']._serialized_start=1050
64
+ _globals['_RETRYPOLICY']._serialized_end=1105
65
+ _globals['_PLATFORMREF']._serialized_start=1107
66
+ _globals['_PLATFORMREF']._serialized_end=1134
67
+ _globals['_PLATFORMLIST']._serialized_start=1136
68
+ _globals['_PLATFORMLIST']._serialized_end=1196
69
+ _globals['_NOTIFY']._serialized_start=1199
70
+ _globals['_NOTIFY']._serialized_end=1636
71
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,366 @@
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 notifyhub.notify.v1 import notify_pb2 as notify_dot_v1_dot_notify__pb2
7
+
8
+ GRPC_GENERATED_VERSION = '1.84.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 notify/v1/notify_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 NotifyStub:
29
+ """NotifyHub —— 多语言通知推送服务。
30
+ 本文件是唯一契约:服务端将来重写(Go/Rust/Zig)时不得变更既有字段的编号与语义。
31
+ """
32
+
33
+ def __init__(self, channel):
34
+ """Constructor.
35
+
36
+ Args:
37
+ channel: A grpc.Channel.
38
+ """
39
+ self.Publish = channel.unary_unary(
40
+ '/notify.v1.Notify/Publish',
41
+ request_serializer=notify_dot_v1_dot_notify__pb2.PublishRequest.SerializeToString,
42
+ response_deserializer=notify_dot_v1_dot_notify__pb2.PublishAck.FromString,
43
+ _registered_method=True)
44
+ self.Subscribe = channel.unary_stream(
45
+ '/notify.v1.Notify/Subscribe',
46
+ request_serializer=notify_dot_v1_dot_notify__pb2.SubscribeRequest.SerializeToString,
47
+ response_deserializer=notify_dot_v1_dot_notify__pb2.Event.FromString,
48
+ _registered_method=True)
49
+ self.PublishStream = channel.stream_stream(
50
+ '/notify.v1.Notify/PublishStream',
51
+ request_serializer=notify_dot_v1_dot_notify__pb2.PublishRequest.SerializeToString,
52
+ response_deserializer=notify_dot_v1_dot_notify__pb2.PublishAck.FromString,
53
+ _registered_method=True)
54
+ self.UpsertPlatform = channel.unary_unary(
55
+ '/notify.v1.Notify/UpsertPlatform',
56
+ request_serializer=notify_dot_v1_dot_notify__pb2.PlatformConfig.SerializeToString,
57
+ response_deserializer=notify_dot_v1_dot_notify__pb2.PlatformConfig.FromString,
58
+ _registered_method=True)
59
+ self.ListPlatforms = channel.unary_unary(
60
+ '/notify.v1.Notify/ListPlatforms',
61
+ request_serializer=notify_dot_v1_dot_notify__pb2.Empty.SerializeToString,
62
+ response_deserializer=notify_dot_v1_dot_notify__pb2.PlatformList.FromString,
63
+ _registered_method=True)
64
+ self.RemovePlatform = channel.unary_unary(
65
+ '/notify.v1.Notify/RemovePlatform',
66
+ request_serializer=notify_dot_v1_dot_notify__pb2.PlatformRef.SerializeToString,
67
+ response_deserializer=notify_dot_v1_dot_notify__pb2.Empty.FromString,
68
+ _registered_method=True)
69
+ self.Ping = channel.unary_unary(
70
+ '/notify.v1.Notify/Ping',
71
+ request_serializer=notify_dot_v1_dot_notify__pb2.Empty.SerializeToString,
72
+ response_deserializer=notify_dot_v1_dot_notify__pb2.Pong.FromString,
73
+ _registered_method=True)
74
+
75
+
76
+ class NotifyServicer:
77
+ """NotifyHub —— 多语言通知推送服务。
78
+ 本文件是唯一契约:服务端将来重写(Go/Rust/Zig)时不得变更既有字段的编号与语义。
79
+ """
80
+
81
+ def Publish(self, request, context):
82
+ """发布通知:按路由推送到外部平台 + 广播给在线订阅者。
83
+ """
84
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
85
+ context.set_details('Method not implemented!')
86
+ raise NotImplementedError('Method not implemented!')
87
+
88
+ def Subscribe(self, request, context):
89
+ """订阅主题,服务端流式推送。
90
+ """
91
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
92
+ context.set_details('Method not implemented!')
93
+ raise NotImplementedError('Method not implemented!')
94
+
95
+ def PublishStream(self, request_iterator, context):
96
+ """批量发布(双向流:每收到一条请求,回执一条 Ack)。
97
+ """
98
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
99
+ context.set_details('Method not implemented!')
100
+ raise NotImplementedError('Method not implemented!')
101
+
102
+ def UpsertPlatform(self, request, context):
103
+ """用代码配置推送平台(运行时生效;重启后需重新注册,或写入配置文件)。
104
+ """
105
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
106
+ context.set_details('Method not implemented!')
107
+ raise NotImplementedError('Method not implemented!')
108
+
109
+ def ListPlatforms(self, request, context):
110
+ """Missing associated documentation comment in .proto file."""
111
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
112
+ context.set_details('Method not implemented!')
113
+ raise NotImplementedError('Method not implemented!')
114
+
115
+ def RemovePlatform(self, request, context):
116
+ """Missing associated documentation comment in .proto file."""
117
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
118
+ context.set_details('Method not implemented!')
119
+ raise NotImplementedError('Method not implemented!')
120
+
121
+ def Ping(self, request, context):
122
+ """健康检查(无需鉴权)。
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_NotifyServicer_to_server(servicer, server):
130
+ rpc_method_handlers = {
131
+ 'Publish': grpc.unary_unary_rpc_method_handler(
132
+ servicer.Publish,
133
+ request_deserializer=notify_dot_v1_dot_notify__pb2.PublishRequest.FromString,
134
+ response_serializer=notify_dot_v1_dot_notify__pb2.PublishAck.SerializeToString,
135
+ ),
136
+ 'Subscribe': grpc.unary_stream_rpc_method_handler(
137
+ servicer.Subscribe,
138
+ request_deserializer=notify_dot_v1_dot_notify__pb2.SubscribeRequest.FromString,
139
+ response_serializer=notify_dot_v1_dot_notify__pb2.Event.SerializeToString,
140
+ ),
141
+ 'PublishStream': grpc.stream_stream_rpc_method_handler(
142
+ servicer.PublishStream,
143
+ request_deserializer=notify_dot_v1_dot_notify__pb2.PublishRequest.FromString,
144
+ response_serializer=notify_dot_v1_dot_notify__pb2.PublishAck.SerializeToString,
145
+ ),
146
+ 'UpsertPlatform': grpc.unary_unary_rpc_method_handler(
147
+ servicer.UpsertPlatform,
148
+ request_deserializer=notify_dot_v1_dot_notify__pb2.PlatformConfig.FromString,
149
+ response_serializer=notify_dot_v1_dot_notify__pb2.PlatformConfig.SerializeToString,
150
+ ),
151
+ 'ListPlatforms': grpc.unary_unary_rpc_method_handler(
152
+ servicer.ListPlatforms,
153
+ request_deserializer=notify_dot_v1_dot_notify__pb2.Empty.FromString,
154
+ response_serializer=notify_dot_v1_dot_notify__pb2.PlatformList.SerializeToString,
155
+ ),
156
+ 'RemovePlatform': grpc.unary_unary_rpc_method_handler(
157
+ servicer.RemovePlatform,
158
+ request_deserializer=notify_dot_v1_dot_notify__pb2.PlatformRef.FromString,
159
+ response_serializer=notify_dot_v1_dot_notify__pb2.Empty.SerializeToString,
160
+ ),
161
+ 'Ping': grpc.unary_unary_rpc_method_handler(
162
+ servicer.Ping,
163
+ request_deserializer=notify_dot_v1_dot_notify__pb2.Empty.FromString,
164
+ response_serializer=notify_dot_v1_dot_notify__pb2.Pong.SerializeToString,
165
+ ),
166
+ }
167
+ generic_handler = grpc.method_handlers_generic_handler(
168
+ 'notify.v1.Notify', rpc_method_handlers)
169
+ server.add_generic_rpc_handlers((generic_handler,))
170
+ server.add_registered_method_handlers('notify.v1.Notify', rpc_method_handlers)
171
+
172
+
173
+ # This class is part of an EXPERIMENTAL API.
174
+ class Notify:
175
+ """NotifyHub —— 多语言通知推送服务。
176
+ 本文件是唯一契约:服务端将来重写(Go/Rust/Zig)时不得变更既有字段的编号与语义。
177
+ """
178
+
179
+ @staticmethod
180
+ def Publish(request,
181
+ target,
182
+ options=(),
183
+ channel_credentials=None,
184
+ call_credentials=None,
185
+ insecure=False,
186
+ compression=None,
187
+ wait_for_ready=None,
188
+ timeout=None,
189
+ metadata=None):
190
+ return grpc.experimental.unary_unary(
191
+ request,
192
+ target,
193
+ '/notify.v1.Notify/Publish',
194
+ notify_dot_v1_dot_notify__pb2.PublishRequest.SerializeToString,
195
+ notify_dot_v1_dot_notify__pb2.PublishAck.FromString,
196
+ options,
197
+ channel_credentials,
198
+ insecure,
199
+ call_credentials,
200
+ compression,
201
+ wait_for_ready,
202
+ timeout,
203
+ metadata,
204
+ _registered_method=True)
205
+
206
+ @staticmethod
207
+ def Subscribe(request,
208
+ target,
209
+ options=(),
210
+ channel_credentials=None,
211
+ call_credentials=None,
212
+ insecure=False,
213
+ compression=None,
214
+ wait_for_ready=None,
215
+ timeout=None,
216
+ metadata=None):
217
+ return grpc.experimental.unary_stream(
218
+ request,
219
+ target,
220
+ '/notify.v1.Notify/Subscribe',
221
+ notify_dot_v1_dot_notify__pb2.SubscribeRequest.SerializeToString,
222
+ notify_dot_v1_dot_notify__pb2.Event.FromString,
223
+ options,
224
+ channel_credentials,
225
+ insecure,
226
+ call_credentials,
227
+ compression,
228
+ wait_for_ready,
229
+ timeout,
230
+ metadata,
231
+ _registered_method=True)
232
+
233
+ @staticmethod
234
+ def PublishStream(request_iterator,
235
+ target,
236
+ options=(),
237
+ channel_credentials=None,
238
+ call_credentials=None,
239
+ insecure=False,
240
+ compression=None,
241
+ wait_for_ready=None,
242
+ timeout=None,
243
+ metadata=None):
244
+ return grpc.experimental.stream_stream(
245
+ request_iterator,
246
+ target,
247
+ '/notify.v1.Notify/PublishStream',
248
+ notify_dot_v1_dot_notify__pb2.PublishRequest.SerializeToString,
249
+ notify_dot_v1_dot_notify__pb2.PublishAck.FromString,
250
+ options,
251
+ channel_credentials,
252
+ insecure,
253
+ call_credentials,
254
+ compression,
255
+ wait_for_ready,
256
+ timeout,
257
+ metadata,
258
+ _registered_method=True)
259
+
260
+ @staticmethod
261
+ def UpsertPlatform(request,
262
+ target,
263
+ options=(),
264
+ channel_credentials=None,
265
+ call_credentials=None,
266
+ insecure=False,
267
+ compression=None,
268
+ wait_for_ready=None,
269
+ timeout=None,
270
+ metadata=None):
271
+ return grpc.experimental.unary_unary(
272
+ request,
273
+ target,
274
+ '/notify.v1.Notify/UpsertPlatform',
275
+ notify_dot_v1_dot_notify__pb2.PlatformConfig.SerializeToString,
276
+ notify_dot_v1_dot_notify__pb2.PlatformConfig.FromString,
277
+ options,
278
+ channel_credentials,
279
+ insecure,
280
+ call_credentials,
281
+ compression,
282
+ wait_for_ready,
283
+ timeout,
284
+ metadata,
285
+ _registered_method=True)
286
+
287
+ @staticmethod
288
+ def ListPlatforms(request,
289
+ target,
290
+ options=(),
291
+ channel_credentials=None,
292
+ call_credentials=None,
293
+ insecure=False,
294
+ compression=None,
295
+ wait_for_ready=None,
296
+ timeout=None,
297
+ metadata=None):
298
+ return grpc.experimental.unary_unary(
299
+ request,
300
+ target,
301
+ '/notify.v1.Notify/ListPlatforms',
302
+ notify_dot_v1_dot_notify__pb2.Empty.SerializeToString,
303
+ notify_dot_v1_dot_notify__pb2.PlatformList.FromString,
304
+ options,
305
+ channel_credentials,
306
+ insecure,
307
+ call_credentials,
308
+ compression,
309
+ wait_for_ready,
310
+ timeout,
311
+ metadata,
312
+ _registered_method=True)
313
+
314
+ @staticmethod
315
+ def RemovePlatform(request,
316
+ target,
317
+ options=(),
318
+ channel_credentials=None,
319
+ call_credentials=None,
320
+ insecure=False,
321
+ compression=None,
322
+ wait_for_ready=None,
323
+ timeout=None,
324
+ metadata=None):
325
+ return grpc.experimental.unary_unary(
326
+ request,
327
+ target,
328
+ '/notify.v1.Notify/RemovePlatform',
329
+ notify_dot_v1_dot_notify__pb2.PlatformRef.SerializeToString,
330
+ notify_dot_v1_dot_notify__pb2.Empty.FromString,
331
+ options,
332
+ channel_credentials,
333
+ insecure,
334
+ call_credentials,
335
+ compression,
336
+ wait_for_ready,
337
+ timeout,
338
+ metadata,
339
+ _registered_method=True)
340
+
341
+ @staticmethod
342
+ def Ping(request,
343
+ target,
344
+ options=(),
345
+ channel_credentials=None,
346
+ call_credentials=None,
347
+ insecure=False,
348
+ compression=None,
349
+ wait_for_ready=None,
350
+ timeout=None,
351
+ metadata=None):
352
+ return grpc.experimental.unary_unary(
353
+ request,
354
+ target,
355
+ '/notify.v1.Notify/Ping',
356
+ notify_dot_v1_dot_notify__pb2.Empty.SerializeToString,
357
+ notify_dot_v1_dot_notify__pb2.Pong.FromString,
358
+ options,
359
+ channel_credentials,
360
+ insecure,
361
+ call_credentials,
362
+ compression,
363
+ wait_for_ready,
364
+ timeout,
365
+ metadata,
366
+ _registered_method=True)
@@ -0,0 +1,64 @@
1
+ Metadata-Version: 2.4
2
+ Name: notifyhub-client
3
+ Version: 0.1.0
4
+ Summary: NotifyHub 多语言通知推送服务 - Python 客户端
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/huangwenfu750/notifyhub
7
+ Project-URL: Repository, https://github.com/huangwenfu750/notifyhub
8
+ Project-URL: Issues, https://github.com/huangwenfu750/notifyhub/issues
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: grpcio>=1.60
12
+ Provides-Extra: dev
13
+ Requires-Dist: grpcio-tools>=1.60; extra == "dev"
14
+
15
+ # NotifyHub Python SDK
16
+
17
+ > English: [README.en.md](README.en.md)
18
+
19
+ ```bash
20
+ pip install -e sdks/python # 本地安装(发布到 PyPI 前的用法)
21
+ ```
22
+
23
+ ```python
24
+ from notifyhub import NotifyClient, PublishRequest
25
+
26
+ with NotifyClient("localhost:9987", token="ntf_xxx") as client:
27
+ client.publish("alert", "部署完成", "v1.2.0 上线", params={"env": "prod"})
28
+ print(client.ping())
29
+ ```
30
+
31
+ 批量发布(一条双向流,单条失败不中断):
32
+
33
+ ```python
34
+ reqs = [
35
+ PublishRequest(topic="alert.db", title="t1", content="c1"),
36
+ PublishRequest(title="缺 topic 的非法请求"),
37
+ ]
38
+ for ack in client.publish_batch(reqs):
39
+ print(ack.accepted, ack.error) # True '' / False 'INVALID_ARGUMENT: topic 不能为空'
40
+ ```
41
+
42
+ 订阅主题:
43
+
44
+ ```python
45
+ sub = client.subscribe(["alert.*"], print)
46
+ ...
47
+ sub.cancel()
48
+ ```
49
+
50
+ 用代码注册推送平台:
51
+
52
+ ```python
53
+ client.upsert_platform(
54
+ name="ding-alert", type="dingtalk",
55
+ webhook="https://oapi.dingtalk.com/robot/send?access_token=xxx",
56
+ secret="SECxxx", topics=["alert"],
57
+ )
58
+ ```
59
+
60
+ 重新生成 stub(改了 proto 之后):
61
+
62
+ ```bash
63
+ ./scripts/gen-protos.sh python
64
+ ```
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/notifyhub/__init__.py
4
+ src/notifyhub/client.py
5
+ src/notifyhub/notify/__init__.py
6
+ src/notifyhub/notify/v1/__init__.py
7
+ src/notifyhub/notify/v1/notify_pb2.py
8
+ src/notifyhub/notify/v1/notify_pb2_grpc.py
9
+ src/notifyhub_client.egg-info/PKG-INFO
10
+ src/notifyhub_client.egg-info/SOURCES.txt
11
+ src/notifyhub_client.egg-info/dependency_links.txt
12
+ src/notifyhub_client.egg-info/requires.txt
13
+ src/notifyhub_client.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ grpcio>=1.60
2
+
3
+ [dev]
4
+ grpcio-tools>=1.60