Skip to content

Commit 914a205

Browse files
Pigbibiclaude
andauthored
fix(notifications): resolve email.py stdlib collision (#122)
Rename email.py → _email.py to avoid shadowing Python's stdlib 'email' package, which breaks smtplib imports when pytest/plugins load this module. Keep email.py as a backward-compat re-export shim. Internal imports updated to use _email directly (3 files). 373 passed, 0 skipped (email-related). Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5bc321b commit 914a205

5 files changed

Lines changed: 79 additions & 65 deletions

File tree

src/quant_platform_kit/notifications/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
SmtpEmailChannel,
1212
TelegramChatChannel,
1313
)
14-
from .email import parse_email_recipients, send_smtp_email
14+
from ._email import parse_email_recipients, send_smtp_email
1515
from .events import NotificationPublisher, RenderedNotification, publish_rendered_notification
1616
from .push import parse_push_recipients, send_ntfy_push, send_pushover_push, send_strategy_plugin_push
1717
from .sms import normalize_sms_recipient, parse_sms_recipients, send_twilio_sms
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""SMTP email notification helpers."""
2+
3+
from __future__ import annotations
4+
5+
import smtplib
6+
from collections.abc import Sequence
7+
from email.message import EmailMessage
8+
9+
10+
def parse_email_recipients(raw_value: str | Sequence[str] | None) -> tuple[str, ...]:
11+
if raw_value is None:
12+
return ()
13+
if isinstance(raw_value, str):
14+
values = raw_value.replace(";", ",").replace("\n", ",").split(",")
15+
else:
16+
values = raw_value
17+
recipients = []
18+
seen = set()
19+
for value in values:
20+
recipient = str(value or "").strip()
21+
if not recipient or recipient in seen:
22+
continue
23+
recipients.append(recipient)
24+
seen.add(recipient)
25+
return tuple(recipients)
26+
27+
28+
def send_smtp_email(
29+
*,
30+
subject: str,
31+
body: str,
32+
smtp_host: str | None,
33+
smtp_port: int,
34+
sender: str | None,
35+
recipients: Sequence[str],
36+
username: str | None = None,
37+
password: str | None = None,
38+
use_starttls: bool = True,
39+
use_ssl: bool = False,
40+
timeout: float = 10.0,
41+
smtp_module=smtplib,
42+
printer=print,
43+
) -> bool:
44+
resolved_recipients = parse_email_recipients(recipients)
45+
host = str(smtp_host or "").strip()
46+
from_addr = str(sender or "").strip()
47+
if not host or not from_addr or not resolved_recipients:
48+
return False
49+
50+
message = EmailMessage()
51+
message["From"] = from_addr
52+
message["To"] = ", ".join(resolved_recipients)
53+
message["Subject"] = str(subject or "").strip() or "strategy alert"
54+
message.set_content(str(body or "").strip())
55+
56+
try:
57+
smtp_cls = smtp_module.SMTP_SSL if use_ssl else smtp_module.SMTP
58+
with smtp_cls(host, int(smtp_port), timeout=timeout) as smtp:
59+
if use_starttls and not use_ssl:
60+
smtp.starttls()
61+
if username:
62+
smtp.login(str(username), str(password or ""))
63+
smtp.send_message(message)
64+
return True
65+
except Exception as exc:
66+
printer(f"Email send failed: {exc}", flush=True)
67+
return False
Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,14 @@
1-
"""SMTP email notification helpers."""
1+
"""Backward-compatibility shim — delegates to _email to avoid stdlib collision.
22
3-
from __future__ import annotations
3+
The SMTP helpers live in ``_email.py`` because ``email.py`` shadows
4+
Python's stdlib ``email`` package, breaking ``smtplib`` imports in some
5+
environments (e.g. when pytest's plugin loader runs alongside this package).
46
5-
import smtplib
6-
from collections.abc import Sequence
7-
from email.message import EmailMessage
7+
Prefer importing from the package root::
88
9+
from quant_platform_kit.notifications import parse_email_recipients, send_smtp_email
10+
"""
911

10-
def parse_email_recipients(raw_value: str | Sequence[str] | None) -> tuple[str, ...]:
11-
if raw_value is None:
12-
return ()
13-
if isinstance(raw_value, str):
14-
values = raw_value.replace(";", ",").replace("\n", ",").split(",")
15-
else:
16-
values = raw_value
17-
recipients = []
18-
seen = set()
19-
for value in values:
20-
recipient = str(value or "").strip()
21-
if not recipient or recipient in seen:
22-
continue
23-
recipients.append(recipient)
24-
seen.add(recipient)
25-
return tuple(recipients)
12+
from ._email import parse_email_recipients, send_smtp_email
2613

27-
28-
def send_smtp_email(
29-
*,
30-
subject: str,
31-
body: str,
32-
smtp_host: str | None,
33-
smtp_port: int,
34-
sender: str | None,
35-
recipients: Sequence[str],
36-
username: str | None = None,
37-
password: str | None = None,
38-
use_starttls: bool = True,
39-
use_ssl: bool = False,
40-
timeout: float = 10.0,
41-
smtp_module=smtplib,
42-
printer=print,
43-
) -> bool:
44-
resolved_recipients = parse_email_recipients(recipients)
45-
host = str(smtp_host or "").strip()
46-
from_addr = str(sender or "").strip()
47-
if not host or not from_addr or not resolved_recipients:
48-
return False
49-
50-
message = EmailMessage()
51-
message["From"] = from_addr
52-
message["To"] = ", ".join(resolved_recipients)
53-
message["Subject"] = str(subject or "").strip() or "strategy alert"
54-
message.set_content(str(body or "").strip())
55-
56-
try:
57-
smtp_cls = smtp_module.SMTP_SSL if use_ssl else smtp_module.SMTP
58-
with smtp_cls(host, int(smtp_port), timeout=timeout) as smtp:
59-
if use_starttls and not use_ssl:
60-
smtp.starttls()
61-
if username:
62-
smtp.login(str(username), str(password or ""))
63-
smtp.send_message(message)
64-
return True
65-
except Exception as exc:
66-
printer(f"Email send failed: {exc}", flush=True)
67-
return False
14+
__all__ = ["parse_email_recipients", "send_smtp_email"]

src/quant_platform_kit/notifications/strategy_plugin_alerts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pathlib import Path
99
from typing import Any
1010

11-
from .email import send_smtp_email
11+
from ._email import send_smtp_email
1212
from .push import send_strategy_plugin_push
1313
from .sms import send_twilio_sms
1414
from .telegram import send_strategy_plugin_telegram

src/quant_platform_kit/notifications/strategy_plugin_email.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
)
1515

1616
from .alert_marker import CloudAlertMarkerStore, _clean_relative_key
17-
from .email import parse_email_recipients, send_smtp_email
17+
from ._email import parse_email_recipients, send_smtp_email
1818

1919

2020
_DEFAULT_EMAIL_SMTP_HOST = "smtp.gmail.com"

0 commit comments

Comments
 (0)