From: Carl Worth Date: Sat, 26 Sep 2020 02:01:48 +0000 (-0700) Subject: Initial commit of turbot X-Git-Url: https://git.cworth.org/git?p=turbot;a=commitdiff_plain;h=6209247caaef8d6eea9a249713e1f590a47f46a0 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. --- 6209247caaef8d6eea9a249713e1f590a47f46a0 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