Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Python Slack Client v2.0 #384

Closed
2 tasks done
RodneyU215 opened this issue Feb 25, 2019 · 24 comments
Closed
2 tasks done

RFC: Python Slack Client v2.0 #384

RodneyU215 opened this issue Feb 25, 2019 · 24 comments
Assignees
Milestone

Comments

@RodneyU215
Copy link
Contributor

RodneyU215 commented Feb 25, 2019

Abstract

Python developers are an important part of the Slack platform ecosystem. From internal apps for a specific organization or team to publicly distributed apps listed in our App Directory, tools and products built upon our SDKs improve people's working lives.

This issue proposes a redesign of the Slack Python SDK/Client to address several design flaws that surfaced as the scope and complexity of the platform grew. Currently every app built on RTM is required to implement a loop over the stream of events coming in. Web HTTP requests could be simpler to write and more performant when scaled.

This issue proposes that we split up the client into an RTM client and a Web client. We'd like to remove any state not explicitly used for the client's function. As well as make it simpler for developers to interact with the Web API and respond to RTM events.

Motivation

The goal of our Python SDK is to make it simple for Python developers to create Slack apps. We believe that developers should be able to go from Readme to a running app in less than 10 minutes.

Currently there are a number of limitations in our existing project that prevent this goal from being realized. From a high level, the complexity in our SDK has led to a number of tough to triage bugs. It also makes it harder for new app developers or possible first time contributors to get up and running quickly.

Working with the Slack Web API:

  • There’s no clear separation between the RTM API Client and the Web API Client.
    • To make an Slack API call, there are 4 nested objects created. Developers who only need to communicate via the Web API deal with unnecessary complexity.
      • Client#api_call→Server#api_call→SlackRequest#do→requests#post
    • For developers who don’t use RTM we also unnecessarily store information such as channel data on what’s currently called Server.py.

Working with the Slack RTM API:

  • Every developer has to create a “while connected” loop that constantly reads line by line from the websocket. What’s worse is that they also have to create nested if statement to check for their event types on every event that comes in. This makes building complex apps rather cumbersome.
  • We’re loosely caching data on the Server object. Which should only be responsible for managing the web socket connection. The problem with this ad hoc approach to caching is sooner or later apps built on this infrastructure will inevitably experience performance and scalability problems as their app increases in complexity or is installed on larger enterprise teams. This approach also lacks proper caching support such as removing or updating stale data.

Other challenges:

  • There are a number of unnecessary/old/deprecated methods and variables that need to be removed. e.g. Refresh tokens
  • We’ve not defined an official API for the Client. This makes it harder to deprecated old code or significantly change code that was intended to be private.

Existing Slack Client Architecture:

Python Slack Client 1.0 Architecture

Example App in v1:

Here's a simple example app that replies "Hi <@userid>!" in a thread if you send it a message containing "Hello".

from slackclient import SlackClient

slack_token = os.environ["SLACK_API_TOKEN"]
client = SlackClient(slack_token)

def say_hello(data):
    if 'Hello' in data['text']:
        channel_id = data['channel']
        thread_ts = data['ts']
        user = data['user']

        client.api_call('chat.postMessage',
            channel=channel_id,
            text="Hi <@{}>!".format(user),
            thread_ts=thread_ts
        )

if client.rtm_connect():
    while client.server.connected is True:
        for data in client.rtm_read():
            if "type" in data and data["type"] == "message":
                say_hello(data)
else:
    print "Connection Failed"

Proposal

Primary Changes

  1. Client Decomposition: Create two independent Slack clients. An HTTP client focused on Slack's Web API and a websocket client focused on Slack's RTM API.
  2. Removal of any pseudo caching layers. Developers should be given a migration guide on how to properly store information for their Slack app.
  3. Event-driven architecture: Whether you're using Slack's RTM API or Events API, the simplest and most efficient way to build apps and bots that respond to activities in Slack is to model your app as an event based system. The new RTMClient would allow you to simply link your application's callbacks to corresponding Slack events.

v2 Slack Client Architecture

Python Slack Client 2.0 Architecture

