Skip to content

Commit

Permalink
Merge branch 'setup/sendgrid' of github.com:HackAtUCI/irvinehacks-sit…
Browse files Browse the repository at this point in the history
…e-2024 into setup/sendgrid
  • Loading branch information
samderanova committed Dec 14, 2023
2 parents c45a410 + 703c09e commit da42bde
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 46 deletions.
80 changes: 60 additions & 20 deletions apps/api/src/services/sendgrid_handler.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# using SendGrid's Python Library
# https://github.com/sendgrid/sendgrid-python
import os
from enum import Enum
from logging import getLogger
from typing import Iterable, Tuple, TypedDict, Union
from typing_extensions import NotRequired
from typing import Iterable, Literal, Tuple, TypedDict, Union, overload

import aiosendgrid
from httpx import HTTPStatusError
Expand All @@ -14,15 +14,57 @@
SENDGRID_API_KEY = os.getenv("SENDGRID_API_KEY")


class Template(str, Enum):
# TODO: provide actual template IDs
CONFIRMATION_EMAIL = "d-2026cde7bebd45ad85723443808c5817"
GUEST_TOKEN = "d-b19f08e584cb4c0f97b55f567ee10afc"


class PersonalizationData(TypedDict):
email: str
first_name: NotRequired[str]
last_name: NotRequired[str]
passphrase: NotRequired[str]


class ConfirmationPersonalization(PersonalizationData):
first_name: str
last_name: str


class GuestTokenPersonalization(PersonalizationData):
passphrase: str


@overload
async def send_email(
template_id: str,
template_id: Literal[Template.CONFIRMATION_EMAIL],
sender_email: Tuple[str, str],
receiver_data: ConfirmationPersonalization,
send_to_multiple: Literal[False] = False,
) -> None:
...


@overload
async def send_email(
template_id: Literal[Template.GUEST_TOKEN],
sender_email: Tuple[str, str],
receiver_data: GuestTokenPersonalization,
send_to_multiple: Literal[False] = False,
) -> None:
...


@overload
async def send_email(
template_id: Literal[Template.CONFIRMATION_EMAIL],
sender_email: Tuple[str, str],
receiver_data: Iterable[ConfirmationPersonalization],
send_to_multiple: Literal[True],
) -> None:
...


