Skip to content

Commit 7ae8172

Browse files
committed
Refactor Slack API Hook and add Connection
1 parent 4bb4fce commit 7ae8172

File tree

11 files changed

+801
-112
lines changed

11 files changed

+801
-112
lines changed

airflow/providers/slack/hooks/slack.py

Lines changed: 210 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,48 @@
1515
# KIND, either express or implied. See the License for the
1616
# specific language governing permissions and limitations
1717
# under the License.
18-
"""Hook for Slack"""
19-
from typing import Any, Optional
18+
19+
import json
20+
import warnings
21+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
2022

2123
from slack_sdk import WebClient
22-
from slack_sdk.web.slack_response import SlackResponse
24+
from slack_sdk.errors import SlackApiError
2325

24-
from airflow.exceptions import AirflowException
26+
from airflow.compat.functools import cached_property
27+
from airflow.exceptions import AirflowException, AirflowNotFoundException
2528
from airflow.hooks.base import BaseHook
29+
from airflow.providers.slack.utils import ConnectionExtraConfig, prefixed_extra_field
30+
from airflow.utils.log.secrets_masker import mask_secret
31+
32+
if TYPE_CHECKING:
33+
from slack_sdk.http_retry import RetryHandler
34+
from slack_sdk.web.slack_response import SlackResponse
2635

2736

2837
class SlackHook(BaseHook):
2938
"""
30-
Creates a Slack connection to be used for calls.
39+
Creates a Slack API Connection to be used for calls.
40+
41+
This class provide a thin wrapper around the ``slack_sdk.WebClient``.
42+
43+
.. seealso::
44+
- :ref:`Slack API connection <howto/connection:slack>`
45+
- https://api.slack.com/messaging
46+
- https://slack.dev/python-slack-sdk/web/index.html
47+
48+
.. warning::
49+
This hook intend to use `Slack API` connection
50+
and might not work correctly with `Slack Webhook` and `HTTP` connections.
3151
3252
Takes both Slack API token directly and connection that has Slack API token. If both are
3353
supplied, Slack API token will be used. Also exposes the rest of slack.WebClient args.
54+
3455
Examples:
35-
.. code-block:: python
56+
.. code-block:: python
3657
3758
# Create hook
38-
slack_hook = SlackHook(token="xxx") # or slack_hook = SlackHook(slack_conn_id="slack")
59+
slack_hook = SlackHook(slack_conn_id="slack_api_default")
3960
4061
# Call generic API with parameters (errors are handled by hook)
4162
# For more details check https://api.slack.com/methods/chat.postMessage
@@ -45,28 +66,124 @@ class SlackHook(BaseHook):
4566
# For more details check https://slack.dev/python-slack-sdk/web/index.html#messaging
4667
slack_hook.client.chat_postMessage(channel="#random", text="Hello world!")
4768
48-
:param token: Slack API token
4969
:param slack_conn_id: :ref:`Slack connection id <howto/connection:slack>`
5070
that has Slack API token in the password field.
51-
:param use_session: A boolean specifying if the client should take advantage of
52-
connection pooling. Default is True.
53-
:param base_url: A string representing the Slack API base URL. Default is
54-
``https://www.slack.com/api/``
55-
:param timeout: The maximum number of seconds the client will wait
56-
to connect and receive a response from Slack. Default is 30 seconds.
71+
:param timeout: The maximum number of seconds the client will wait to connect
72+
and receive a response from Slack. If not set than default WebClient value will use.
73+
:param base_url: A string representing the Slack API base URL.
74+
If not set than default WebClient BASE_URL will use (``https://www.slack.com/api/``).
75+
:param proxy: Proxy to make the Slack Incoming Webhook call.
76+
:param retry_handlers: List of handlers to customize retry logic in WebClient.
77+
:param token: (deprecated) Slack API Token.
5778
"""
5879

