Skip to content

Commit

Permalink
Add HTML email template string to config (gsi 19) (#3)
Browse files Browse the repository at this point in the history
* Update dependency for event

Add notifier port

* Implement basic template and a test

* Remove unused mypy error suppressor

* Fix type hinting on _construct_email()

* Add plaintext email template

Silence mypy errors

---------

Co-authored-by: Byron Himes <[email protected]>
  • Loading branch information
TheByronHimes and TheByronHimes authored Apr 17, 2023
1 parent eeaccb6 commit 3855120
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 6 deletions.
2 changes: 2 additions & 0 deletions .devcontainer/.dev_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ notification_event_topic: "notifications"
notification_event_type: "notification"
service_instance_id: 001
kafka_servers: ["kafka:9092"]
plaintext_email_template: "Dear $recipient_name,\n\n$plaintext_body\n\nWarm regards,\n\nThe GHGA Team"
html_email_template: '<!DOCTYPE html><html><head></head><body style="color: #00393f;padding: 12px;"><h2>Dear $recipient_name,</h2><p>$plaintext_body</p><p>Warm regards,</p><h3>The GHGA Team</h3></body></html>'
18 changes: 17 additions & 1 deletion config_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,29 @@
"items": {
"type": "string"
}
},
"plaintext_email_template": {
"title": "Plaintext Email Template",
"env_names": [
"ns_plaintext_email_template"
],
"type": "string"
},
"html_email_template": {
"title": "Html Email Template",
"env_names": [
"ns_html_email_template"
],
"type": "string"
}
},
"required": [
"notification_event_topic",
"notification_event_type",
"service_instance_id",
"kafka_servers"
"kafka_servers",
"plaintext_email_template",
"html_email_template"
],
"additionalProperties": false
}
13 changes: 13 additions & 0 deletions example_config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
html_email_template: '<!DOCTYPE html><html><head></head><body style="color: #00393f;padding:
12px;"><h2>Dear $recipient_name,</h2><p>$plaintext_body</p><p>Warm regards,</p><h3>The
GHGA Team</h3></body></html>'
kafka_servers:
- kafka:9092
notification_event_topic: notifications
notification_event_type: notification
plaintext_email_template: 'Dear $recipient_name,
$plaintext_body
Warm regards,
The GHGA Team'
service_instance_id: '1'
service_name: ns
2 changes: 2 additions & 0 deletions ns/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class Config(KafkaConfig, EventSubTranslatorConfig):
"""Config parameters and their defaults."""

service_name: str = "ns"
plaintext_email_template: str
html_email_template: str


CONFIG = Config()
4 changes: 4 additions & 0 deletions ns/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@

from ns.adapters.inbound.akafka import EventSubTranslator
from ns.config import Config
from ns.core.notifier import Notifier


class Container(ContainerBase):
"""Dependency-Injection Container"""

config = get_configurator(Config)

# domain components
notifier = get_constructor(Notifier, config=config)

# inbound translators
event_sub_translator = get_constructor(EventSubTranslator, config=config)

Expand Down
74 changes: 74 additions & 0 deletions ns/core/notifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
# for the German Human Genome-Phenome Archive (GHGA)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Contains the concrete implementation of a NotifierPort"""

from email.message import EmailMessage
from string import Template

from ghga_event_schemas import pydantic_ as event_schemas

from ns.config import Config
from ns.ports.inbound.notifier import NotifierPort


class Notifier(NotifierPort):
"""Implementation of the Notifier Port"""

def __init__(self, *, config: Config):
"""Initialize the Notifier with configured host, port, and so on"""
self._config = config

async def send_notification(self, *, notification: event_schemas.Notification):
"""Sends notifications based on the channel info provided (e.g. email addresses)"""
raise NotImplementedError

def _construct_email(
self, *, notification: event_schemas.Notification
) -> EmailMessage:
"""Constructs an EmailMessage object from the contents of an email notification event"""
message = EmailMessage()
message["To"] = notification.recipient_email
message["Cc"] = notification.email_cc
message["Bcc"] = notification.email_bcc
message["Subject"] = notification.subject

payload_as_dict = notification.dict()

# create plaintext html with template
plaintext_template = Template(self._config.plaintext_email_template)
try:
plaintext_email = plaintext_template.substitute(payload_as_dict)
except KeyError as err:
raise self.VariableNotSuppliedError(variable=err.args[0]) from err
except ValueError as err:
raise self.BadTemplateFormat(problem=err.args[0]) from err

message.set_content(plaintext_email)

# create html version of email, replacing variables of $var format
html_template = Template(self._config.html_email_template)

try:
html_email = html_template.substitute(payload_as_dict)
except KeyError as err:
raise self.VariableNotSuppliedError(variable=err.args[0]) from err
except ValueError as err:
raise self.BadTemplateFormat(problem=err.args[0]) from err

