Skip to content

Commit 5ca06ac

Browse files
committed
Initial commit on new repo
- add Dockerfile - refactor main method - web route for /img - add LICENSE - test for main + code coverage back to 99% - update README (docker, deployment, examples, instructions) - fix test_async_issue test - fix linting
1 parent 51a3016 commit 5ca06ac

File tree

13 files changed

+279
-93
lines changed

13 files changed

+279
-93
lines changed

.coveragerc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[report]
2+
3+
exclude_lines =
4+
if __name__ == .__main__.:

.dockerignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.vscode
2+
__pycache__
3+
*.json
4+
*.md
5+
htmlcov
6+
.coverage
7+
docs
8+
tests
9+
.gitignore
10+
.pydocstylerc
11+
.pylintrc

.gcloudignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ README.md
77
*.template
88
*.yaml
99
tests
10-
.pylintrc
11-
.pydocstylerc
10+
*rc
1211
htmlcov
1312
.coverage
1413
docs
14+
Dockerfile
15+
.dockerignore
16+
LICENSE

Dockerfile

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
### STEP 1: build dependencies
2+
3+
FROM python:3.8-alpine3.14 as builder
4+
5+
RUN apk add gcc==10.3.1_git20210424-r2 musl-dev==1.2.2-r3
6+
7+
COPY requirements.txt .
8+
9+
RUN pip install -r requirements.txt
10+
11+
12+
### STEP 2: assemble runtime
13+
14+
FROM python:3.8-alpine3.14
15+
16+
ENV GUNICORN_PORT=3000
17+
ENV GUNICORN_WORKERS=1
18+
ENV GUNICORN_THREADS=8
19+
ENV GOOGLE_APPLICATION_CREDENTIALS=/credentials.json
20+
21+
EXPOSE ${GUNICORN_PORT}
22+
23+
LABEL maintainer="ccaruceru"
24+
25+
# copy built packages
26+
COPY --from=builder /usr/local/lib/python3.8 /usr/local/lib/python3.8
27+
28+
WORKDIR /app
29+
30+
COPY multi_reaction_add multi_reaction_add
31+
COPY resources resources
32+
33+
ENTRYPOINT [ "/bin/sh", "-c" ]
34+
35+
CMD ["python -m gunicorn --bind :${GUNICORN_PORT} --workers ${GUNICORN_WORKERS} \
36+
--threads ${GUNICORN_THREADS} --timeout 0 --worker-class aiohttp.GunicornWebWorker \
37+
multi_reaction_add.handlers:entrypoint"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021- ccaruceru
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 115 additions & 69 deletions
Large diffs are not rendered by default.

multi_reaction_add/__main__.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,30 @@
44
Example:
55
$ python multi_reaction_add
66
7-
TODO:
7+
Todo:
88
* create new client for emoji update to avoid concurrency problems(?)
99
* use X-Cloud-Trace-Context to group logs:
1010
https://cloud.google.com/appengine/docs/standard/python3/writing-application-logs#writing_structured_logs
1111
"""
1212

1313
import os
1414
import logging
15-
from aiohttp import web
1615

1716
from multi_reaction_add.handlers import app
1817
from multi_reaction_add.internals import check_env
1918

2019

21-
if __name__ == "__main__":
20+
def main():
21+
"""Main entrypoint for standalone module run.
22+
23+
Example:
24+
$ python -m multi_react_add
25+
"""
2226
check_env()
23-
# add static /img route for debugging
24-
app.web_app().add_routes([web.static("/img", "resources/img")])
2527
port = int(os.environ.get("PORT", 3000))
2628
logging.info("Listening on port %d", port)
2729
app.start(port)
30+
31+
32+
if __name__ == "__main__":
33+
main()

multi_reaction_add/handlers.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ async def save_or_display_reactions(
107107
client (AsyncWebClient): an initialzied slack web client to communicate with slack API
108108
command (dict): json payload with information about the command triggered by the user
109109
E.g.
110-
{'token': 'MKxqqxT4PQMBnyNJdjAocKOF', 'team_id': 'T01S9QDF7AT', 'team_domain': 'ccc-yzo4468', 'channel_id': 'D01T36EUQTT', 'channel_name': 'directmessage', 'user_id': 'U01SWNKJR6G', 'user_name': 'user.name', 'command': '/multireact', 'text': ':+1::raised_hands::eyes::clap::large_blue_circle::100::heart:', 'api_app_id': 'A01S9QL1VAT', 'is_enterprise_install': 'false', 'response_url': 'https://hooks.slack...WRrsPnTqV8QtqZ5Un0', 'trigger_id': '1917238991...9a29a6da4'} # pylint: disable=line-too-long
111-
{'api_app_id': 'A01TWV8NAEN', 'channel_id': 'D01SQA99362', 'channel_name': 'directmessage', 'command': '/multireact', 'enterprise_id': 'EG26V10SE', 'enterprise_name': 'ABK-SB', 'is_enterprise_install': 'false', 'response_url': 'https://hooks.slack....UFIRvQgGs0', 'team_domain': 'kingcom-sandbox', 'team_id': 'T8T3GKUKZ', 'token': 'wF8MDfFS8BLd30KFYLT1MlZa', 'trigger_id': '1995489393941.299118...b1d2b8f830', 'user_id': 'U01SXA059B5', 'user_name': 'cris.c'}
110+
{'token': 'MKxqqxT4PQMBnyNJdjAocKOF', 'team_id': '<tid>', 'team_domain': '<tname>', 'channel_id': 'D01T36EUQTT', 'channel_name': 'directmessage', 'user_id': 'U01SWNKJR6G', 'user_name': 'user.name', 'command': '/multireact', 'text': ':+1::eyes::clap:', 'api_app_id': '<id>', 'is_enterprise_install': 'false', 'response_url': 'https://hooks.slack...WRrsPnTqV8QtqZ5Un0', 'trigger_id': '1917238991...9a29a6da4'} # pylint: disable=line-too-long
111+
{'api_app_id': '<id>', 'channel_id': 'D01SQA99362', 'channel_name': 'directmessage', 'command': '/multireact', 'enterprise_id': '<eid>', 'enterprise_name': '<ename>', 'is_enterprise_install': 'false', 'response_url': 'https://hooks.slack....UFIRvQgGs0', 'team_domain': '<tname>', 'team_id': '<tid>', 'token': 'wF8MDfFS8BLd30KFYLT1MlZa', 'trigger_id': '1995489393941.299118...b1d2b8f830', 'user_id': 'U01SXA059B5', 'user_name': '<uname>'}
112112
respond (AsyncRespond): function that sends an ephemeral response for slack commands
113113
logger (Logger): optional logger passed to all handlers
114114
"""
@@ -173,8 +173,8 @@ async def add_reactions(
173173
ack (AsyncAck): function to inform slack that an event has been received
174174
shortcut (dict): json payload with information about the shortcut triggered by the user
175175
E.g.
176-
{'type': 'message_action', 'token': 'MKxqqxT4PQMBnyNJdjAocKOF', 'action_ts': '1617307130.812650', 'team': {'id': 'T01S9QDF7AT', 'domain': 'ccc-yzo4468'}, 'user': {'id': 'U01SWNKJR6G', 'username': 'user.name', 'team_id': 'T01S9QDF7AT', 'name': 'User Name'}, 'channel': {'id': 'C01S9QDFYNT', 'name': 'general'}, 'is_enterprise_install': False, 'enterprise': None, 'callback_id': 'add_reactions', 'trigger_id': '1921893944...3e6a446fbc18', 'response_url': 'https://hooks.slack...L9Vdrbee00sZG', 'message_ts': '1617307089.003400', 'message': {'client_msg_id': '6f9e6469-1...70b17af92b', 'type': 'message', 'text': 'asdfghjkl', 'user': 'U01SWNKJR6G', 'ts': '1617307089.003400', 'team': 'T01S9QDF7AT', 'blocks': [{'type': 'rich_text', 'block_id': 'm3V', 'elements': [{'type': 'rich_text_section', 'elements': [{'type': 'text', 'text': 'asdfghjkl'}]}]}]}} # pylint: disable=line-too-long
177-
{'action_ts': '1619339417.302061', 'callback_id': 'add_reactions', 'channel': {'id': 'C01V7KXNRRS', 'name': 'slapp-1119'}, 'enterprise': {'id': 'EG26V10SE', 'name': 'ABK-SB'}, 'is_enterprise_install': False, 'message': {'blocks': [...], 'client_msg_id': '437b2675-6524-4724-a...69a4e0b9f6', 'team': 'T8T3GKUKZ', 'text': '210', 'ts': '1619339414.000200', 'type': 'message', 'user': 'U01SXA059B5'}, 'message_ts': '1619339414.000200', 'response_url': 'https://hooks.slack....tkt5yMCogg', 'team': {'domain': 'kingcom-sandbox', 'enterprise_id': 'EG26V10SE', 'enterprise_name': 'ABK-SB', 'id': 'T8T3GKUKZ'}, 'token': 'wF8MDfFS8BLd30KFYLT1MlZa', 'trigger_id': '1991796659910.299118...7619ad8bd4', 'type': 'message_action', 'user': {'id': 'U01SXA059B5', 'name': 'cris.c', 'team_id': 'T8T3GKUKZ', 'username': 'cris.c'}}
176+
{'type': 'message_action', 'token': 'MKxqqxT4PQMBnyNJdjAocKOF', 'action_ts': '1617307130.812650', 'team': {'id': '<tid>', 'domain': '<domain>'}, 'user': {'id': 'U01SWNKJR6G', 'username': 'user.name', 'team_id': '<tid>', 'name': 'User Name'}, 'channel': {'id': 'C01S9QDFYNT', 'name': 'general'}, 'is_enterprise_install': False, 'enterprise': None, 'callback_id': 'add_reactions', 'trigger_id': '1921893944...3e6a446fbc18', 'response_url': 'https://hooks.slack...L9Vdrbee00sZG', 'message_ts': '1617307089.003400', 'message': {'client_msg_id': '6f9e6469-1...70b17af92b', 'type': 'message', 'text': 'asdfghjkl', 'user': 'U01SWNKJR6G', 'ts': '1617307089.003400', 'team': '<tid>', 'blocks': [{'type': 'rich_text', 'block_id': 'm3V', 'elements': [{'type': 'rich_text_section', 'elements': [{'type': 'text', 'text': 'asdfghjkl'}]}]}]}} # pylint: disable=line-too-long
177+
{'action_ts': '1619339417.302061', 'callback_id': 'add_reactions', 'channel': {'id': 'C01V7KXNRRS', 'name': '<chname>'}, 'enterprise': {'id': '<eid>', 'name': '<ename>'}, 'is_enterprise_install': False, 'message': {'blocks': [...], 'client_msg_id': '437b2675-6524-4724-a...69a4e0b9f6', 'team': '<tid>', 'text': '210', 'ts': '1619339414.000200', 'type': 'message', 'user': 'U01SXA059B5'}, 'message_ts': '1619339414.000200', 'response_url': 'https://hooks.slack....tkt5yMCogg', 'team': {'domain': '<domain>', 'enterprise_id': '<eid>', 'enterprise_name': '<ename>', 'id': '<tid>'}, 'token': 'wF8MDfFS8BLd30KFYLT1MlZa', 'trigger_id': '1991796659910.299118...7619ad8bd4', 'type': 'message_action', 'user': {'id': 'U01SXA059B5', 'name': '<uname>', 'team_id': '<tid>', 'username': '<uname>'}}
178178
client (AsyncWebClient): an initialzied slack web client to communicate with slack API
179179
logger (Logger): optional logger passed to all handlers
180180
context (AsyncBoltContext): a dictionary added to all handlers which can be used to enrich events with
@@ -309,6 +309,8 @@ async def update_home_tab(
309309
Args:
310310
client (AsyncWebClient): an initialzied slack web client to communicate with slack API
311311
event (dict): payload from slack server for app home opened event
312+
E.g.
313+
{'type': 'app_home_opened', 'user': 'U01SWNKJR6G', 'channel': 'D02FP4JLJ56', 'tab': 'home', 'event_ts': '1632136426.209403'} # pylint: disable=line-too-long
312314
logger (Logger): optional logger passed to all handlers
313315
request (AsyncBoltRequest): entire request payload from slack server
314316
"""
@@ -330,3 +332,5 @@ async def update_home_tab(
330332

331333
# add the warmup route for aiohttp
332334
app.web_app().add_routes([web.get("/_ah/warmup", warmup)])
335+
# add static /img route for local runs/debugging
336+
app.web_app().add_routes([web.static("/img", "resources/img")])

multi_reaction_add/internals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ async def _get_reactions_in_team(client: AsyncWebClient, logger: logging.Logger)
9494
"""Gets the custom + standard emojis available in a Slack workspace, and community emojis too.
9595
9696
It returns the basic community emojis from emojidex.com because they are not included in the
97-
Slack API response. https://king.slack.com/help/requests/3477073
97+
Slack API response.
9898
9999
Args:
100100
client (AsyncWebClient): an initialized slack WebClient for API calls

tests/helpers.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# -*- coding: utf-8 -*-
2+
"""Module holding boilerplate code for running the tests"""
3+
4+
import os
5+
from contextlib import contextmanager, ContextDecorator
6+
from unittest.mock import patch
7+
8+
@contextmanager
9+
def patch_import() -> ContextDecorator:
10+
"""Returns a context manager where the google storage client library has been patched for testing
11+
12+
Yields:
13+
ContextDecorator: a context manager
14+
"""
15+
# necessary OS env vars for handlers.py module
16+
keys = ["SLACK_CLIENT_ID", "SLACK_CLIENT_SECRET", "SLACK_SIGNING_SECRET",
17+
"SLACK_INSTALLATION_GOOGLE_BUCKET_NAME", "SLACK_STATE_GOOGLE_BUCKET_NAME", "USER_DATA_BUCKET_NAME"]
18+
# patch google storage client call and os env vars
19+
with patch.dict(os.environ, {k:"" for k in keys}) as mock_env: # pylint: disable=unused-variable
20+
with patch("google.cloud.storage.Client") as mock_storage_client:
21+
yield mock_storage_client

0 commit comments

Comments
 (0)