diff --git a/.gitignore b/.gitignore index 3f3c5a3a..fbd1fc83 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ Makefile.custom.config -# MPWO_API +# API ############### __pycache__ uploads @@ -13,8 +13,9 @@ coverage.xml .pytest_cache .venv /fittrackee.egg-info/ +/dist -# MPWO_CLIENT +# CLIENT ############### # dependencies diff --git a/Makefile.config b/Makefile.config index a06b9b07..80bfcd34 100644 --- a/Makefile.config +++ b/Makefile.config @@ -3,12 +3,9 @@ API_PORT = 5000 CLIENT_PORT = 3000 export FLASK_APP = $(PWD)/fittrackee/server.py -export APP_SETTINGS=fittrackee.config.DevelopmentConfig export FLASK_ENV=development export TEST_APP_URL = http://$(HOST):$(API_PORT) export TEST_CLIENT_URL = http://$(HOST):$(CLIENT_PORT) -export DATABASE_URL = postgres://fittrackee:fittrackee@$(HOST):5432/fittrackee -export DATABASE_TEST_URL = postgres://fittrackee:fittrackee@$(HOST):5432/fittrackee_test export MIGRATIONS = $(PWD)/fittrackee/migrations # Python env diff --git a/Makefile.custom.config.example b/Makefile.custom.config.example index e936c562..c69d31cb 100644 --- a/Makefile.custom.config.example +++ b/Makefile.custom.config.example @@ -1,6 +1,10 @@ +# dramatiq (task queue) WORKERS_PROCESSES = 1 -export REACT_APP_API_URL= +export DATABASE_URL = postgres://fittrackee:fittrackee@$(HOST):5432/fittrackee +export DATABASE_TEST_URL = postgres://fittrackee:fittrackee@$(HOST):5432/fittrackee_test +export APP_SETTINGS=fittrackee.config.DevelopmentConfig +export DATABASE_DISABLE_POOLING=True export TILE_SERVER_URL= export MAP_ATTRIBUTION= export WEATHER_API= @@ -8,6 +12,7 @@ export UI_URL= export EMAIL_URL= export SENDER_EMAIL= export REDIS_URL= +export UPLOAD_FOLDER= # for dev env -export CODACY_PROJECT_TOKEN= +export REACT_APP_API_URL= diff --git a/fittrackee/__init__.py b/fittrackee/__init__.py index 94d6df2c..7a0d2cba 100644 --- a/fittrackee/__init__.py +++ b/fittrackee/__init__.py @@ -24,7 +24,9 @@ def create_app(): # set config with app.app_context(): - app_settings = os.getenv('APP_SETTINGS') + app_settings = os.getenv( + 'APP_SETTINGS', 'fittrackee.config.ProductionConfig' + ) if app_settings == 'fittrackee.config.TestingConfig': # reload config on tests config = import_module('fittrackee.config') diff --git a/fittrackee/__main__.py b/fittrackee/__main__.py new file mode 100644 index 00000000..4ce12a68 --- /dev/null +++ b/fittrackee/__main__.py @@ -0,0 +1,53 @@ +# source: http://docs.gunicorn.org/en/stable/custom.html +import os + +import gunicorn.app.base +from fittrackee import create_app +from fittrackee.database_utils import init_database +from flask_dramatiq import worker +from flask_migrate import upgrade + +HOST = os.getenv('HOST', '0.0.0.0') +PORT = os.getenv('API_PORT', '5000') +WORKERS = os.getenv('APP_WORKERS', 1) +BASEDIR = os.path.abspath(os.path.dirname(__file__)) +app = create_app() +dramatiq_worker = worker + + +class StandaloneApplication(gunicorn.app.base.BaseApplication): + def __init__(self, current_app, options=None): + self.options = options or {} + self.application = current_app + super().__init__() + + def load_config(self): + config = { + key: value + for key, value in self.options.items() + if key in self.cfg.settings and value is not None + } + for key, value in config.items(): + self.cfg.set(key.lower(), value) + + def load(self): + return self.application + + +def upgrade_db(): + with app.app_context(): + upgrade(directory=BASEDIR + '/migrations') + + +def init_data(): + with app.app_context(): + init_database(app) + + +def main(): + options = {'bind': f'{HOST}:{PORT}', 'workers': WORKERS} + StandaloneApplication(app, options).run() + + +if __name__ == '__main__': + main() diff --git a/fittrackee/config.py b/fittrackee/config.py index bcc77c60..d79e711e 100644 --- a/fittrackee/config.py +++ b/fittrackee/config.py @@ -3,6 +3,7 @@ import os from dramatiq.brokers.redis import RedisBroker from dramatiq.brokers.stub import StubBroker from flask import current_app +from sqlalchemy.pool import NullPool if os.getenv('APP_SETTINGS') == 'fittrackee.config.TestingConfig': broker = StubBroker @@ -20,7 +21,9 @@ class BaseConfig: TOKEN_EXPIRATION_DAYS = 30 TOKEN_EXPIRATION_SECONDS = 0 PASSWORD_TOKEN_EXPIRATION_SECONDS = 3600 - UPLOAD_FOLDER = os.path.join(current_app.root_path, 'uploads') + UPLOAD_FOLDER = os.path.join( + os.getenv('UPLOAD_FOLDER', current_app.root_path), 'uploads' + ) PICTURE_ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'} ACTIVITY_ALLOWED_EXTENSIONS = {'gpx', 'zip'} TEMPLATES_FOLDER = os.path.join(current_app.root_path, 'email/templates') @@ -68,3 +71,18 @@ class TestingConfig(BaseConfig): TOKEN_EXPIRATION_SECONDS = 3 PASSWORD_TOKEN_EXPIRATION_SECONDS = 3 UPLOAD_FOLDER = '/tmp/fitTrackee/uploads' + + +class ProductionConfig(BaseConfig): + """Production configuration""" + + DEBUG = False + # https://docs.sqlalchemy.org/en/13/core/pooling.html#using-connection-pools-with-multiprocessing-or-os-fork # noqa + SQLALCHEMY_ENGINE_OPTIONS = ( + {'poolclass': NullPool} + if os.getenv('DATABASE_DISABLE_POOLING', True) + else {} + ) + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') + SECRET_KEY = os.getenv('APP_SECRET_KEY') + DRAMATIQ_BROKER_URL = os.getenv('REDIS_URL', 'redis://') diff --git a/fittrackee/database_utils.py b/fittrackee/database_utils.py new file mode 100644 index 00000000..c58e377a --- /dev/null +++ b/fittrackee/database_utils.py @@ -0,0 +1,46 @@ +from fittrackee import db +from fittrackee.activities.models import Sport +from fittrackee.application.utils import ( + init_config, + update_app_config_from_database, +) +from fittrackee.users.models import User + + +def init_database(app): + """Init the database.""" + admin = User( + username='admin', email='admin@example.com', password='mpwoadmin' + ) + admin.admin = True + admin.timezone = 'Europe/Paris' + db.session.add(admin) + sport = Sport(label='Cycling (Sport)') + sport.img = '/img/sports/cycling-sport.png' + sport.is_default = True + db.session.add(sport) + sport = Sport(label='Cycling (Transport)') + sport.img = '/img/sports/cycling-transport.png' + sport.is_default = True + db.session.add(sport) + sport = Sport(label='Hiking') + sport.img = '/img/sports/hiking.png' + sport.is_default = True + db.session.add(sport) + sport = Sport(label='Mountain Biking') + sport.img = '/img/sports/mountain-biking.png' + sport.is_default = True + db.session.add(sport) + sport = Sport(label='Running') + sport.img = '/img/sports/running.png' + sport.is_default = True + db.session.add(sport) + sport = Sport(label='Walking') + sport.img = '/img/sports/walking.png' + sport.is_default = True + db.session.add(sport) + db.session.commit() + _, db_app_config = init_config() + update_app_config_from_database(app, db_app_config) + + print('Initial data stored in database.') diff --git a/fittrackee/server.py b/fittrackee/server.py index 66e595fb..af9d4dd3 100644 --- a/fittrackee/server.py +++ b/fittrackee/server.py @@ -1,15 +1,13 @@ import shutil from fittrackee import create_app, db -from fittrackee.activities.models import Activity, Sport +from fittrackee.activities.models import Activity from fittrackee.activities.utils import update_activity -from fittrackee.application.utils import ( - init_config, - update_app_config_from_database, -) -from fittrackee.users.models import User +from fittrackee.application.utils import init_config from tqdm import tqdm +from .database_utils import init_database + app = create_app() @@ -26,43 +24,8 @@ def drop_db(): @app.cli.command() def initdata(): - """Init the database.""" - admin = User( - username='admin', email='admin@example.com', password='mpwoadmin' - ) - admin.admin = True - admin.timezone = 'Europe/Paris' - db.session.add(admin) - sport = Sport(label='Cycling (Sport)') - sport.img = '/img/sports/cycling-sport.png' - sport.is_default = True - db.session.add(sport) - sport = Sport(label='Cycling (Transport)') - sport.img = '/img/sports/cycling-transport.png' - sport.is_default = True - db.session.add(sport) - sport = Sport(label='Hiking') - sport.img = '/img/sports/hiking.png' - sport.is_default = True - db.session.add(sport) - sport = Sport(label='Mountain Biking') - sport.img = '/img/sports/mountain-biking.png' - sport.is_default = True - db.session.add(sport) - sport = Sport(label='Running') - sport.img = '/img/sports/running.png' - sport.is_default = True - db.session.add(sport) - sport = Sport(label='Walking') - sport.img = '/img/sports/walking.png' - sport.is_default = True - db.session.add(sport) - db.session.commit() - # update app config - _, db_app_config = init_config() - update_app_config_from_database(app, db_app_config) - - print('Initial data stored in database.') + """Init the database and application config.""" + init_database(app) @app.cli.command() diff --git a/pyproject.toml b/pyproject.toml index 12664e6b..673ab8ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,24 @@ [tool.poetry] name = "fittrackee" version = "0.3.0-beta" -description = "" -authors = ["Your Name "] +description = "Self-hosted workout/activity tracker" +authors = ["SamR1"] license = "GPL-3.0" +readme = "README.md" +homepage = "https://github.com/SamR1/FitTrackee" +documentation = "https://samr1.github.io/FitTrackee" +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Framework :: Flask", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: JavaScript" +] +exclude = ["fittrackee/tests"] [tool.poetry.dependencies] python = "^3.7" @@ -38,8 +53,11 @@ pyopenssl = "^19.0" freezegun = "^1.0.0" pytest-selenium = "^2.0.0" -[tool.pytest] -norecursedirs = "fittrackee/.venv" +[tool.poetry.scripts] +fittrackee = 'fittrackee.__main__:main' +fittrackee_init_data = 'fittrackee.__main__:init_data' +fittrackee_upgrade_db = 'fittrackee.__main__:upgrade_db' +fittrackee_worker = 'fittrackee.__main__:dramatiq_worker' [tool.black] line-length = 79