Skip to content

Commit 1aac7ac

Browse files
authored
Use SlackEventAdapter with existing instance of Flask (#27)
* Add the ability for SlackServer to accept existing Flask instance as a param * Add server tests for passing an existing Flask instance into the adapter * Updated README with Bring Your Own Flask example * Add python 3 to test environments * Add package version info header
1 parent 561ccc6 commit 1aac7ac

File tree

8 files changed

+185
-61
lines changed

8 files changed

+185
-61
lines changed

.travis.yml

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
sudo: false
2+
dist: trusty
23
language: python
34
python:
45
- "2.7"
5-
env:
6-
matrix:
7-
- TOX_ENV=py27
8-
- TOX_ENV=flake8
9-
cache: pip
6+
- "3.3"
7+
- "3.4"
8+
- "3.5"
9+
- "3.6"
1010
install:
11-
- "travis_retry pip install setuptools --upgrade"
12-
- "travis_retry pip install tox"
11+
- travis_retry pip install -r requirements.txt
12+
- travis_retry pip install flake8
13+
- travis_retry pip install -r requirements-dev.txt
1314
script:
14-
- tox -e $TOX_ENV
15-
after_script:
16-
- cat .tox/$TOX_ENV/log/*.log
15+
- flake8 slackeventsapi
16+
- py.test --cov-report= --cov=slackeventsapi tests
17+
after_success:
18+
- codecov -e $TRAVIS_PYTHON_VERSION

README.rst

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ receiving Team Events.
4646
user has authorized your app.
4747

4848
🤖 Development workflow:
49-
------------------------------
49+
===========================
5050

5151
(1) Create a Slack app on https://api.slack.com/apps/
5252
(2) Add a `bot user` for your app
@@ -57,11 +57,11 @@ user has authorized your app.
5757

5858
**🎉 Once your app has been authorized, you will begin receiving Slack Events**
5959

60-
⚠️ We strongly discourage using ngrok for
61-
anything but development. It’s not well-suited for production use.
60+
⚠️ Ngrok is a great tool for developing Slack apps, but we don't recommend using ngrok
61+
for production apps.
6262

6363
🤖 Usage
64-
---------
64+
----------
6565
**⚠️ Keep your app's credentials safe!**
6666

6767
- For development, keep them in virtualenv variables.
@@ -75,34 +75,68 @@ user has authorized your app.
7575
SLACK_VERIFICATION_TOKEN = os.environ["SLACK_VERIFICATION_TOKEN"]
7676
7777
Create a Slack Event Adapter for receiving actions via the Events API
78+
-----------------------------------------------------------------------
79+
**Using the built-in Flask server:**
7880

7981
.. code:: python
8082
8183
from slackeventsapi import SlackEventAdapter
8284
83-
slack_events_adapter = SlackEventAdapter(SLACK_VERIFICATION_TOKEN, endpoint="/slack_events")
8485
85-
Create an event listener for "reaction_added" events and print the emoji name
86+
slack_events_adapter = SlackEventAdapter(SLACK_VERIFICATION_TOKEN, endpoint="/slack_events")
8687
87-
.. code:: python
8888
89+
# Create an event listener for "reaction_added" events and print the emoji name
8990
@slack_events_adapter.on("reaction_added")
9091
def reaction_added(event):
9192
emoji = event.get("reaction")
9293
print(emoji)
9394
9495
95-
Start the server on port 3000
96+
# Start the server on port 3000
97+
slack_events_adapter.start(port=3000)
9698
97-
.. code-block:: python
9899
99-
slack_events_adapter.start(port=3000)
100+
**Using your existing Flask instance:**
101+
102+
103+
.. code:: python
104+
105+
from flask import Flask
106+
from slackeventsapi import SlackEventAdapter
107+
108+
109+
# This `app` represents your existing Flask app
110+
app = Flask(__name__)
111+
112+
113+
# An example of one of your Flask app's routes
114+
@app.route("/")
115+
def hello():
116+
return "Hello there!"
117+
118+
119+
# Bind the Events API route to your existing Flask app by passing the server
120+
# instance as the last param, or with `server=app`.
121+
slack_events_adapter = SlackEventAdapter(SLACK_VERIFICATION_TOKEN, "/slack/events", app)
122+
123+
124+
# Create an event listener for "reaction_added" events and print the emoji name
125+
@slack_events_adapter.on("reaction_added")
126+
def reaction_added(event):
127+
emoji = event.get("reaction")
128+
print(emoji)
129+
130+
131+
# Start the server on port 3000
132+
if __name__ == "__main__":
133+
app.run(port=3000)
100134
101135
For a comprehensive list of available Slack `Events` and more information on
102136
`Scopes`, see https://api.slack.com/events-api
103137

104-
🤖 Examples
105-
------------
138+
🤖 Example event listeners
139+
-----------------------------
106140

107141
See `example.py`_ for usage examples. This example also utilizes the
108142
SlackClient Web API client.

requirements-dev.txt

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@ coveralls==1.1
22
ipdb==0.9.3
33
ipython==4.1.2
44
pdbpp==0.8.3
5-
pytest>=2.8.2
6-
pytest-flask>=0.10
7-
pytest-mock>=1.1
8-
pytest-cov==2.2.1
9-
pytest-pythonpath>=0.3
10-
Sphinx==1.4.4
11-
sphinx-rtd-theme==0.1.9
12-
testfixtures==4.9.1
13-
tox>=1.8.0
5+
pytest>=3.2.0
6+
pytest-flask==0.10.0
7+
pytest-mock>=1.6.3
8+
pytest-cov==2.5.1
9+
pytest-pythonpath==0.7.1
10+
testfixtures==5.3.1
11+
tox==2.9.1

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
appdirs==1.4.0
2-
pyee>=3.0.3
2+
pyee==5.0.0
33
flask==0.12
44
packaging==16.8
55
pyparsing==2.1.10

slackeventsapi/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@
55
class SlackEventAdapter(EventEmitter):
66
# Initialize the Slack event server
77
# If no endpoint is provided, default to listening on '/slack/events'
8-
def __init__(self, verification_token, endpoint="/slack/events"):
8+
def __init__(self, verification_token, endpoint="/slack/events", server=None):
99
EventEmitter.__init__(self)
1010
self.verification_token = verification_token
11-
self.server = SlackServer(verification_token, endpoint, self)
11+
self.server = SlackServer(verification_token, endpoint, self, server)
1212

1313
def start(self, host='127.0.0.1', port=None, debug=False, **kwargs):
14+
"""
15+
Start the built in webserver, bound to the host and port you'd like.
16+
Default host is `127.0.0.1` and port 8080.
17+
18+
:param host: The host you want to bind the build in webserver to
19+
:param port: The port number you want the webserver to run on
20+
:param debug: Set to `True` to enable debug level logging
21+
:param kwargs: Additional arguments you'd like to pass to Flask
22+
"""
1423
self.server.run(host=host, port=port, debug=debug, **kwargs)

slackeventsapi/server.py

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,48 @@
11
from flask import Flask, request, make_response
22
import json
3+
import platform
4+
import sys
5+
from .version import __version__
36

47

58
class SlackServer(Flask):
6-
def __init__(self, verification_token, endpoint, emitter):
7-
Flask.__init__(self, __name__)
9+
def __init__(self, verification_token, endpoint, emitter, server):
810
self.verification_token = verification_token
11+
self.emitter = emitter
12+
self.endpoint = endpoint
13+
self.package_info = self.get_package_info()
914

10-
@self.route(endpoint, methods=['GET', 'POST'])
15+
# If a server is passed in, bind the event handler routes to it,
16+
# otherwise create a new Flask instance.
17+
if server:
18+
if isinstance(server, Flask):
19+
self.bind_route(server)
20+
else:
21+
raise TypeError("Server must be an instance of Flask")
22+
else:
23+
Flask.__init__(self, __name__)
24+
self.bind_route(self)
25+
26+
def get_package_info(self):
27+
client_name = __name__.split('.')[0]
28+
client_version = __version__ # Version is returned from version.py
29+
30+
# Collect the package info, Python version and OS version.
31+
package_info = {
32+
"client": "{0}/{1}".format(client_name, client_version),
33+
"python": "Python/{v.major}.{v.minor}.{v.micro}".format(v=sys.version_info),
34+
"system": "{0}/{1}".format(platform.system(), platform.release())
35+
}
36+
37+
# Concatenate and format the user-agent string to be passed into request headers
38+
ua_string = []
39+
for key, val in package_info.items():
40+
ua_string.append(val)
41+
42+
return " ".join(ua_string)
43+
44+
def bind_route(self, server):
45+
@server.route(self.endpoint, methods=['GET', 'POST'])
1146
def event():
1247
# If a GET request is made, return 404.
1348
if request.method == 'GET':
@@ -25,11 +60,13 @@ def event():
2560
# Verify the request token
2661
request_token = event_data.get("token")
2762
if self.verification_token != request_token:
28-
emitter.emit('error', 'invalid verification token')
63+
self.emitter.emit('error', 'invalid verification token')
2964
return make_response("Request contains invalid Slack verification token", 403)
3065

3166
# Parse the Event payload and emit the event to the event listener
3267
if "event" in event_data:
3368
event_type = event_data["event"]["type"]
34-
emitter.emit(event_type, event_data)
35-
return make_response("", 200)
69+
self.emitter.emit(event_type, event_data)
70+
response = make_response("", 200)
71+
response.headers['X-Slack-Powered-By'] = self.package_info
72+
return response

tests/test_server.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
import json
2+
from flask import Flask
23
import pytest
4+
import sys
5+
from slackeventsapi import SlackEventAdapter
6+
from slackeventsapi.version import __version__
7+
8+
9+
def test_existing_flask():
10+
valid_flask = Flask(__name__)
11+
valid_adapter = SlackEventAdapter("vFO9LARnLI7GflLR8tGqHgdy", "/slack/events", valid_flask)
12+
assert isinstance(valid_adapter, SlackEventAdapter)
13+
14+
15+
def test_server_not_flask():
16+
with pytest.raises(TypeError) as e:
17+
invalid_flask = "I am not a Flask"
18+
SlackEventAdapter("vFO9LARnLI7GflLR8tGqHgdy", "/slack/events", invalid_flask)
19+
assert e.value.args[0] == 'Server must be an instance of Flask'
320

421

522
def test_event_endpoint_get(client):
@@ -15,13 +32,35 @@ def test_url_challenge(client):
1532
data=data,
1633
content_type='application/json')
1734
assert res.status_code == 200
18-
assert res.data == "valid_challenge_token"
35+
assert bytes.decode(res.data) == "valid_challenge_token"
36+
37+
38+
def test_valid_event_request(client):
39+
data = pytest.reaction_event_fixture
40+
res = client.post(
41+
'/slack/events',
42+
data=data,
43+
content_type='application/json')
44+
assert res.status_code == 200
1945

2046

21-
def test_valid_event(client):
47+
def test_version_header(client):
48+
# Verify [package metadata header is set
49+
package_info = SlackEventAdapter("token").server.package_info
50+
2251
data = pytest.reaction_event_fixture
2352
res = client.post(
2453
'/slack/events',
2554
data=data,
2655
content_type='application/json')
56+
2757
assert res.status_code == 200
58+
assert res.headers["X-Slack-Powered-By"] == package_info
59+
60+
61+
def test_server_start(mocker):
62+
# Verify server started with correct params
63+
slack_events_adapter = SlackEventAdapter("token")
64+
mocker.spy(slack_events_adapter, 'server')
65+
slack_events_adapter.start(port=3000)
66+
slack_events_adapter.server.run.assert_called_once_with(debug=False, host='127.0.0.1', port=3000)

tox.ini

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,34 @@
11
[tox]
2+
; you probably don't have all of these python versions on your machine. when you invoke tox, you should pick an
3+
; environment that you have (e.g. `tox -e py27,py36,flake8`).
4+
; for quality analysis, use `tox -e flake8` or just `flake8 slackeventsapi`
5+
; to build the docs, use `tox -e docs`
26
envlist=
3-
py{27},
4-
flake8
5-
skipsdist=true
6-
7-
[flake8]
8-
max-line-length= 100
9-
exclude= tests/*
7+
py{27,33,34,35,36},
8+
flake8,
9+
docs
1010

1111
[testenv]
12-
passenv = TOXENV CI TRAVIS TRAVIS_*
13-
commands =
14-
py.test --cov-report= --cov=slackeventsapi {posargs:tests}
15-
codecov -e TOXENV
16-
1712
deps =
1813
-r{toxinidir}/requirements-dev.txt
1914
-r{toxinidir}/requirements.txt
2015
codecov>=1.4.0
21-
basepython =
22-
py27: python2.7
16+
commands =
17+
; `--cov-report=html:cov_html`: suppress terminal output, html report in `cov_html/`, populate `.coverage/`
18+
; `--cov=slackeventsapi`: name project
19+
; `{posargs:tests}`: tests located in `tests` by default unless otherwise overriden by tox positional args
20+
py.test --cov-report=html:cov_html --cov=slackeventsapi {posargs:tests}
21+
; `codecov` will run the `coverage` utility and then upload results in xml format
22+
; `coverage` utility has configuration in `.coveragerc`
23+
; CI systems use their own build matricies and virtualenvs and don't need tox. therefore tox shouldn't be used
24+
; to upload coverage to codecov
25+
; codecov -e TOXENV
26+
27+
# Flake8 Configuration
28+
[flake8]
29+
max-line-length = 100
2330

2431
[testenv:flake8]
25-
basepython=python
26-
deps=flake8
27-
commands=
28-
flake8 \
29-
{toxinidir}/slackeventsapi
32+
basepython = python
33+
deps = flake8
34+
commands = flake8 slackeventsapi

0 commit comments

Comments
 (0)