Example App in v2:

Here's that same simple example app that replies "Hi <@userid>!" in a thread if you send it a message containing "Hello".

import slack

slack_token = os.environ["SLACK_API_TOKEN"]
rtmclient = slack.RTMClient(slack_token)

def say_hello(data):
    if 'Hello' in data['text']:
        channel_id = data['channel']
        thread_ts = data['ts']
        user = data['user']

        webclient = slack.WebClient(slack_token)
        webclient.chat_postMessage(
            channel=channel_id,
            text="Hi <@{}>!".format(user),
            thread_ts=thread_ts
        )

rtmclient.on(event='message', callback=say_hello)
rtmclient.start()

Additional Features and enhancements

WebClient

  • By default we now use requests.Sessions. This takes advantage of built-in connection-pooling which improves the performance of web requests by reusing an established TCP connection.
  • SlackAPIMixin: Built-in support for all Slack API methods.
    • Docstrings: Getting Slack API information in your editor increases developer productivity.
    • xoxb vs xoxp: We validate the token used is supported for that method. This solves issues like #326.
  • JSON support: By default and where applicable leverage the requests library's json feature. This ensures all data sent will be automatically properly encoded. This solves issues like #337.
  • Requests return a SlackResponse object. This is an iterable container. It allows users to access the response data like a normal dict. It also allows us to easily build pagination support for any responses that contain the next_cursor attribute. See #343.
  • It’s completely decoupled from RTMClient so it can be imported independently without the bloat.

RTMClient

  • The event-driven architecture of this new client removes complexity from existing apps. No more manual looping! - This was heavily inspired by the open-source Ruby Slack SDK by Daniel Doubrovkine.
    • This also encourages developers to build their apps in a way that’s easily portable to the Slack Events API.
  • Short lived internal Web Client. We locally create a WebClient in RTM to retrieve the websocket url, but then quickly dispose of it. This encourages developers to rely on the RTMClient only to receive events from Slack.

Issues resolved

Unresolved discussions:

  • Decorators: Decorators in Python enable developers to modify the behavior of a function or class. It's possible to use a class decorator on the RTMClient to store callbacks. This could in theory simplify the code. However, storing event-based callbacks on the class would mean every instance every created of an RTMClient would be impacted. This could lead to erroneous behaviors that are hard to triage because the state of the class could be modified in another function.

Detailed design

slack.WebClient

A WebClient allows apps to communicate with the Slack Platform's Web API. The Slack Web API is an interface for querying information from and enacting change in a Slack workspace. This client handles constructing and sending HTTP requests to Slack as well as parsing any responses received into a SlackResponse.

Attributes:

  • #token (str): A string specifying an xoxp or xoxb token.
  • #use_session (bool): An boolean specifying if the client should take advantage of urllib3's connection pooling. Default is True.
  • #base_url (str): A string representing the Slack API base URL. Default is https://www.slack.com/api/
  • #proxies (dict): If you need to use a proxy, you can pass a dict of proxy configs. e.g. {'https': "https://127.0.0.1:8080"} Default is None.
  • #timeout (int): The maximum number of seconds the client will wait to connect and receive a response from Slack. Default is 30 seconds.

Methods:

  • #api_call(): Constructs a request and executes the API call to Slack.

Recommended usage:

import slack

client = slack.WebClient(token=os.environ['SLACK_API_TOKEN'])
response = client.chat_postMessage(
    channel='#random',
    text="Hello world!")
assert response["ok"]
assert response["message"]["text"] == "Hello world!"

Example manually creating an API request: (Mostly for backwards compatibility)

import slack

client = slack.WebClient(token=os.environ['SLACK_API_TOKEN'])
response = client.api_call(
    api_method='chat.postMessage',
    json={'channel': '#random', 'text': "Hello world!"}
)
assert response["ok"]
assert response["message"]["text"] == "Hello world!"

Note:

  • All Slack API methods are available thanks to the SlackAPIMixin class. By using the methods provided you allow the client to handle the heavy lifting of constructing the requests as it is expected. This mixin also serves users as a reference to Slack's API.
  • Any attributes or methods prefixed with _underscores are intended to be "private" internal use only. They may be changed or removed at anytime.

