Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/strategy_plugin_runtime_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
12 changes: 0 additions & 12 deletions src/quant_platform_kit/notifications/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,18 +14,13 @@
__all__ = [
"NotificationPublisher",
"RenderedNotification",
"StrategyPluginEmailAlertDelivery",
"StrategyPluginEmailAlertMarkerStore",
"StrategyPluginEmailAlertPublishResult",
"StrategyPluginEmailSettings",
"StrategyPluginGoogleVoiceAlertDelivery",
"StrategyPluginGoogleVoiceAlertMarkerStore",
"StrategyPluginGoogleVoiceAlertPublishResult",
"StrategyPluginGoogleVoiceSettings",
"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",
]
53 changes: 0 additions & 53 deletions src/quant_platform_kit/notifications/strategy_plugin_email.py

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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("/"):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() == ()