Skip to content

Commit

Permalink
[MAINTENANCE] Refactor EmailAction for V1 (#9725)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdkini committed Apr 9, 2024
1 parent d4bad56 commit 1f98547
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 11 deletions.
26 changes: 26 additions & 0 deletions great_expectations/checkpoint/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,32 @@ def _root_validate_email_params(cls, values: dict) -> dict:

return values

@override
def v1_run(self, checkpoint_result: CheckpointResult) -> str | dict:
success = checkpoint_result.success or False
if not self._is_enabled(success=success):
return {"email_result": ""}

title, html = self.renderer.v1_render(checkpoint_result=checkpoint_result)
receiver_emails_list = list(map(lambda x: x.strip(), self.receiver_emails.split(",")))

# this will actually send the email
email_result = send_email(
title=title,
html=html,
smtp_address=self.smtp_address,
smtp_port=self.smtp_port,
sender_login=self.sender_login,
sender_password=self.sender_password,
sender_alias=self.sender_alias,
receiver_emails_list=receiver_emails_list,
use_tls=self.use_tls,
use_ssl=self.use_ssl,
)

# sending payload back as dictionary
return {"email_result": email_result}

@override
def _run( # type: ignore[override] # signature does not match parent # noqa: PLR0913
self,
Expand Down
46 changes: 46 additions & 0 deletions great_expectations/render/renderer/email_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,60 @@

import logging
import textwrap
from typing import TYPE_CHECKING

logger = logging.getLogger(__name__)

from great_expectations.core.id_dict import BatchKwargs
from great_expectations.render.renderer.renderer import Renderer

if TYPE_CHECKING:
from great_expectations.checkpoint.v1_checkpoint import CheckpointResult
from great_expectations.core.expectation_validation_result import (
ExpectationSuiteValidationResult,
)


class EmailRenderer(Renderer):
def v1_render(self, checkpoint_result: CheckpointResult) -> tuple[str, str]:
checkpoint_name = checkpoint_result.checkpoint_config.name
status = checkpoint_result.success
title = f"{checkpoint_name}: {status}"

text_blocks: list[str] = []
for result in checkpoint_result.run_results.values():
html = self._render_validation_result(result=result)
text_blocks.append(html)

return title, self._concatenate_blocks(text_blocks=text_blocks)

def _render_validation_result(self, result: ExpectationSuiteValidationResult) -> str:
suite_name = result.suite_name
asset_name = result.asset_name or "__no_asset_name__"
n_checks_succeeded = result.statistics["successful_expectations"]
n_checks = result.statistics["evaluated_expectations"]
run_id = result.meta.get("run_id", "__no_run_id__")
batch_id = result.batch_id
check_details_text = f"<strong>{n_checks_succeeded}</strong> of <strong>{n_checks}</strong> expectations were met" # noqa: E501
status = "Success 🎉" if result.success else "Failed ❌"

title = f"<h3><u>{suite_name}</u></h3>"
html = textwrap.dedent(
f"""\
<p><strong>{title}</strong></p>
<p><strong>Batch Validation Status</strong>: {status}</p>
<p><strong>Expectation Suite Name</strong>: {suite_name}</p>
<p><strong>Data Asset Name</strong>: {asset_name}</p>
<p><strong>Run ID</strong>: {run_id}</p>
<p><strong>Batch ID</strong>: {batch_id}</p>
<p><strong>Summary</strong>: {check_details_text}</p>"""
)

return html

def _concatenate_blocks(self, text_blocks: list[str]) -> str:
return "<br>".join(text_blocks)

def render( # noqa: C901, PLR0912
self, validation_result=None, data_docs_pages=None, notify_with=None
):
Expand Down
63 changes: 55 additions & 8 deletions tests/actions/test_core_actions.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from __future__ import annotations

import json
import logging
from contextlib import contextmanager
from types import ModuleType
from typing import Iterator, Type
from typing import TYPE_CHECKING, Iterator, Type
from unittest import mock

import pytest
import requests
from freezegun import freeze_time
from pytest_mock import MockerFixture
from requests import Session
from typing_extensions import Never

from great_expectations import set_context
from great_expectations.checkpoint.actions import (
Expand Down Expand Up @@ -46,6 +46,10 @@
)
from great_expectations.util import is_library_loadable

if TYPE_CHECKING:
from pytest_mock import MockerFixture
from typing_extensions import Never

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -868,14 +872,57 @@ def test_APINotificationAction_run(self, checkpoint_result: CheckpointResult):
)

@pytest.mark.unit
@pytest.mark.xfail(
reason="Not yet implemented for this class", strict=True, raises=NotImplementedError
@pytest.mark.parametrize(
"emails, expected_email_list",
[
pytest.param("test1@gmail.com", ["test1@gmail.com"], id="single_email"),
pytest.param(
"test1@gmail.com, test2@hotmail.com",
["test1@gmail.com", "test2@hotmail.com"],
id="multiple_emails",
),
pytest.param(
"test1@gmail.com,test2@hotmail.com",
["test1@gmail.com", "test2@hotmail.com"],
id="multiple_emails_no_space",
),
],
)
def test_EmailAction_run(self, checkpoint_result: CheckpointResult):
def test_EmailAction_run(
self, checkpoint_result: CheckpointResult, emails: str, expected_email_list: list[str]
):
action = EmailAction(
smtp_address="test", smtp_port=587, receiver_emails="test1@gmail.com, test2@hotmail.com"
smtp_address="test",
smtp_port="587",
receiver_emails=emails,
)
action.v1_run(checkpoint_result=checkpoint_result)

with mock.patch("great_expectations.checkpoint.actions.send_email") as mock_send_email:
out = action.v1_run(checkpoint_result=checkpoint_result)

# Should contain success/failure in title
assert "True" in mock_send_email.call_args.kwargs["title"]

# Should contain suite names and other relevant domain object identifiers in the body
run_results = tuple(checkpoint_result.run_results.values())
suite_a = run_results[0].suite_name
suite_b = run_results[1].suite_name
mock_html = mock_send_email.call_args.kwargs["html"]
assert suite_a in mock_html and suite_b in mock_html

mock_send_email.assert_called_once_with(
title=mock.ANY,
html=mock.ANY,
receiver_emails_list=expected_email_list,
sender_alias=None,
sender_login=None,
sender_password=None,
smtp_address="test",
smtp_port="587",
use_ssl=None,
use_tls=None,
)
assert out == {"email_result": mock_send_email()}

@pytest.mark.unit
@pytest.mark.xfail(
Expand Down
29 changes: 26 additions & 3 deletions tests/render/test_EmailRenderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
)
from great_expectations.render.renderer import EmailRenderer

# module level markers
pytestmark = pytest.mark.big


@pytest.mark.big
def test_EmailRenderer_validation_results_with_datadocs():
validation_result_suite = ExpectationSuiteValidationResult(
results=[],
Expand Down Expand Up @@ -64,6 +62,7 @@ def test_EmailRenderer_validation_results_with_datadocs():
assert rendered_output == expected_output


@pytest.mark.big
def test_EmailRenderer_checkpoint_validation_results_with_datadocs():
batch_definition = LegacyBatchDefinition(
datasource_name="test_datasource",
Expand Down Expand Up @@ -120,6 +119,7 @@ def test_EmailRenderer_checkpoint_validation_results_with_datadocs():
assert rendered_output == expected_output


@pytest.mark.big
def test_EmailRenderer_get_report_element():
email_renderer = EmailRenderer()

Expand All @@ -130,3 +130,26 @@ def test_EmailRenderer_get_report_element():

# this should work
assert email_renderer._get_report_element(docs_link="i_should_work") is not None


@pytest.mark.unit
def test_EmailRenderer_v1_render(v1_checkpoint_result):
email_renderer = EmailRenderer()
_, raw_html = email_renderer.v1_render(checkpoint_result=v1_checkpoint_result)
html_blocks = raw_html.split("\n")

assert html_blocks == [
"<p><strong><h3><u>my_bad_suite</u></h3></strong></p>",
"<p><strong>Batch Validation Status</strong>: Failed ❌</p>",
"<p><strong>Expectation Suite Name</strong>: my_bad_suite</p>",
"<p><strong>Data Asset Name</strong>: my_first_asset</p>",
"<p><strong>Run ID</strong>: __no_run_id__</p>",
"<p><strong>Batch ID</strong>: my_batch</p>",
"<p><strong>Summary</strong>: <strong>3</strong> of <strong>5</strong> expectations were met</p><br><p><strong><h3><u>my_good_suite</u></h3></strong></p>", # noqa: E501
"<p><strong>Batch Validation Status</strong>: Success 🎉</p>",
"<p><strong>Expectation Suite Name</strong>: my_good_suite</p>",
"<p><strong>Data Asset Name</strong>: __no_asset_name__</p>",
"<p><strong>Run ID</strong>: my_run_id</p>",
"<p><strong>Batch ID</strong>: my_other_batch</p>",
"<p><strong>Summary</strong>: <strong>1</strong> of <strong>1</strong> expectations were met</p>", # noqa: E501
]

0 comments on commit 1f98547

Please sign in to comment.