slack.web.SlackResponse

An iterable container of response data.

Attributes:

  • #data (dict): The json-encoded content of the response. Along with the headers and status code information.

Methods:

  • #validate(): Check if the response from Slack was successful.
  • #get(): Retrieves any key from the response data.
  • #next(): Retrieves the next portion of results, if 'next_cursor' is present.

Example:

import slack

client = slack.WebClient(token=os.environ['SLACK_API_TOKEN'])

response1 = client.auth_revoke(test='true')
assert not response1['revoked']

response2 = client.auth_test()
assert response2.get('ok', False)

users = []
for page in client.users_list(limit=2):
    users = users + page['members']

Note:

  • Some responses return collections of information like channel and user lists. If they do it's likely that you'll only receive a portion of results. This object allows you to iterate over the response which makes subsequent API requests until your code hits 'break' or there are no more results to be found.
  • Any attributes or methods prefixed with _underscores are intended to be "private" internal use only. They may be changed or removed at anytime.

slack.RTMClient

An RTMClient allows apps to communicate with the Slack Platform's RTM API. The event-driven architecture of this client allows you to simply link callbacks to their corresponding events. When an event occurs this client executes your callback while passing along any information it receives.

Attributes:

  • #token (str): A string specifying an xoxp or xoxb token.
  • #connect_method (str): An string specifying if the client will connect with rtm.connect or rtm.start. Default is rtm.connect.
  • #auto_reconnect (bool): When true the client will automatically reconnect when (not manually) disconnected. Default is True.
  • #ping_interval (int): automatically send "ping" command every specified period of seconds. If set to 0, do not send automatically. Default is 30.
  • #ping_timeout (int): The amount of seconds the ping should timeout. If the pong message is not received. Default is 10.
  • #proxies (dict): If you need to use a proxy, you can pass a dict of proxy configs. e.g. {'https': "https://user:pass@127.0.0.1:8080"} Default is None.
  • #base_url (str): A string representing the Slack API base URL. Default is https://www.slack.com/api/

Methods:

  • #ping(): Sends a ping message over the websocket to Slack.
  • #typing(): Sends a typing indicator to the specified channel.
  • #on(): Stores and links callbacks to websocket and Slack events.
  • #start(): Starts an RTM Session with Slack.
  • #stop(): Closes the websocket connection and ensures it won't reconnect.

Example:

import slack

slack_token = os.environ["SLACK_API_TOKEN"]
rtmclient = slack.RTMClient(slack_token)

def say_hello(data):
    if 'Hello' in data['text']:
        channel_id = data['channel']
        thread_ts = data['ts']
        user = data['user']

        webclient = slack.WebClient(slack_token)
        webclient.api_call('chat.postMessage',
            channel=channel_id,
            text="Hi <@{}>!".format(user),
            thread_ts=thread_ts
        )

rtmclient.on(event='message', callback=say_hello)
rtmclient.start()

Note:

  • The initial state returned when establishing an RTM connection will be available as the payload to the open event. This data is not and will not be stored on the RTM Client.
  • Any attributes or methods prefixed with _underscores are intended to be "private" internal use only. They may be changed or removed at anytime.

Migrating to 2.x

Import changes: The goal of this project is to provide a set of tools that ease the creation of Python Slack apps. To better align with this goal we’re renaming the main module to slack. From slack developers can import various tools.

# Before:
# import slackclient

# After:
from slack import WebClient

RTM Client API Changes:

The RTM client has been completely redesigned please refer above for the new API.

We no longer store any team data.: In the current 1.x version of the client we store some channel and user information internally on Server.py in client. This data will now be available in the open event for consumption. Developers are then free to store any information they choose. Here's an example:

# Retrieving the team domain.
# Before:
# team_domain = client.server.login_data["team"]["domain"]

# After:
def get_team_data(data):
    team_domain = data['team']['domain']
rtm_client.on(event='open', callback=get_team_data)

Web Client API Changes:

