Skip to content

Commit f50e974

Browse files
Pigbibiclaude
andcommitted
feat(notifications): add pluggable NotificationChannel Protocol layer
- channel.py: define SmsChannel, PushChannel, EmailChannel, ChatChannel Protocols - 5 default implementations: TwilioSmsChannel, PushoverChannel, NtfyChannel, SmtpEmailChannel, TelegramChatChannel — thin wrappers around existing functions - Users can now swap providers by implementing a Protocol and passing it as send_notification to publish_strategy_plugin_*_alerts() - Export all channels from notifications/__init__.py This completes the notification decoupling: previously Twilio/Pushover/SMTP/Telegram were hardcoded; now any provider implementing the Protocol can be dropped in. 373 passed, 0 failed. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a1b6b57 commit f50e974

2 files changed

Lines changed: 243 additions & 0 deletions

File tree

src/quant_platform_kit/notifications/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
"""Notification integrations."""
22

3+
from .channel import (
4+
SmsChannel,
5+
PushChannel,
6+
EmailChannel,
7+
ChatChannel,
8+
TwilioSmsChannel,
9+
PushoverChannel,
10+
NtfyChannel,
11+
SmtpEmailChannel,
12+
TelegramChatChannel,
13+
)
314
from .email import parse_email_recipients, send_smtp_email
415
from .events import NotificationPublisher, RenderedNotification, publish_rendered_notification
516
from .push import parse_push_recipients, send_ntfy_push, send_pushover_push, send_strategy_plugin_push
@@ -42,6 +53,15 @@
4253
)
4354

4455
__all__ = [
56+
"SmsChannel",
57+
"PushChannel",
58+
"EmailChannel",
59+
"ChatChannel",
60+
"TwilioSmsChannel",
61+
"PushoverChannel",
62+
"NtfyChannel",
63+
"SmtpEmailChannel",
64+
"TelegramChatChannel",
4565
"NotificationPublisher",
4666
"RenderedNotification",
4767
"StrategyPluginAlertChannelStores",
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
"""Notification channel abstraction — pluggable senders for SMS, push, email, and chat.
2+
3+
Each channel type has a Protocol defining the send signature.
4+
The default implementations wire to Twilio (SMS), Pushover/Ntfy (push),
5+
SMTP (email), and Telegram Bot API (chat).
6+
7+
To replace a provider, implement the corresponding Protocol and pass it
8+
as ``send_notification`` to the ``publish_strategy_plugin_*`` function.
9+
10+
Example (custom SMS provider)::
11+
12+
class AliyunSmsChannel:
13+
def send_sms(self, recipient: str, body: str, *, sender: str | None = None) -> bool:
14+
# call Aliyun SMS API
15+
return True
16+
17+
publish_strategy_plugin_sms_alerts(
18+
signals,
19+
sms_settings=settings,
20+
send_notification=AliyunSmsChannel().send_sms,
21+
)
22+
"""
23+
24+
from __future__ import annotations
25+
26+
from typing import Protocol
27+
28+
29+
# ──────────────────────────────────────────────────────────────────────
30+
# Channel Protocols
31+
# ──────────────────────────────────────────────────────────────────────
32+
33+
34+
class SmsChannel(Protocol):
35+
"""Send an SMS message. Return True on success, False on failure."""
36+
37+
def send_sms(self, recipient: str, body: str, *, sender: str | None = None) -> bool:
38+
...
39+
40+
41+
class PushChannel(Protocol):
42+
"""Send a push notification. Return True on success, False on failure.
43+
44+
``target`` is provider-specific: Pushover user key, Ntfy topic, etc.
45+
``provider`` identifies the backend (e.g. "pushover", "ntfy").
46+
"""
47+
48+
def send_push(
49+
self,
50+
title: str,
51+
body: str,
52+
*,
53+
target: str,
54+
provider: str = "pushover",
55+
url: str | None = None,
56+
url_title: str | None = None,
57+
priority: str = "normal",
58+
api_base_url: str | None = None,
59+
) -> bool:
60+
...
61+
62+
63+
class EmailChannel(Protocol):
64+
"""Send an email message. Return True on success, False on failure."""
65+
66+
def send_email(
67+
self,
68+
subject: str,
69+
body: str,
70+
*,
71+
recipients: list[str],
72+
sender: str | None = None,
73+
smtp_host: str = "smtp.gmail.com",
74+
smtp_port: int = 465,
75+
security: str = "ssl",
76+
username: str | None = None,
77+
password: str | None = None,
78+
) -> bool:
79+
...
80+
81+
82+
class ChatChannel(Protocol):
83+
"""Send a message to a chat platform. Return True on success, False on failure.
84+
85+
``chat_id`` and ``token`` are specific to Telegram Bot API.
86+
For other platforms (Slack, Discord, WeChat), wrap their API
87+
in this signature.
88+
"""
89+
90+
def send_message(
91+
self,
92+
chat_id: str,
93+
text: str,
94+
*,
95+
token: str,
96+
api_base_url: str = "https://api.telegram.org",
97+
parse_mode: str = "HTML",
98+
) -> bool:
99+
...
100+
101+
102+
# ──────────────────────────────────────────────────────────────────────
103+
# Default channel implementations (thin wrappers around existing functions)
104+
# ──────────────────────────────────────────────────────────────────────
105+
106+
107+
class TwilioSmsChannel:
108+
"""Default SMS channel — wraps send_twilio_sms()."""
109+
110+
def send_sms(self, recipient: str, body: str, *, sender: str | None = None) -> bool:
111+
from .sms import send_twilio_sms
112+
return send_twilio_sms(
113+
recipient=recipient,
114+
body=body,
115+
account_sid=None,
116+
auth_token=None,
117+
sender=sender,
118+
)
119+
120+
121+
class PushoverChannel:
122+
"""Pushover push channel — wraps send_pushover_push()."""
123+
124+
def send_push(
125+
self,
126+
title: str,
127+
body: str,
128+
*,
129+
target: str,
130+
provider: str = "pushover",
131+
url: str | None = None,
132+
url_title: str | None = None,
133+
priority: str = "normal",
134+
api_base_url: str | None = None,
135+
) -> bool:
136+
from .push import send_pushover_push
137+
return send_pushover_push(
138+
user_key=target,
139+
message=body,
140+
title=title,
141+
url=url,
142+
url_title=url_title,
143+
priority=priority,
144+
api_base_url=api_base_url,
145+
)
146+
147+
148+
class NtfyChannel:
149+
"""Ntfy push channel — wraps send_ntfy_push()."""
150+
151+
def send_push(
152+
self,
153+
title: str,
154+
body: str,
155+
*,
156+
target: str,
157+
provider: str = "ntfy",
158+
url: str | None = None,
159+
url_title: str | None = None,
160+
priority: str = "normal",
161+
api_base_url: str | None = None,
162+
) -> bool:
163+
from .push import send_ntfy_push
164+
return send_ntfy_push(
165+
topic=target,
166+
message=body,
167+
title=title,
168+
url=url,
169+
priority=priority,
170+
api_base_url=api_base_url,
171+
)
172+
173+
174+
class SmtpEmailChannel:
175+
"""Default email channel — wraps send_smtp_email()."""
176+
177+
def send_email(
178+
self,
179+
subject: str,
180+
body: str,
181+
*,
182+
recipients: list[str],
183+
sender: str | None = None,
184+
smtp_host: str = "smtp.gmail.com",
185+
smtp_port: int = 465,
186+
security: str = "ssl",
187+
username: str | None = None,
188+
password: str | None = None,
189+
) -> bool:
190+
from .email import send_smtp_email
191+
return send_smtp_email(
192+
recipients=recipients,
193+
subject=subject,
194+
body=body,
195+
sender=sender,
196+
smtp_host=smtp_host,
197+
smtp_port=smtp_port,
198+
security=security,
199+
username=username,
200+
password=password,
201+
)
202+
203+
204+
class TelegramChatChannel:
205+
"""Default chat channel — wraps send_telegram_message()."""
206+
207+
def send_message(
208+
self,
209+
chat_id: str,
210+
text: str,
211+
*,
212+
token: str,
213+
api_base_url: str = "https://api.telegram.org",
214+
parse_mode: str = "HTML",
215+
) -> bool:
216+
from .telegram import send_telegram_message
217+
return send_telegram_message(
218+
chat_id=chat_id,
219+
text=text,
220+
token=token,
221+
api_base_url=api_base_url,
222+
parse_mode=parse_mode,
223+
)

0 commit comments

Comments
 (0)