Lee-W commented on code in PR #56911:
URL: https://github.com/apache/airflow/pull/56911#discussion_r2498199826


##########
providers/discord/src/airflow/providers/discord/hooks/discord_webhook.py:
##########
@@ -19,10 +19,74 @@
 
 import json
 import re
-from typing import Any
+from typing import TYPE_CHECKING, Any
 
-from airflow.exceptions import AirflowException
-from airflow.providers.http.hooks.http import HttpHook
+import aiohttp
+
+from airflow.providers.common.compat.connection import get_async_connection
+from airflow.providers.http.hooks.http import HttpAsyncHook, HttpHook
+
+if TYPE_CHECKING:
+    from airflow.providers.common.compat.sdk import Connection
+
+
+class DiscordCommonHandler:
+    """Contains the common functionality."""
+
+    def get_webhook_endpoint(self, conn: Connection | None, webhook_endpoint: 
str | None) -> str:
+        """
+        Return the default webhook endpoint or override if a webhook_endpoint 
is manually supplied.
+
+        :param conn: Airflow Discord connection
+        :param webhook_endpoint: The manually provided webhook endpoint
+        :return: Webhook endpoint (str) to use
+        """
+        if webhook_endpoint:
+            endpoint = webhook_endpoint
+        elif conn:
+            extra = conn.extra_dejson
+            endpoint = extra.get("webhook_endpoint", "")
+        else:
+            raise ValueError(
+                "Cannot get webhook endpoint: No valid Discord webhook 
endpoint or http_conn_id supplied."
+            )
+
+        # make sure endpoint matches the expected Discord webhook format
+        if not re.fullmatch("webhooks/[0-9]+/[a-zA-Z0-9_-]+", endpoint):
+            raise ValueError(
+                'Expected Discord webhook endpoint in the form of 
"webhooks/{webhook.id}/{webhook.token}".'
+            )
+
+        return endpoint
+
+    def build_discord_payload(
+        self, tts: bool, message: str, username: str | None, avatar_url: str | 
None
+    ) -> str:
+        """
+        Build a valid Discord JSON payload.
+
+        :param tts: Is a text-to-speech message
+        :param message: The message you want to send to your Discord channel
+                        (max 2000 characters)
+        :param username: Override the default username of the webhook
+        :param avatar_url: Override the default avatar of the webhook
+        :return: Discord payload (str) to send
+        """
+        payload: dict[str, Any] = {}
+
+        if username:
+            payload["username"] = username
+        if avatar_url:
+            payload["avatar_url"] = avatar_url
+
+        payload["tts"] = tts
+
+        if len(message) <= 2000:
+            payload["content"] = message
+        else:
+            raise ValueError("Discord message length must be 2000 or fewer 
characters.")

Review Comment:
   ```suggestion
           if len(message) > 200:
               raise ValueError("Discord message length must be 2000 or fewer 
characters.")
           payload: dict[str, Any] = {
               "content": message,
               "tts": tts,
           }
   
           if username:
               payload["username"] = username
           if avatar_url:
               payload["avatar_url"] = avatar_url
   ```



##########
providers/discord/src/airflow/providers/discord/hooks/discord_webhook.py:
##########
@@ -19,10 +19,74 @@
 
 import json
 import re
-from typing import Any
+from typing import TYPE_CHECKING, Any
 
-from airflow.exceptions import AirflowException
-from airflow.providers.http.hooks.http import HttpHook
+import aiohttp
+
+from airflow.providers.common.compat.connection import get_async_connection
+from airflow.providers.http.hooks.http import HttpAsyncHook, HttpHook
+
+if TYPE_CHECKING:
+    from airflow.providers.common.compat.sdk import Connection
+
+
+class DiscordCommonHandler:
+    """Contains the common functionality."""
+
+    def get_webhook_endpoint(self, conn: Connection | None, webhook_endpoint: 
str | None) -> str:
+        """
+        Return the default webhook endpoint or override if a webhook_endpoint 
is manually supplied.
+
+        :param conn: Airflow Discord connection
+        :param webhook_endpoint: The manually provided webhook endpoint
+        :return: Webhook endpoint (str) to use
+        """
+        if webhook_endpoint:
+            endpoint = webhook_endpoint
+        elif conn:
+            extra = conn.extra_dejson
+            endpoint = extra.get("webhook_endpoint", "")
+        else:
+            raise ValueError(
+                "Cannot get webhook endpoint: No valid Discord webhook 
endpoint or http_conn_id supplied."
+            )
+
+        # make sure endpoint matches the expected Discord webhook format
+        if not re.fullmatch("webhooks/[0-9]+/[a-zA-Z0-9_-]+", endpoint):
+            raise ValueError(
+                'Expected Discord webhook endpoint in the form of 
"webhooks/{webhook.id}/{webhook.token}".'
+            )
+
+        return endpoint
+
+    def build_discord_payload(
+        self, tts: bool, message: str, username: str | None, avatar_url: str | 
None

Review Comment:
   let's make it keyword only, the order of these parameters does not matter or 
mean something
   
   ```suggestion
           self, *, tts: bool, message: str, username: str | None, avatar_url: 
str | None
   ```



##########
providers/discord/src/airflow/providers/discord/hooks/discord_webhook.py:
##########
@@ -19,10 +19,74 @@
 
 import json
 import re
-from typing import Any
+from typing import TYPE_CHECKING, Any
 
-from airflow.exceptions import AirflowException
-from airflow.providers.http.hooks.http import HttpHook
+import aiohttp
+
+from airflow.providers.common.compat.connection import get_async_connection
+from airflow.providers.http.hooks.http import HttpAsyncHook, HttpHook
+
+if TYPE_CHECKING:
+    from airflow.providers.common.compat.sdk import Connection
+
+
+class DiscordCommonHandler:
+    """Contains the common functionality."""
+
+    def get_webhook_endpoint(self, conn: Connection | None, webhook_endpoint: 
str | None) -> str:
+        """
+        Return the default webhook endpoint or override if a webhook_endpoint 
is manually supplied.
+
+        :param conn: Airflow Discord connection
+        :param webhook_endpoint: The manually provided webhook endpoint
+        :return: Webhook endpoint (str) to use
+        """
+        if webhook_endpoint:
+            endpoint = webhook_endpoint
+        elif conn:
+            extra = conn.extra_dejson
+            endpoint = extra.get("webhook_endpoint", "")
+        else:
+            raise ValueError(
+                "Cannot get webhook endpoint: No valid Discord webhook 
endpoint or http_conn_id supplied."
+            )
+
+        # make sure endpoint matches the expected Discord webhook format
+        if not re.fullmatch("webhooks/[0-9]+/[a-zA-Z0-9_-]+", endpoint):
+            raise ValueError(
+                'Expected Discord webhook endpoint in the form of 
"webhooks/{webhook.id}/{webhook.token}".'
+            )
+
+        return endpoint
+
+    def build_discord_payload(
+        self, tts: bool, message: str, username: str | None, avatar_url: str | 
None

Review Comment:
   do we want to rename `message` as `content` for consistency?



##########
providers/discord/src/airflow/providers/discord/hooks/discord_webhook.py:
##########
@@ -148,11 +177,100 @@ def execute(self) -> None:
             # we only need https proxy for Discord
             proxies = {"https": self.proxy}
 
-        discord_payload = self._build_discord_payload()
+        discord_payload = self.handler.build_discord_payload(
+            tts=self.tts, message=self.message, username=self.username, 
avatar_url=self.avatar_url
+        )
 
         self.run(
             endpoint=self.webhook_endpoint,
             data=discord_payload,
             headers={"Content-type": "application/json"},
             extra_options={"proxies": proxies},
         )
+
+
+class DiscordWebhookAsyncHook(HttpAsyncHook):
+    """
+    This hook allows you to post messages to Discord using incoming webhooks 
using async HTTP.
+
+    Takes a Discord connection ID with a default relative webhook endpoint. The
+    default endpoint can be overridden using the webhook_endpoint parameter
+    (https://discordapp.com/developers/docs/resources/webhook).
+
+    Each Discord webhook can be pre-configured to use a specific username and
+    avatar_url. You can override these defaults in this hook.
+
+    :param http_conn_id: Http connection ID with host as 
"https://discord.com/api/"; and
+                         default webhook endpoint in the extra field in the 
form of
+                         {"webhook_endpoint": 
"webhooks/{webhook.id}/{webhook.token}"}
+    :param webhook_endpoint: Discord webhook endpoint in the form of
+                             "webhooks/{webhook.id}/{webhook.token}"
+    :param message: The message you want to send to your Discord channel
+                    (max 2000 characters)
+    :param username: Override the default username of the webhook
+    :param avatar_url: Override the default avatar of the webhook
+    :param tts: Is a text-to-speech message
+    :param proxy: Proxy to use to make the Discord webhook call
+    """
+
+    default_headers = {
+        "Content-Type": "application/json",
+    }
+    conn_name_attr = "http_conn_id"
+    default_conn_name = "discord_default"
+    conn_type = "discord"
+    hook_name = "Async Discord"
+
+    def __init__(
+        self,
+        http_conn_id: str = "",

Review Comment:
   ```suggestion
           *,
           http_conn_id: str = "",
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to