Token refresh removed: This feature originally shipped as a part of Workspace Tokens. Since we're heading in a new direction it's safe to remove this, along with any related attributes stored on the client.

  • refresh_token
  • token_update_callback
  • client_id
  • client_secret

#api_call():

  • timeout param has been removed. Timeout is passed at the client level now.
  • kwargs param has been removed. You must specify where the data you pass belongs in the request. e.g. 'data' vs 'params' vs 'files'...etc
# Before:
# from slackclient import SlackClient
#
# client = SlackClient(os.environ["SLACK_API_TOKEN"])
# client.api_call('chat.postMessage',
#     timeout=30,
#     channel='C0123456',
#     text="Hi!")

# After:
import slack

client = slack.WebClient(os.environ["SLACK_API_TOKEN"], timeout=30)
client.api_call('chat.postMessage', json={
    'channel': 'C0123456',
    'text': 'Hi!'})

# Note: The SlackAPIMixin allows you to write it like this.
client.chat_postMessage(
    channel='C0123456',
    text='Hi!')

SlackAPIMixin: The Slack API mixin provides built-in methods for Slack's Web API. These methods act as helpers enabling you to focus less on how the request is constructed. Here are a few things that this mixin provides:

  • Basic information about each method through the docstring.
  • Easy File Uploads: You can now pass in the location of a file and the library will handle opening and retrieving the file object to be transmitted.
  • Token type validation: This gives you better error messaging when you're attempting to consume an api method that your token doesn't have access to.
  • Constructs requests using Slack's preferred HTTP methods and content-types.

Costs and Tradeoffs

Separating Web from RTM:

  • Slack apps built using the Events API and Web API do not need RTM. So we’re separating the Web HTTP Client from the RTM Websocket Client so that you only have to import the parts that your app needs.
    • This change requires all apps built on top of RTM to create their own instance of the WebClient.

WebClient SlackAPIMixin:

  • Maintenance:
    • Until this is automated 130+ methods (including their docstrings) need to be kept up-to-date manually.
  • Developer Interface: In order to mirror Slack’s API with . separated methods we’d have to implement nested classes for each method family. However valuing maintainability we’ve opted to use a partial snake_case naming convention replacing every . with an _.
    • We’ve deviated from Slack’s API which could add cognitive load when building apps.

RTMClient redesign:

  • Event-driven architecture: Most basic Slack apps are essentially a collection of functions that are called when an event occurs. By developing your app with an event-driven architecture the components of your app can be loosely coupled and easier to maintain.
    • With increasingly complex functions your app can become exponentially slower. Implementing some sort of threading/concurrency is an inevitability.
  • Data Store Removed: Data was originally being stored on Server.py as a convenience for apps to reuse the initial state without having to query for additional data. The challenge with this approach is that this data can become stale/inaccurate very quickly depending on the team. We also didn't store all of the information available.
    • No longer storing any data requires all apps to implement a mechanism to cache the initial RTM payload.

Rejected Alternatives

Do Nothing

If we avoid a redesign we’d still have to address existing bugs in v1. New features will be slower to add and first-time contributors would face an increasingly steep ramp-up period. Debugging will also remain difficult as the existing classes today are tightly coupled.

Requirements

@RodneyU215 RodneyU215 self-assigned this Feb 25, 2019
@RodneyU215 RodneyU215 added this to the 2.0 milestone Feb 25, 2019
@RodneyU215 RodneyU215 pinned this issue Feb 25, 2019
@ovv
Copy link

ovv commented Mar 1, 2019

Sounds great !

  1. Does that new client aims to keep support for python 2.7 ? I read RFC: Python 2.7 Support Timeline #368 but there is no mention of the v2 client.
  2. Is there any plan to support asynchronous programming using asyncio or other implementations ?

If that is not in your plan, it would be great for non-official client to be able to import some logic from this new client. Such as message parsing, method definition, token validation, etc

@bredman
Copy link

bredman commented Mar 1, 2019

Some initial feedback (mostly small stuff):

2. Removal of any pseudo caching layers. Developers are encouraged and given a migration guide on how to properly store information for their Slack app.

