diff --git a/.python-version b/.python-version index 7c69a55..1981190 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.7.0 +3.8.0 diff --git a/Dockerfile b/Dockerfile index 4bd2285..c5b4822 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # base image -FROM python:3.7.0-alpine +FROM python:3.8.0-alpine # set working directory RUN mkdir -p /usr/src/app @@ -10,3 +10,6 @@ ADD ./requirements.txt /usr/src/app/requirements.txt # install requirements RUN pip install -r requirements.txt + +# copy project +COPY . /usr/src/app diff --git a/LICENSE b/LICENSE index 54a7a05..a59e7e6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,8 +1,21 @@ -The MIT License (MIT) -Copyright (c) 2018 Michael Herman +MIT License -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright (c) 2019 Michael Herman -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 7c6f621..036c953 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ Spin up the containers: $ docker-compose up -d --build ``` -Open your browser to http://localhost:5004 to view the app or to http://localhost:9181 to view the RQ dashboard. +Open your browser to http://localhost:5004 to view the app or to http://localhost:9181 to view the RQ dashboard. -### Want to learn how to build this? +### Want to learn how to build this? Check out the [post](https://testdriven.io/asynchronous-tasks-with-flask-and-redis-queue). diff --git a/docker-compose.yml b/docker-compose.yml index ba005c4..9a9a9dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: image: web container_name: web ports: - - '5004:5000' + - 5004:5000 command: python manage.py run -h 0.0.0.0 volumes: - .:/usr/src/app @@ -28,12 +28,14 @@ services: - redis redis: - image: redis:4.0.11-alpine + image: redis:5.0.7-alpine dashboard: build: ./project/dashboard image: dashboard container_name: dashboard ports: - - '9181:9181' + - 9181:9181 command: rq-dashboard -H redis + depends_on: + - redis diff --git a/manage.py b/manage.py index 88f6d90..f58c2b4 100644 --- a/manage.py +++ b/manage.py @@ -17,21 +17,21 @@ @cli.command() def test(): """Runs the unit tests without test coverage.""" - tests = unittest.TestLoader().discover('project/tests', pattern='test*.py') + tests = unittest.TestLoader().discover("project/tests", pattern="test*.py") result = unittest.TextTestRunner(verbosity=2).run(tests) if result.wasSuccessful(): return 0 return 1 -@cli.command('run_worker') +@cli.command("run_worker") def run_worker(): - redis_url = app.config['REDIS_URL'] + redis_url = app.config["REDIS_URL"] redis_connection = redis.from_url(redis_url) with Connection(redis_connection): - worker = Worker(app.config['QUEUES']) + worker = Worker(app.config["QUEUES"]) worker.work() -if __name__ == '__main__': +if __name__ == "__main__": cli() diff --git a/project/dashboard/Dockerfile b/project/dashboard/Dockerfile index 2ea9771..2ea03e9 100644 --- a/project/dashboard/Dockerfile +++ b/project/dashboard/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.0-alpine +FROM python:3.8.0-alpine RUN pip install rq-dashboard diff --git a/project/server/__init__.py b/project/server/__init__.py index c768578..8e0bc2b 100644 --- a/project/server/__init__.py +++ b/project/server/__init__.py @@ -6,7 +6,6 @@ from flask import Flask from flask_bootstrap import Bootstrap - # instantiate the extensions bootstrap = Bootstrap() @@ -16,12 +15,12 @@ def create_app(script_info=None): # instantiate the app app = Flask( __name__, - template_folder='../client/templates', - static_folder='../client/static' + template_folder="../client/templates", + static_folder="../client/static", ) # set config - app_settings = os.getenv('APP_SETTINGS') + app_settings = os.getenv("APP_SETTINGS") app.config.from_object(app_settings) # set up extensions @@ -29,9 +28,10 @@ def create_app(script_info=None): # register blueprints from project.server.main.views import main_blueprint + app.register_blueprint(main_blueprint) # shell context for flask cli - app.shell_context_processor({'app': app}) + app.shell_context_processor({"app": app}) return app diff --git a/project/server/config.py b/project/server/config.py index 94fe3c4..0ae95aa 100644 --- a/project/server/config.py +++ b/project/server/config.py @@ -1,23 +1,27 @@ # project/server/config.py import os + basedir = os.path.abspath(os.path.dirname(__file__)) class BaseConfig(object): """Base configuration.""" + WTF_CSRF_ENABLED = True - REDIS_URL = 'redis://redis:6379/0' - QUEUES = ['default'] + REDIS_URL = "redis://redis:6379/0" + QUEUES = ["default"] class DevelopmentConfig(BaseConfig): """Development configuration.""" + WTF_CSRF_ENABLED = False class TestingConfig(BaseConfig): """Testing configuration.""" + TESTING = True WTF_CSRF_ENABLED = False PRESERVE_CONTEXT_ON_EXCEPTION = False diff --git a/project/server/main/views.py b/project/server/main/views.py index 0a2c1f3..8aa8a7e 100644 --- a/project/server/main/views.py +++ b/project/server/main/views.py @@ -3,49 +3,47 @@ import redis from rq import Queue, Connection -from flask import render_template, Blueprint, jsonify, \ - request, current_app +from flask import render_template, Blueprint, jsonify, request, current_app from project.server.main.tasks import create_task +main_blueprint = Blueprint("main", __name__,) -main_blueprint = Blueprint('main', __name__,) - -@main_blueprint.route('/', methods=['GET']) +@main_blueprint.route("/", methods=["GET"]) def home(): - return render_template('main/home.html') + return render_template("main/home.html") -@main_blueprint.route('/tasks', methods=['POST']) +@main_blueprint.route("/tasks", methods=["POST"]) def run_task(): - task_type = request.form['type'] - with Connection(redis.from_url(current_app.config['REDIS_URL'])): + task_type = request.form["type"] + with Connection(redis.from_url(current_app.config["REDIS_URL"])): q = Queue() task = q.enqueue(create_task, task_type) response_object = { - 'status': 'success', - 'data': { - 'task_id': task.get_id() + "status": "success", + "data": { + "task_id": task.get_id() } } return jsonify(response_object), 202 -@main_blueprint.route('/tasks/', methods=['GET']) +@main_blueprint.route("/tasks/", methods=["GET"]) def get_status(task_id): - with Connection(redis.from_url(current_app.config['REDIS_URL'])): + with Connection(redis.from_url(current_app.config["REDIS_URL"])): q = Queue() task = q.fetch_job(task_id) if task: response_object = { - 'status': 'success', - 'data': { - 'task_id': task.get_id(), - 'task_status': task.get_status(), - 'task_result': task.result, - } + "status": "success", + "data": { + "task_id": task.get_id(), + "task_status": task.get_status(), + "task_result": task.result, + }, } else: - response_object = {'status': 'error'} + response_object = {"status": "error"} return jsonify(response_object) diff --git a/project/tests/__init__.py b/project/tests/__init__.py index b452ab6..efc6a40 100644 --- a/project/tests/__init__.py +++ b/project/tests/__init__.py @@ -1 +1 @@ -# project/server/tests/__init__.py +# project/tests/__init__.py diff --git a/project/tests/base.py b/project/tests/base.py index 6ad9fc1..d0753c9 100644 --- a/project/tests/base.py +++ b/project/tests/base.py @@ -1,4 +1,4 @@ -# project/server/tests/base.py +# project/tests/base.py from flask_testing import TestCase @@ -9,9 +9,8 @@ class BaseTestCase(TestCase): - def create_app(self): - app.config.from_object('project.server.config.TestingConfig') + app.config.from_object("project.server.config.TestingConfig") return app def setUp(self): diff --git a/project/tests/helpers.py b/project/tests/helpers.py index 70af7a7..18b0079 100644 --- a/project/tests/helpers.py +++ b/project/tests/helpers.py @@ -1 +1 @@ -# tests/helpers.py +# project/tests/helpers.py diff --git a/project/tests/test__config.py b/project/tests/test__config.py index 85defa0..64a52b9 100644 --- a/project/tests/test__config.py +++ b/project/tests/test__config.py @@ -1,4 +1,4 @@ -# project/server/tests/test__config.py +# project/tests/test__config.py import unittest @@ -12,29 +12,27 @@ class TestDevelopmentConfig(TestCase): - def create_app(self): - app.config.from_object('project.server.config.DevelopmentConfig') + app.config.from_object("project.server.config.DevelopmentConfig") return app def test_app_is_development(self): - self.assertFalse(current_app.config['TESTING']) - self.assertTrue(app.config['DEBUG'] is True) - self.assertTrue(app.config['WTF_CSRF_ENABLED'] is False) + self.assertFalse(current_app.config["TESTING"]) + self.assertTrue(app.config["DEBUG"] is True) + self.assertTrue(app.config["WTF_CSRF_ENABLED"] is False) self.assertFalse(current_app is None) class TestTestingConfig(TestCase): - def create_app(self): - app.config.from_object('project.server.config.TestingConfig') + app.config.from_object("project.server.config.TestingConfig") return app def test_app_is_testing(self): - self.assertTrue(current_app.config['TESTING']) - self.assertTrue(app.config['DEBUG'] is True) - self.assertTrue(app.config['WTF_CSRF_ENABLED'] is False) + self.assertTrue(current_app.config["TESTING"]) + self.assertTrue(app.config["DEBUG"] is True) + self.assertTrue(app.config["WTF_CSRF_ENABLED"] is False) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/project/tests/test_main.py b/project/tests/test_main.py index 25acb97..dc866cd 100644 --- a/project/tests/test_main.py +++ b/project/tests/test_main.py @@ -1,4 +1,4 @@ -# project/server/tests/test_main.py +# project/tests/test_main.py import unittest @@ -7,12 +7,11 @@ class TestMainBlueprint(BaseTestCase): - def test_index(self): # Ensure Flask is setup. - response = self.client.get('/', follow_redirects=True) + response = self.client.get("/", follow_redirects=True) self.assertEqual(response.status_code, 200) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/requirements.txt b/requirements.txt index de510a5..c4e71f2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -Flask==1.0.2 +Flask==1.1.1 Flask-Bootstrap==3.3.7.1 Flask-Testing==0.7.1 Flask-WTF==0.14.2 -redis==2.10.6 -rq==0.12.0 +redis==3.3.11 +rq==1.1.0