PawTrace / backend /app /services /notify.py
Elliott Duke
PawTrace: HF demo image with dog dataset + demo photos
6213763
Raw
History Blame Contribute Delete
2.79 kB
"""Notifier interface + console (default) and SMTP backends (spec §10).
Console backend writes to stdout/log and persists a notifications row; real SMTP only when
env-configured. SMS is an off-by-default adapter (paid + network — do not enable without
confirmation).
"""
from __future__ import annotations
import logging
import smtplib
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from email.message import EmailMessage
from sqlalchemy.orm import Session
from ..config import settings
from ..models import Notification
from ..models.base import NotificationChannel, NotificationStatus
logger = logging.getLogger("pawtrace.notify")
class Notifier(ABC):
@abstractmethod
def send(self, *, to: str, subject: str, body: str) -> bool:
"""Deliver a message; return True on success."""
class ConsoleNotifier(Notifier):
def send(self, *, to: str, subject: str, body: str) -> bool:
logger.info("EMAIL -> %s | %s\n%s", to, subject, body)
print(f"\n[EMAIL] to={to} subject={subject!r}\n{body}\n")
return True
class SMTPNotifier(Notifier): # pragma: no cover - requires a live/dev SMTP server
def send(self, *, to: str, subject: str, body: str) -> bool:
msg = EmailMessage()
msg["From"] = settings.smtp_from
msg["To"] = to
msg["Subject"] = subject
msg.set_content(body)
try:
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=10) as s:
if settings.smtp_user:
s.starttls()
s.login(settings.smtp_user, settings.smtp_password)
s.send_message(msg)
return True
except Exception: # noqa: BLE001
logger.exception("SMTP send failed")
return False
def get_notifier() -> Notifier:
if settings.notifier == "smtp" and settings.smtp_host:
return SMTPNotifier()
return ConsoleNotifier()
def send_notification(
db: Session,
*,
user_id: int | None,
to: str,
subject: str,
body: str,
case_id: int | None = None,
match_id: int | None = None,
) -> Notification:
"""Send via the configured Notifier and persist a notifications row (spec §7.8)."""
notif = Notification(
user_id=user_id,
case_id=case_id,
match_id=match_id,
channel=NotificationChannel.email,
to_address=to,
payload=f"{subject}\n\n{body}",
status=NotificationStatus.queued,
)
db.add(notif)
db.flush()
ok = get_notifier().send(to=to, subject=subject, body=body)
notif.status = NotificationStatus.sent if ok else NotificationStatus.failed
if ok:
notif.sent_at = datetime.now(timezone.utc)
db.flush()
return notif