diff --git a/Makefile b/Makefile index e8305c9d..c35f2162 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,9 @@ run-client: run-server: cd fittrackee_api && $(GUNICORN) -b 127.0.0.1:5000 "fittrackee_api:create_app()" --error-logfile ../gunicorn-error.log +run-workers: + $(FLASK) worker --processes=1 + serve-python: $(FLASK) run --with-threads -h $(HOST) -p $(API_PORT) diff --git a/fittrackee_api/fittrackee_api/__init__.py b/fittrackee_api/fittrackee_api/__init__.py index 7824bdd4..fbd68fd4 100644 --- a/fittrackee_api/fittrackee_api/__init__.py +++ b/fittrackee_api/fittrackee_api/__init__.py @@ -4,6 +4,7 @@ from importlib import import_module, reload from flask import Flask from flask_bcrypt import Bcrypt +from flask_dramatiq import Dramatiq from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy @@ -13,6 +14,7 @@ db = SQLAlchemy() bcrypt = Bcrypt() migrate = Migrate() email_service = Email() +dramatiq = Dramatiq() appLog = logging.getLogger('fittrackee_api') @@ -33,6 +35,7 @@ def create_app(): db.init_app(app) bcrypt.init_app(app) migrate.init_app(app, db) + dramatiq.init_app(app) # set up email email_service.init_email(app) diff --git a/fittrackee_api/fittrackee_api/config.py b/fittrackee_api/fittrackee_api/config.py index 9ed95d8a..7f04c29c 100644 --- a/fittrackee_api/fittrackee_api/config.py +++ b/fittrackee_api/fittrackee_api/config.py @@ -1,7 +1,15 @@ import os +from dramatiq.brokers.redis import RedisBroker +from dramatiq.brokers.stub import StubBroker from flask import current_app +if os.getenv('APP_SETTINGS') == 'fittrackee_api.config.Testing': + broker = StubBroker + broker.emit_after("process_boot") +else: + broker = RedisBroker + class BaseConfig: """Base configuration""" @@ -20,6 +28,7 @@ class BaseConfig: UI_URL = os.environ.get('UI_URL') EMAIL_URL = os.environ.get('EMAIL_URL') SENDER_EMAIL = os.environ.get('SENDER_EMAIL') + DRAMATIQ_BROKER = broker class DevelopmentConfig(BaseConfig): @@ -31,6 +40,7 @@ class DevelopmentConfig(BaseConfig): USERNAME = 'admin' PASSWORD = 'default' BCRYPT_LOG_ROUNDS = 4 + DRAMATIQ_BROKER_URL = os.getenv('REDIS_URL', 'redis://') class TestingConfig(BaseConfig): diff --git a/fittrackee_api/fittrackee_api/tasks.py b/fittrackee_api/fittrackee_api/tasks.py new file mode 100644 index 00000000..8ea0f363 --- /dev/null +++ b/fittrackee_api/fittrackee_api/tasks.py @@ -0,0 +1,11 @@ +from fittrackee_api import dramatiq, email_service + + +@dramatiq.actor() +def reset_password_email(user, email_data): + email_service.send( + template='password_reset_request', + lang=user['language'], + recipient=user['email'], + data=email_data, + ) diff --git a/fittrackee_api/fittrackee_api/tests/conftest.py b/fittrackee_api/fittrackee_api/tests/conftest.py index c870d6cf..12c55a9b 100644 --- a/fittrackee_api/fittrackee_api/tests/conftest.py +++ b/fittrackee_api/fittrackee_api/tests/conftest.py @@ -8,10 +8,10 @@ from fittrackee_api.application.models import AppConfig from fittrackee_api.application.utils import update_app_config_from_database from fittrackee_api.users.models import User -os.environ["FLASK_ENV"] = 'testing' -os.environ["APP_SETTINGS"] = 'fittrackee_api.config.TestingConfig' +os.environ['FLASK_ENV'] = 'testing' +os.environ['APP_SETTINGS'] = 'fittrackee_api.config.TestingConfig' # to avoid resetting dev database during tests -os.environ["DATABASE_URL"] = os.getenv("DATABASE_TEST_URL") +os.environ['DATABASE_URL'] = os.getenv('DATABASE_TEST_URL') def get_app_config(with_config=False): diff --git a/fittrackee_api/fittrackee_api/users/auth.py b/fittrackee_api/fittrackee_api/users/auth.py index 40ae317f..42cd6e98 100644 --- a/fittrackee_api/fittrackee_api/users/auth.py +++ b/fittrackee_api/fittrackee_api/users/auth.py @@ -2,7 +2,8 @@ import datetime import os import jwt -from fittrackee_api import appLog, bcrypt, db, email_service +from fittrackee_api import appLog, bcrypt, db +from fittrackee_api.tasks import reset_password_email from flask import Blueprint, current_app, jsonify, request from sqlalchemy import exc, or_ from werkzeug.exceptions import RequestEntityTooLarge @@ -712,12 +713,11 @@ def request_password_reset(): 'operating_system': request.user_agent.platform, 'browser_name': request.user_agent.browser, } - email_service.send( - template='password_reset_request', - lang=user.language if user.language else 'en', - recipient=user.email, - data=email_data, - ) + user_data = { + 'language': user.language if user.language else 'en', + 'email': user.email, + } + reset_password_email.send(user_data, email_data) response_object = { 'status': 'success', 'message': 'Password reset request processed.', diff --git a/fittrackee_api/poetry.lock b/fittrackee_api/poetry.lock index 1b15e4f4..417342a9 100644 --- a/fittrackee_api/poetry.lock +++ b/fittrackee_api/poetry.lock @@ -205,6 +205,29 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "0.16" +[[package]] +category = "main" +description = "Background Processing for Python 3." +name = "dramatiq" +optional = false +python-versions = ">=3.5" +version = "1.9.0" + +[package.dependencies] +prometheus-client = ">=0.2" + +[package.dependencies.redis] +optional = true +version = ">=2.0,<4.0" + +[package.extras] +all = ["watchdog-gevent (0.1)", "watchdog (>=0.8,<0.9)", "pylibmc (>=1.5,<2.0)", "pika (>=1.0,<2.0)", "redis (>=2.0,<4.0)"] +dev = ["watchdog-gevent (0.1)", "watchdog (>=0.8,<0.9)", "pylibmc (>=1.5,<2.0)", "pika (>=1.0,<2.0)", "redis (>=2.0,<4.0)", "alabaster", "sphinx (<1.8)", "sphinxcontrib-napoleon", "flake8", "flake8-bugbear", "flake8-quotes", "isort", "bumpversion", "hiredis", "twine", "wheel", "pytest (<4)", "pytest-benchmark", "pytest-cov", "tox"] +memcached = ["pylibmc (>=1.5,<2.0)"] +rabbitmq = ["pika (>=1.0,<2.0)"] +redis = ["redis (>=2.0,<4.0)"] +watch = ["watchdog (>=0.8,<0.9)", "watchdog-gevent (0.1)"] + [[package]] category = "dev" description = "the modular source code checker: pep8 pyflakes and co" @@ -253,6 +276,17 @@ version = "0.7.1" Flask = "*" bcrypt = "*" +[[package]] +category = "main" +description = "Adds Dramatiq support to your Flask application" +name = "flask-dramatiq" +optional = false +python-versions = ">=3.6,<4.0" +version = "0.6.0" + +[package.dependencies] +dramatiq = ">=1.5,<2.0" + [[package]] category = "main" description = "SQLAlchemy database migrations for Flask applications using Alembic" @@ -475,6 +509,17 @@ version = ">=0.12" [package.extras] dev = ["pre-commit", "tox"] +[[package]] +category = "main" +description = "Python client for the Prometheus monitoring system." +name = "prometheus-client" +optional = false +python-versions = "*" +version = "0.8.0" + +[package.extras] +twisted = ["twisted"] + [[package]] category = "main" description = "psycopg2 - Python-PostgreSQL Database Adapter" @@ -708,6 +753,17 @@ commonmark = ">=0.8.1" docutils = ">=0.11" sphinx = ">=1.3.1" +[[package]] +category = "main" +description = "Python client for Redis key-value store" +name = "redis" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "3.5.3" + +[package.extras] +hiredis = ["hiredis (>=0.1.3)"] + [[package]] category = "dev" description = "Alternative regular expression module, to replace re." @@ -997,7 +1053,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "a9201e432af36210daf0112aafdd4bc39ba5a7299f4010bf4919c85340ab40b9" +content-hash = "b8d219c99cac540afe974e9170fad8b4b0e1cd726148389edfcf29e2233cc121" python-versions = "^3.7" [metadata.files] @@ -1163,6 +1219,10 @@ docutils = [ {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, ] +dramatiq = [ + {file = "dramatiq-1.9.0-py3-none-any.whl", hash = "sha256:360cd436a434a513c87a9769943543c1d065835e3fa0b01f96c4fdd959bfa1c3"}, + {file = "dramatiq-1.9.0.tar.gz", hash = "sha256:8112941ab2eda4f0288bacd137a991f9b1b1c600fe3dd5960eaba4256c873839"}, +] flake8 = [ {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, @@ -1174,6 +1234,10 @@ flask = [ flask-bcrypt = [ {file = "Flask-Bcrypt-0.7.1.tar.gz", hash = "sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f"}, ] +flask-dramatiq = [ + {file = "flask-dramatiq-0.6.0.tar.gz", hash = "sha256:63709e73d7c8d2e5d9bc554d1e859d91c5c5c9a4ebc9461752655bf1e0b87420"}, + {file = "flask_dramatiq-0.6.0-py3-none-any.whl", hash = "sha256:7d4a9289721577f726183f7c44c6713a16bbdff54b946f27abc2ffcc65768adf"}, +] flask-migrate = [ {file = "Flask-Migrate-2.5.3.tar.gz", hash = "sha256:a69d508c2e09d289f6e55a417b3b8c7bfe70e640f53d2d9deb0d056a384f37ee"}, {file = "Flask_Migrate-2.5.3-py2.py3-none-any.whl", hash = "sha256:4dc4a5cce8cbbb06b8dc963fd86cf8136bd7d875aabe2d840302ea739b243732"}, @@ -1308,6 +1372,10 @@ pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] +prometheus-client = [ + {file = "prometheus_client-0.8.0-py2.py3-none-any.whl", hash = "sha256:983c7ac4b47478720db338f1491ef67a100b474e3bc7dafcbaefb7d0b8f9b01c"}, + {file = "prometheus_client-0.8.0.tar.gz", hash = "sha256:c6e6b706833a6bd1fd51711299edee907857be10ece535126a158f911ee80915"}, +] psycopg2-binary = [ {file = "psycopg2-binary-2.8.5.tar.gz", hash = "sha256:ccdc6a87f32b491129ada4b87a43b1895cf2c20fdb7f98ad979647506ffc41b6"}, {file = "psycopg2_binary-2.8.5-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:96d3038f5bd061401996614f65d27a4ecb62d843eb4f48e212e6d129171a721f"}, @@ -1417,6 +1485,10 @@ recommonmark = [ {file = "recommonmark-0.6.0-py2.py3-none-any.whl", hash = "sha256:2ec4207a574289355d5b6ae4ae4abb29043346ca12cdd5f07d374dc5987d2852"}, {file = "recommonmark-0.6.0.tar.gz", hash = "sha256:29cd4faeb6c5268c633634f2d69aef9431e0f4d347f90659fd0aab20e541efeb"}, ] +redis = [ + {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, + {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, +] regex = [ {file = "regex-2020.6.8-cp27-cp27m-win32.whl", hash = "sha256:fbff901c54c22425a5b809b914a3bfaf4b9570eee0e5ce8186ac71eb2025191c"}, {file = "regex-2020.6.8-cp27-cp27m-win_amd64.whl", hash = "sha256:112e34adf95e45158c597feea65d06a8124898bdeac975c9087fe71b572bd938"}, diff --git a/fittrackee_api/pyproject.toml b/fittrackee_api/pyproject.toml index 2833136e..92dbb8d2 100644 --- a/fittrackee_api/pyproject.toml +++ b/fittrackee_api/pyproject.toml @@ -19,6 +19,8 @@ python-forecastio = "^1.4" gunicorn = "^20.0" tqdm = "^4.42" humanize = "^2.5.0" +dramatiq = {extras = ["redis"], version = "^1.9.0"} +flask-dramatiq = "^0.6.0" [tool.poetry.dev-dependencies] black = "^19.10b0"