Turbot will eventually be a Slack bot for the Halibut That Bass team.
As of this commit, turbot.py is just a simple flask app implementing a
REST API (to query, add, delete TODO items). Additionally, this commit
includes the Makefile pieces to manage python dependencies and to
deploy the program to our server.
There's not yet any actual Slack-application code here yet.
--- /dev/null
+[flake8]
+ignore = E305
--- /dev/null
+turbot.wsgi
+.turbot.env
--- /dev/null
+DEPLOY_HOST=halibut.cworth.org
+DEPLOY_DIR=/srv/halibut.cworth.org/turbot
+export DEPLOY_DIR
+DO_NOT_DEPLOY=env .gitignore
+DO_NOT_DELETE=.turbot.env
+
+help:
+ @echo "Available targets (in rough order of expected use):"
+ @echo
+ @echo " make bootstrap Setup python virtual environment"
+ @echo " make reqs Install dependencies into venv"
+ @echo " make run Run a local server for testing"
+ @echo " make deploy Deploy code to production"
+ @echo
+
+.PHONY: require-venv
+require-venv:
+ifeq (, $(wildcard env))
+ $(error "No virtualenv found. Try 'make bootstrap'")
+endif
+ifndef VIRTUAL_ENV
+ $(error "No virtualenv active. Try '. ./env/bin/activate'")
+endif
+
+.PHONY: bootstrap
+bootstrap:
+ @echo "=== Creating python virtual environment ==="
+ python3 -m virtualenv env
+
+ @echo
+ @echo "=== Installing pip-tools (to compile dependency list) ==="
+ (. ./env/bin/activate && pip install pip-tools)
+
+ @echo
+ @echo "Virtual environment is now available."
+ @echo "You must activate it by sourcing the activation script, such as:"
+ @echo
+ @echo " . ./env/bin/activate"
+ @echo
+ @echo "After that you can run 'make reqs' to install dependencies"
+
+.PHONY: reqs
+reqs: require-venv requirements.txt
+ pip install --require-hashes --upgrade -r requirements.txt
+ @echo
+ @echo "Dependencies are now installed. You can now do 'make run' or 'make deploy'"
+
+requirements.txt: requirements.in
+ifeq (, $(shell which pip-compile))
+ $(error "No pip-compile found. Try 'make bootstrap'")
+endif
+ pip-compile --no-index --generate-hashes --allow-unsafe
+
+run: require-venv
+ python3 ./turbot.py
+
+turbot.wsgi: turbot.wsgi.in Makefile
+ envsubst < turbot.wsgi.in > turbot.wsgi
+
+deploy:
+ rm -rf .deploy-source
+ git clone . .deploy-source
+ rm -rf .deploy-source/.git
+ make -C .deploy-source turbot.wsgi
+ (cd .deploy-source; rsync -avz \
+ $(DO_NOT_DEPLOY:%=--exclude=%) \
+ --exclude=$(DO_NOT_DELETE) \
+ --delete \
+ --delete-after \
+ ./ $(DEPLOY_HOST):$(DEPLOY_DIR) )
+ rm -rf .deploy-source
+ ssh $(DEPLOY_HOST) '(cd $(DEPLOY_DIR); make bootstrap; . env/bin/activate; make reqs)'
--- /dev/null
+flask
+flask_restful
+python-dotenv
+
--- /dev/null
+#
+# This file is autogenerated by pip-compile
+# To update, run:
+#
+# pip-compile --allow-unsafe --generate-hashes --no-index
+#
+aniso8601==8.0.0 \
+ --hash=sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072 \
+ --hash=sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a \
+ # via flask-restful
+click==7.1.2 \
+ --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a \
+ --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc \
+ # via flask
+flask-restful==0.3.8 \
+ --hash=sha256:5ea9a5991abf2cb69b4aac19793faac6c032300505b325687d7c305ffaa76915 \
+ --hash=sha256:d891118b951921f1cec80cabb4db98ea6058a35e6404788f9e70d5b243813ec2 \
+ # via -r requirements.in
+flask==1.1.2 \
+ --hash=sha256:4efa1ae2d7c9865af48986de8aeb8504bf32c7f3d6fdc9353d34b21f4b127060 \
+ --hash=sha256:8a4fdd8936eba2512e9c85df320a37e694c93945b33ef33c89946a340a238557 \
+ # via -r requirements.in, flask-restful
+itsdangerous==1.1.0 \
+ --hash=sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19 \
+ --hash=sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749 \
+ # via flask
+jinja2==2.11.2 \
+ --hash=sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0 \
+ --hash=sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035 \
+ # via flask
+markupsafe==1.1.1 \
+ --hash=sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473 \
+ --hash=sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161 \
+ --hash=sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235 \
+ --hash=sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5 \
+ --hash=sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42 \
+ --hash=sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff \
+ --hash=sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b \
+ --hash=sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1 \
+ --hash=sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e \
+ --hash=sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183 \
+ --hash=sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66 \
+ --hash=sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b \
+ --hash=sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1 \
+ --hash=sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15 \
+ --hash=sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1 \
+ --hash=sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e \
+ --hash=sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b \
+ --hash=sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905 \
+ --hash=sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735 \
+ --hash=sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d \
+ --hash=sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e \
+ --hash=sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d \
+ --hash=sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c \
+ --hash=sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21 \
+ --hash=sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2 \
+ --hash=sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5 \
+ --hash=sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b \
+ --hash=sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6 \
+ --hash=sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f \
+ --hash=sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f \
+ --hash=sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2 \
+ --hash=sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7 \
+ --hash=sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be \
+ # via jinja2
+python-dotenv==0.14.0 \
+ --hash=sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d \
+ --hash=sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423 \
+ # via -r requirements.in
+pytz==2020.1 \
+ --hash=sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed \
+ --hash=sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048 \
+ # via flask-restful
+six==1.15.0 \
+ --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \
+ --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \
+ # via flask-restful
+werkzeug==1.0.1 \
+ --hash=sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43 \
+ --hash=sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c \
+ # via flask
--- /dev/null
+#!/usr/bin/env python3
+
+from flask import Flask
+from flask_restful import reqparse, abort, Api, Resource
+
+app = Flask(__name__)
+api = Api(app)
+
+TODOS = {}
+
+def abort_if_todo_doesnt_exist(todo_id):
+ if todo_id not in TODOS:
+ abort(404, message="Todo {} doesn't exist".format(todo_id))
+
+parser = reqparse.RequestParser()
+parser.add_argument('task')
+
+class Todo(Resource):
+ def get(self, todo_id):
+ abort_if_todo_doesnt_exist(todo_id)
+ return TODOS[todo_id]
+
+ def delete(self, todo_id):
+ abort_if_todo_doesnt_exist(todo_id)
+ del TODOS[todo_id]
+ return '', 204
+
+ def put(self, todo_id):
+ args = parser.parse_args()
+ task = {'task': args['task']}
+ TODOS[todo_id] = task
+ return task, 201
+
+class TodoList(Resource):
+ def get(self):
+ return TODOS
+
+ def post(self):
+ args = parser.parse_args()
+ try:
+ todo_id = int(max(TODOS.keys()).lstrip('todo')) + 1
+ except:
+ todo_id = 1
+ todo_id = 'todo%i' % todo_id
+ TODOS[todo_id] = {'task': args['task']}
+ return TODOS[todo_id], 201
+
+api.add_resource(TodoList, '/todos')
+api.add_resource(Todo, '/todos/<todo_id>')
+
+if __name__ == '__main__':
+ app.run(debug=True)
--- /dev/null
+import sys
+sys.path.insert(0, '${DEPLOY_DIR}')
+
+from dotenv import load_dotenv
+load_dotenv('.turbot.env')
+
+from turbot import app as application