Skip to content

Commit

Permalink
[FEATURE] Add GX Cloud hyperlink to slack notification (#6740)
Browse files Browse the repository at this point in the history
Co-authored-by: Jose Tobar <jose@greatexpectations.com>
Co-authored-by: Nathan Farmer <nathan@greatexpectations.io>
Co-authored-by: Nathan Farmer <NathanFarmer@users.noreply.github.com>
  • Loading branch information
4 people committed Jan 12, 2023
1 parent 94c5d44 commit e27683f
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 49 deletions.
26 changes: 24 additions & 2 deletions great_expectations/checkpoint/actions.py
Expand Up @@ -10,7 +10,10 @@
from typing import TYPE_CHECKING, Dict, Optional, Union
from urllib.parse import urljoin

from typing_extensions import Final

from great_expectations.core import ExpectationSuiteValidationResult
from great_expectations.data_context.cloud_constants import CLOUD_APP_DEFAULT_BASE_URL
from great_expectations.data_context.types.refs import GXCloudResourceRef

try:
Expand Down Expand Up @@ -152,7 +155,6 @@ class SlackNotificationAction(ValidationAction):
module_name: great_expectations.render.renderer.slack_renderer
class_name: SlackRenderer
show_failed_expectations: *Optional* (boolean) shows a list of failed expectation types. default is false.
"""

def __init__(
Expand Down Expand Up @@ -236,13 +238,32 @@ def _run(

validation_success = validation_result_suite.success
data_docs_pages = None

if payload:
# process the payload
for action_names in payload.keys():
if payload[action_names]["class"] == "UpdateDataDocsAction":
data_docs_pages = payload[action_names]

# Assemble complete GX Cloud URL for a specific validation result
data_docs_urls: list[
dict[str, str | None]
] = self.data_context.get_docs_sites_urls(
resource_identifier=validation_result_suite_identifier
)

validation_result_urls: list[str] = [
data_docs_url["site_url"]
for data_docs_url in data_docs_urls
if data_docs_url["site_url"]
]
if (
isinstance(validation_result_suite_identifier, GXCloudIdentifier)
and validation_result_suite_identifier.cloud_id
):
cloud_base_url: Final[str] = CLOUD_APP_DEFAULT_BASE_URL
validation_result_url = f"{cloud_base_url}?validationResultId={validation_result_suite_identifier.cloud_id}"
validation_result_urls.append(validation_result_url)

if (
self.notify_on == "all"
or self.notify_on == "success"
Expand All @@ -255,6 +276,7 @@ def _run(
data_docs_pages,
self.notify_with,
self.show_failed_expectations,
validation_result_urls,
)

# this will actually send the POST request to the Slack webapp server
Expand Down
1 change: 1 addition & 0 deletions great_expectations/data_context/cloud_constants.py
Expand Up @@ -4,6 +4,7 @@

SUPPORT_EMAIL = "support@greatexpectations.io"
CLOUD_DEFAULT_BASE_URL: Final[str] = "https://api.greatexpectations.io/"
CLOUD_APP_DEFAULT_BASE_URL: Final[str] = "https://app.greatexpectations.io/"


class GXCloudEnvironmentVariable(str, Enum):
Expand Down
19 changes: 18 additions & 1 deletion great_expectations/render/renderer/slack_renderer.py
Expand Up @@ -17,6 +17,7 @@ def render( # noqa: C901 - 17
data_docs_pages=None,
notify_with=None,
show_failed_expectations: bool = False,
validation_result_urls: List[str] = [],
):
default_text = (
"No validation occurred. Please ensure you passed a validation_result."
Expand Down Expand Up @@ -75,8 +76,24 @@ def render( # noqa: C901 - 17
failed_expectations_text = self.create_failed_expectations_text(
validation_result["results"]
)
summary_text = ""
if validation_result_urls:
# This adds hyperlinks for defined URL
if len(validation_result_urls) == 1:
title_hlink = f"*<{validation_result_urls[0]} | Validation Result>*"
else:
title_hlink = "*Validation Result*"
batch_validation_status_hlinks = "".join(
f"*Batch Validation Status*: *<{validation_result_url} | {status}>*"
for validation_result_url in validation_result_urls
)
summary_text += f"""{title_hlink}
{batch_validation_status_hlinks}
"""
else:
summary_text += f"*Batch Validation Status*: {status}"

summary_text = f"""*Batch Validation Status*: {status}
summary_text += f"""
*Expectation suite name*: `{expectation_suite_name}`
*Data asset name*: `{data_asset_name}`
*Run ID*: `{run_id}`
Expand Down
124 changes: 78 additions & 46 deletions tests/render/test_SlackRenderer.py
@@ -1,12 +1,61 @@
import pytest

from great_expectations.core.batch import BatchDefinition, IDDict
from great_expectations.core.expectation_validation_result import (
ExpectationSuiteValidationResult,
)
from great_expectations.render.renderer import SlackRenderer


def test_SlackRenderer_validation_results_with_datadocs():
validation_result_suite = ExpectationSuiteValidationResult(
@pytest.fixture
def failed_expectation_suite_validation_result():
return ExpectationSuiteValidationResult(
results=[
{
"exception_info": {
"raised_exception": False,
"exception_traceback": None,
"exception_message": None,
},
"success": False,
"meta": {},
"result": {"observed_value": 8565},
"expectation_config": {
"meta": {},
"kwargs": {
"column": "my_column",
"max_value": 10000,
"min_value": 10000,
"batch_id": "b9e06d3884bbfb6e3352ced3836c3bc8",
},
"expectation_type": "expect_column_values_to_be_between",
},
}
],
success=False,
statistics={
"evaluated_expectations": 0,
"successful_expectations": 0,
"unsuccessful_expectations": 0,
"success_percent": None,
},
meta={
"great_expectations_version": "v0.8.0__develop",
"batch_kwargs": {"data_asset_name": "x/y/z"},
"data_asset_name": {
"datasource": "x",
"generator": "y",
"generator_asset": "z",
},
"expectation_suite_name": "default",
"run_id": "2019-09-25T060538.829112Z",
},
)


@pytest.fixture
def success_expectation_suite_validation_result():
return ExpectationSuiteValidationResult(
results=[],
success=True,
statistics={
Expand All @@ -28,6 +77,12 @@ def test_SlackRenderer_validation_results_with_datadocs():
},
)


def test_SlackRenderer_validation_results_with_datadocs(
success_expectation_suite_validation_result,
):
validation_result_suite = success_expectation_suite_validation_result

rendered_output = SlackRenderer().render(validation_result_suite)

expected_output = {
Expand Down Expand Up @@ -367,56 +422,33 @@ def test_create_failed_expectations_text():
)


def test_SlackRenderer_show_failed_expectations():
validation_result = ExpectationSuiteValidationResult(
results=[
{
"exception_info": {
"raised_exception": False,
"exception_traceback": None,
"exception_message": None,
},
"success": False,
"meta": {},
"result": {"observed_value": 8565},
"expectation_config": {
"meta": {},
"kwargs": {
"column": "my_column",
"max_value": 10000,
"min_value": 10000,
"batch_id": "b9e06d3884bbfb6e3352ced3836c3bc8",
},
"expectation_type": "expect_column_values_to_be_between",
},
}
],
success=False,
statistics={
"evaluated_expectations": 0,
"successful_expectations": 0,
"unsuccessful_expectations": 0,
"success_percent": None,
},
meta={
"great_expectations_version": "v0.8.0__develop",
"batch_kwargs": {"data_asset_name": "x/y/z"},
"data_asset_name": {
"datasource": "x",
"generator": "y",
"generator_asset": "z",
},
"expectation_suite_name": "default",
"run_id": "2019-09-25T060538.829112Z",
},
)
def test_SlackRenderer_show_failed_expectations(
failed_expectation_suite_validation_result,
):
slack_renderer = SlackRenderer()
rendered_msg = slack_renderer.render(
validation_result, show_failed_expectations=True
validation_result=failed_expectation_suite_validation_result,
show_failed_expectations=True,
)

assert (
"""*Failed Expectations*:
:x:expect_column_values_to_be_between (my_column)"""
in rendered_msg["blocks"][0]["text"]["text"]
)


def test_slack_renderer_shows_gx_cloud_url(failed_expectation_suite_validation_result):
slack_renderer = SlackRenderer()
cloud_url = "app.greatexpectations.io/?validationResultId=123-456-789"
rendered_msg = slack_renderer.render(
validation_result=failed_expectation_suite_validation_result,
show_failed_expectations=True,
validation_result_urls=[cloud_url],
)

assert (
""
f"*<{cloud_url} | Failed :x:>*"
"" in rendered_msg["blocks"][0]["text"]["text"]
)

0 comments on commit e27683f

Please sign in to comment.