Repository: slackapi/python-slackclient
Pull request #466 (2019-06-29 10:05:01)

Fixing bugs that were recently introduced in master
###  Summary
RTMClient:
1. Updated the send_over_websocket method to use the same event loop.
2. Added back a method `_execute_in_thread` to executescallbacks in a separate thread for non-async methods.

WebClient:
1. Fix a bug where we incorrectly created a new event loop regardless if there was already a loop running.
2. Added `charset` to the headers to prevent the missing charset warning from Slack.
3. Refactored how we opened and closed files because the file has to be open while it's being transported.
4. Added a Future return type to the Web API methods.

Other updates:
1. Updated ReadMe to include details around how to install optional extras.
2. Added optional headers attributes to the RTM and Web clients.
3. Minor test updates.

### Requirements
* [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slackclient/blob/master/.github/contributing.md) and have done my best effort to follow them.
* [x] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).

codecov[bot]: # [Codecov](https://codecov.io/gh/slackapi/python-slackclient/pull/466?src=pr&el=h1) Report
> Merging [#466](https://codecov.io/gh/slackapi/python-slackclient/pull/466?src=pr&el=desc) into [master](https://codecov.io/gh/slackapi/python-slackclient/commit/66017f39adfbe6d0c38cb1f0d6e551aff949398c?src=pr&el=desc) will **increase** coverage by `0.24%`.
> The diff coverage is `96.77%`.

[![Impacted file tree graph](https://codecov.io/gh/slackapi/python-slackclient/pull/466/graphs/tree.svg?width=650&token=F0O7meYmtD&height=150&src=pr)](https://codecov.io/gh/slackapi/python-slackclient/pull/466?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #466      +/-   ##
==========================================
+ Coverage   69.33%   69.58%   +0.24%     
==========================================
  Files          15       15              
  Lines        1634     1654      +20     
  Branches       90       91       +1     
==========================================
+ Hits         1133     1151      +18     
- Misses        479      481       +2     
  Partials       22       22
```


| [Impacted Files](https://codecov.io/gh/slackapi/python-slackclient/pull/466?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [slack/rtm/client.py](https://codecov.io/gh/slackapi/python-slackclient/pull/466/diff?src=pr&el=tree#diff-c2xhY2svcnRtL2NsaWVudC5weQ==) | `84.15% <100%> (+0.81%)` | :arrow_up: |
| [slack/web/client.py](https://codecov.io/gh/slackapi/python-slackclient/pull/466/diff?src=pr&el=tree#diff-c2xhY2svd2ViL2NsaWVudC5weQ==) | `33.87% <100%> (+0.15%)` | :arrow_up: |
| [slack/web/base\_client.py](https://codecov.io/gh/slackapi/python-slackclient/pull/466/diff?src=pr&el=tree#diff-c2xhY2svd2ViL2Jhc2VfY2xpZW50LnB5) | `81.44% <80.76%> (-0.17%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/slackapi/python-slackclient/pull/466?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/slackapi/python-slackclient/pull/466?src=pr&el=footer). Last update [66017f3...f2b823b](https://codecov.io/gh/slackapi/python-slackclient/pull/466?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index d80fb74..f2cb2f1 100755
--- a/README.md
+++ b/README.md
@@ -57,7 +57,7 @@ We recommend using [PyPI][pypi] to install the Slack Developer Kit for Python.
 
 
 ```bash
-pip3 install slackclient==2.0.0
+$ pip3 install slackclient
 ```
 
 ### Getting started tutorial
@@ -204,7 +204,14 @@ async def send_async_message(channel='#random', text='')
 
 ### Advanced Options
 
-The Python slackclient v2 now uses [AIOHttp][aiohttp] under the hood so it allows us to use their built-in [SSL](https://docs.aiohttp.org/en/stable/client_advanced.html#ssl-control-for-tcp-sockets) and [Proxy](https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support) support. You can pass these options directly into both the RTM and the Web client.
+The Python slackclient v2 now uses [AIOHttp][aiohttp] under the hood.
+
+Looking for a performance boost? Installing the optional dependencies (aiodns) may help speed up DNS resolving by the client. We've included it as an extra called "optional":
+```bash
+$ pip3 install slackclient[optional]
+```
+
+Interested in SSL or Proxy support? Simply use their built-in [SSL](https://docs.aiohttp.org/en/stable/client_advanced.html#ssl-control-for-tcp-sockets) and [Proxy](https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support) arguments. You can pass these options directly into both the RTM and the Web client.
 
 ```python
 import os
diff --git a/slack/rtm/client.py b/slack/rtm/client.py
index 55012f0..98a4c2b 100644
--- a/slack/rtm/client.py
+++ b/slack/rtm/client.py
@@ -5,6 +5,7 @@ import os
 import logging
 import random
 import collections
+import concurrent
 import inspect
 import signal
 from typing import Optional, Callable, DefaultDict
@@ -113,6 +114,7 @@ class RTMClient(object):
         connect_method: Optional[str] = None,
         ping_interval: Optional[int] = 30,
         loop: Optional[asyncio.AbstractEventLoop] = None,
+        headers: Optional[dict] = {},
     ):
         self.token = token
         self.run_async = run_async
@@ -123,6 +125,7 @@ class RTMClient(object):
         self.base_url = base_url
         self.connect_method = connect_method
         self.ping_interval = ping_interval
+        self.headers = headers
         self._event_loop = loop or asyncio.get_event_loop()
         self._web_client = None
         self._websocket = None
@@ -223,7 +226,7 @@ class RTMClient(object):
         Raises:
             SlackClientNotConnectedError: Websocket connection is closed.
         """
-        return asyncio.ensure_future(self._send_json(payload))
+        return asyncio.ensure_future(self._send_json(payload), loop=self._event_loop)
 
     async def _send_json(self, payload):
         if self._websocket is None or self._event_loop is None:
@@ -434,7 +437,7 @@ class RTMClient(object):
                         rtm_client=self, web_client=self._web_client, data=data
                     )
                 else:
-                    callback(rtm_client=self, web_client=self._web_client, data=data)
+                    self._execute_in_thread(callback, data)
             except Exception as err:
                 name = callback.__name__
                 module = callback.__module__
@@ -442,6 +445,25 @@ class RTMClient(object):
                 self._logger.error(msg)
                 raise
 
+    def _execute_in_thread(self, callback, data):
+        """Execute the callback in another thread. Wait for and return the results."""
+        web_client = WebClient(
+            token=self.token,
+            base_url=self.base_url,
+            ssl=self.ssl,
+            proxy=self.proxy,
+            headers=self.headers,
+        )
+        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
+            future = executor.submit(
+                callback, rtm_client=self, web_client=web_client, data=data
+            )
+
+            while future.running():
+                pass
+
+            future.result()
+
     async def _retreive_websocket_info(self):
         """Retreives the WebSocket info from Slack.
 
@@ -472,6 +494,7 @@ class RTMClient(object):
                 run_async=True,
                 loop=self._event_loop,
                 session=self._session,
+                headers=self.headers,
             )
         self._logger.debug("Retrieving websocket info.")
         if self.connect_method in ["rtm.start", "rtm_start"]:
diff --git a/slack/web/base_client.py b/slack/web/base_client.py
index 5f8b3ff..b65c482 100644
--- a/slack/web/base_client.py
+++ b/slack/web/base_client.py
@@ -34,6 +34,7 @@ class BaseClient:
         proxy=None,
         run_async=False,
         session=None,
+        headers: Optional[dict] = None,
     ):
         self.token = token
         self.base_url = base_url
@@ -42,17 +43,45 @@ class BaseClient:
         self.proxy = proxy
         self.run_async = run_async
         self.session = session
+        self.headers = headers or {}
         self._logger = logging.getLogger(__name__)
         self._event_loop = loop
 
-    def _set_event_loop(self):
-        if self.run_async:
+    def _get_event_loop(self):
+        """Retrieves the event loop or creates a new one."""
+        try:
             return asyncio.get_event_loop()
-        else:
+        except RuntimeError:
             loop = asyncio.new_event_loop()
             asyncio.set_event_loop(loop)
             return loop
 
+    def _get_headers(self, has_json, has_files):
+        """Contructs the headers need for a request.
+        Args:
+            has_json (bool): Whether or not the request has json.
+        Returns:
+            The headers dictionary.
+                e.g. {
+                    'Content-Type': 'application/json;charset=utf-8',
+                    'Authorization': 'Bearer xoxb-1234-1243',
+                    'User-Agent': 'Python/3.6.8 slack/2.1.0 Darwin/17.7.0'
+                }
+        """
+        headers = {
+            "User-Agent": self._get_user_agent(),
+            "Authorization": "Bearer {}".format(self.token),
+            "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
+        }
+        if has_json:
+            headers.update({"Content-Type": "application/json;charset=utf-8"})
+
+        if has_files:
+            # These are set automatically by the aiohttp library.
+            headers.pop("Content-Type", None)
+
+        return headers
+
     def api_call(
         self,
         api_method: str,
@@ -62,7 +91,7 @@ class BaseClient:
         data: Union[dict, FormData] = None,
         params: dict = None,
         json: dict = None,
-    ):
+    ) -> Union[asyncio.Future, SlackResponse]:
         """Create a request and execute the API call to Slack.
 
         Args:
@@ -93,33 +122,18 @@ class BaseClient:
             SlackRequestError: Json data can only be submitted as
                 POST requests.
         """
-        if json is not None and http_verb != "POST":
+        has_json = json is not None
+        has_files = files is not None
+        if has_json and http_verb != "POST":
             msg = "Json data can only be submitted as POST requests. GET requests should use the 'params' argument."
             raise err.SlackRequestError(msg)
 
         api_url = self._get_url(api_method)
-        headers = {
-            "User-Agent": self._get_user_agent(),
-            "Authorization": "Bearer {}".format(self.token),
-        }
-        if files is not None:
-            form_data = FormData()
-            for k, v in files.items():
-                if isinstance(v, str):
-                    with open(v, "rb") as fd:
-                        form_data.add_field(k, fd)
-                else:
-                    form_data.add_field(k, v)
-
-            if isinstance(data, dict):
-                for k, v in data.items():
-                    form_data.add_field(k, str(v))
-
-            data = form_data
 
         req_args = {
-            "headers": headers,
+            "headers": self._get_headers(has_json, has_files),
             "data": data,
+            "files": files,
             "params": params,
             "json": json,
             "ssl": self.ssl,
@@ -127,7 +141,7 @@ class BaseClient:
         }
 
         if self._event_loop is None:
-            self._event_loop = self._set_event_loop()
+            self._event_loop = self._get_event_loop()
 
         future = asyncio.ensure_future(
             self._send(http_verb=http_verb, api_url=api_url, req_args=req_args),
@@ -182,9 +196,24 @@ class BaseClient:
         Returns:
             The response parsed into a SlackResponse object.
         """
+        open_files = []
+        files = req_args.pop("files", None)
+        if files is not None:
+            for k, v in files.items():
+                if isinstance(v, str):
+                    f = open(v.encode("ascii", "ignore"), "rb")
+                    open_files.append(f)
+                    req_args["data"].update({k: f})
+                else:
+                    req_args["data"].update({k: v})
+
         res = await self._request(
             http_verb=http_verb, api_url=api_url, req_args=req_args
         )
+
+        for f in open_files:
+            f.close()
+
         data = {
             "client": self,
             "http_verb": http_verb,
diff --git a/slack/web/client.py b/slack/web/client.py
index 3251c05..53c9c3b 100644
--- a/slack/web/client.py
+++ b/slack/web/client.py
@@ -3,6 +3,7 @@
 # Standard Imports
 from typing import Union, List
 from io import IOBase
+from asyncio import Future
 
 # Internal Imports
 from slack.web.base_client import BaseClient, SlackResponse
@@ -65,23 +66,25 @@ class WebClient(BaseClient):
         removed at anytime.
     """
 
-    def api_test(self, **kwargs) -> SlackResponse:
+    def api_test(self, **kwargs) -> Union[Future, SlackResponse]:
         """Checks API calling code."""
         return self.api_call("api.test", json=kwargs)
 
-    def auth_revoke(self, **kwargs) -> SlackResponse:
+    def auth_revoke(self, **kwargs) -> Union[Future, SlackResponse]:
         """Revokes a token."""
         return self.api_call("auth.revoke", http_verb="GET", params=kwargs)
 
-    def auth_test(self, **kwargs) -> SlackResponse:
+    def auth_test(self, **kwargs) -> Union[Future, SlackResponse]:
         """Checks authentication & identity."""
         return self.api_call("auth.test", json=kwargs)
 
-    def bots_info(self, **kwargs) -> SlackResponse:
+    def bots_info(self, **kwargs) -> Union[Future, SlackResponse]:
         """Gets information about a bot user."""
         return self.api_call("bots.info", http_verb="GET", params=kwargs)
 
-    def channels_archive(self, *, channel: str, **kwargs) -> SlackResponse:
+    def channels_archive(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Archives a channel.
 
         Args:
@@ -91,7 +94,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("channels.archive", json=kwargs)
 
-    def channels_create(self, *, name: str, **kwargs) -> SlackResponse:
+    def channels_create(self, *, name: str, **kwargs) -> Union[Future, SlackResponse]:
         """Creates a channel.
 
         Args:
@@ -101,7 +104,9 @@ class WebClient(BaseClient):
         kwargs.update({"name": name})
         return self.api_call("channels.create", json=kwargs)
 
-    def channels_history(self, *, channel: str, **kwargs) -> SlackResponse:
+    def channels_history(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Fetches history of messages and events from a channel.
 
         Args:
@@ -110,7 +115,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("channels.history", http_verb="GET", params=kwargs)
 
-    def channels_info(self, *, channel: str, **kwargs) -> SlackResponse:
+    def channels_info(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Gets information about a channel.
 
         Args:
@@ -119,7 +124,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("channels.info", http_verb="GET", params=kwargs)
 
-    def channels_invite(self, *, channel: str, user: str, **kwargs) -> SlackResponse:
+    def channels_invite(
+        self, *, channel: str, user: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Invites a user to a channel.
 
         Args:
@@ -130,7 +137,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "user": user})
         return self.api_call("channels.invite", json=kwargs)
 
-    def channels_join(self, *, name: str, **kwargs) -> SlackResponse:
+    def channels_join(self, *, name: str, **kwargs) -> Union[Future, SlackResponse]:
         """Joins a channel, creating it if needed.
 
         Args:
@@ -140,7 +147,9 @@ class WebClient(BaseClient):
         kwargs.update({"name": name})
         return self.api_call("channels.join", json=kwargs)
 
-    def channels_kick(self, *, channel: str, user: str, **kwargs) -> SlackResponse:
+    def channels_kick(
+        self, *, channel: str, user: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Removes a user from a channel.
 
         Args:
@@ -151,7 +160,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "user": user})
         return self.api_call("channels.kick", json=kwargs)
 
-    def channels_leave(self, *, channel: str, **kwargs) -> SlackResponse:
+    def channels_leave(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Leaves a channel.
 
         Args:
@@ -161,11 +170,13 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("channels.leave", json=kwargs)
 
-    def channels_list(self, **kwargs) -> SlackResponse:
+    def channels_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists all channels in a Slack team."""
         return self.api_call("channels.list", http_verb="GET", params=kwargs)
 
-    def channels_mark(self, *, channel: str, ts: str, **kwargs) -> SlackResponse:
+    def channels_mark(
+        self, *, channel: str, ts: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Sets the read cursor in a channel.
 
         Args:
@@ -175,7 +186,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "ts": ts})
         return self.api_call("channels.mark", json=kwargs)
 
-    def channels_rename(self, *, channel: str, name: str, **kwargs) -> SlackResponse:
+    def channels_rename(
+        self, *, channel: str, name: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Renames a channel.
 
         Args:
@@ -188,7 +201,7 @@ class WebClient(BaseClient):
 
     def channels_replies(
         self, *, channel: str, thread_ts: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Retrieve a thread of messages posted to a channel
 
         Args:
@@ -201,7 +214,7 @@ class WebClient(BaseClient):
 
     def channels_setPurpose(
         self, *, channel: str, purpose: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Sets the purpose for a channel.
 
         Args:
@@ -211,7 +224,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "purpose": purpose})
         return self.api_call("channels.setPurpose", json=kwargs)
 
-    def channels_setTopic(self, *, channel: str, topic: str, **kwargs) -> SlackResponse:
+    def channels_setTopic(
+        self, *, channel: str, topic: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Sets the topic for a channel.
 
         Args:
@@ -221,7 +236,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "topic": topic})
         return self.api_call("channels.setTopic", json=kwargs)
 
-    def channels_unarchive(self, *, channel: str, **kwargs) -> SlackResponse:
+    def channels_unarchive(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Unarchives a channel.
 
         Args:
@@ -231,7 +248,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("channels.unarchive", json=kwargs)
 
-    def chat_delete(self, *, channel: str, ts: str, **kwargs) -> SlackResponse:
+    def chat_delete(
+        self, *, channel: str, ts: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Deletes a message.
 
         Args:
@@ -243,7 +262,7 @@ class WebClient(BaseClient):
 
     def chat_deleteScheduledMessage(
         self, *, channel: str, scheduled_message_id: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Deletes a scheduled message.
 
         Args:
@@ -257,7 +276,7 @@ class WebClient(BaseClient):
 
     def chat_getPermalink(
         self, *, channel: str, message_ts: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Retrieve a permalink URL for a specific extant message
 
         Args:
@@ -267,7 +286,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "message_ts": message_ts})
         return self.api_call("chat.getPermalink", http_verb="GET", params=kwargs)
 
-    def chat_meMessage(self, *, channel: str, text: str, **kwargs) -> SlackResponse:
+    def chat_meMessage(
+        self, *, channel: str, text: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Share a me message into a channel.
 
         Args:
@@ -277,7 +298,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "text": text})
         return self.api_call("chat.meMessage", json=kwargs)
 
-    def chat_postEphemeral(self, *, channel: str, user: str, **kwargs) -> SlackResponse:
+    def chat_postEphemeral(
+        self, *, channel: str, user: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Sends an ephemeral message to a user in a channel.
 
         Args:
@@ -292,7 +315,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "user": user})
         return self.api_call("chat.postEphemeral", json=kwargs)
 
-    def chat_postMessage(self, *, channel: str, **kwargs) -> SlackResponse:
+    def chat_postMessage(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Sends a message to a channel.
 
         Args:
@@ -308,7 +333,7 @@ class WebClient(BaseClient):
 
     def chat_scheduleMessage(
         self, *, channel: str, post_at: str, text: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Schedules a message.
 
         Args:
@@ -321,7 +346,7 @@ class WebClient(BaseClient):
 
     def chat_unfurl(
         self, *, channel: str, ts: str, unfurls: dict, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Provide custom unfurl behavior for user-posted URLs.
 
         Args:
@@ -334,7 +359,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "ts": ts, "unfurls": unfurls})
         return self.api_call("chat.unfurl", json=kwargs)
 
-    def chat_update(self, *, channel: str, ts: str, **kwargs) -> SlackResponse:
+    def chat_update(
+        self, *, channel: str, ts: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Updates a message in a channel.
 
         Args:
@@ -349,11 +376,13 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "ts": ts})
         return self.api_call("chat.update", json=kwargs)
 
-    def chat_scheduledMessages_list(self, **kwargs) -> SlackResponse:
+    def chat_scheduledMessages_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists all scheduled messages."""
         return self.api_call("chat.scheduledMessages.list", json=kwargs)
 
-    def conversations_archive(self, *, channel: str, **kwargs) -> SlackResponse:
+    def conversations_archive(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Archives a conversation.
 
         Args:
@@ -363,7 +392,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("conversations.archive", json=kwargs)
 
-    def conversations_close(self, *, channel: str, **kwargs) -> SlackResponse:
+    def conversations_close(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Closes a direct message or multi-person direct message.
 
         Args:
@@ -372,7 +403,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("conversations.close", json=kwargs)
 
-    def conversations_create(self, *, name: str, **kwargs) -> SlackResponse:
+    def conversations_create(
+        self, *, name: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Initiates a public or private channel-based conversation
 
         Args:
@@ -382,7 +415,9 @@ class WebClient(BaseClient):
         kwargs.update({"name": name})
         return self.api_call("conversations.create", json=kwargs)
 
-    def conversations_history(self, *, channel: str, **kwargs) -> SlackResponse:
+    def conversations_history(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Fetches a conversation's history of messages and events.
 
         Args:
@@ -391,7 +426,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("conversations.history", http_verb="GET", params=kwargs)
 
-    def conversations_info(self, *, channel: str, **kwargs) -> SlackResponse:
+    def conversations_info(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Retrieve information about a conversation.
 
         Args:
@@ -402,7 +439,7 @@ class WebClient(BaseClient):
 
     def conversations_invite(
         self, *, channel: str, users: List[str], **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Invites users to a channel.
 
         Args:
@@ -413,7 +450,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "users": users})
         return self.api_call("conversations.invite", json=kwargs)
 
-    def conversations_join(self, *, channel: str, **kwargs) -> SlackResponse:
+    def conversations_join(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Joins an existing conversation.
 
         Args:
@@ -423,7 +462,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("conversations.join", json=kwargs)
 
-    def conversations_kick(self, *, channel: str, user: str, **kwargs) -> SlackResponse:
+    def conversations_kick(
+        self, *, channel: str, user: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Removes a user from a conversation.
 
         Args:
@@ -434,7 +475,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "user": user})
         return self.api_call("conversations.kick", json=kwargs)
 
-    def conversations_leave(self, *, channel: str, **kwargs) -> SlackResponse:
+    def conversations_leave(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Leaves a conversation.
 
         Args:
@@ -444,11 +487,13 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("conversations.leave", json=kwargs)
 
-    def conversations_list(self, **kwargs) -> SlackResponse:
+    def conversations_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists all channels in a Slack team."""
         return self.api_call("conversations.list", http_verb="GET", params=kwargs)
 
-    def conversations_members(self, *, channel: str, **kwargs) -> SlackResponse:
+    def conversations_members(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Retrieve members of a conversation.
 
         Args:
@@ -457,13 +502,13 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("conversations.members", http_verb="GET", params=kwargs)
 
-    def conversations_open(self, **kwargs) -> SlackResponse:
+    def conversations_open(self, **kwargs) -> Union[Future, SlackResponse]:
         """Opens or resumes a direct message or multi-person direct message."""
         return self.api_call("conversations.open", json=kwargs)
 
     def conversations_rename(
         self, *, channel: str, name: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Renames a conversation.
 
         Args:
@@ -476,7 +521,7 @@ class WebClient(BaseClient):
 
     def conversations_replies(
         self, *, channel: str, ts: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Retrieve a thread of messages posted to a conversation
 
         Args:
@@ -488,7 +533,7 @@ class WebClient(BaseClient):
 
     def conversations_setPurpose(
         self, *, channel: str, purpose: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Sets the purpose for a conversation.
 
         Args:
@@ -500,7 +545,7 @@ class WebClient(BaseClient):
 
     def conversations_setTopic(
         self, *, channel: str, topic: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Sets the topic for a conversation.
 
         Args:
@@ -510,7 +555,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "topic": topic})
         return self.api_call("conversations.setTopic", json=kwargs)
 
-    def conversations_unarchive(self, *, channel: str, **kwargs) -> SlackResponse:
+    def conversations_unarchive(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Reverses conversation archival.
 
         Args:
@@ -520,7 +567,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("conversations.unarchive", json=kwargs)
 
-    def dialog_open(self, *, dialog: dict, trigger_id: str, **kwargs) -> SlackResponse:
+    def dialog_open(
+        self, *, dialog: dict, trigger_id: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Open a dialog with a user.
 
         Args:
@@ -549,21 +598,23 @@ class WebClient(BaseClient):
         kwargs.update({"dialog": dialog, "trigger_id": trigger_id})
         return self.api_call("dialog.open", json=kwargs)
 
-    def dnd_endDnd(self, **kwargs) -> SlackResponse:
+    def dnd_endDnd(self, **kwargs) -> Union[Future, SlackResponse]:
         """Ends the current user's Do Not Disturb session immediately."""
         self._validate_xoxp_token()
         return self.api_call("dnd.endDnd", json=kwargs)
 
-    def dnd_endSnooze(self, **kwargs) -> SlackResponse:
+    def dnd_endSnooze(self, **kwargs) -> Union[Future, SlackResponse]:
         """Ends the current user's snooze mode immediately."""
         self._validate_xoxp_token()
         return self.api_call("dnd.endSnooze", json=kwargs)
 
-    def dnd_info(self, **kwargs) -> SlackResponse:
+    def dnd_info(self, **kwargs) -> Union[Future, SlackResponse]:
         """Retrieves a user's current Do Not Disturb status."""
         return self.api_call("dnd.info", http_verb="GET", params=kwargs)
 
-    def dnd_setSnooze(self, *, num_minutes: int, **kwargs) -> SlackResponse:
+    def dnd_setSnooze(
+        self, *, num_minutes: int, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Turns on Do Not Disturb mode for the current user, or changes its duration.
 
         Args:
@@ -573,15 +624,17 @@ class WebClient(BaseClient):
         kwargs.update({"num_minutes": num_minutes})
         return self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs)
 
-    def dnd_teamInfo(self, **kwargs) -> SlackResponse:
+    def dnd_teamInfo(self, **kwargs) -> Union[Future, SlackResponse]:
         """Retrieves the Do Not Disturb status for users on a team."""
         return self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs)
 
-    def emoji_list(self, **kwargs) -> SlackResponse:
+    def emoji_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists custom emoji for a team."""
         return self.api_call("emoji.list", http_verb="GET", params=kwargs)
 
-    def files_comments_delete(self, *, file: str, id: str, **kwargs) -> SlackResponse:
+    def files_comments_delete(
+        self, *, file: str, id: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Deletes an existing comment on a file.
 
         Args:
@@ -591,7 +644,7 @@ class WebClient(BaseClient):
         kwargs.update({"file": file, "id": id})
         return self.api_call("files.comments.delete", json=kwargs)
 
-    def files_delete(self, *, file: str, **kwargs) -> SlackResponse:
+    def files_delete(self, *, file: str, **kwargs) -> Union[Future, SlackResponse]:
         """Deletes a file.
 
         Args:
@@ -600,7 +653,7 @@ class WebClient(BaseClient):
         kwargs.update({"file": file})
         return self.api_call("files.delete", json=kwargs)
 
-    def files_info(self, *, file: str, **kwargs) -> SlackResponse:
+    def files_info(self, *, file: str, **kwargs) -> Union[Future, SlackResponse]:
         """Gets information about a team file.
 
         Args:
@@ -609,12 +662,14 @@ class WebClient(BaseClient):
         kwargs.update({"file": file})
         return self.api_call("files.info", http_verb="GET", params=kwargs)
 
-    def files_list(self, **kwargs) -> SlackResponse:
+    def files_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists & filters team files."""
         self._validate_xoxp_token()
         return self.api_call("files.list", http_verb="GET", params=kwargs)
 
-    def files_revokePublicURL(self, *, file: str, **kwargs) -> SlackResponse:
+    def files_revokePublicURL(
+        self, *, file: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Revokes public/external sharing access for a file
 
         Args:
@@ -624,7 +679,9 @@ class WebClient(BaseClient):
         kwargs.update({"file": file})
         return self.api_call("files.revokePublicURL", json=kwargs)
 
-    def files_sharedPublicURL(self, *, file: str, **kwargs) -> SlackResponse:
+    def files_sharedPublicURL(
+        self, *, file: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Enables a file for public/external sharing.
 
         Args:
@@ -636,7 +693,7 @@ class WebClient(BaseClient):
 
     def files_upload(
         self, *, file: Union[str, IOBase] = None, content: str = None, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Uploads or creates a file.
 
         Args:
@@ -660,7 +717,7 @@ class WebClient(BaseClient):
         data.update({"content": content})
         return self.api_call("files.upload", data=data)
 
-    def groups_archive(self, *, channel: str, **kwargs) -> SlackResponse:
+    def groups_archive(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Archives a private channel.
 
         Args:
@@ -670,7 +727,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("groups.archive", json=kwargs)
 
-    def groups_create(self, *, name: str, **kwargs) -> SlackResponse:
+    def groups_create(self, *, name: str, **kwargs) -> Union[Future, SlackResponse]:
         """Creates a private channel.
 
         Args:
@@ -680,7 +737,9 @@ class WebClient(BaseClient):
         kwargs.update({"name": name})
         return self.api_call("groups.create", json=kwargs)
 
-    def groups_createChild(self, *, channel: str, **kwargs) -> SlackResponse:
+    def groups_createChild(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Clones and archives a private channel.
 
         Args:
@@ -690,7 +749,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("groups.createChild", http_verb="GET", params=kwargs)
 
-    def groups_history(self, *, channel: str, **kwargs) -> SlackResponse:
+    def groups_history(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Fetches history of messages and events from a private channel.
 
         Args:
@@ -699,7 +758,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("groups.history", http_verb="GET", params=kwargs)
 
-    def groups_info(self, *, channel: str, **kwargs) -> SlackResponse:
+    def groups_info(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Gets information about a private channel.
 
         Args:
@@ -708,7 +767,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("groups.info", http_verb="GET", params=kwargs)
 
-    def groups_invite(self, *, channel: str, user: str, **kwargs) -> SlackResponse:
+    def groups_invite(
+        self, *, channel: str, user: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Invites a user to a private channel.
 
         Args:
@@ -719,7 +780,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "user": user})
         return self.api_call("groups.invite", json=kwargs)
 
-    def groups_kick(self, *, channel: str, user: str, **kwargs) -> SlackResponse:
+    def groups_kick(
+        self, *, channel: str, user: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Removes a user from a private channel.
 
         Args:
@@ -730,7 +793,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "user": user})
         return self.api_call("groups.kick", json=kwargs)
 
-    def groups_leave(self, *, channel: str, **kwargs) -> SlackResponse:
+    def groups_leave(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Leaves a private channel.
 
         Args:
@@ -740,11 +803,13 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("groups.leave", json=kwargs)
 
-    def groups_list(self, **kwargs) -> SlackResponse:
+    def groups_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists private channels that the calling user has access to."""
         return self.api_call("groups.list", http_verb="GET", params=kwargs)
 
-    def groups_mark(self, *, channel: str, ts: str, **kwargs) -> SlackResponse:
+    def groups_mark(
+        self, *, channel: str, ts: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Sets the read cursor in a private channel.
 
         Args:
@@ -754,7 +819,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "ts": ts})
         return self.api_call("groups.mark", json=kwargs)
 
-    def groups_open(self, *, channel: str, **kwargs) -> SlackResponse:
+    def groups_open(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Opens a private channel.
 
         Args:
@@ -763,7 +828,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("groups.open", json=kwargs)
 
-    def groups_rename(self, *, channel: str, name: str, **kwargs) -> SlackResponse:
+    def groups_rename(
+        self, *, channel: str, name: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Renames a private channel.
 
         Args:
@@ -776,7 +843,7 @@ class WebClient(BaseClient):
 
     def groups_replies(
         self, *, channel: str, thread_ts: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Retrieve a thread of messages posted to a private channel
 
         Args:
@@ -790,7 +857,7 @@ class WebClient(BaseClient):
 
     def groups_setPurpose(
         self, *, channel: str, purpose: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Sets the purpose for a private channel.
 
         Args:
@@ -800,7 +867,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "purpose": purpose})
         return self.api_call("groups.setPurpose", json=kwargs)
 
-    def groups_setTopic(self, *, channel: str, topic: str, **kwargs) -> SlackResponse:
+    def groups_setTopic(
+        self, *, channel: str, topic: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Sets the topic for a private channel.
 
         Args:
@@ -810,7 +879,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "topic": topic})
         return self.api_call("groups.setTopic", json=kwargs)
 
-    def groups_unarchive(self, *, channel: str, **kwargs) -> SlackResponse:
+    def groups_unarchive(
+        self, *, channel: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Unarchives a private channel.
 
         Args:
@@ -820,7 +891,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("groups.unarchive", json=kwargs)
 
-    def im_close(self, *, channel: str, **kwargs) -> SlackResponse:
+    def im_close(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Close a direct message channel.
 
         Args:
@@ -829,7 +900,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("im.close", json=kwargs)
 
-    def im_history(self, *, channel: str, **kwargs) -> SlackResponse:
+    def im_history(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Fetches history of messages and events from direct message channel.
 
         Args:
@@ -838,11 +909,13 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("im.history", http_verb="GET", params=kwargs)
 
-    def im_list(self, **kwargs) -> SlackResponse:
+    def im_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists direct message channels for the calling user."""
         return self.api_call("im.list", http_verb="GET", params=kwargs)
 
-    def im_mark(self, *, channel: str, ts: str, **kwargs) -> SlackResponse:
+    def im_mark(
+        self, *, channel: str, ts: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Sets the read cursor in a direct message channel.
 
         Args:
@@ -852,7 +925,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "ts": ts})
         return self.api_call("im.mark", json=kwargs)
 
-    def im_open(self, *, user: str, **kwargs) -> SlackResponse:
+    def im_open(self, *, user: str, **kwargs) -> Union[Future, SlackResponse]:
         """Opens a direct message channel.
 
         Args:
@@ -861,7 +934,9 @@ class WebClient(BaseClient):
         kwargs.update({"user": user})
         return self.api_call("im.open", json=kwargs)
 
-    def im_replies(self, *, channel: str, thread_ts: str, **kwargs) -> SlackResponse:
+    def im_replies(
+        self, *, channel: str, thread_ts: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Retrieve a thread of messages posted to a direct message conversation
 
         Args:
@@ -872,7 +947,9 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "thread_ts": thread_ts})
         return self.api_call("im.replies", http_verb="GET", params=kwargs)
 
-    def migration_exchange(self, *, users: List[str], **kwargs) -> SlackResponse:
+    def migration_exchange(
+        self, *, users: List[str], **kwargs
+    ) -> Union[Future, SlackResponse]:
         """For Enterprise Grid workspaces, map local user IDs to global user IDs
 
         Args:
@@ -882,7 +959,7 @@ class WebClient(BaseClient):
         kwargs.update({"users": users})
         return self.api_call("migration.exchange", http_verb="GET", params=kwargs)
 
-    def mpim_close(self, *, channel: str, **kwargs) -> SlackResponse:
+    def mpim_close(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Closes a multiparty direct message channel.
 
         Args:
@@ -891,7 +968,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("mpim.close", json=kwargs)
 
-    def mpim_history(self, *, channel: str, **kwargs) -> SlackResponse:
+    def mpim_history(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Fetches history of messages and events from a multiparty direct message.
 
         Args:
@@ -900,11 +977,13 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("mpim.history", http_verb="GET", params=kwargs)
 
-    def mpim_list(self, **kwargs) -> SlackResponse:
+    def mpim_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists multiparty direct message channels for the calling user."""
         return self.api_call("mpim.list", http_verb="GET", params=kwargs)
 
-    def mpim_mark(self, *, channel: str, ts: str, **kwargs) -> SlackResponse:
+    def mpim_mark(
+        self, *, channel: str, ts: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Sets the read cursor in a multiparty direct message channel.
 
         Args:
@@ -916,7 +995,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel, "ts": ts})
         return self.api_call("mpim.mark", json=kwargs)
 
-    def mpim_open(self, *, users: List[str], **kwargs) -> SlackResponse:
+    def mpim_open(self, *, users: List[str], **kwargs) -> Union[Future, SlackResponse]:
         """This method opens a multiparty direct message.
 
         Args:
@@ -927,7 +1006,9 @@ class WebClient(BaseClient):
         kwargs.update({"users": users})
         return self.api_call("mpim.open", json=kwargs)
 
-    def mpim_replies(self, *, channel: str, thread_ts: str, **kwargs) -> SlackResponse:
+    def mpim_replies(
+        self, *, channel: str, thread_ts: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Retrieve a thread of messages posted to a direct message conversation from a
         multiparty direct message.
 
@@ -942,7 +1023,7 @@ class WebClient(BaseClient):
 
     def oauth_access(
         self, *, client_id: str, client_secret: str, code: str, **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Exchanges a temporary OAuth verifier code for an access token.
 
         Args:
@@ -955,7 +1036,7 @@ class WebClient(BaseClient):
         )
         return self.api_call("oauth.access", data=kwargs)
 
-    def pins_add(self, *, channel: str, **kwargs) -> SlackResponse:
+    def pins_add(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Pins an item to a channel.
 
         Args:
@@ -967,7 +1048,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("pins.add", json=kwargs)
 
-    def pins_list(self, *, channel: str, **kwargs) -> SlackResponse:
+    def pins_list(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Lists items pinned to a channel.
 
         Args:
@@ -976,7 +1057,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("pins.list", http_verb="GET", params=kwargs)
 
-    def pins_remove(self, *, channel: str, **kwargs) -> SlackResponse:
+    def pins_remove(self, *, channel: str, **kwargs) -> Union[Future, SlackResponse]:
         """Un-pins an item from a channel.
 
         Args:
@@ -988,7 +1069,7 @@ class WebClient(BaseClient):
         kwargs.update({"channel": channel})
         return self.api_call("pins.remove", json=kwargs)
 
-    def reactions_add(self, *, name: str, **kwargs) -> SlackResponse:
+    def reactions_add(self, *, name: str, **kwargs) -> Union[Future, SlackResponse]:
         """Adds a reaction to an item.
 
         Args:
@@ -1000,15 +1081,15 @@ class WebClient(BaseClient):
         kwargs.update({"name": name})
         return self.api_call("reactions.add", json=kwargs)
 
-    def reactions_get(self, **kwargs) -> SlackResponse:
+    def reactions_get(self, **kwargs) -> Union[Future, SlackResponse]:
         """Gets reactions for an item."""
         return self.api_call("reactions.get", http_verb="GET", params=kwargs)
 
-    def reactions_list(self, **kwargs) -> SlackResponse:
+    def reactions_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists reactions made by a user."""
         return self.api_call("reactions.list", http_verb="GET", params=kwargs)
 
-    def reactions_remove(self, *, name: str, **kwargs) -> SlackResponse:
+    def reactions_remove(self, *, name: str, **kwargs) -> Union[Future, SlackResponse]:
         """Removes a reaction from an item.
 
         Args:
@@ -1017,7 +1098,9 @@ class WebClient(BaseClient):
         kwargs.update({"name": name})
         return self.api_call("reactions.remove", json=kwargs)
 
-    def reminders_add(self, *, text: str, time: str, **kwargs) -> SlackResponse:
+    def reminders_add(
+        self, *, text: str, time: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Creates a reminder.
 
         Args:
@@ -1031,7 +1114,9 @@ class WebClient(BaseClient):
         kwargs.update({"text": text, "time": time})
         return self.api_call("reminders.add", json=kwargs)
 
-    def reminders_complete(self, *, reminder: str, **kwargs) -> SlackResponse:
+    def reminders_complete(
+        self, *, reminder: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Marks a reminder as complete.
 
         Args:
@@ -1042,7 +1127,9 @@ class WebClient(BaseClient):
         kwargs.update({"reminder": reminder})
         return self.api_call("reminders.complete", json=kwargs)
 
-    def reminders_delete(self, *, reminder: str, **kwargs) -> SlackResponse:
+    def reminders_delete(
+        self, *, reminder: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Deletes a reminder.
 
         Args:
@@ -1052,7 +1139,9 @@ class WebClient(BaseClient):
         kwargs.update({"reminder": reminder})
         return self.api_call("reminders.delete", json=kwargs)
 
-    def reminders_info(self, *, reminder: str, **kwargs) -> SlackResponse:
+    def reminders_info(
+        self, *, reminder: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Gets information about a reminder.
 
         Args:
@@ -1062,20 +1151,20 @@ class WebClient(BaseClient):
         kwargs.update({"reminder": reminder})
         return self.api_call("reminders.info", http_verb="GET", params=kwargs)
 
-    def reminders_list(self, **kwargs) -> SlackResponse:
+    def reminders_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists all reminders created by or for a given user."""
         self._validate_xoxp_token()
         return self.api_call("reminders.list", http_verb="GET", params=kwargs)
 
-    def rtm_connect(self, **kwargs) -> SlackResponse:
+    def rtm_connect(self, **kwargs) -> Union[Future, SlackResponse]:
         """Starts a Real Time Messaging session."""
         return self.api_call("rtm.connect", http_verb="GET", params=kwargs)
 
-    def rtm_start(self, **kwargs) -> SlackResponse:
+    def rtm_start(self, **kwargs) -> Union[Future, SlackResponse]:
         """Starts a Real Time Messaging session."""
         return self.api_call("rtm.start", http_verb="GET", params=kwargs)
 
-    def search_all(self, *, query: str, **kwargs) -> SlackResponse:
+    def search_all(self, *, query: str, **kwargs) -> Union[Future, SlackResponse]:
         """Searches for messages and files matching a query.
 
         Args:
@@ -1086,7 +1175,7 @@ class WebClient(BaseClient):
         kwargs.update({"query": query})
         return self.api_call("search.all", http_verb="GET", params=kwargs)
 
-    def search_files(self, *, query: str, **kwargs) -> SlackResponse:
+    def search_files(self, *, query: str, **kwargs) -> Union[Future, SlackResponse]:
         """Searches for files matching a query.
 
         Args:
@@ -1097,7 +1186,7 @@ class WebClient(BaseClient):
         kwargs.update({"query": query})
         return self.api_call("search.files", http_verb="GET", params=kwargs)
 
-    def search_messages(self, *, query: str, **kwargs) -> SlackResponse:
+    def search_messages(self, *, query: str, **kwargs) -> Union[Future, SlackResponse]:
         """Searches for messages matching a query.
 
         Args:
@@ -1108,7 +1197,7 @@ class WebClient(BaseClient):
         kwargs.update({"query": query})
         return self.api_call("search.messages", http_verb="GET", params=kwargs)
 
-    def stars_add(self, **kwargs) -> SlackResponse:
+    def stars_add(self, **kwargs) -> Union[Future, SlackResponse]:
         """Adds a star to an item.
 
         Args:
@@ -1120,12 +1209,12 @@ class WebClient(BaseClient):
         """
         return self.api_call("stars.add", json=kwargs)
 
-    def stars_list(self, **kwargs) -> SlackResponse:
+    def stars_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists stars for a user."""
         self._validate_xoxp_token()
         return self.api_call("stars.list", http_verb="GET", params=kwargs)
 
-    def stars_remove(self, **kwargs) -> SlackResponse:
+    def stars_remove(self, **kwargs) -> Union[Future, SlackResponse]:
         """Removes a star from an item.
 
         Args:
@@ -1137,31 +1226,31 @@ class WebClient(BaseClient):
         """
         return self.api_call("stars.remove", json=kwargs)
 
-    def team_accessLogs(self, **kwargs) -> SlackResponse:
+    def team_accessLogs(self, **kwargs) -> Union[Future, SlackResponse]:
         """Gets the access logs for the current team."""
         self._validate_xoxp_token()
         return self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
 
-    def team_billableInfo(self, **kwargs) -> SlackResponse:
+    def team_billableInfo(self, **kwargs) -> Union[Future, SlackResponse]:
         """Gets billable users information for the current team."""
         self._validate_xoxp_token()
         return self.api_call("team.billableInfo", http_verb="GET", params=kwargs)
 
-    def team_info(self, **kwargs) -> SlackResponse:
+    def team_info(self, **kwargs) -> Union[Future, SlackResponse]:
         """Gets information about the current team."""
         return self.api_call("team.info", http_verb="GET", params=kwargs)
 
-    def team_integrationLogs(self, **kwargs) -> SlackResponse:
+    def team_integrationLogs(self, **kwargs) -> Union[Future, SlackResponse]:
         """Gets the integration logs for the current team."""
         self._validate_xoxp_token()
         return self.api_call("team.integrationLogs", http_verb="GET", params=kwargs)
 
-    def team_profile_get(self, **kwargs) -> SlackResponse:
+    def team_profile_get(self, **kwargs) -> Union[Future, SlackResponse]:
         """Retrieve a team's profile."""
         self._validate_xoxp_token()
         return self.api_call("team.profile.get", http_verb="GET", params=kwargs)
 
-    def usergroups_create(self, *, name: str, **kwargs) -> SlackResponse:
+    def usergroups_create(self, *, name: str, **kwargs) -> Union[Future, SlackResponse]:
         """Create a User Group
 
         Args:
@@ -1172,7 +1261,9 @@ class WebClient(BaseClient):
         kwargs.update({"name": name})
         return self.api_call("usergroups.create", json=kwargs)
 
-    def usergroups_disable(self, *, usergroup: str, **kwargs) -> SlackResponse:
+    def usergroups_disable(
+        self, *, usergroup: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Disable an existing User Group
 
         Args:
@@ -1183,7 +1274,9 @@ class WebClient(BaseClient):
         kwargs.update({"usergroup": usergroup})
         return self.api_call("usergroups.disable", json=kwargs)
 
-    def usergroups_enable(self, *, usergroup: str, **kwargs) -> SlackResponse:
+    def usergroups_enable(
+        self, *, usergroup: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Enable a User Group
 
         Args:
@@ -1194,12 +1287,14 @@ class WebClient(BaseClient):
         kwargs.update({"usergroup": usergroup})
         return self.api_call("usergroups.enable", json=kwargs)
 
-    def usergroups_list(self, **kwargs) -> SlackResponse:
+    def usergroups_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """List all User Groups for a team"""
         self._validate_xoxp_token()
         return self.api_call("usergroups.list", http_verb="GET", params=kwargs)
 
-    def usergroups_update(self, *, usergroup: str, **kwargs) -> SlackResponse:
+    def usergroups_update(
+        self, *, usergroup: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Update an existing User Group
 
         Args:
@@ -1210,7 +1305,9 @@ class WebClient(BaseClient):
         kwargs.update({"usergroup": usergroup})
         return self.api_call("usergroups.update", json=kwargs)
 
-    def usergroups_users_list(self, *, usergroup: str, **kwargs) -> SlackResponse:
+    def usergroups_users_list(
+        self, *, usergroup: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """List all users in a User Group
 
         Args:
@@ -1223,7 +1320,7 @@ class WebClient(BaseClient):
 
     def usergroups_users_update(
         self, *, usergroup: str, users: List[str], **kwargs
-    ) -> SlackResponse:
+    ) -> Union[Future, SlackResponse]:
         """Update the list of users for a User Group
 
         Args:
@@ -1236,16 +1333,16 @@ class WebClient(BaseClient):
         kwargs.update({"usergroup": usergroup, "users": users})
         return self.api_call("usergroups.users.update", json=kwargs)
 
-    def users_conversations(self, **kwargs) -> SlackResponse:
+    def users_conversations(self, **kwargs) -> Union[Future, SlackResponse]:
         """List conversations the calling user may access."""
         return self.api_call("users.conversations", http_verb="GET", params=kwargs)
 
-    def users_deletePhoto(self, **kwargs) -> SlackResponse:
+    def users_deletePhoto(self, **kwargs) -> Union[Future, SlackResponse]:
         """Delete the user profile photo"""
         self._validate_xoxp_token()
         return self.api_call("users.deletePhoto", http_verb="GET", params=kwargs)
 
-    def users_getPresence(self, *, user: str, **kwargs) -> SlackResponse:
+    def users_getPresence(self, *, user: str, **kwargs) -> Union[Future, SlackResponse]:
         """Gets user presence information.
 
         Args:
@@ -1255,12 +1352,12 @@ class WebClient(BaseClient):
         kwargs.update({"user": user})
         return self.api_call("users.getPresence", http_verb="GET", params=kwargs)
 
-    def users_identity(self, **kwargs) -> SlackResponse:
+    def users_identity(self, **kwargs) -> Union[Future, SlackResponse]:
         """Get a user's identity."""
         self._validate_xoxp_token()
         return self.api_call("users.identity", http_verb="GET", params=kwargs)
 
-    def users_info(self, *, user: str, **kwargs) -> SlackResponse:
+    def users_info(self, *, user: str, **kwargs) -> Union[Future, SlackResponse]:
         """Gets information about a user.
 
         Args:
@@ -1270,11 +1367,13 @@ class WebClient(BaseClient):
         kwargs.update({"user": user})
         return self.api_call("users.info", http_verb="GET", params=kwargs)
 
-    def users_list(self, **kwargs) -> SlackResponse:
+    def users_list(self, **kwargs) -> Union[Future, SlackResponse]:
         """Lists all users in a Slack team."""
         return self.api_call("users.list", http_verb="GET", params=kwargs)
 
-    def users_lookupByEmail(self, *, email: str, **kwargs) -> SlackResponse:
+    def users_lookupByEmail(
+        self, *, email: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Find a user with an email address.
 
         Args:
@@ -1284,7 +1383,9 @@ class WebClient(BaseClient):
         kwargs.update({"email": email})
         return self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs)
 
-    def users_setPhoto(self, *, image: Union[str, IOBase], **kwargs) -> SlackResponse:
+    def users_setPhoto(
+        self, *, image: Union[str, IOBase], **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Set the user profile photo
 
         Args:
@@ -1294,7 +1395,9 @@ class WebClient(BaseClient):
         self._validate_xoxp_token()
         return self.api_call("users.setPhoto", files={"image": image}, data=kwargs)
 
-    def users_setPresence(self, *, presence: str, **kwargs) -> SlackResponse:
+    def users_setPresence(
+        self, *, presence: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
         """Manually sets user presence.
 
         Args:
@@ -1303,12 +1406,12 @@ class WebClient(BaseClient):
         kwargs.update({"presence": presence})
         return self.api_call("users.setPresence", json=kwargs)
 
-    def users_profile_get(self, **kwargs) -> SlackResponse:
+    def users_profile_get(self, **kwargs) -> Union[Future, SlackResponse]:
         """Retrieves a user's profile information."""
         self._validate_xoxp_token()
         return self.api_call("users.profile.get", http_verb="GET", params=kwargs)
 
-    def users_profile_set(self, **kwargs) -> SlackResponse:
+    def users_profile_set(self, **kwargs) -> Union[Future, SlackResponse]:
         """Set the profile information for a user."""
         self._validate_xoxp_token()
         return self.api_call("users.profile.set", json=kwargs)
diff --git a/tutorial/PythOnBoardingBot/app.py b/tutorial/PythOnBoardingBot/app.py
index 35edd54..c43d98e 100644
--- a/tutorial/PythOnBoardingBot/app.py
+++ b/tutorial/PythOnBoardingBot/app.py
@@ -67,6 +67,9 @@ def update_emoji(**payload):
     channel_id = data["item"]["channel"]
     user_id = data["user"]
 
+    if channel_id not in onboarding_tutorials_sent:
+        return
+
     # Get the original tutorial sent.
     onboarding_tutorial = onboarding_tutorials_sent[channel_id][user_id]
 
diff --git a/tutorial/PythOnBoardingBot/requirements.txt b/tutorial/PythOnBoardingBot/requirements.txt
index 9d9df3f..6be2361 100644
--- a/tutorial/PythOnBoardingBot/requirements.txt
+++ b/tutorial/PythOnBoardingBot/requirements.txt
@@ -1,2 +1,2 @@
-slackclient>=2.0.0
+slackclient>=2.1.0
 certifi
\ No newline at end of file


Test patch:
diff --git a/tests/helpers.py b/tests/helpers.py
index 961c0de..f695976 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -20,6 +20,19 @@ def fake_req_args(headers=ANY, data=ANY, params=ANY, json=ANY):
     return req_args
 
 
+def fake_send_req_args(headers=ANY, data=ANY, params=ANY, json=ANY):
+    req_args = {
+        "headers": headers,
+        "data": data,
+        "params": params,
+        "json": json,
+        "ssl": ANY,
+        "proxy": ANY,
+        "files": ANY,
+    }
+    return req_args
+
+
 def mock_rtm_response():
     coro = Mock(name="RTMResponse")
     data = {
diff --git a/tests/rtm/test_rtm_client_functional.py b/tests/rtm/test_rtm_client_functional.py
index 1e546e6..62d3cc5 100644
--- a/tests/rtm/test_rtm_client_functional.py
+++ b/tests/rtm/test_rtm_client_functional.py
@@ -11,7 +11,7 @@ import json
 # Internal Imports
 import slack
 import slack.errors as e
-from tests.helpers import fake_req_args, mock_rtm_response
+from tests.helpers import fake_send_req_args, mock_rtm_response
 
 
 @mock.patch("slack.WebClient._send", new_callable=mock_rtm_response)
@@ -124,7 +124,7 @@ class TestRTMClientFunctional(unittest.TestCase):
         mock_rtm_response.assert_called_once_with(
             http_verb="GET",
             api_url="https://www.slack.com/api/rtm.connect",
-            req_args=fake_req_args(),
+            req_args=fake_send_req_args(),
         )
 
     def test_start_calls_rtm_start_when_specified(self, mock_rtm_response):
@@ -139,7 +139,7 @@ class TestRTMClientFunctional(unittest.TestCase):
         mock_rtm_response.assert_called_once_with(
             http_verb="GET",
             api_url="https://www.slack.com/api/rtm.start",
-            req_args=fake_req_args(),
+            req_args=fake_send_req_args(),
         )
 
     def test_send_over_websocket_sends_expected_message(self, mock_rtm_response):
diff --git a/tests/web/test_web_client.py b/tests/web/test_web_client.py
index 2907bae..ee22f2e 100644
--- a/tests/web/test_web_client.py
+++ b/tests/web/test_web_client.py
@@ -129,29 +129,13 @@ class TestWebClient(unittest.TestCase):
         with self.assertRaises(err.SlackApiError):
             self.client.api_test()
 
-    @mock.patch("aiohttp.FormData.add_field")
-    def test_the_api_call_files_argument_creates_the_expected_data(
-        self, mock_add_field, mock_request
-    ):
+    def test_the_api_call_files_argument_creates_the_expected_data(self, mock_request):
         self.client.token = "xoxa-123"
         with mock.patch("builtins.open", mock.mock_open(read_data="fake")):
             self.client.users_setPhoto(image="/fake/path")
 
-        mock_add_field.assert_called_once_with("image", mock.ANY)
         mock_request.assert_called_once_with(
             http_verb="POST",
             api_url="https://www.slack.com/api/users.setPhoto",
             req_args=fake_req_args(),
         )
-
-    @mock.patch("aiohttp.FormData.add_field")
-    def test_the_api_call_files_argument_combines_with_additional_data(
-        self, mock_add_field, mock_request
-    ):
-        self.client.token = "xoxa-123"
-        with mock.patch("builtins.open", mock.mock_open(read_data="fake")) as mock_file:
-            self.client.users_setPhoto(image=mock_file(), name="photo")
-
-        mock_add_field.assert_has_calls(
-            [mock.call("image", mock.ANY), mock.call("name", "photo")]
-        )