Source code for airflow.providers.slack.hooks.slack_webhook
## Licensed to the Apache Software Foundation (ASF) under one# or more contributor license agreements. See the NOTICE file# distributed with this work for additional information# regarding copyright ownership. The ASF licenses this file# to you under the Apache License, Version 2.0 (the# "License"); you may not use this file except in compliance# with the License. You may obtain a copy of the License at## http://www.apache.org/licenses/LICENSE-2.0## Unless required by applicable law or agreed to in writing,# software distributed under the License is distributed on an# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY# KIND, either express or implied. See the License for the# specific language governing permissions and limitations# under the License.from__future__importannotationsimportjsonimportwarningsfromfunctoolsimportcached_property,wrapsfromtypingimportTYPE_CHECKING,Any,Callablefromslack_sdkimportWebhookClientfromairflow.exceptionsimportAirflowException,AirflowNotFoundExceptionfromairflow.hooks.baseimportBaseHookfromairflow.providers.slack.utilsimportConnectionExtraConfigifTYPE_CHECKING:fromslack_sdk.http_retryimportRetryHandler
[docs]defcheck_webhook_response(func:Callable)->Callable:"""Check WebhookResponse and raise an error if status code != 200."""@wraps(func)defwrapper(*args,**kwargs)->Callable:resp=func(*args,**kwargs)ifresp.status_code!=200:raiseAirflowException(f"Response body: {resp.body!r}, Status Code: {resp.status_code}. ""See: https://api.slack.com/messaging/webhooks#handling_errors")returnrespreturnwrapper
[docs]classSlackWebhookHook(BaseHook):""" This class provide a thin wrapper around the ``slack_sdk.WebhookClient``. This hook allows you to post messages to Slack by using Incoming Webhooks. .. seealso:: - :ref:`Slack Incoming Webhook connection <howto/connection:slack-incoming-webhook>` - https://api.slack.com/messaging/webhooks - https://slack.dev/python-slack-sdk/webhook/index.html .. note:: You cannot override the default channel (chosen by the user who installed your app), username, or icon when you're using Incoming Webhooks to post messages. Instead, these values will always inherit from the associated Slack App configuration (`link <https://api.slack.com/messaging/webhooks#advanced_message_formatting>`_). It is possible to change this values only in `Legacy Slack Integration Incoming Webhook <https://api.slack.com/legacy/custom-integrations/messaging/webhooks#legacy-customizations>`_. .. warning:: This hook intend to use `Slack Incoming Webhook` connection and might not work correctly with `Slack API` connection. Examples: .. code-block:: python # Create hook hook = SlackWebhookHook(slack_webhook_conn_id="slack_default") # Post message in Slack channel by JSON formatted message # See: https://api.slack.com/messaging/webhooks#posting_with_webhooks hook.send_dict({"text": "Hello world!"}) # Post simple message in Slack channel hook.send_text("Hello world!") # Use ``slack_sdk.WebhookClient`` hook.client.send(text="Hello world!") :param slack_webhook_conn_id: Slack Incoming Webhook connection id that has Incoming Webhook token in the password field. :param timeout: The maximum number of seconds the client will wait to connect and receive a response from Slack. If not set than default WebhookClient value will use. :param proxy: Proxy to make the Slack Incoming Webhook call. :param retry_handlers: List of handlers to customize retry logic in ``slack_sdk.WebhookClient``. """
if"webhook_token"inextra_client_args:warnings.warn(f"Provide `webhook_token` as part of {type(self).__name__!r} parameters is disallowed, "f"please use Airflow Connection.",UserWarning,stacklevel=2,)extra_client_args.pop("webhook_token")if"logger"notinextra_client_args:extra_client_args["logger"]=self.log
[docs]defclient(self)->WebhookClient:"""Get the underlying slack_sdk.webhook.WebhookClient (cached)."""returnWebhookClient(**self._get_conn_params())
[docs]defget_conn(self)->WebhookClient:"""Get the underlying slack_sdk.webhook.WebhookClient (cached)."""returnself.client
def_get_conn_params(self)->dict[str,Any]:"""Fetch connection params as a dict and merge it with hook parameters."""conn=self.get_connection(self.slack_webhook_conn_id)ifnotconn.passwordornotconn.password.strip():raiseAirflowNotFoundException(f"Connection ID {self.slack_webhook_conn_id!r} does not contain password "f"(Slack Webhook Token).")webhook_token=conn.password.strip()ifwebhook_tokenand"://"inconn.password:self.log.debug("Retrieving Slack Webhook Token URL from webhook token.")url=webhook_tokenelse:self.log.debug("Constructing Slack Webhook Token URL.")base_url=conn.hostifnotbase_urlor"://"notinbase_url:base_url=f"{conn.schemaor'https'}://{conn.hostor'hooks.slack.com/services'}"url=(base_url.rstrip("/")+"/"+webhook_token.lstrip("/")).rstrip("/")conn_params:dict[str,Any]={"url":url,"retry_handlers":self.retry_handlers}extra_config=ConnectionExtraConfig(conn_type=self.conn_type,conn_id=conn.conn_id,extra=conn.extra_dejson)# Merge Hook parameters with Connection configconn_params.update({"timeout":self.timeoutorextra_config.getint("timeout",default=None),"proxy":self.proxyorextra_config.get("proxy",default=None),})# Add additional client argsconn_params.update(self.extra_client_args)return{k:vfork,vinconn_params.items()ifvisnotNone}@check_webhook_response
[docs]defsend_dict(self,body:dict[str,Any]|str,*,headers:dict[str,str]|None=None):""" Perform a Slack Incoming Webhook request with given JSON data block. :param body: JSON data structure, expected dict or JSON-string. :param headers: Request headers for this request. """ifisinstance(body,str):try:body=json.loads(body)exceptjson.JSONDecodeErroraserr:raiseAirflowException(f"Body expected valid JSON string, got {body!r}. Original error:\n * {err}")fromNoneifnotisinstance(body,dict):raiseTypeError(f"Body expected dictionary, got {type(body).__name__}.")ifany(legacy_attrinbodyforlegacy_attrin("channel","username","icon_emoji","icon_url")):warnings.warn("You cannot override the default channel (chosen by the user who installed your app), ""username, or icon when you're using Incoming Webhooks to post messages. ""Instead, these values will always inherit from the associated Slack app configuration. ""See: https://api.slack.com/messaging/webhooks#advanced_message_formatting. ""It is possible to change this values only in Legacy Slack Integration Incoming Webhook: ""https://api.slack.com/legacy/custom-integrations/messaging/webhooks#legacy-customizations",UserWarning,stacklevel=2,)returnself.client.send_dict(body,headers=headers)
[docs]defsend(self,*,text:str|None=None,blocks:list[dict[str,Any]]|None=None,response_type:str|None=None,replace_original:bool|None=None,delete_original:bool|None=None,unfurl_links:bool|None=None,unfurl_media:bool|None=None,headers:dict[str,str]|None=None,attachments:list[dict[str,Any]]|None=None,**kwargs,):""" Perform a Slack Incoming Webhook request with given arguments. :param text: The text message (even when having blocks, setting this as well is recommended as it works as fallback). :param blocks: A collection of Block Kit UI components. :param response_type: The type of message (either 'in_channel' or 'ephemeral'). :param replace_original: True if you use this option for response_url requests. :param delete_original: True if you use this option for response_url requests. :param unfurl_links: Option to indicate whether text url should unfurl. :param unfurl_media: Option to indicate whether media url should unfurl. :param headers: Request headers for this request. :param attachments: (legacy) A collection of attachments. """body={"text":text,"attachments":attachments,"blocks":blocks,"response_type":response_type,"replace_original":replace_original,"delete_original":delete_original,"unfurl_links":unfurl_links,"unfurl_media":unfurl_media,# Legacy Integration Parameters**kwargs,}body={k:vfork,vinbody.items()ifvisnotNone}returnself.send_dict(body=body,headers=headers)
[docs]defsend_text(self,text:str,*,unfurl_links:bool|None=None,unfurl_media:bool|None=None,headers:dict[str,str]|None=None,):""" Perform a Slack Incoming Webhook request with given text. :param text: The text message. :param unfurl_links: Option to indicate whether text url should unfurl. :param unfurl_media: Option to indicate whether media url should unfurl. :param headers: Request headers for this request. """returnself.send(text=text,unfurl_links=unfurl_links,unfurl_media=unfurl_media,headers=headers)
@classmethod
[docs]defget_connection_form_widgets(cls)->dict[str,Any]:"""Return dictionary of widgets to be added for the hook to handle extra values."""fromflask_appbuilder.fieldwidgetsimportBS3TextFieldWidgetfromflask_babelimportlazy_gettextfromwtformsimportIntegerField,StringFieldfromwtforms.validatorsimportNumberRange,Optionalreturn{"timeout":IntegerField(lazy_gettext("Timeout"),widget=BS3TextFieldWidget(),validators=[Optional(),NumberRange(min=1)],description="Optional. The maximum number of seconds the client will wait to connect ""and receive a response from Slack Incoming Webhook.",),"proxy":StringField(lazy_gettext("Proxy"),widget=BS3TextFieldWidget(),description="Optional. Proxy to make the Slack Incoming Webhook call.",),}
@classmethod
[docs]defget_ui_field_behaviour(cls)->dict[str,Any]:"""Return custom field behaviour."""return{"hidden_fields":["login","port","extra"],"relabeling":{"host":"Slack Webhook Endpoint","password":"Webhook Token",},"placeholders":{"schema":"https","host":"hooks.slack.com/services","password":"T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX","timeout":"30","proxy":"http://localhost:9000",},}