Repository: great-expectations/great_expectations
Pull request #6902 (2023-01-23 19:51:07)

[FEATURE] Add API action
Hi,
This is a new validation action (that we are using already in our set up) which sends a POST request with the suite name, asset name, and validation results to a URL which is specified in the action config as an additional parameter - "url". 
It can be useful for anyone wanting to send the validation results to some endpoint.

This is an initial version, as @talagluck requested. 
Can you please review? 
Thanks!

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy request for *niobium-lead-7998* pending review.
[Visit the deploys page to approve it](https://app.netlify.com/sites/niobium-lead-7998/deploys)

|  Name | Link |
|---------------------------------|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 552949f8bae2173dbcf963ebe13480d853b877a5 |
Shinnnyshinshin: Hi @itaise this is a great start, and I'm very excited to see this merged. Would you be able to add some tests for the the API action? They can be added along with the Slack and PagerDuty actions in `tests/actions/test_core_actions.py`
Shinnnyshinshin: Hi @itaise, wanting to follow up on this PR. Are you still interested in contributing the API action? 
itaise: Hi, yes, sorry for delay, will add the tests soon :)
itaise: > Hi @itaise, wanting to follow up on this PR. Are you still interested in contributing the API action?
@Shinnnyshinshin  I added 2 tests, its ready for review
Shinnnyshinshin: > > Hi @itaise, wanting to follow up on this PR. Are you still interested in contributing the API action?
> > @Shinnnyshinshin  I added 2 tests, its ready for review

Awesome. The tests look great, and I saw that there was a `lint` error. This is the result of some changes on our end, so I can get that fixed for you :) 
itaise: Thanks a lot!

בתאריך יום ה׳, 9 בפבר׳ 2023, 20:28, מאת William Shin ‏<
***@***.***>:

> Hi @itaise <https://github.com/itaise>, wanting to follow up on this PR.
> Are you still interested in contributing the API action?
> @Shinnnyshinshin <https://github.com/Shinnnyshinshin> I added 2 tests,
> its ready for review
>
> Awesome. The tests look great, and I saw that there was a lint error.
> This is the result of some changes on our end, so I can get that fixed for
> you :)
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/great-expectations/great_expectations/pull/6902#issuecomment-1424635212>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/APN7A4AGROOY63K7G2YIXBDWWUZLBANCNFSM6AAAAAAUEH2MXI>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>


Patch:
diff --git a/great_expectations/checkpoint/actions.py b/great_expectations/checkpoint/actions.py
index 555ad33db..ef0c8c898 100644
--- a/great_expectations/checkpoint/actions.py
+++ b/great_expectations/checkpoint/actions.py
@@ -5,13 +5,14 @@ The only requirement from an action is for it to have a take_action method.
 """
 from __future__ import annotations
 
+import json
 import logging
 import warnings
-from typing import TYPE_CHECKING, Dict, List, Optional, Union
+from typing import TYPE_CHECKING, Dict, List, Union
 
+import requests
 from typing_extensions import Final
 
-from great_expectations.core import ExpectationSuiteValidationResult  # noqa: TCH001
 from great_expectations.core._docs_decorators import deprecated_argument
 from great_expectations.data_context.cloud_constants import CLOUD_APP_DEFAULT_BASE_URL
 from great_expectations.data_context.types.refs import (
@@ -23,6 +24,8 @@ try:
 except ImportError:
     pypd = None
 
+from typing import Optional
+
 from great_expectations.checkpoint.util import (
     send_email,
     send_microsoft_teams_notifications,
@@ -31,6 +34,10 @@ from great_expectations.checkpoint.util import (
     send_sns_notification,
 )
 from great_expectations.core._docs_decorators import public_api
+from great_expectations.core.expectation_validation_result import (
+    ExpectationSuiteValidationResult,  # noqa: TCH001
+)
+from great_expectations.core.util import convert_to_json_serializable
 from great_expectations.data_context.store.metric_store import MetricStore
 from great_expectations.data_context.types.resource_identifiers import (
     ExpectationSuiteIdentifier,
@@ -1242,3 +1249,69 @@ class SNSNotificationAction(ValidationAction):
         return send_sns_notification(
             self.sns_topic_arn, subject, validation_result_suite.__str__(), **kwargs
         )
+
+
+class APINotificationAction(ValidationAction):
+    def __init__(self, data_context, url) -> None:
+        super().__init__(data_context)
+        self.url = url
+
+    def _run(
+        self,
+        validation_result_suite: ExpectationSuiteValidationResult,
+        validation_result_suite_identifier: ValidationResultIdentifier,
+        data_asset,
+        expectation_suite_identifier: Optional[ExpectationSuiteIdentifier] = None,
+        checkpoint_identifier=None,
+        **kwargs,
+    ):
+        suite_name: str = validation_result_suite.meta["expectation_suite_name"]
+        if "batch_kwargs" in validation_result_suite.meta:
+            data_asset_name = validation_result_suite.meta["batch_kwargs"].get(
+                "data_asset_name", "__no_data_asset_name__"
+            )
+        elif "active_batch_definition" in validation_result_suite.meta:
+            data_asset_name = (
+                validation_result_suite.meta["active_batch_definition"].data_asset_name
+                if validation_result_suite.meta[
+                    "active_batch_definition"
+                ].data_asset_name
+                else "__no_data_asset_name__"
+            )
+        else:
+            data_asset_name = "__no_data_asset_name__"
+
+        validation_results: list = validation_result_suite.get("results")
+        validation_results_serializable: list = convert_to_json_serializable(
+            validation_results
+        )
+
+        payload = self.create_payload(
+            data_asset_name, suite_name, validation_results_serializable
+        )
+
+        response = self.send_results(payload)
+        return (
+            f"Successfully Posted results to API, status code - {response.status_code}"
+        )
+
+    def send_results(self, payload) -> None:
+        try:
+            headers = {"Content-Type": "application/json"}
+            return requests.post(self.url, headers=headers, data=payload)
+        except Exception as e:
+            print(f"Exception when sending data to API - {e}")
+            raise e
+
+    @staticmethod
+    def create_payload(
+        data_asset_name, suite_name, validation_results_serializable
+    ) -> str:
+        payload = json.dumps(
+            {
+                "test_suite_name": suite_name,
+                "data_asset_name": data_asset_name,
+                "validation_results": validation_results_serializable,
+            }
+        )
+        return payload


Test patch:
diff --git a/tests/actions/test_core_actions.py b/tests/actions/test_core_actions.py
index d6155d7fb..7d4c4e001 100644
--- a/tests/actions/test_core_actions.py
+++ b/tests/actions/test_core_actions.py
@@ -6,7 +6,10 @@ import pytest
 from freezegun import freeze_time
 from requests import Session
 
-from great_expectations.checkpoint.actions import SNSNotificationAction
+from great_expectations.checkpoint.actions import (
+    APINotificationAction,
+    SNSNotificationAction,
+)
 from great_expectations.checkpoint.util import smtplib
 from great_expectations.core.expectation_validation_result import (
     ExpectationSuiteValidationResult,
@@ -26,6 +29,7 @@ from great_expectations.validation_operators import (
     SlackNotificationAction,
     StoreValidationResultAction,
 )
+from tests.test_ge_utils import file_data_asset
 
 logger = logging.getLogger(__name__)
 
@@ -579,6 +583,38 @@ def test_EmailAction(
         ) == {"email_result": expected}
 
 
+def test_api_action_create_payload():
+    mock_data_context = ""
+    mock_validation_results = []
+    expected_payload = '{"test_suite_name": "my_suite", "data_asset_name": "my_schema.my_table", "validation_results": []}'
+    api_notification_action = APINotificationAction(
+        mock_data_context, "http://www.example.com"
+    )
+    payload = api_notification_action.create_payload(
+        "my_schema.my_table", "my_suite", mock_validation_results
+    )
+    assert payload == expected_payload
+
+
+@mock.patch("great_expectations.checkpoint.actions.requests")
+def test_api_action_run(
+    mock_requests,
+    validation_result_suite,
+    validation_result_suite_id,
+    data_context_simple_expectation_suite,
+):
+    mock_response = mock.MagicMock()
+    mock_response.status_code = 200
+    mock_requests.post.return_value = mock_response
+    api_notification_action = APINotificationAction(
+        data_context_simple_expectation_suite, "http://www.example.com"
+    )
+    response = api_notification_action.run(
+        validation_result_suite, validation_result_suite_id, file_data_asset
+    )
+    assert response == "Successfully Posted results to API, status code - 200"
+
+
 # def test_ExtractAndStoreEvaluationParamsAction():
 #     fake_in_memory_store = ValidationsStore(
 #         root_directory = None,