From d8b6fbcb21910b0e1cbe9eaa427621aacd911e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20D=C3=ADaz?= Date: Thu, 9 Oct 2025 13:41:31 -0300 Subject: [PATCH 1/5] feature(settings): Add env vars and settings for sendgrid --- .env.example | 2 ++ settings.py | 4 ++++ 2 files changed, 6 insertions(+) create mode 100644 .env.example create mode 100644 settings.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..973d3aa --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +SENDGRID_API_KEY=dump_api_key +SENDGRID_FROM_EMAIL=dump_from_email \ No newline at end of file diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..447d6e6 --- /dev/null +++ b/settings.py @@ -0,0 +1,4 @@ +import os + +SENDGRID_API_KEY = os.environ.get("SENDGRID_API_KEY") +SENDGRID_FROM_EMAIL = os.environ.get("SENDGRID_FROM_EMAIL") From 1cd3c63abcd8302f80095b2f0a79349e903b18c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20D=C3=ADaz?= Date: Thu, 9 Oct 2025 13:41:49 -0300 Subject: [PATCH 2/5] chore(requirements): Add ipython as local requirements --- requirements/local.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements/local.txt b/requirements/local.txt index e4f2440..aa95f04 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -1,4 +1,5 @@ -r base.txt pre-commit==4.3.0 -setuptools==80.9.0 \ No newline at end of file +setuptools==80.9.0 +ipython==9.6.0 \ No newline at end of file From bcf9794dead3b446a30bd4234b2126986941dd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20D=C3=ADaz?= Date: Thu, 9 Oct 2025 13:42:03 -0300 Subject: [PATCH 3/5] chore(requirements): Add sendgrid as base requirements --- requirements/base.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements/base.txt b/requirements/base.txt index 51570ac..2986aca 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,3 +1,4 @@ black==25.9.0 pytest==8.4.2 -pytest-mock==3.15.1 \ No newline at end of file +pytest-mock==3.15.1 +sendgrid==6.12.5 \ No newline at end of file From d61b52a0bbc796b1c206876797e33f00b496e271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20D=C3=ADaz?= Date: Thu, 9 Oct 2025 13:42:24 -0300 Subject: [PATCH 4/5] chore(envvars): Add instructions about the env.example use --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 6c3582a..afc2d44 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,9 @@ docker compose build docker compose up ``` +## Env vars +Copy `.env.example` to `.env` and adjust as needed. + Notes: - The docker-compose.yml mounts the project into /app and runs `pytest -q` by default. - Stop the stack with Ctrl+C (in the same terminal) or by running `docker compose down` in another terminal. From 079aab990ae92c2e833d8fc7d14ce1bf06d90adb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20D=C3=ADaz?= Date: Thu, 9 Oct 2025 13:43:16 -0300 Subject: [PATCH 5/5] feature(sendgrid): Add Sendgrid client --- mailing/README.md | 67 +++++++++++++++++++++++++++++++++++++++++++++ mailing/__init__.py | 0 mailing/mailing.py | 49 +++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 mailing/README.md create mode 100644 mailing/__init__.py create mode 100644 mailing/mailing.py diff --git a/mailing/README.md b/mailing/README.md new file mode 100644 index 0000000..2c0502e --- /dev/null +++ b/mailing/README.md @@ -0,0 +1,67 @@ +Mailing module — using mailing.mailing.Email + +This module wraps SendGrid to send emails from your Python project. + +Prerequisites +- Install dependencies (SendGrid is already listed in requirements): + pip install -r requirements/base.txt +- Set required environment variables (used by settings.py): + - SENDGRID_API_KEY: Your SendGrid API key (optional if you pass api_key to Email). + - SENDGRID_FROM_EMAIL: Default sender email (must be verified in SendGrid). + +Basic usage +1) Import and construct an Email instance +```python +from mailing.mailing import Email + +email = Email( + subject="Hello from python-mailing", + message="This is the plain-text fallback body.", + # recipient_list=["recipient@example.com"], + # Optional overrides (each parameter is optional): + # from_email="no-reply@yourdomain.com", + # api_key="SG.xxxxxx.yyyyyy", # overrides settings.SENDGRID_API_KEY for this Email + html_content="

This is the HTML body.

", +) +# Send with SendGrid +response = email.send_sendgrid_email() +if response is not None: + print("Sent! Status:", response.status_code) +else: + print("Sending failed — see logs for details.") +``` + +Notes and tips +- recipient_list can include multiple addresses: + Email(subject="...", message="...", recipient_list=["a@x.com", "b@y.com"]). +- If you don’t need HTML, omit html_content and only plain text will be sent. +- You can override the default sender by passing from_email. If not provided, settings.FROM_EMAIL is used. +- You can override the SendGrid API key per email by passing api_key to Email(...). If not provided, settings.SENDGRID_API_KEY is used. +- On exceptions, send_sendgrid_email returns None and logs the error (including SendGrid error body when available). +- Make sure your SENDGRID_FROM_EMAIL and any sender domain are verified in SendGrid to avoid 403/unauthorized or 400 errors. +- Ensure SENDGRID_API_KEY has Mail Send permissions. + +Example with multiple recipients and custom from_email +```python +email = Email( + subject="Weekly report", + message="See the attached summary.", + recipient_list=["team1@example.com", "team2@example.com"], + from_email="reports@yourdomain.com", +) +email.send_sendgrid_email() +``` + + +Example overriding SendGrid API key per Email instance +```python +from mailing.mailing import Email + +email = Email( + subject="On-demand API key", + message="Body", + recipient_list=["user@example.com"], + api_key="SG.xxxxxx.yyyyyy", # this value will be used instead of settings.SENDGRID_API_KEY +) +email.send_sendgrid_email() +``` diff --git a/mailing/__init__.py b/mailing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mailing/mailing.py b/mailing/mailing.py new file mode 100644 index 0000000..6b6fc45 --- /dev/null +++ b/mailing/mailing.py @@ -0,0 +1,49 @@ +"""Send emails.""" + +import logging +from typing import Any + +from sendgrid import SendGridAPIClient, Mail + +import settings + +logger = logging.getLogger(__name__) + + +class Email: + def __init__( + self, + subject: str, + message: str, + recipient_list: list[str], + from_email: str = None, + html_content: str = None, + api_key: str = None, + **kwargs: Any, + ): + """Initialize the Email class.""" + self.subject = subject + self.message = message + self.from_email = from_email or settings.SENDGRID_FROM_EMAIL + self.recipient_list = recipient_list + self.html_content = html_content + self.api_key = api_key + + def send_sendgrid_email(self, **kwargs: Any) -> Any | None: + """Send an email using SendGrid.""" + sg = SendGridAPIClient(self.api_key or settings.SENDGRID_API_KEY) + message = Mail( + from_email=self.from_email, + to_emails=self.recipient_list, + subject=self.subject, + html_content=self.html_content or None, + plain_text_content=self.message, + ) + try: + response = sg.send(message) + return response + except Exception as e: + if hasattr(e, "body"): + logger.error("SendGrid error body: %s", e.body) + logger.exception("SendGrid exception") + return None