From 6209247caaef8d6eea9a249713e1f590a47f46a0 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Fri, 25 Sep 2020 19:01:48 -0700 Subject: [PATCH] Initial commit of turbot 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. --- .flake8 | 2 ++ .gitignore | 2 ++ Makefile | 72 ++++++++++++++++++++++++++++++++++++++++++ requirements.in | 4 +++ requirements.txt | 81 ++++++++++++++++++++++++++++++++++++++++++++++++ turbot.py | 52 +++++++++++++++++++++++++++++++ turbot.wsgi.in | 7 +++++ 7 files changed, 220 insertions(+) create mode 100644 .flake8 create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 requirements.in create mode 100644 requirements.txt create mode 100755 turbot.py create mode 100644 turbot.wsgi.in diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..f474fae --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +ignore = E305 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53633f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +turbot.wsgi +.turbot.env diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5199e6a --- /dev/null +++ b/Makefile @@ -0,0 +1,72 @@ +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)' diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..c025edd --- /dev/null +++ b/requirements.in @@ -0,0 +1,4 @@ +flask +flask_restful +python-dotenv + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..96afde4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,81 @@ +# +# 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 diff --git a/turbot.py b/turbot.py new file mode 100755 index 0000000..1cb6e68 --- /dev/null +++ b/turbot.py @@ -0,0 +1,52 @@ +#!/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/') + +if __name__ == '__main__': + app.run(debug=True) diff --git a/turbot.wsgi.in b/turbot.wsgi.in new file mode 100644 index 0000000..70a9865 --- /dev/null +++ b/turbot.wsgi.in @@ -0,0 +1,7 @@ +import sys +sys.path.insert(0, '${DEPLOY_DIR}') + +from dotenv import load_dotenv +load_dotenv('.turbot.env') + +from turbot import app as application -- 2.43.0