diff --git a/great_expectations/checkpoint/actions.py b/great_expectations/checkpoint/actions.py index 4363e852a27e..b730f2ad75b1 100644 --- a/great_expectations/checkpoint/actions.py +++ b/great_expectations/checkpoint/actions.py @@ -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: @@ -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__( @@ -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" @@ -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 diff --git a/great_expectations/data_context/cloud_constants.py b/great_expectations/data_context/cloud_constants.py index f70d57712ec5..51ba17c5ad21 100644 --- a/great_expectations/data_context/cloud_constants.py +++ b/great_expectations/data_context/cloud_constants.py @@ -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): diff --git a/great_expectations/render/renderer/slack_renderer.py b/great_expectations/render/renderer/slack_renderer.py index 4039932128be..4da421cbe391 100644 --- a/great_expectations/render/renderer/slack_renderer.py +++ b/great_expectations/render/renderer/slack_renderer.py @@ -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." @@ -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}` diff --git a/tests/render/test_SlackRenderer.py b/tests/render/test_SlackRenderer.py index 369e8d966f28..21198748b3cb 100644 --- a/tests/render/test_SlackRenderer.py +++ b/tests/render/test_SlackRenderer.py @@ -1,3 +1,5 @@ +import pytest + from great_expectations.core.batch import BatchDefinition, IDDict from great_expectations.core.expectation_validation_result import ( ExpectationSuiteValidationResult, @@ -5,8 +7,55 @@ 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={ @@ -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 = { @@ -367,52 +422,13 @@ 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 ( @@ -420,3 +436,19 @@ def test_SlackRenderer_show_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"] + )