Sounds like there's a typo or missing word around "Developers are encouraged and"

3. Event-driven architecture: Wether you're using Slack's RTM API or Events API the simplest and most ...

I think this should be "Whether you're using Slack's RTM API or Events API, the"

simply link your applications callbacks

simply link your application's callbacks

Docstrings: Getting Slack API information in your editor increases developer productivity.

This sounds awesome 🙌

xoxb vs xoxp: We validate the token used is supported for that method. This solves issues like ...

This is interesting and implies that we should consider adding support to vend this data via API for clients to ingest. The other thing that sticks out here is that my understanding is that we report a pretty clear error in cases where you use an unsupported token type from our API but it seems like we're not surfacing that well to developers. I haven't finished reading the spec yet but are we doing something to remedy that issue?

@RodneyU215
Copy link
Contributor Author

@ovv Thanks for reading and being the first to comment on this issue. The current plan is to continue supporting Python 2.7 as you've read in #368. So this v2 client ideally would work for both Python 2.7 and 3.

If there's enough demand however, we may consider creating a Python 3.6+ version that takes advantage of asyncio. 👍 this comment if you'd like to see this happen.

@RodneyU215
Copy link
Contributor Author

@bredman Thanks for the feedback. I'll fix those typo/grammar errors now!

This is interesting and implies that we should consider adding support to vend this data via API for clients to ingest. The other thing that sticks out here is that my understanding is that we report a pretty clear error in cases where you use an unsupported token type from our API but it seems like we're not surfacing that well to developers. I haven't finished reading the spec yet but are we doing something to remedy that issue?

We do provide this API response: {'ok': False, 'error': 'not_allowed_token_type'}. However based on the comments/issues opened on this project I thought it would be helpful check this upfront and give them a more verbose error message. I was thinking of something like this:

raise BotUserAccessError("The API method 'channels.create' cannot be called with a Bot Token.")

@Roach
Copy link
Contributor

Roach commented Mar 2, 2019

@bredman @RodneyU215 token type is also something we should add to the API spec (cc @episod)

@wittekm
Copy link

wittekm commented Mar 5, 2019

Any chance we could ask for pep484 annotations on methods like chat_postMessage, or is it going to remain generally kwargs-based?

@joshzana
Copy link

joshzana commented Mar 6, 2019

Looks great!

  1. Can you give us an idea of the logisitcs of this release? Will there be a beta? Will you continue to backport bug fixes to the 1.x line?
  2. Can you also add the #base_url attribute to RTMClient or was that left off on purpose?

@RodneyU215
Copy link
Contributor Author

@wittekm I appreciate the question. It's something we could do for sure. I imagine the implementation would be different though if we wanted to ensure it was Python 2.7 compatible. Regardless I believe this is a great idea.

@RodneyU215
Copy link
Contributor Author

RodneyU215 commented Mar 7, 2019

@joshzana Thanks for the feedback and the really insightful questions.

  1. I'm planning on making this clearer by 03/12/2019. I have an internal Slack review on that date. It's also very dependent on the feedback I receive here from the community.
  2. Yes, you're right! We should also accept #base_url in the RTMClient. I'll update that now.

@ovv
Copy link

ovv commented Mar 7, 2019

If there's enough demand however, we may consider creating a Python 3.6+ version that takes advantage of asyncio. +1 this comment if you'd like to see this happen.

That would be great

If that is not in your plan, it would be great for non-official client to be able to import some logic from this new client. Such as message parsing, method definition, token validation, etc

To expend on this. In my opinion a second packages handling everything non I/O related would be the best. Other client could rely on it without too much dependency and it would make maintenance and upgrade easier for everyone involved.

@ajmacd
Copy link

ajmacd commented Mar 11, 2019

I had the same question about asynchronous requests while reading the new design. We might consider powering this with a Requests add-on, like requests-futures. Apparently Futures are natively available in Python 3.2 but have been backported to Python 2.

@ajmacd
Copy link

ajmacd commented Mar 11, 2019

Until this is automated 130+ methods (including their docstrings) need to be kept up-to-date manually

Have you considered using our newly available JSON schema to generate Python bindings for the web API? This seems to be the most widely used generator:
https://github.com/cwacek/python-jsonschema-objects

Response validation appears to be typically done with jsonschema.

@titosand
Copy link

titosand commented Mar 11, 2019

From a high level the complexity in our SDK has led to a number of tough to triage bugs

Think this reads better as From a high level, ...

To make an Slack API call there are 4 nested objects created.

To make a Slack API call, there are 4 nested objects created.

#auto_reconnect (bool): When true the client will automatically reconnect when (not manually) disconnected.

#ping_interval (int): automatically send "ping" command every specified period of seconds. If set to 0, do not send automatically.

#ping_timeout (int): The amount of seconds the ping should timeout. If the pong message is not received.

Can we include the defaults for these attributes?

@bredman
Copy link

bredman commented Mar 11, 2019

Token refresh removed: This feature originally shipped as a part of Workspace Tokens. Since we're heading in a new direction it's safe to remove this, along with any related attributes stored on the client.

I think it's probably best to get rid of this for now but one thing to be aware of is that token rotation is coming back in the future for our regular tokens. See the top of the page about WTA apps and token rotation.

@bredman
Copy link

bredman commented Mar 11, 2019

#data (dict): The json-encoded content of the response. Along with the headers and status code information.

This is probably only important if this is a reference document in the future but it would be nice to mention how to access this stuff?

Also should we maybe have separate variables for users to interact with for headers, status code info, and payload? These are related but very separate items and it seems a bit weird to shove them all into a single dictionary.

@bredman
Copy link

bredman commented Mar 11, 2019

For slack.WebClient given that we have a single standard spot in the response JSON document for error info to appear (the error key) I would consider adding that information as a top level thing to the SlackResponse class.

slack.RTMClient has a #base_url attribute, is that also applied to the websocket URL we connect to? This is probably mostly interesting to those of us building Slack itself vs. public users of this client but it would be worthwhile to note.

@bredman
Copy link

bredman commented Mar 11, 2019

From "Costs and Tradeoffs"

This change requires all apps built on top of RTM to create their own instance of the WebClient

Is this just if they want to interact with the API? Or is there some other reason they need their own WebClient?

@RodneyU215
Copy link
Contributor Author

RodneyU215 commented Mar 11, 2019

Thanks for the perspective and feedback @ajmacd!

I had the same question about asynchronous requests while reading the new design. We might consider powering this with a Requests add-on, like requests-futures. Apparently Futures are natively available in Python 3.2 but have been backported to Python 2.

I’d like to move forward with creating a branch for Python 3.6+ with Async support and other goodies built-in. That being said this will come as a lower priority when compared to other tasks such as bug fixing critical client 1.x and getting client 2.x shipped asap.

Have you considered using our newly available JSON schema to generate Python bindings for the web API?

I think it’s a great idea to to use our JSON schema to generate our Web Client API. I’ve not yet personally used python_jsonschema_objects, but after reviewing their readme it seems like it’s something that may be better used to generate Python classes out of the responses sent back from the Slack API.

I’ve also been looking into swagger-codegen, but it doesn’t seem simple to add. I believe it may be best to work on this post-release of client 2.x. 🤔

@RodneyU215
Copy link
Contributor Author

Thanks for the feedback @bredman!

#data (dict): The json-encoded content of the response. Along with the headers and status code information.

This is probably only important if this is a reference document in the future but it would be nice to mention how to access this stuff?

Also should we maybe have separate variables for users to interact with for headers, status code info, and payload? These are related but very separate items and it seems a bit weird to shove them all into a single dictionary.

Really great points here. I’ll update our readme to ensure it’s clear how to interact with the response data. I’ll also disentangle headers and status code from the response body.

For slack.WebClient given that we have a single standard spot in the response JSON document for error info to appear (the error key) I would consider adding that information as a top level thing to the SlackResponse class.

The SlackResponse class will implement both a get and __getitem__ method which should allow for apps to access both ok and error from the top level.

slack.RTMClient has a #base_url attribute, is that also applied to the websocket URL we connect to? This is probably mostly interesting to those of us building Slack itself vs. public users of this client but it would be worthwhile to note.

No! The base_url in slack.RTMClient is only used in the WebClient when making the initial RTM request to retrieve the web socket url.

This change requires all apps built on top of RTM to create their own instance of the WebClient

Is this just if they want to interact with the API? Or is there some other reason they need their own WebClient?

This is only if they’d like to interact with the Slack Web API. The RTM API only supports posting simple messages formatted using our default message formatting mode. It does not support attachments, Blocks or other message formatting modes. So I’d like to encourage them to use the WebClient if they’d like to send messages.

@RodneyU215
Copy link
Contributor Author

Thanks to everyone who has shared feedback with us so far! I really appreciate every single comment and reaction added to this issue.

New changes: Based on the feedback that I’ve received on this RFC I’ve made a few notable changes in the implementation.

  1. Our new client will target Python 3.6+. This was a very deeply debated decision for us at Slack. It’s very important to us that we support a wide variety of environment needs. The decision ultimately came down to the following:
    1. 85% of the daily downloads we’re seeing in PyPI Stats are from Python 3.
    2. Existing Slack apps using this SDK on Python 2 has dropped to around 35%. This number continues to decline as we all move towards Python 2’s EOL.
    3. Targeting Python 3.6 enables us to take advantage of the “significant usability and performance improvements” of asyncio released in that version. (Thanks @ovv & @ajmacd)
  2. New internal websocket dependency. Previously this project depended on websocket-client. I’ve decided to switch to websockets. The biggest win here is that this sets us up to build-in complete async support with asyncio in a subsequent minor release. (We’re also considering adding support for aiohttp in a future as well.)
  3. We'll now be adding PEP 484 -- Type Hints to all methods. (Thanks @wittekm)
  4. We’ve added a run_on decorator to make it even simpler to register callbacks. (Thanks @ajferrick)

Client v1 Support Plan:

  • Python 2 Timeline: Python 2.7 will continue to be supported in the 1.x version of the client until Dec 31st, 2019. After this time we will immediately end of life our support.
  • New Slack features: We’ll continue to add support for any new Slack features that are released as they become available on the Platform. Support for Token Rotation is an example of a Slack feature.
  • Client-specific features: We will NOT be adding any new client specific functionality to v1. Support for “asynchronous programming” is an example of of a client feature. Another example is storing additional data on the client.
  • Bug and security fixes: We’ll be continuing to address bug fixes throughout the remaining lifetime of v1.
  • Github Branching: Once v2 is officially released we’ll be merging the v2 branch into master. The existing code will be maintained in a v1 branch.