# add the html version to the EmailMessage object
message.add_alternative(html_email, subtype="html")

return message
42 changes: 42 additions & 0 deletions ns/ports/inbound/notifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright 2021 - 2023 Universität Tübingen, DKFZ, EMBL, and Universität zu Köln
# for the German Human Genome-Phenome Archive (GHGA)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Contains a port for the notifier"""
from abc import ABC, abstractmethod

from ghga_event_schemas import pydantic_ as event_schemas


class NotifierPort(ABC):
"""Describes a notifier service in basic detail"""

class VariableNotSuppliedError(KeyError):
"""Raised when an html template contains a variable that isn't supplied"""

def __init__(self, *, variable: str):
message = f"Nothing supplied for template variable {variable}"
super().__init__(message)

class BadTemplateFormat(ValueError):
"""Raised when the html template contains improperly formatted content"""

def __init__(self, *, problem: str):
message = f"Problem with HTML template: {problem}"
super().__init__(message)

@abstractmethod
async def send_notification(self, *, notification: event_schemas.Notification):
"""Sends out notifications based on the event details"""
...
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ packages = find:
install_requires =
typer==0.7.0
ghga-service-commons==0.2.1
ghga-event-schemas==0.8.0
ghga-event-schemas==0.10.1
hexkit[akafka]==0.9.2

python_requires = >= 3.9
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/test_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ notification_event_topic: "notifications"
notification_event_type: "notification"
service_instance_id: 001
kafka_servers: ["kafka:9092"]
plaintext_email_template: "Dear $recipient_name,\n\n$plaintext_body\n\nWarm regards,\n\nThe GHGA Team"
html_email_template: '<!DOCTYPE html><html><head></head><body style="color: #00393f;padding: 12px;"><h2>Dear $recipient_name,</h2><p>$plaintext_body</p><p>Warm regards,</p><h3>The GHGA Team</h3></body></html>'
54 changes: 50 additions & 4 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,68 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Test dummy."""
"""Test basic event consumption"""
import pytest
from ghga_event_schemas import pydantic_ as event_schemas
from hexkit.providers.akafka.testutils import kafka_fixture # noqa: F401

from ns.core.notifier import Notifier
from tests.fixtures.config import get_config
from tests.fixtures.joint import JointFixture, joint_fixture # noqa: F401


@pytest.mark.asyncio
async def test_basic_consume(joint_fixture: JointFixture): # noqa: F811
"""Verify that the consumer runs the dummy _send_email() and raises the error."""
async def test_basic_path(joint_fixture: JointFixture): # noqa: F811
"""Verify that the event is correctly translated into a basic email object"""
await joint_fixture.kafka.publish_event(
payload={"key": "value"},
payload={
"recipient_email": "[email protected]",
"email_cc": [],
"email_bcc": [],
"subject": "Test123",
"recipient_name": "Yolanda Martinez",
"plaintext_body": "Where are you, where are you, Yolanda?",
},
type_=joint_fixture.config.notification_event_type,
topic=joint_fixture.config.notification_event_topic,
)

event_subscriber = await joint_fixture.container.kafka_event_subscriber()
with pytest.raises(NotImplementedError):
await event_subscriber.run(forever=False)


@pytest.mark.asyncio
async def test_email_construction():
"""Verify that the email is getting constructed properly from the template."""
notification = event_schemas.Notification(
recipient_email="[email protected]",
email_cc=[],
email_bcc=[],
subject="Test123",
recipient_name="Yolanda Martinez",
plaintext_body="Where are you, where are you, Yolanda?",
)

notifier = Notifier(config=get_config())
msg = notifier._construct_email(
notification=notification
) # pylint: disable=protected-access

assert msg is not None

plaintext_body = msg.get_body(preferencelist=("plain"))
assert plaintext_body is not None

plaintext_content = plaintext_body.get_content() # type: ignore[attr-defined]
expected_plaintext = "Dear Yolanda Martinez,\n\nWhere are you, where are you, Yolanda?\n\nWarm regards,\n\nThe GHGA Team"
assert plaintext_content.strip() == expected_plaintext

html_body = msg.get_body(preferencelist=("html"))
assert html_body is not None

html_content = html_body.get_content() # type: ignore[attr-defined]
assert html_content is not None

expected_html = '<!DOCTYPE html><html><head></head><body style="color: #00393f;padding: 12px;"><h2>Dear Yolanda Martinez,</h2><p>Where are you, where are you, Yolanda?</p><p>Warm regards,</p><h3>The GHGA Team</h3></body></html>'
assert html_content.strip() == expected_html

0 comments on commit 3855120

Please sign in to comment.