]> git.cworth.org Git - turbot/commitdiff
Initial commit of turbot
authorCarl Worth <cworth@cworth.org>
Sat, 26 Sep 2020 02:01:48 +0000 (19:01 -0700)
committerCarl Worth <cworth@cworth.org>
Sat, 26 Sep 2020 02:57:12 +0000 (19:57 -0700)
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 [new file with mode: 0644]
.gitignore [new file with mode: 0644]
Makefile [new file with mode: 0644]
requirements.in [new file with mode: 0644]
requirements.txt [new file with mode: 0644]
turbot.py [new file with mode: 0755]
turbot.wsgi.in [new file with mode: 0644]

diff --git a/.flake8 b/.flake8
new file mode 100644 (file)
index 0000000..f474fae
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,2 @@
+[flake8]
+ignore = E305
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..53633f5
--- /dev/null
@@ -0,0 +1,2 @@
+turbot.wsgi
+.turbot.env
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
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 (file)
index 0000000..c025edd
--- /dev/null
@@ -0,0 +1,4 @@
+flask
+flask_restful
+python-dotenv
+
diff --git a/requirements.txt b/requirements.txt
new file mode 100644 (file)
index 0000000..96afde4
--- /dev/null
@@ -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 (executable)
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/<todo_id>')
+
+if __name__ == '__main__':
+    app.run(debug=True)
diff --git a/turbot.wsgi.in b/turbot.wsgi.in
new file mode 100644 (file)
index 0000000..70a9865
--- /dev/null
@@ -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