Provisional timeline: I’ve created the v2 branch (#394) and have begun development on this. As of today, March 29th, we’re in “Pre-Alpha”. My goal is to be in Beta by April 24th. This ultimately depends on the feedback I receive from the community here as well as my QA and security team’s review at Slack.

Thank you all again! 🙇

@twhaples
Copy link

I have been invited by Slack support to send API commentary to this issue. (Please accept my apologies if this feedback is better addressed elsewhere; I would be happy to move it there.)

As I use the slack web client API and not RTM, I look forward to a simpler Slack client. However, one of my primary concerns is validating that I am using the API correctly. While it is convenient to send unstructured key-value data to Slack, it is very hard to validate that this code is being used correctly, and it is not clear that this is an area of interest in the new designs. I think this will be of particular interest to people who are going to have to maintain additional client state themselves.

I've handrolled a simple substitute of this for use in my app, and reproduce some portions here to give an idea of how to help API consumers access the API correctly. For instance, the following code excerpt facilitates the construction of message payloads. (You will note that it targets the older Slack attachment format, not blocks; please also forgive the use of Typing.NamedTuple here, when the Python 3.7 implementation really ought to be a data type, and a more idiomatic <3.7 version might be a plain class.)

Payload = Dict[str, Any]

class SlackString(str):
    def for_slack(self) -> str:
        return text_for_slack(self)

class TrustedSlackString(SlackString):
    def for_slack(self) -> str:
        return self

def text_for_slack(text: str) -> str:
    """
    Slack requires these escapes and does not support escapes other than these three.
    """
    return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")

class SlackAttachment(NamedTuple):
    # scalar fields (in Slack documentation order)
    fallback: Optional[SlackString] = None
    pretext: Optional[SlackString] = None
    author_name: Optional[SlackString] = None
    author_link: Optional[SlackString] = None
    author_icon: Optional[SlackString] = None
    title: Optional[SlackString] = None
    title_link: Optional[SlackString] = None
    text: Optional[SlackString] = None
    image_url: Optional[SlackString] = None
    thumb_url: Optional[SlackString] = None
    footer: Optional[SlackString] = None
    footer_icon: Optional[SlackString] = None
    ts: Optional[int] = None

    # list fields
    fields: Optional[List[SlackField]] = None
    actions: Optional[List[SlackAction]] = None

    # rendering control fields
    mrkdwn_in: Optional[List[SlackPayloadFieldName]] = None

    def for_slack(self) -> Payload:
        d = named_tuple_to_payload(self)
        if not d.get("fallback") and self.actions:
            d["fallback"] = "\n".join(
                action.fallback() for action in self.actions or []
            )
        return d

def named_tuple_to_payload(self: Any) -> Payload:
    return dict_to_payload(self._asdict())

def dict_to_payload(dictionary: Dict[str, Any]) -> Payload:
    values = {}

    for k, v in dictionary.items():
        if v and hasattr(v.__class__, "for_slack"):
            values[k] = v.for_slack()
        elif isinstance(v, list):
            values[k] = list(item.for_slack() for item in v)
        elif v is not None:
            values[k] = v
    return values

The specific implementation here is less important than the idea that it's really nice to use type checking to validate that a generated SlackMessage is in fact a valid SlackMessage. This permits us to check messages against a known good template as we run unit tests, linters, and other such tools — instead of waiting until runtime to validate this against a live API.

I understand if Slack, organizationally, is less than thrilled at the idea of maintaining these definitions across all sorts of different clients in different languages — but the alternative is that you make integrators do the same work, without the same organizational knowledge.

@wittekm
Copy link

wittekm commented Apr 19, 2019

^ They've approved using PEP 484 (type annotations) for the new client :)

(Also, you may want to look into TypedDict, a dict equivalent of NamedTuple - just removes that conversion step)

@RodneyU215
Copy link
Contributor Author

RodneyU215 commented Apr 20, 2019

Hi @twhaples, Thanks for providing feedback on this. Like @wittekm mentioned I'm working on add type annotations for all of the API methods. Initially only for the required arguments, but in a subsequent PR I'll add types for all of the optional parameters as well. Like you mentioned this will only solve the problem for simple payloads. For structures such as Blocks these can be fairly complex. I believe we'll explore creating a builtIn Message class for this, but it may take some time and experimentation to get it right. I'm happy to review any PR's 😉for what this could look like.

RodneyU215 added a commit that referenced this issue Apr 29, 2019
This PR implements the changes specified in RFC #384. However, the most notable deviation is that we've decided that v2 will target Python 3.6+ only! Python 2.7+ will continue to be supported in v1 until Dec 31st, 2019. See additional details in #384!
@RodneyU215
Copy link
Contributor Author

We've officially uploaded the v2 version of the SDK as a pre-release beta. Those who'd like to test it out can install it like this:

pip install slackclient==2.0.0b1

I'll now be closing this RFC. Please feel free to open a new issue with the label v2 to provide any additional feedback.

Thanks again to everyone who's help guide the development of this release. ❤️

@RodneyU215 RodneyU215 unpinned this issue Apr 29, 2019
c-goosen pushed a commit to c-goosen/python-slackclient that referenced this issue Jun 18, 2019
This PR implements the changes specified in RFC slackapi#384. However, the most notable deviation is that we've decided that v2 will target Python 3.6+ only! Python 2.7+ will continue to be supported in v1 until Dec 31st, 2019. See additional details in slackapi#384!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants