diff --git a/docs/strategy_plugin_runtime_contract.md b/docs/strategy_plugin_runtime_contract.md index a6e9581..75712d2 100644 --- a/docs/strategy_plugin_runtime_contract.md +++ b/docs/strategy_plugin_runtime_contract.md @@ -113,7 +113,7 @@ SMTP should use The publisher builds the shared subject/body, prefixes platform context, returns structured sent/skipped/failed diagnostics, and can use `StrategyPluginGoogleVoiceAlertMarkerStore` to skip alert keys that were already -sent. The older `strategy_plugin_email` module remains as a compatibility alias -for deployed platforms. +sent. Platforms should expose this as Google Voice notification config, not as a +generic email alert surface. This keeps the Crisis Response plugin behavior consistent across IBKR, Schwab, LongBridge, Firstrade, and future platform runtimes. diff --git a/src/quant_platform_kit/notifications/__init__.py b/src/quant_platform_kit/notifications/__init__.py index c76f185..4be0424 100644 --- a/src/quant_platform_kit/notifications/__init__.py +++ b/src/quant_platform_kit/notifications/__init__.py @@ -2,13 +2,6 @@ from .email import parse_email_recipients, send_smtp_email from .events import NotificationPublisher, RenderedNotification, publish_rendered_notification -from .strategy_plugin_email import ( - StrategyPluginEmailAlertDelivery, - StrategyPluginEmailAlertMarkerStore, - StrategyPluginEmailAlertPublishResult, - StrategyPluginEmailSettings, - publish_strategy_plugin_email_alerts, -) from .strategy_plugin_google_voice import ( StrategyPluginGoogleVoiceAlertDelivery, StrategyPluginGoogleVoiceAlertMarkerStore, @@ -21,10 +14,6 @@ __all__ = [ "NotificationPublisher", "RenderedNotification", - "StrategyPluginEmailAlertDelivery", - "StrategyPluginEmailAlertMarkerStore", - "StrategyPluginEmailAlertPublishResult", - "StrategyPluginEmailSettings", "StrategyPluginGoogleVoiceAlertDelivery", "StrategyPluginGoogleVoiceAlertMarkerStore", "StrategyPluginGoogleVoiceAlertPublishResult", @@ -32,7 +21,6 @@ "build_strategy_plugin_alert_context_label", "parse_email_recipients", "publish_rendered_notification", - "publish_strategy_plugin_email_alerts", "publish_strategy_plugin_google_voice_alerts", "send_smtp_email", ] diff --git a/src/quant_platform_kit/notifications/strategy_plugin_email.py b/src/quant_platform_kit/notifications/strategy_plugin_email.py deleted file mode 100644 index 5f914c4..0000000 --- a/src/quant_platform_kit/notifications/strategy_plugin_email.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Backward-compatible aliases for strategy plugin Google Voice alerts.""" - -from __future__ import annotations - -from collections.abc import Callable, Sequence -from typing import Any - -from .email import send_smtp_email -from .strategy_plugin_google_voice import ( - StrategyPluginGoogleVoiceAlertDelivery, - StrategyPluginGoogleVoiceAlertMarkerStore, - StrategyPluginGoogleVoiceAlertPublishResult, - StrategyPluginGoogleVoiceSettings, - build_strategy_plugin_alert_context_label, - publish_strategy_plugin_google_voice_alerts, -) - -StrategyPluginEmailSettings = StrategyPluginGoogleVoiceSettings -StrategyPluginEmailAlertDelivery = StrategyPluginGoogleVoiceAlertDelivery -StrategyPluginEmailAlertPublishResult = StrategyPluginGoogleVoiceAlertPublishResult -StrategyPluginEmailAlertMarkerStore = StrategyPluginGoogleVoiceAlertMarkerStore - -__all__ = [ - "StrategyPluginEmailSettings", - "StrategyPluginEmailAlertDelivery", - "StrategyPluginEmailAlertPublishResult", - "StrategyPluginEmailAlertMarkerStore", - "build_strategy_plugin_alert_context_label", - "publish_strategy_plugin_email_alerts", -] - - -def publish_strategy_plugin_email_alerts( - signals: Sequence[object], - *, - email_settings: StrategyPluginEmailSettings | object, - translator: Callable[..., str] | None = None, - strategy_label: str | None = None, - context_label: str | None = None, - alert_store: StrategyPluginEmailAlertMarkerStore | object | None = None, - send_email: Callable[..., bool] = send_smtp_email, - log_message: Callable[..., Any] = print, -) -> StrategyPluginEmailAlertPublishResult: - return publish_strategy_plugin_google_voice_alerts( - signals, - google_voice_settings=email_settings, - translator=translator, - strategy_label=strategy_label, - context_label=context_label, - alert_store=alert_store, - send_notification=send_email, - log_message=log_message, - ) diff --git a/src/quant_platform_kit/notifications/strategy_plugin_google_voice.py b/src/quant_platform_kit/notifications/strategy_plugin_google_voice.py index aadb397..6de417b 100644 --- a/src/quant_platform_kit/notifications/strategy_plugin_google_voice.py +++ b/src/quant_platform_kit/notifications/strategy_plugin_google_voice.py @@ -37,15 +37,8 @@ def from_object(cls, value: object) -> "StrategyPluginGoogleVoiceSettings": return cls( smtp_host=_get_value(value, "crisis_alert_smtp_host"), smtp_port=int(_get_value(value, "crisis_alert_smtp_port", 587) or 587), - sender=_first_non_empty( - _get_value(value, "crisis_alert_smtp_from"), - _get_value(value, "crisis_alert_google_voice_from"), - _get_value(value, "crisis_alert_email_from"), - ), - recipients=_merge_recipients( - _get_value(value, "crisis_alert_google_voice_to", ()), - _get_value(value, "crisis_alert_email_to", ()), - ), + sender=_first_non_empty(_get_value(value, "crisis_alert_smtp_from")), + recipients=tuple(parse_email_recipients(_get_value(value, "crisis_alert_google_voice_to", ()))), username=_get_value(value, "crisis_alert_smtp_username"), password=_get_value(value, "crisis_alert_smtp_password"), use_starttls=_coerce_bool(_get_value(value, "crisis_alert_smtp_starttls", True), default=True), @@ -57,9 +50,9 @@ def missing_fields(self) -> tuple[str, ...]: if not str(self.smtp_host or "").strip(): missing.append("CRISIS_ALERT_SMTP_HOST") if not str(self.sender or "").strip(): - missing.append("CRISIS_ALERT_SMTP_FROM/CRISIS_ALERT_EMAIL_FROM") + missing.append("CRISIS_ALERT_SMTP_FROM") if not parse_email_recipients(self.recipients): - missing.append("CRISIS_ALERT_GOOGLE_VOICE_TO/CRISIS_ALERT_EMAIL_TO") + missing.append("CRISIS_ALERT_GOOGLE_VOICE_TO") return tuple(missing) @property @@ -124,16 +117,13 @@ class StrategyPluginGoogleVoiceAlertMarkerStore: gcs_prefix_uri: str | None = None gcp_project_id: str | None = None namespace: str = "strategy_plugin_google_voice_alerts" - legacy_namespaces: tuple[str, ...] = ("strategy_plugin_email_alerts",) client_factory: Any = None def has_alert(self, alert_key: str) -> bool: - for candidate_key in _alert_key_candidates(alert_key): - for namespace in (self.namespace, *self.legacy_namespaces): - if self.gcs_prefix_uri and self._gcs_blob(candidate_key, namespace=namespace).exists(): - return True - if self.local_dir and self._local_path(candidate_key, namespace=namespace).exists(): - return True + if self.gcs_prefix_uri and self._gcs_blob(alert_key, namespace=self.namespace).exists(): + return True + if self.local_dir and self._local_path(alert_key, namespace=self.namespace).exists(): + return True return False def record_alert( @@ -377,18 +367,6 @@ def _first_non_empty(*values: Any) -> str | None: return None -def _merge_recipients(*values: Any) -> tuple[str, ...]: - recipients: list[str] = [] - seen = set() - for value in values: - for recipient in parse_email_recipients(value): - if recipient in seen: - continue - recipients.append(recipient) - seen.add(recipient) - return tuple(recipients) - - def _coerce_bool(value: Any, *, default: bool) -> bool: if value is None: return default @@ -404,18 +382,6 @@ def _fallback_alert_key(message: StrategyPluginAlertMessage) -> str: return "strategy_plugin_google_voice_alert/" + _clean_relative_key(message.subject or "unknown") -def _alert_key_candidates(alert_key: str) -> tuple[str, ...]: - key = str(alert_key or "") - legacy_key = key.replace( - "strategy_plugin_google_voice_alert/", - "strategy_plugin_email_alert/", - 1, - ) - if legacy_key != key: - return (key, legacy_key) - return (key,) - - def _clean_relative_key(value: str) -> str: parts = [] for raw_part in str(value or "").replace("\\", "/").split("/"): diff --git a/tests/test_email.py b/tests/test_google_voice_notifications.py similarity index 89% rename from tests/test_email.py rename to tests/test_google_voice_notifications.py index d1fa0aa..0dff4a3 100644 --- a/tests/test_email.py +++ b/tests/test_google_voice_notifications.py @@ -148,29 +148,16 @@ def test_publish_strategy_plugin_google_voice_alerts_skips_duplicate_marker(tmp_ assert second.deliveries[0].reason == "duplicate_alert" -def test_google_voice_marker_store_reads_legacy_email_namespace(tmp_path): - store = StrategyPluginGoogleVoiceAlertMarkerStore(local_dir=tmp_path) - legacy_store = StrategyPluginGoogleVoiceAlertMarkerStore( - local_dir=tmp_path, - namespace="strategy_plugin_email_alerts", - legacy_namespaces=(), - ) - legacy_store.record_alert("strategy_plugin_email_alert/example") - - assert store.has_alert("strategy_plugin_google_voice_alert/example") - - -def test_google_voice_settings_read_new_names_and_legacy_email_recipients(): +def test_google_voice_settings_read_google_voice_names_only(): settings = StrategyPluginGoogleVoiceSettings.from_object( SimpleNamespace( crisis_alert_smtp_host="smtp.gmail.com", crisis_alert_smtp_from="sender@gmail.com", crisis_alert_google_voice_to="gateway@txt.voice.google.com", - crisis_alert_email_to="ops@example.com,gateway@txt.voice.google.com", crisis_alert_smtp_username="sender@gmail.com", ) ) assert settings.sender == "sender@gmail.com" - assert settings.recipients == ("gateway@txt.voice.google.com", "ops@example.com") + assert settings.recipients == ("gateway@txt.voice.google.com",) assert settings.missing_fields() == ()