Skip to content

Commit 43b2ecb

Browse files
authored
Generalize Google Voice sender config (#54)
1 parent 705667f commit 43b2ecb

3 files changed

Lines changed: 124 additions & 32 deletions

File tree

docs/strategy_plugin_runtime_contract.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,16 @@ also surface the Google Voice prompt. The public configuration names should be
139139
channel specific:
140140

141141
- `CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS`
142-
- `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER`
143-
- `CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD`
142+
- `CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL`
143+
- `CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD`
144+
145+
By default the transport uses Gmail SMTP (`smtp.gmail.com`, port `465`, SSL),
146+
but the sender is not part of the Google Voice channel contract. Non-Gmail
147+
senders can override:
148+
149+
- `CRISIS_ALERT_GOOGLE_VOICE_SMTP_HOST`
150+
- `CRISIS_ALERT_GOOGLE_VOICE_SMTP_PORT`
151+
- `CRISIS_ALERT_GOOGLE_VOICE_SMTP_SECURITY` (`ssl`, `starttls`, or `none`)
144152

145153
Future direct email notifications should use a separate namespace such as
146154
`CRISIS_ALERT_EMAIL_*`.

src/quant_platform_kit/notifications/strategy_plugin_google_voice.py

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,27 @@
1818
from .email import parse_email_recipients, send_smtp_email
1919

2020

21-
_GOOGLE_VOICE_SMTP_HOST = "smtp.gmail.com"
22-
_GOOGLE_VOICE_SMTP_PORT = 465
23-
_GOOGLE_VOICE_SMTP_STARTTLS = False
24-
_GOOGLE_VOICE_SMTP_SSL = True
21+
_DEFAULT_GOOGLE_VOICE_SMTP_HOST = "smtp.gmail.com"
22+
_DEFAULT_GOOGLE_VOICE_SMTP_PORT = 465
23+
_DEFAULT_GOOGLE_VOICE_SMTP_SECURITY = "ssl"
24+
_SMTP_SECURITY_NONE = "none"
25+
_SMTP_SECURITY_SSL = "ssl"
26+
_SMTP_SECURITY_STARTTLS = "starttls"
27+
_SMTP_SECURITY_VALUES = {
28+
_SMTP_SECURITY_NONE,
29+
_SMTP_SECURITY_SSL,
30+
_SMTP_SECURITY_STARTTLS,
31+
}
2532

2633

2734
@dataclass(frozen=True)
2835
class StrategyPluginGoogleVoiceSettings:
2936
recipients: tuple[str, ...] = ()
30-
gmail_user: str | None = None
31-
gmail_app_password: str | None = field(default=None, repr=False)
37+
sender_email: str | None = None
38+
sender_password: str | None = field(default=None, repr=False)
39+
smtp_host: str = _DEFAULT_GOOGLE_VOICE_SMTP_HOST
40+
smtp_port: int = _DEFAULT_GOOGLE_VOICE_SMTP_PORT
41+
smtp_security: str = _DEFAULT_GOOGLE_VOICE_SMTP_SECURITY
3242
timeout: float = 10.0
3343

3444
@classmethod
@@ -39,18 +49,29 @@ def from_object(cls, value: object) -> "StrategyPluginGoogleVoiceSettings":
3949
recipients=tuple(
4050
parse_email_recipients(_get_value(value, "crisis_alert_google_voice_recipients", ()))
4151
),
42-
gmail_user=_first_non_empty(_get_value(value, "crisis_alert_google_voice_gmail_user")),
43-
gmail_app_password=_get_value(value, "crisis_alert_google_voice_gmail_app_password"),
52+
sender_email=_first_non_empty(_get_value(value, "crisis_alert_google_voice_sender_email")),
53+
sender_password=_get_value(value, "crisis_alert_google_voice_sender_password"),
54+
smtp_host=_first_non_empty(
55+
_get_value(value, "crisis_alert_google_voice_smtp_host")
56+
)
57+
or _DEFAULT_GOOGLE_VOICE_SMTP_HOST,
58+
smtp_port=_coerce_int(
59+
_get_value(value, "crisis_alert_google_voice_smtp_port"),
60+
_DEFAULT_GOOGLE_VOICE_SMTP_PORT,
61+
),
62+
smtp_security=_coerce_smtp_security(
63+
_get_value(value, "crisis_alert_google_voice_smtp_security")
64+
),
4465
)
4566

4667
def missing_fields(self) -> tuple[str, ...]:
4768
missing: list[str] = []
4869
if not parse_email_recipients(self.recipients):
4970
missing.append("CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS")
50-
if not str(self.gmail_user or "").strip():
51-
missing.append("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER")
52-
if not str(self.gmail_app_password or "").strip():
53-
missing.append("CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD")
71+
if not str(self.sender_email or "").strip():
72+
missing.append("CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL")
73+
if not str(self.sender_password or "").strip():
74+
missing.append("CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD")
5475
return tuple(missing)
5576

5677
@property
@@ -278,14 +299,14 @@ def _send_message(
278299
sent = send_notification(
279300
subject=message.subject,
280301
body=message.body,
281-
smtp_host=_GOOGLE_VOICE_SMTP_HOST,
282-
smtp_port=_GOOGLE_VOICE_SMTP_PORT,
283-
sender=settings.gmail_user,
302+
smtp_host=settings.smtp_host,
303+
smtp_port=settings.smtp_port,
304+
sender=settings.sender_email,
284305
recipients=settings.recipients,
285-
username=settings.gmail_user,
286-
password=settings.gmail_app_password,
287-
use_starttls=_GOOGLE_VOICE_SMTP_STARTTLS,
288-
use_ssl=_GOOGLE_VOICE_SMTP_SSL,
306+
username=settings.sender_email,
307+
password=settings.sender_password,
308+
use_starttls=settings.smtp_security == _SMTP_SECURITY_STARTTLS,
309+
use_ssl=settings.smtp_security == _SMTP_SECURITY_SSL,
289310
timeout=settings.timeout,
290311
)
291312
except Exception as exc:
@@ -365,6 +386,23 @@ def _first_non_empty(*values: Any) -> str | None:
365386
return None
366387

367388

389+
def _coerce_int(value: Any, default: int) -> int:
390+
text = str(value or "").strip()
391+
if not text:
392+
return default
393+
try:
394+
return int(text)
395+
except (TypeError, ValueError):
396+
return default
397+
398+
399+
def _coerce_smtp_security(value: Any) -> str:
400+
security = str(value or "").strip().lower()
401+
if security in _SMTP_SECURITY_VALUES:
402+
return security
403+
return _DEFAULT_GOOGLE_VOICE_SMTP_SECURITY
404+
405+
368406
def _fallback_alert_key(message: StrategyPluginAlertMessage) -> str:
369407
return "strategy_plugin_google_voice_alert/" + _clean_relative_key(message.subject or "unknown")
370408

tests/test_google_voice_notifications.py

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ def test_publish_strategy_plugin_google_voice_alerts_skips_missing_config():
8787
assert result.skipped_count == 1
8888
assert result.deliveries[0].reason == "missing_google_voice_config"
8989
assert "CRISIS_ALERT_GOOGLE_VOICE_RECIPIENTS" in result.deliveries[0].error
90-
assert "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_USER" in result.deliveries[0].error
91-
assert "CRISIS_ALERT_GOOGLE_VOICE_GMAIL_APP_PASSWORD" in result.deliveries[0].error
90+
assert "CRISIS_ALERT_GOOGLE_VOICE_SENDER_EMAIL" in result.deliveries[0].error
91+
assert "CRISIS_ALERT_GOOGLE_VOICE_SENDER_PASSWORD" in result.deliveries[0].error
9292
assert observed == []
9393

9494

@@ -100,8 +100,8 @@ def test_publish_strategy_plugin_google_voice_alerts_sends_and_records_marker(tm
100100
[_alert_signal()],
101101
google_voice_settings=StrategyPluginGoogleVoiceSettings(
102102
recipients=("risk@example.com",),
103-
gmail_user="bot@example.com",
104-
gmail_app_password="app-password",
103+
sender_email="bot@example.com",
104+
sender_password="app-password",
105105
),
106106
strategy_label="TQQQ",
107107
context_label="ibkr / paper / tqqq",
@@ -129,8 +129,8 @@ def test_publish_strategy_plugin_google_voice_alerts_skips_duplicate_marker(tmp_
129129
store = StrategyPluginGoogleVoiceAlertMarkerStore(local_dir=tmp_path)
130130
settings = StrategyPluginGoogleVoiceSettings(
131131
recipients=("risk@example.com",),
132-
gmail_user="bot@example.com",
133-
gmail_app_password="app-password",
132+
sender_email="bot@example.com",
133+
sender_password="app-password",
134134
)
135135
first = publish_strategy_plugin_google_voice_alerts(
136136
[_alert_signal()],
@@ -158,16 +158,62 @@ def test_publish_strategy_plugin_google_voice_alerts_skips_duplicate_marker(tmp_
158158
assert second.deliveries[0].reason == "duplicate_alert"
159159

160160

161-
def test_google_voice_settings_reads_google_voice_recipient_names_only():
161+
def test_publish_strategy_plugin_google_voice_alerts_uses_transport_overrides():
162+
observed = []
163+
164+
result = publish_strategy_plugin_google_voice_alerts(
165+
[_alert_signal()],
166+
google_voice_settings=StrategyPluginGoogleVoiceSettings(
167+
recipients=("voice@example.com",),
168+
sender_email="bot@example.com",
169+
sender_password="secret",
170+
smtp_host="smtp.example.com",
171+
smtp_port=587,
172+
smtp_security="starttls",
173+
),
174+
strategy_label="TQQQ",
175+
context_label="ibkr / paper / tqqq",
176+
send_notification=lambda **kwargs: observed.append(kwargs) or True,
177+
log_message=lambda *_args, **_kwargs: None,
178+
)
179+
180+
assert result.sent_count == 1
181+
assert observed[0]["smtp_host"] == "smtp.example.com"
182+
assert observed[0]["smtp_port"] == 587
183+
assert observed[0]["use_starttls"] is True
184+
assert observed[0]["use_ssl"] is False
185+
186+
187+
def test_google_voice_settings_reads_sender_and_default_transport_names_only():
162188
settings = StrategyPluginGoogleVoiceSettings.from_object(
163189
SimpleNamespace(
164190
crisis_alert_google_voice_recipients="alerts@example.com; voice@example.com",
165-
crisis_alert_google_voice_gmail_user="sender@gmail.com",
166-
crisis_alert_google_voice_gmail_app_password="app-password",
191+
crisis_alert_google_voice_sender_email="sender@example.com",
192+
crisis_alert_google_voice_sender_password="app-password",
167193
)
168194
)
169195

170-
assert settings.gmail_user == "sender@gmail.com"
196+
assert settings.sender_email == "sender@example.com"
171197
assert settings.recipients == ("alerts@example.com", "voice@example.com")
172-
assert settings.gmail_app_password == "app-password"
198+
assert settings.sender_password == "app-password"
199+
assert settings.smtp_host == "smtp.gmail.com"
200+
assert settings.smtp_port == 465
201+
assert settings.smtp_security == "ssl"
173202
assert settings.missing_fields() == ()
203+
204+
205+
def test_google_voice_settings_reads_optional_smtp_transport_overrides():
206+
settings = StrategyPluginGoogleVoiceSettings.from_object(
207+
SimpleNamespace(
208+
crisis_alert_google_voice_recipients="voice@example.com",
209+
crisis_alert_google_voice_sender_email="sender@example.com",
210+
crisis_alert_google_voice_sender_password="secret",
211+
crisis_alert_google_voice_smtp_host="smtp.example.com",
212+
crisis_alert_google_voice_smtp_port="587",
213+
crisis_alert_google_voice_smtp_security="starttls",
214+
)
215+
)
216+
217+
assert settings.smtp_host == "smtp.example.com"
218+
assert settings.smtp_port == 587
219+
assert settings.smtp_security == "starttls"

0 commit comments

Comments
 (0)