async def send_email(
template_id: Template,
sender_email: Tuple[str, str],
receiver_data: Union[PersonalizationData, Iterable[PersonalizationData]],
send_to_multiple: bool = False,
Expand All @@ -34,29 +76,27 @@ async def send_email(
email_message = Mail()

if send_to_multiple:
if not isinstance(receiver_data, list):
if isinstance(receiver_data, dict):
raise TypeError(
f"Expected {list} for receiver_data but got {type(receiver_data)}"
)
else:
for r in receiver_data:
p = Personalization()
p.add_to(Email(email=r["email"], dynamic_template_data=r))
email_message.add_personalization(p)
for r in receiver_data:
p = Personalization()
p.add_to(Email(email=r["email"], dynamic_template_data=r))
email_message.add_personalization(p)
else:
if not isinstance(receiver_data, dict):
raise TypeError(
f"Expected {dict} for receiver_data but got {type(receiver_data)}"
)
else:
p = Personalization()
p.add_to(
Email(
email=receiver_data["email"],
dynamic_template_data=receiver_data,
)
p = Personalization()
p.add_to(
Email(
email=receiver_data["email"],
dynamic_template_data=receiver_data,
)
email_message.add_personalization(p)
)
email_message.add_personalization(p)

email_message.from_email = sender_email
email_message.template_id = template_id
Expand Down
37 changes: 18 additions & 19 deletions apps/api/src/utils/email_handler.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from enum import Enum
from typing import Protocol

from pydantic import EmailStr

from services import sendgrid_handler
from services.sendgrid_handler import PersonalizationData
from services.sendgrid_handler import Template

IH_SENDER = ("[email protected]", "IrvineHacks 2024 Applications")

Expand All @@ -15,27 +14,27 @@ class ContactInfo(Protocol):
last_name: str


class Template(str, Enum):
# TODO: provide actual template IDs
CONFIRMATION_EMAIL = "d-e053b7a4bedd449bafda46c6512d531c"
GUEST_TOKEN = "d-d962252a87844188880d7bd19a5b2fbf"


async def send_application_confirmation_email(user: ContactInfo) -> None:
"""Send a confirmation email after a user submits an application.
Will propagate exceptions from SendGrid."""
send_data: PersonalizationData = {
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
}
await sendgrid_handler.send_email(Template.CONFIRMATION_EMAIL, IH_SENDER, send_data)
await sendgrid_handler.send_email(
Template.CONFIRMATION_EMAIL,
IH_SENDER,
{
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
},
)


async def send_guest_login_email(email: EmailStr, passphrase: str) -> None:
"""Email login passphrase to guest."""
send_data: PersonalizationData = {
"email": email,
"passphrase": passphrase,
}
await sendgrid_handler.send_email(Template.GUEST_TOKEN, IH_SENDER, send_data)
await sendgrid_handler.send_email(
Template.GUEST_TOKEN,
IH_SENDER,
{
"email": email,
"passphrase": passphrase,
},
)
16 changes: 9 additions & 7 deletions apps/api/tests/test_sendgrid_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from httpx import HTTPStatusError, Request, Response

from services import sendgrid_handler
from services.sendgrid_handler import PersonalizationData
from services.sendgrid_handler import ConfirmationPersonalization, Template

SAMPLE_SENDER = ("[email protected]", "No Reply IrvineHacks")
SAMPLE_RECIPIENTS: list[PersonalizationData] = [
SAMPLE_RECIPIENTS: list[ConfirmationPersonalization] = [
{
"email": "[email protected]",
"first_name": "Hacker",
Expand All @@ -31,7 +31,9 @@ async def test_send_single_email(mock_AsyncClient: AsyncMock) -> None:

recipient_data = SAMPLE_RECIPIENTS[0]

await sendgrid_handler.send_email("my-template-id", SAMPLE_SENDER, recipient_data)
await sendgrid_handler.send_email(
Template.CONFIRMATION_EMAIL, SAMPLE_SENDER, recipient_data
)
mock_client.send_mail_v3.assert_awaited_once_with(
body={
"from": {"name": SAMPLE_SENDER[1], "email": SAMPLE_SENDER[0]},
Expand All @@ -41,7 +43,7 @@ async def test_send_single_email(mock_AsyncClient: AsyncMock) -> None:
"dynamic_template_data": recipient_data,
}
],
"template_id": "my-template-id",
"template_id": Template.CONFIRMATION_EMAIL,
}
)

Expand All @@ -54,7 +56,7 @@ async def test_send_multiple_emails(mock_AsyncClient: AsyncMock) -> None:
mock_AsyncClient.return_value.__aenter__.return_value = mock_client

await sendgrid_handler.send_email(
"my-template-id", SAMPLE_SENDER, SAMPLE_RECIPIENTS, True
Template.CONFIRMATION_EMAIL, SAMPLE_SENDER, SAMPLE_RECIPIENTS, True
)
mock_client.send_mail_v3.assert_awaited_once_with(
body={
Expand All @@ -69,7 +71,7 @@ async def test_send_multiple_emails(mock_AsyncClient: AsyncMock) -> None:
"dynamic_template_data": SAMPLE_RECIPIENTS[0],
},
],
"template_id": "my-template-id",
"template_id": Template.CONFIRMATION_EMAIL,
}
)

Expand All @@ -88,5 +90,5 @@ async def test_sendgrid_error_causes_runtime_error(mock_AsyncClient: AsyncMock)

with pytest.raises(RuntimeError):
await sendgrid_handler.send_email(
"my-template-id", SAMPLE_SENDER, SAMPLE_RECIPIENTS, True
Template.CONFIRMATION_EMAIL, SAMPLE_SENDER, SAMPLE_RECIPIENTS, True
)

0 comments on commit da42bde

Please sign in to comment.