80+
conn_name_attr = 'slack_conn_id'
81+
default_conn_name = 'slack_api_default'
82+
conn_type = 'slack'
83+
hook_name = 'Slack API'
84+
5985
def __init__(
6086
self,
6187
token: Optional[str] = None,
6288
slack_conn_id: Optional[str] = None,
63-
**client_args: Any,
89+
base_url: Optional[str] = None,
90+
timeout: Optional[int] = None,
91+
proxy: Optional[str] = None,
92+
retry_handlers: Optional[List["RetryHandler"]] = None,
93+
**extra_client_args: Any,
6494
) -> None:
95+
if not token and not slack_conn_id:
96+
raise AirflowException("Either `slack_conn_id` or `token` should be provided.")
97+
if token:
98+
mask_secret(token)
99+
warnings.warn(
100+
"Provide token as hook argument deprecated by security reason and will be removed "
101+
"in a future releases. Please specify token in `Slack API` connection.",
102+
DeprecationWarning,
103+
stacklevel=2,
104+
)
105+
if not slack_conn_id:
106+
warnings.warn(
107+
"You have not set parameter `slack_conn_id`. Currently `Slack API` connection id optional "
108+
"but in a future release it will mandatory.",
109+
FutureWarning,
110+
stacklevel=2,
111+
)
112+
65113
super().__init__()
66-
self.token = self.__get_token(token, slack_conn_id)
67-
self.client = WebClient(self.token, **client_args)
114+
self._token = token
115+
self.slack_conn_id = slack_conn_id
116+
self.base_url = base_url
117+
self.timeout = timeout
118+
self.proxy = proxy
119+
self.retry_handlers = retry_handlers
120+
self.extra_client_args = extra_client_args
121+
if self.extra_client_args.pop("use_session", None) is not None:
122+
warnings.warn("`use_session` has no affect in slack_sdk.WebClient.", UserWarning, stacklevel=2)
123+
124+
@cached_property
125+
def client(self) -> WebClient:
126+
"""Get the underlying slack_sdk.WebClient (cached)."""
127+
return WebClient(**self._get_conn_params())
128+
129+
def get_conn(self) -> WebClient:
130+
"""Get the underlying slack_sdk.WebClient (cached)."""
131+
return self.client
132+
133+
def _get_conn_params(self) -> Dict[str, Any]:
134+
"""Fetch connection params as a dict and merge it with hook parameters."""
135+
conn = self.get_connection(self.slack_conn_id) if self.slack_conn_id else None
136+
conn_params: Dict[str, Any] = {}
137+
138+
if self._token:
139+
conn_params["token"] = self._token
140+
elif conn:
141+
if not conn.password:
142+
raise AirflowNotFoundException(
143+
f"Connection ID {self.slack_conn_id!r} does not contain password (Slack API Token)."
144+
)
145+
conn_params["token"] = conn.password
146+
147+
extra_config = ConnectionExtraConfig(
148+
conn_type=self.conn_type,
149+
conn_id=conn.conn_id if conn else None,
150+
extra=conn.extra_dejson if conn else {},
151+
)
152+
153+
# Merge Hook parameters with Connection config
154+
conn_params.update(
155+
{
156+
"timeout": self.timeout or extra_config.getint("timeout", default=None),
157+
"base_url": self.base_url or extra_config.get("base_url", default=None),
158+
"proxy": self.proxy or extra_config.get("proxy", default=None),
159+
"retry_handlers": (
160+
self.retry_handlers or extra_config.getimports("retry_handlers", default=None)
161+
),
162+
}
163+
)
164+
165+
# Add additional client args
166+
conn_params.update(self.extra_client_args)
167+
if "logger" not in conn_params:
168+
conn_params["logger"] = self.log
169+
170+
return {k: v for k, v in conn_params.items() if v is not None}
171+
172+
@cached_property
173+
def token(self) -> str:
174+
warnings.warn(
175+
"`SlackHook.token` property deprecated and will be removed in a future releases.",
176+
DeprecationWarning,
177+
stacklevel=2,
178+
)
179+
return self._get_conn_params()["token"]
68180

69181
def __get_token(self, token: Any, slack_conn_id: Any) -> str:
182+
warnings.warn(
183+
"`SlackHook.__get_token` method deprecated and will be removed in a future releases.",
184+
DeprecationWarning,
185+
stacklevel=2,
186+
)
70187
if token is not None:
71188
return token
72189

@@ -79,7 +196,7 @@ def __get_token(self, token: Any, slack_conn_id: Any) -> str:
79196

80197
raise AirflowException('Cannot get token: No valid Slack token nor slack_conn_id supplied.')
81198

82-
def call(self, api_method: str, **kwargs) -> SlackResponse:
199+
def call(self, api_method: str, **kwargs) -> "SlackResponse":
83200
"""
84201
Calls Slack WebClient `WebClient.api_call` with given arguments.
85202
@@ -95,3 +212,78 @@ def call(self, api_method: str, **kwargs) -> SlackResponse:
95212
iterated on to execute subsequent requests.
96213
"""
97214
return self.client.api_call(api_method, **kwargs)
215+
216+
def test_connection(self):
217+
"""Tests the Slack API connection.
218+
219+
.. seealso::
220+
https://api.slack.com/methods/auth.test
221+
"""
222+
try:
223+
response = self.call("auth.test")
224+
response.validate()
225+
except SlackApiError as e:
226+
return False, str(e)
227+
except Exception as e:
228+
return False, f"Unknown error occurred while testing connection: {e}"
229+
230+
if isinstance(response.data, bytes):
231+
# If response data binary then return simple message
232+
return True, f"Connection successfully tested (url: {response.api_url})."
233+
234+
try:
235+
return True, json.dumps(response.data)
236+
except TypeError:
237+
return True, str(response)
238+
239+
@classmethod
240+
def get_connection_form_widgets(cls) -> Dict[str, Any]:
241+
"""Returns dictionary of widgets to be added for the hook to handle extra values."""
242+
from flask_appbuilder.fieldwidgets import BS3TextFieldWidget
243+
from flask_babel import lazy_gettext
244+
from wtforms import IntegerField, StringField
245+
246+
return {
247+
prefixed_extra_field("timeout", cls.conn_type): IntegerField(
248+
lazy_gettext("Timeout"),
249+
widget=BS3TextFieldWidget(),
250+
description="Optional. The maximum number of seconds the client will wait to connect "
251+
"and receive a response from Slack API.",
252+
),
253+
prefixed_extra_field("base_url", cls.conn_type): StringField(
254+
lazy_gettext('Base URL'),
255+
widget=BS3TextFieldWidget(),
256+
description="Optional. A string representing the Slack API base URL.",
257+
),
258+
prefixed_extra_field("proxy", cls.conn_type): StringField(
259+
lazy_gettext('Proxy'),
260+
widget=BS3TextFieldWidget(),
261+
description="Optional. Proxy to make the Slack API call.",
262+
),
263+
prefixed_extra_field("retry_handlers", cls.conn_type): StringField(
264+
lazy_gettext('Retry Handlers'),
265+
widget=BS3TextFieldWidget(),
266+
description="Optional. Comma separated list of import paths to zero-argument callable "
267+
"which returns retry handler for Slack WebClient.",
268+
),
269+
}
270+
271+
@classmethod
272+
def get_ui_field_behaviour(cls) -> Dict[str, Any]:
273+
"""Returns custom field behaviour."""
274+
return {
275+
"hidden_fields": ["login", "port", "host", "schema", "extra"],
276+
"relabeling": {
277+
"password": "Slack API Token",
278+
},
279+
"placeholders": {
280+
"password": "xoxb-1234567890123-09876543210987-AbCdEfGhIjKlMnOpQrStUvWx",
281+
prefixed_extra_field("timeout", cls.conn_type): "30",
282+
prefixed_extra_field("base_url", cls.conn_type): "https://www.slack.com/api/",
283+
prefixed_extra_field("proxy", cls.conn_type): "http://localhost:9000",
284+
prefixed_extra_field("retry_handlers", cls.conn_type): (
285+
"slack_sdk.http_retry.builtin_handlers.ConnectionErrorRetryHandler,"
286+
"slack_sdk.http_retry.builtin_handlers.RateLimitErrorRetryHandler"
287+
),
288+
},
289+
}

airflow/providers/slack/hooks/slack_webhook.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ class SlackWebhookHook(HttpHook):
3131
If both supplied, http_conn_id will be used as base_url,
3232
and webhook_token will be taken as endpoint, the relative path of the url.
3333
34+
.. warning::
35+
This hook intend to use `Slack Webhook` connection
36+
and might not work correctly with `Slack API` connection.
37+
3438
Each Slack webhook token can be pre-configured to use a specific channel, username and
3539
icon. You can override these defaults in this hook.
3640

airflow/providers/slack/operators/slack.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class SlackAPIOperator(BaseOperator):
2929
In the future additional Slack API Operators will be derived from this class as well.
3030
Only one of `slack_conn_id` and `token` is required.
3131
32-
:param slack_conn_id: :ref:`Slack connection id <howto/connection:slack>`
32+
:param slack_conn_id: :ref:`Slack API Connection <howto/connection:slack>`
3333
which its password is Slack API token. Optional
3434
:param token: Slack API token (https://api.slack.com/web). Optional
3535
:param method: The Slack API Method to Call (https://api.slack.com/methods). Optional

airflow/providers/slack/provider.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,7 @@ transfers:
6868
how-to-guide: /docs/apache-airflow-providers-slack/operators/sql_to_slack.rst
6969

7070
connection-types:
71+
- hook-class-name: airflow.providers.slack.hooks.slack.SlackHook
72+
connection-type: slack
7173
- hook-class-name: airflow.providers.slack.hooks.slack_webhook.SlackWebhookHook
7274
connection-type: slackwebhook

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy