├── core ├── api │ ├── common │ │ ├── __init__.py │ │ ├── middleware │ │ │ ├── __init__.py │ │ │ ├── response.py │ │ │ └── request.py │ │ ├── exceptions.py │ │ ├── database.py │ │ ├── serializers.py │ │ └── validation.py │ ├── foss │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── test_unit.py │ │ │ ├── test_integration.py │ │ │ └── test_models.py │ │ ├── __init__.py │ │ ├── models.py │ │ ├── views.py │ │ ├── backend.py │ │ └── domain.py │ ├── specifications │ │ └── schemas │ │ │ └── foss │ │ │ ├── update_foss.json │ │ │ └── create_foss.json │ ├── __init__.py │ └── conftest.py ├── migrations │ ├── __init__.py │ ├── README │ ├── script.py.mako │ ├── alembic.ini │ └── env.py ├── requirements.txt ├── Dockerfile └── run.py ├── nginx ├── Dockerfile └── sites-enabled │ └── nginx.conf ├── .travis.yml ├── docker-compose.yml ├── .gitignore ├── Makefile └── README.md /core/api/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/api/foss/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/api/foss/tests/test_unit.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /core/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /core/api/foss/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | 4 | BP_NAME = 'foss' 5 | bp = Blueprint(BP_NAME, __name__) 6 | 7 | from . import views 8 | -------------------------------------------------------------------------------- /nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | RUN rm /etc/nginx/conf.d/default.conf 4 | 5 | ADD sites-enabled/nginx.conf /etc/nginx/conf.d/default.conf 6 | -------------------------------------------------------------------------------- /core/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.8.8 2 | Flask==1.0.2 3 | gunicorn==19.6.0 4 | jsonschema==2.5.1 5 | psycopg2==2.6.2 6 | pytest==3.0.3 7 | SQLAlchemy==1.3.3 8 | SQLAlchemy-Utils==0.32.9 9 | -------------------------------------------------------------------------------- /core/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.5 2 | MAINTAINER @dimmg 3 | 4 | ENV APP_PATH /usr/src/app 5 | 6 | RUN mkdir -p $APP_PATH 7 | WORKDIR $APP_PATH/core 8 | 9 | COPY requirements.txt requirements.txt 10 | RUN pip install -r requirements.txt 11 | 12 | COPY . . 13 | -------------------------------------------------------------------------------- /nginx/sites-enabled/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; 4 | 5 | underscores_in_headers on; 6 | 7 | location / { 8 | proxy_pass http://api:5000; 9 | proxy_set_header Host $host; 10 | proxy_set_header X-Real-IP $remote_addr; 11 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/api/foss/tests/test_integration.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def test_create_foss(test_client, custom_headers): 5 | payload = { 6 | 'name': 'foo', 7 | 'email': 'bar@foo.com' 8 | } 9 | r = test_client.post('/foss', data=json.dumps(payload), 10 | headers=custom_headers) 11 | 12 | assert r.status_code == 200 13 | -------------------------------------------------------------------------------- /core/api/foss/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | 3 | from ..common.database import BaseModel 4 | from ..common.serializers import ModelSerializerMixin 5 | 6 | 7 | class Foss(BaseModel, ModelSerializerMixin): 8 | id = Column(Integer, primary_key=True, autoincrement=True) 9 | name = Column(String) 10 | email = Column(String, nullable=False, unique=True) 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: 2 | - required 3 | 4 | language: 5 | - python 6 | 7 | services: 8 | - postgresql 9 | 10 | python: 11 | - 3.5 12 | 13 | install: 14 | - pip install -r core/requirements.txt 15 | 16 | before_script: 17 | - chown -R 1000:1000 . 18 | - find . -name .cache -prune -exec rm -rf {} + 19 | - psql -c 'CREATE DATABASE "flusk-test";' -U postgres 20 | 21 | script: pytest -s 22 | -------------------------------------------------------------------------------- /core/api/specifications/schemas/foss/update_foss.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Update FOSS", 3 | "description": "Update a FOSS object", 4 | "type":"object", 5 | "$schema": "http://json-schema.org/draft-04/schema", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "minLength": 2 10 | }, 11 | "email": { 12 | "type": "string", 13 | "format": "email", 14 | "minLength": 3 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /core/run.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from werkzeug.contrib.fixers import ProxyFix 4 | 5 | from api import create_app 6 | 7 | 8 | app = create_app() 9 | 10 | def run(): 11 | debug = os.environ.get('APP_DEBUG', True) 12 | host = os.environ.get('APP_HOST', '0.0.0.0') 13 | port = int(os.environ.get('APP_PORT', 5000)) 14 | 15 | app.run(debug=debug, host=host, port=port) 16 | 17 | 18 | wsgi = ProxyFix(app.wsgi_app) 19 | 20 | 21 | if __name__ == '__main__': 22 | run() 23 | -------------------------------------------------------------------------------- /core/api/specifications/schemas/foss/create_foss.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Create FOSS", 3 | "description": "Create a FOSS object", 4 | "type":"object", 5 | "$schema": "http://json-schema.org/draft-04/schema", 6 | "required": [ 7 | "name", 8 | "email" 9 | ], 10 | "properties": { 11 | "name": { 12 | "type": "string", 13 | "minLength": 2 14 | }, 15 | "email": { 16 | "type": "string", 17 | "format": "email", 18 | "minLength": 3 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/api/foss/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from ..models import Foss 2 | 3 | 4 | def test_create_foss_implicitly(): 5 | payload = { 6 | 'name': 'foss', 7 | 'email': 'foss@example.com' 8 | } 9 | 10 | foss = Foss(**payload) 11 | foss.save() 12 | 13 | assert foss.email == payload['email'] 14 | 15 | 16 | def test_create_foss_with_session(session): 17 | payload = { 18 | 'name': 'bar', 19 | 'email': 'bar@example.com' 20 | } 21 | 22 | foss = Foss(**payload) 23 | session.add(foss) 24 | session.flush() 25 | 26 | assert foss.name == payload['name'] 27 | -------------------------------------------------------------------------------- /core/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | def upgrade(): 19 | ${upgrades if upgrades else "pass"} 20 | 21 | 22 | def downgrade(): 23 | ${downgrades if downgrades else "pass"} 24 | -------------------------------------------------------------------------------- /core/api/common/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | from . import request 2 | from . import response 3 | 4 | 5 | def before_request_middleware(app): 6 | app.before_request_funcs.setdefault(None, [ 7 | request.ensure_content_type, 8 | request.ensure_public_unavailability, 9 | ]) 10 | 11 | 12 | def after_request_middleware(app): 13 | app.after_request_funcs.setdefault(None, [ 14 | request.enable_cors, 15 | request.commit_session, 16 | ]) 17 | 18 | 19 | def teardown_appcontext_middleware (app): 20 | app.teardown_appcontext_funcs = [ 21 | request.shutdown_session, 22 | ] 23 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | api: 2 | restart: always 3 | build: ./core 4 | expose: 5 | - "5000" 6 | volumes: 7 | - .:/usr/src/app 8 | links: 9 | - postgres:postgres 10 | env_file: ./core/.env 11 | command: 12 | # gunicorn -w 1 -b 0.0.0.0:5000 run:wsgi 13 | tail -f /dev/null 14 | 15 | nginx: 16 | restart: always 17 | build: ./nginx 18 | ports: 19 | - "80:80" 20 | volumes_from: 21 | - api 22 | links: 23 | - api:api 24 | 25 | data: 26 | restart: "no" 27 | image: postgres:latest 28 | volumes: 29 | - /var/lib/postgresql 30 | command: "true" 31 | 32 | postgres: 33 | restart: always 34 | image: postgres:latest 35 | volumes_from: 36 | - data 37 | ports: 38 | - "5432:5432" 39 | -------------------------------------------------------------------------------- /core/api/foss/views.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | 3 | from ..common.validation import schema 4 | 5 | from . import bp 6 | from . import domain 7 | 8 | 9 | @bp.route('/foss', methods=['POST']) 10 | @schema('create_foss.json') 11 | def create_foss(): 12 | return domain.create_foss(request.json) 13 | 14 | 15 | @bp.route('/foss', methods=['GET']) 16 | def get_fosses(): 17 | return domain.get_all_fosses() 18 | 19 | 20 | @bp.route('/foss/', methods=['GET']) 21 | def get_foss(foss_id): 22 | return domain.get_foss_by_id(foss_id) 23 | 24 | 25 | @bp.route('/foss/', methods=['PUT']) 26 | @schema('/update_foss.json') 27 | def update_foss(foss_id): 28 | return domain.update_foss(request.json, foss_id) 29 | 30 | 31 | @bp.route('/foss/', methods=['DELETE']) 32 | def delete_foss(foss_id): 33 | domain.delete_foss(foss_id) 34 | 35 | return { 36 | 'message': 'Foss with `id: %s` has been deleted.' % foss_id 37 | } 38 | -------------------------------------------------------------------------------- /core/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | from .common.database import init_db 4 | from .common.middleware import after_request_middleware, before_request_middleware, teardown_appcontext_middleware 5 | from .common.middleware import response 6 | from .foss import bp as foss_bp 7 | 8 | 9 | def create_app(): 10 | # initialize flask application 11 | app = Flask(__name__) 12 | 13 | # register all blueprints 14 | app.register_blueprint(foss_bp) 15 | 16 | # register custom response class 17 | app.response_class = response.JSONResponse 18 | 19 | # register before request middleware 20 | before_request_middleware(app=app) 21 | 22 | # register after request middleware 23 | after_request_middleware(app=app) 24 | 25 | # register after app context teardown middleware 26 | teardown_appcontext_middleware(app=app) 27 | 28 | # register custom error handler 29 | response.json_error_handler(app=app) 30 | 31 | # initialize the database 32 | init_db() 33 | 34 | return app 35 | -------------------------------------------------------------------------------- /core/api/foss/backend.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.exc import IntegrityError 2 | from sqlalchemy.orm.exc import NoResultFound 3 | 4 | from ..common.exceptions import RecordAlreadyExists, RecordNotFound 5 | 6 | from .models import Foss 7 | 8 | 9 | def create_foss(foss_data): 10 | foss = Foss(**foss_data) 11 | try: 12 | foss.save() 13 | except IntegrityError: 14 | msg = 'Email `%s` already has been taken' % foss_data['email'] 15 | raise RecordAlreadyExists(message=msg) 16 | 17 | return foss 18 | 19 | 20 | def get_foss_by_id(foss_id): 21 | try: 22 | result = Foss.query.filter(Foss.id == foss_id).one() 23 | except NoResultFound: 24 | msg = 'There is no Foss with `id: %s`' % id 25 | raise RecordNotFound(message=msg) 26 | 27 | return result 28 | 29 | 30 | def get_all_fosses(): 31 | return Foss.query.all() 32 | 33 | 34 | def update_foss(foss_data, foss_id): 35 | foss = get_foss_by_id(foss_id) 36 | foss.update(**foss_data) 37 | 38 | return foss 39 | 40 | 41 | def delete_foss(foss_id): 42 | foss = get_foss_by_id(foss_id) 43 | foss.delete() 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # Jupyter Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | .venv/ 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | -------------------------------------------------------------------------------- /core/api/foss/domain.py: -------------------------------------------------------------------------------- 1 | from . import backend 2 | 3 | 4 | def create_foss(foss_data): 5 | """Create Foss. 6 | :param foss_data: Foss information 7 | :type foss_data: dict 8 | :returns: serialized Foss object 9 | :rtype: dict 10 | """ 11 | foss = backend.create_foss(foss_data) 12 | 13 | return foss.to_dict() 14 | 15 | 16 | def get_foss_by_id(foss_id): 17 | """Get Foss by id. 18 | :param foss_id: id of the foss to be retrived 19 | :type foss_id: integer 20 | :returns: serialized Foss object 21 | :rtype: dict 22 | """ 23 | foss = backend.get_foss_by_id(foss_id) 24 | 25 | return foss.to_dict() 26 | 27 | 28 | def get_all_fosses(): 29 | """Get all Fosses. 30 | :returns: serialized Foss objects 31 | :rtype: list 32 | """ 33 | fosses = backend.get_all_fosses() 34 | return [ 35 | foss.to_dict() for foss in fosses 36 | ] 37 | 38 | 39 | def update_foss(foss_data, foss_id): 40 | """Update Foss. 41 | :param foss_data: Foss information 42 | :type foss_data: dict 43 | :param foss_id: id of the Foss to be updated 44 | :type foss_id: integer 45 | :returns: serialized Foss object 46 | :rtype: dict 47 | """ 48 | foss = backend.update_foss(foss_data, foss_id) 49 | 50 | return foss.to_dict() 51 | 52 | 53 | def delete_foss(foss_id): 54 | """Delete Foss. 55 | :param foss_id: id of the Foss to be deleted 56 | :type foss_id: integer 57 | """ 58 | backend.delete_foss(foss_id) 59 | -------------------------------------------------------------------------------- /core/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = . 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # max length of characters to apply to the 11 | # "slug" field 12 | #truncate_slug_length = 40 13 | 14 | # set to 'true' to run the environment during 15 | # the 'revision' command, regardless of autogenerate 16 | # revision_environment = false 17 | 18 | # set to 'true' to allow .pyc and .pyo files without 19 | # a source .py file to be detected as revisions in the 20 | # versions/ directory 21 | # sourceless = false 22 | 23 | # version location specification; this defaults 24 | # to migrations/versions. When using multiple version 25 | # directories, initial revisions must be specified with --version-path 26 | # version_locations = %(here)s/bar %(here)s/bat migrations/versions 27 | 28 | # the output encoding used when revision files 29 | # are written from script.py.mako 30 | # output_encoding = utf-8 31 | 32 | 33 | # Logging configuration 34 | [loggers] 35 | keys = root,sqlalchemy,alembic 36 | 37 | [handlers] 38 | keys = console 39 | 40 | [formatters] 41 | keys = generic 42 | 43 | [logger_root] 44 | level = WARN 45 | handlers = console 46 | qualname = 47 | 48 | [logger_sqlalchemy] 49 | level = WARN 50 | handlers = 51 | qualname = sqlalchemy.engine 52 | 53 | [logger_alembic] 54 | level = INFO 55 | handlers = 56 | qualname = alembic 57 | 58 | [handler_console] 59 | class = StreamHandler 60 | args = (sys.stderr,) 61 | level = NOTSET 62 | formatter = generic 63 | 64 | [formatter_generic] 65 | format = %(levelname)-5.5s [%(name)s] %(message)s 66 | datefmt = %H:%M:%S 67 | -------------------------------------------------------------------------------- /core/api/common/exceptions.py: -------------------------------------------------------------------------------- 1 | from werkzeug.exceptions import Conflict, NotFound, Unauthorized 2 | 3 | 4 | class JSONException(Exception): 5 | """Custom JSON based exception. 6 | 7 | :param status_code: response status_code 8 | :param message: exception message 9 | """ 10 | status_code = NotFound.code 11 | message = '' 12 | 13 | def __init__(self, message=None, status_code=None): 14 | Exception.__init__(self) 15 | if message is not None: 16 | self.message = message 17 | if status_code is not None: 18 | self.status_code = status_code 19 | 20 | def to_dict(self): 21 | return { 22 | 'error': { 23 | 'code': self.status_code, 24 | 'message': self.message, 25 | 'type': str(self.__class__.__name__) 26 | } 27 | } 28 | 29 | 30 | class InvalidContentType(JSONException): 31 | """ 32 | Raised when an invalid Content-Type is provided. 33 | """ 34 | pass 35 | 36 | 37 | class InvalidPermissions(JSONException): 38 | status_code = Unauthorized.code 39 | 40 | 41 | class InvalidAPIRequest(JSONException): 42 | """ 43 | Raised when an invalid request has been made. 44 | (e.g. accessed unexisting url, the schema validation did 45 | not pass) 46 | """ 47 | pass 48 | 49 | 50 | class DatabaseError(JSONException): 51 | """ 52 | Generic database interaction error. 53 | Inherit this error for all subsequent 54 | errors that are related to database. 55 | """ 56 | pass 57 | 58 | 59 | class RecordNotFound(DatabaseError): 60 | """ 61 | Raised when the record was not found in the database. 62 | """ 63 | pass 64 | 65 | 66 | class RecordAlreadyExists(DatabaseError): 67 | """ 68 | Raised in the case of violation of a unique constraint. 69 | """ 70 | status_code = Conflict.code 71 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ENV_FILE = $(shell pwd)$(shell echo '/core/.env') 2 | 3 | include $(ENV_FILE) 4 | export $(shell sed 's/=.*//' `pwd`/core/.env) 5 | 6 | _DEVELOPMENT_DATABASE_URI=$(DEVELOPMENT_DATABASE_URI) 7 | _TEST_DATABASE_URI=$(TEST_DATABASE_URI) 8 | 9 | .PHONY: clear run-server run-tests help dcompose-start dcompose-start dcompose-stop dcleanup 10 | 11 | clear: 12 | @find . -name __pycache__ -prune -exec rm -rf {} + 13 | @find . -name "*.pyc" -prune -exec rm -rf {} + 14 | @find . -name .cache -prune -exec rm -rf {} + 15 | 16 | db-revision: 17 | @(\ 18 | cd core/migrations && alembic revision --autogenerate -m "$(msg)"; \ 19 | ) 20 | 21 | db-upgrade: 22 | $(shell cd core/migrations && alembic upgrade head) 23 | 24 | db-upgrade-sql: 25 | @(\ 26 | cd core/migrations && alembic upgrade head --sql; \ 27 | ) 28 | 29 | dcompose-start: 30 | @docker-compose stop; 31 | @docker-compose build; 32 | @docker-compose up -d; 33 | 34 | dcompose-restart: 35 | @docker-compose stop; 36 | @docker-compose build; 37 | @docker-compose up -d; 38 | 39 | dcompose-stop: 40 | @docker-compose stop 41 | 42 | dcleanup: 43 | @docker rm $(shell docker ps -qa --no-trunc --filter "status=exited") 44 | @docker rmi $(shell docker images --filter "dangling=true" -q --no-trunc) 45 | 46 | run-server: 47 | @(\ 48 | export SQLALCHEMY_DATABASE_URI=$(_DEVELOPMENT_DATABASE_URI); \ 49 | python ./core/run.py; \ 50 | ) 51 | 52 | run-tests: clear 53 | @(\ 54 | export SQLALCHEMY_DATABASE_URI=$(_TEST_DATABASE_URI); \ 55 | pytest -s; \ 56 | ) 57 | 58 | help: 59 | @echo 'dcompose-start:' 60 | @echo ' Build and start containers' 61 | @echo 'dcompose-stop:' 62 | @echo ' Stop running containers' 63 | @echo 'dcleanup:' 64 | @echo ' Remove docker containers with status `exited`' 65 | @echo ' Remove unused docker images' 66 | @echo 'run-server:' 67 | @echo ' Start application server' 68 | @echo 'run-tests:' 69 | @echo ' Run tests' 70 | @echo 'clear': 71 | @echo ' Remove *.pyc files, __pycache__ and .cache folders' 72 | 73 | -------------------------------------------------------------------------------- /core/api/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from api import create_app 6 | from api.common.database import db_session, init_db, drop_db 7 | 8 | HEADERS = { 9 | 'content-type': 'application/json', 10 | '_secure_key': os.environ.get('SECURE_API_KEY') 11 | } 12 | 13 | 14 | @pytest.fixture(scope='session') 15 | def app(request): 16 | """ 17 | Create new application. 18 | Establish a context so all application parts 19 | are properly functioning. 20 | """ 21 | app = create_app() 22 | 23 | ctx = app.app_context() 24 | ctx.push() 25 | 26 | def teardown(): 27 | ctx.pop() 28 | 29 | request.addfinalizer(teardown) 30 | 31 | return app 32 | 33 | 34 | @pytest.fixture(scope='session') 35 | def test_client(app, request): 36 | """ 37 | Init flask's test client. 38 | """ 39 | client = app.test_client() 40 | client.__enter__() 41 | 42 | request.addfinalizer( 43 | lambda: client.__exit__(None, None, None) 44 | ) 45 | 46 | return client 47 | 48 | 49 | @pytest.fixture(scope='session') 50 | def session(request): 51 | def teardown(): 52 | """ 53 | For testing purposes the `flush` is sufficient. 54 | Flask middleware is not available in test environment, 55 | therefore records are not comitted to the database. 56 | In order to override this behaviour, adapt the 57 | `commit_session` middleware here. 58 | """ 59 | db_session.remove() 60 | 61 | request.addfinalizer(teardown) 62 | 63 | return db_session 64 | 65 | 66 | @pytest.fixture(scope='session') 67 | def custom_headers(): 68 | return HEADERS 69 | 70 | 71 | @pytest.fixture(scope="session", autouse=True) 72 | def database_management(request): 73 | """ 74 | Create database before running the first test. 75 | Drop the database after running the last test. 76 | """ 77 | init_db() 78 | 79 | def teardown(): 80 | db_session.close() 81 | db_session.remove() 82 | drop_db() 83 | 84 | request.addfinalizer(teardown) 85 | -------------------------------------------------------------------------------- /core/api/common/middleware/response.py: -------------------------------------------------------------------------------- 1 | import json 2 | from functools import singledispatch 3 | 4 | from flask import jsonify, Response, request 5 | from werkzeug.exceptions import NotFound 6 | 7 | from ..exceptions import JSONException, InvalidAPIRequest 8 | 9 | 10 | @singledispatch 11 | def to_serializable(rv): 12 | """ 13 | Define a generic serializable function. 14 | """ 15 | pass 16 | 17 | 18 | @to_serializable.register(dict) 19 | def ts_dict(rv): 20 | """Register the `dict` type 21 | for the generic serializable function. 22 | :param rv: object to be serialized 23 | :type rv: dict 24 | :returns: flask Response object 25 | """ 26 | return jsonify(rv) 27 | 28 | 29 | @to_serializable.register(list) 30 | def ts_list(rv): 31 | """Register the `list` type 32 | for the generic serializable function. 33 | :param rv: objects to be serialized 34 | :type rv: list 35 | :returns: flask Response object 36 | """ 37 | return Response(json.dumps(rv, indent=4, sort_keys=True)) 38 | 39 | 40 | class JSONResponse(Response): 41 | """ 42 | Custom `Response` class that will be 43 | used as the default one for the application. 44 | All responses will be of type 45 | `application-json`. 46 | """ 47 | @classmethod 48 | def force_type(cls, rv, environ=None): 49 | rv = to_serializable(rv) 50 | return super(JSONResponse, cls).force_type(rv, environ) 51 | 52 | 53 | def json_error_handler(app): 54 | @app.errorhandler(JSONException) 55 | def handle_invalid_usage(error): 56 | """ 57 | Custom `Exception` class that will be 58 | used as the default one for the application. 59 | Returns pretty formatted JSON error 60 | with detailed information. 61 | 62 | :message: error message 63 | :status_code: response status code 64 | :type: error type 65 | """ 66 | response = jsonify(error.to_dict()) 67 | response.status_code = error.status_code 68 | return response 69 | 70 | 71 | @app.errorhandler(NotFound.code) 72 | def resource_not_found(error): 73 | """ 74 | Custom `errorhandler` for 404 pages. 75 | Returns a JSON object with a message 76 | that accessed URL was not found. 77 | """ 78 | msg = 'The requested URL `%s` was not found on the server.' % request.path 79 | response = jsonify(InvalidAPIRequest(message=msg).to_dict()) 80 | response.status_code = NotFound.code 81 | return response 82 | -------------------------------------------------------------------------------- /core/api/common/middleware/request.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import current_app, request 4 | from sqlalchemy.exc import DatabaseError 5 | 6 | from ..database import db_session 7 | from ..exceptions import InvalidContentType, InvalidPermissions 8 | 9 | 10 | def ensure_content_type(): 11 | """ 12 | Ensures that the Content-Type for all requests 13 | is `application-json`, otherwise appropriate error 14 | is raised. 15 | :raises: InvalidContentType if Content-Type is not `application-json` 16 | """ 17 | content_type = request.headers.get('Content-type') 18 | if not content_type == 'application/json': 19 | raise InvalidContentType( 20 | message='Invalid content-type. Only `application-json` is allowed.' 21 | ) 22 | 23 | 24 | def ensure_public_unavailability(): 25 | if not request.headers.get('_secure_key', '') == os.environ.get('SECURE_API_KEY'): 26 | raise InvalidPermissions( 27 | message='You don\'t have enough permissions to perform this action.' 28 | ) 29 | 30 | 31 | ACL_ORIGIN = 'Access-Control-Allow-Origin' 32 | ACL_METHODS = 'Access-Control-Allow-Methods' 33 | ACL_ALLOWED_HEADERS = 'Access-Control-Allow-Headers' 34 | 35 | OPTIONS_METHOD = 'OPTIONS' 36 | ALLOWED_ORIGINS = 'http://localhost:8080' 37 | ALLOWED_METHODS = 'GET, POST, PUT, DELETE, OPTIONS' 38 | ALLOWED_HEADERS = 'Authorization, DNT, X-CustomHeader, Keep-Alive, User-Agent, ' \ 39 | 'X-Requested-With, If-Modified-Since, Cache-Control, Content-Type' 40 | 41 | 42 | def enable_cors(response): 43 | """ 44 | Enable Cross-origin resource sharing. 45 | These headers are needed for the clients that 46 | will consume the API via AJAX requests. 47 | """ 48 | if request.method == OPTIONS_METHOD: 49 | response = current_app.make_default_options_response() 50 | response.headers[ACL_ORIGIN] = ALLOWED_ORIGINS 51 | response.headers[ACL_METHODS] = ALLOWED_METHODS 52 | response.headers[ACL_ALLOWED_HEADERS] = ALLOWED_HEADERS 53 | 54 | return response 55 | 56 | 57 | def commit_session(response): 58 | """ 59 | Try to commit the db session in the case 60 | of a successful request with status_code 61 | under 400. 62 | """ 63 | if response.status_code >= 400: 64 | return response 65 | try: 66 | db_session.commit() 67 | except DatabaseError: 68 | db_session.rollback() 69 | return response 70 | 71 | 72 | def shutdown_session(exception=None): 73 | """ 74 | Remove the db session and detach from the 75 | database driver after application shutdown. 76 | """ 77 | db_session.remove() 78 | -------------------------------------------------------------------------------- /core/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | import os 3 | import sys 4 | 5 | from alembic import context 6 | from sqlalchemy import engine_from_config, pool 7 | from logging.config import fileConfig 8 | 9 | sys.path.append(os.path.join(os.path.dirname(__file__), '..')) 10 | 11 | from api.common.database import BaseModel 12 | 13 | 14 | # this is the Alembic Config object, which provides 15 | # access to the values within the .ini file in use. 16 | config = context.config 17 | 18 | # Interpret the config file for Python logging. 19 | # This line sets up loggers basically. 20 | fileConfig(config.config_file_name) 21 | 22 | # add your model's MetaData object here 23 | # for 'autogenerate' support 24 | # from myapp import mymodel 25 | # target_metadata = mymodel.Base.metadata 26 | target_metadata = BaseModel.metadata 27 | 28 | # other values from the config, defined by the needs of env.py, 29 | # can be acquired: 30 | # my_important_option = config.get_main_option("my_important_option") 31 | # ... etc. 32 | 33 | 34 | # override the default `sqlalchemy.url` from the .ini file 35 | config.set_main_option('sqlalchemy.url', os.environ.get('SQLALCHEMY_DATABASE_URI')) 36 | 37 | def run_migrations_offline(): 38 | """Run migrations in 'offline' mode. 39 | 40 | This configures the context with just a URL 41 | and not an Engine, though an Engine is acceptable 42 | here as well. By skipping the Engine creation 43 | we don't even need a DBAPI to be available. 44 | 45 | Calls to context.execute() here emit the given string to the 46 | script output. 47 | 48 | """ 49 | url = config.get_main_option('sqlalchemy.url') 50 | context.configure( 51 | url=url, 52 | target_metadata=target_metadata, 53 | literal_binds=True, 54 | compare_type=True, 55 | compare_server_default=True) 56 | 57 | with context.begin_transaction(): 58 | context.run_migrations() 59 | 60 | 61 | def run_migrations_online(): 62 | """Run migrations in 'online' mode. 63 | 64 | In this scenario we need to create an Engine 65 | and associate a connection with the context. 66 | 67 | """ 68 | connectable = engine_from_config( 69 | config.get_section(config.config_ini_section), 70 | prefix='sqlalchemy.', 71 | poolclass=pool.NullPool) 72 | 73 | with connectable.connect() as connection: 74 | context.configure( 75 | connection=connection, 76 | target_metadata=target_metadata 77 | ) 78 | 79 | with context.begin_transaction(): 80 | context.run_migrations() 81 | 82 | if context.is_offline_mode(): 83 | run_migrations_offline() 84 | else: 85 | run_migrations_online() 86 | -------------------------------------------------------------------------------- /core/api/common/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.exc import DatabaseError 5 | from sqlalchemy.ext.declarative import declarative_base, declared_attr 6 | from sqlalchemy.orm import scoped_session, sessionmaker 7 | from sqlalchemy_utils import database_exists, create_database, drop_database 8 | 9 | 10 | engine = create_engine(os.environ.get( 11 | 'SQLALCHEMY_DATABASE_URI'), convert_unicode=True) 12 | db_session = scoped_session(sessionmaker(autocommit=False, 13 | autoflush=False, 14 | bind=engine)) 15 | 16 | 17 | class CustomBase(object): 18 | """This overrides the default 19 | `_declarative_constructor` constructor. 20 | It skips the attributes that are not present 21 | for the model, thus if a dict is passed with some 22 | unknown attributes for the model on creation, 23 | it won't complain for `unkwnown field`s. 24 | """ 25 | def __init__(self, **kwargs): 26 | cls_ = type(self) 27 | for k in kwargs: 28 | if hasattr(cls_, k): 29 | setattr(self, k, kwargs[k]) 30 | else: 31 | continue 32 | 33 | """ 34 | Set default tablename 35 | """ 36 | @declared_attr 37 | def __tablename__(cls): 38 | return cls.__name__.lower() 39 | 40 | """ 41 | Add and try to flush. 42 | """ 43 | def save(self): 44 | db_session.add(self) 45 | self._flush() 46 | return self 47 | 48 | """ 49 | Update and try to flush. 50 | """ 51 | def update(self, **kwargs): 52 | for attr, value in kwargs.items(): 53 | if hasattr(self, attr): 54 | setattr(self, attr, value) 55 | return self.save() 56 | 57 | """ 58 | Delete and try to flush. 59 | """ 60 | def delete(self): 61 | db_session.delete(self) 62 | self._flush() 63 | 64 | """ 65 | Try to flush. If an error is raised, 66 | the session is rollbacked. 67 | """ 68 | def _flush(self): 69 | try: 70 | db_session.flush() 71 | except DatabaseError: 72 | db_session.rollback() 73 | 74 | 75 | BaseModel = declarative_base(cls=CustomBase, constructor=None) 76 | BaseModel.query = db_session.query_property() 77 | 78 | 79 | def init_db(): 80 | """ 81 | Create database if doesn't exist and 82 | create all tables. 83 | """ 84 | if not database_exists(engine.url): 85 | create_database(engine.url) 86 | BaseModel.metadata.create_all(bind=engine) 87 | 88 | 89 | def drop_db(): 90 | """ 91 | Drop all of the record from tables and the tables 92 | themselves. 93 | Drop the database as well. 94 | """ 95 | BaseModel.metadata.drop_all(bind=engine) 96 | drop_database(engine.url) 97 | -------------------------------------------------------------------------------- /core/api/common/serializers.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | from functools import singledispatch 4 | 5 | 6 | @singledispatch 7 | def serialize(rv): 8 | """ 9 | Define a generic serializable function. 10 | """ 11 | return rv 12 | 13 | 14 | @serialize.register(datetime.datetime) 15 | def serialize_dt(rv): 16 | """Register the `datetime.datetime` type 17 | for the generic serializable function. 18 | 19 | Serialize a `datetime` object to `string` 20 | according to strict-rfc3339. 21 | :param rv: object to be serialized 22 | :type rv: datetetime.datetime 23 | :returns: string 24 | """ 25 | return datetime.datetime.strftime(rv, '%Y-%m-%dT%H:%M:%S.%fZ') 26 | 27 | 28 | @serialize.register(uuid.UUID) 29 | def serialize_uuid(rv): 30 | """Register the `uuid.UUID` type 31 | for the generic serializable function. 32 | :param rv: object to be serialized 33 | :type rv: uuid.UUID 34 | :returns: string 35 | """ 36 | return str(rv) 37 | 38 | 39 | class ModelSerializerMixin(object): 40 | """ 41 | Serializable Mixin for the SQLAlchemy objects. 42 | """ 43 | def to_dict(self, exclude=None, only=None): 44 | """Convert SQLAlchemy object to `dict`. 45 | 46 | :param exclude: attrs to be excluded, defaults to None 47 | :type exclude: list, optional 48 | :param only: attrs to be serialized, defaults to None 49 | :type only: list, optional 50 | :returns: serialized SQLAlchemy object 51 | :rtype: dict 52 | 53 | The method cannot receive both `exclude` and `only` arguments 54 | at the same time. If this use case is reproduced, appropriate 55 | ValueError is raised. 56 | 57 | e.g. of usage 58 | 59 | ... 60 | return sql_alchemy_obj.to_dict(exclude=['name', 'email']) 61 | ... 62 | 63 | """ 64 | if exclude and only: 65 | msg = 'ModelSerializer can receive either `exclude` or `only`, not both.' 66 | raise ValueError(msg) 67 | 68 | if exclude is None: 69 | exclude = [] 70 | if only is None: 71 | only = [] 72 | 73 | return self._to_dict(exclude, only) 74 | 75 | def _to_dict(self, exclude, only): 76 | serialized_model = {} 77 | _mapper = self.__mapper__.c.keys() 78 | 79 | if exclude: 80 | for attr in _mapper: 81 | if attr not in exclude: 82 | serialized_model[attr] = self._serialize_attr(attr) 83 | elif only: 84 | for attr in only: 85 | if attr in _mapper: 86 | serialized_model[attr] = self._serialize_attr(attr) 87 | else: 88 | raise ValueError( 89 | 'The `only` attribute contains an invalid key: `%s`' % attr) 90 | else: 91 | for attr in _mapper: 92 | serialized_model[attr] = self._serialize_attr(attr) 93 | 94 | return serialized_model 95 | 96 | def _serialize_attr(self, attr): 97 | _val = getattr(self, attr) 98 | return serialize(_val) 99 | -------------------------------------------------------------------------------- /core/api/common/validation.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import json 3 | import os 4 | from functools import wraps 5 | 6 | from flask import request 7 | import jsonschema 8 | 9 | from .exceptions import InvalidAPIRequest 10 | 11 | SCHEMAS_DIR = 'core/api/specifications/schemas' 12 | SCHEMAS_PATH = os.path.join(os.getcwd(), SCHEMAS_DIR) 13 | 14 | 15 | def get_request_payload(method): 16 | """Get request payload based on the 17 | request.method. 18 | """ 19 | return { 20 | 'GET': _get_url_params_as_dict, 21 | 'POST': _get_request_body, 22 | 'PUT': _get_request_body 23 | }[method] 24 | 25 | 26 | def _get_url_params_as_dict(_request): 27 | """ 28 | Get url query params as `dict`. 29 | """ 30 | return _multi_dict_to_dict(_request.args) 31 | 32 | 33 | def _get_request_body(_request): 34 | """ 35 | Get the json payload of the request. 36 | """ 37 | return _request.json 38 | 39 | 40 | def _multi_dict_to_dict(_md): 41 | """Converts a `MultiDict` to a 42 | `dict` object. 43 | :param _md: object 44 | :type _md: MultiDict 45 | :returns: converted MultiDict object 46 | :rtype: dict 47 | """ 48 | result = dict(_md) 49 | for key, value in result.items(): 50 | if len(value) == 1: 51 | result[key] = serialize_number(value[0]) 52 | else: 53 | result[key] = [serialize_number(v) for v in value] 54 | return result 55 | 56 | 57 | def serialize_number(value): 58 | """ 59 | Tries to convert `string` to `int`, if it can't - 60 | tries to convert to `float`, if it fails again - 61 | the `string` itself is returned. 62 | """ 63 | try: 64 | _val = int(value) 65 | except ValueError: 66 | pass 67 | try: 68 | _val = float(value) 69 | except ValueError: 70 | return value 71 | return _val 72 | 73 | 74 | def get_schema(path): 75 | """ 76 | Read a .json file and return its content. 77 | """ 78 | with open(path, 'r') as f: 79 | return json.load(f) 80 | 81 | 82 | def validate_schema(payload, schema): 83 | """Validates the payload against a 84 | defined json schema for the requested 85 | endpoint. 86 | 87 | :param payload: incoming request data 88 | :type payload: dict 89 | :param schema: the schema the request payload should 90 | be validated against 91 | :type schema: .json file 92 | :returns: errors if any 93 | :rtype: list 94 | """ 95 | errors = [] 96 | validator = jsonschema.Draft4Validator(schema, 97 | format_checker=jsonschema.FormatChecker()) 98 | for error in sorted(validator.iter_errors(payload), key=str): 99 | errors.append(error.message) 100 | 101 | return errors 102 | 103 | 104 | def _get_path_for_function(func): 105 | return os.path.dirname(os.path.realpath(inspect.getfile(func))) 106 | 107 | 108 | def schema(path=None): 109 | """Validate the request payload with a JSONSchema. 110 | 111 | Decorator func that will be used to specify 112 | the path to the schema that the route/endpoint 113 | will be validated against. 114 | 115 | :param path: path to the schema file 116 | :type path: string 117 | :returns: list of errors if there are any 118 | :raises: InvalidAPIRequest if there are any errors 119 | 120 | ex: 121 | 122 | @schema('/path/to/schema.json') 123 | @app.route('/app-route') 124 | def app_route(): 125 | ... 126 | """ 127 | def decorator(func): 128 | @wraps(func) 129 | def wrapped(*args, **kwargs): 130 | _path = path.lstrip('/') 131 | schema_path = os.path.join(SCHEMAS_PATH, request.blueprint, _path) 132 | payload = get_request_payload(request.method)(request) 133 | 134 | errors = validate_schema(payload, get_schema(schema_path)) 135 | if errors: 136 | raise InvalidAPIRequest(message=errors) 137 | 138 | return func(*args, **kwargs) 139 | return wrapped 140 | return decorator 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flusk 2 | 3 | Flask - SQLAlchemy's declarative base - Docker - custom middleware. 4 | 5 | ## Specifications 6 | 7 | ##### Application factory 8 | 9 | Factories helps in creating many instances of the application. 10 | In this project, testing environment creates a new app instance whenever tests are ran. 11 | 12 | [Read More](http://flask.pocoo.org/docs/0.11/patterns/appfactories/) 13 | 14 | ##### Blueprints 15 | 16 | Blueprints helps to split large application in small modular packages (one would say - similar to `django apps`). 17 | 18 | [Read More](http://flask.pocoo.org/docs/0.11/blueprints/#blueprints) 19 | 20 | ##### Logic separation 21 | 22 | The logic is splitted in the following layers: 23 | 24 | - `backend` (persistence layer) - where resides the code to read/write/delete records to disk or database 25 | - `domain` (domain layer) - where resides the bussines logic and external api integrations, i/o operations for a blueprint 26 | - `views` (presentation layer) - knows only about HTTP `request` and `response`. Its duty is to process a request and pass data to lower layers and to return responses. 27 | - `models` (data model layer) - where all blueprint models are defined 28 | 29 | ##### Middleware 30 | 31 | The application tends to use middlewares instead of decorators or custom functions across the project. 32 | 33 | - **application/json requests** - ensures that all incoming requests are of `application/json` Content-Type 34 | - **schema validation** - validates the request payload against a JSON Schema 35 | - **cors** - allow cors requests for consumer apps 36 | - **json exceptions** - custom exception handler used to raise JSON exceptions 37 | - **json responses** - custom response handler used to return JSON objects 38 | 39 | ##### Extensions 40 | 41 | The project tends to use the framework agnostic extensions over the flask ones, because they are usually wrappers and besides that, they may add additional functionality that you don't actually need (e.g. managers) 42 | 43 | ##### Docker 44 | 45 | Ensures that the application you develop on local machine, behaves exactly in production. 46 | 47 | [Official Site](https://www.docker.com/) 48 | 49 | ## Directory layout 50 | 51 | ``` 52 | . 53 | ├── core # main codebase for the application 54 | │   ├── api # API specific codebase 55 | │   │   ├── common # shared logic used by the application 56 | │   │   │   ├── database.py # common database logic 57 | │   │   │   ├── exceptions.py # custom exception classes 58 | │   │   │   ├── __init__.py 59 | │   │   │   ├── middleware # application middleware 60 | │   │   │   │   ├── __init__.py # define application middlewares 61 | │   │   │   │   ├── request.py # `request` related middleware 62 | │   │   │   │   └── response.py # `response` related middleware 63 | │   │   │   ├── serializers.py # custom defined serializers 64 | │   │   │   └── validation.py # JSON schema validation logic 65 | │   │   ├── conftest.py # pytest configurations and custom fixtures 66 | │   │   ├── foss # flask blueprint 67 | │   │   │   ├── backend.py # logic related to database queries 68 | │   │   │   ├── domain.py # business logic and external integrations 69 | │   │   │   ├── __init__.py # blueprint config 70 | │   │   │   ├── models.py # blueprint models 71 | │   │   │   ├── tests # blueprint tests 72 | │   │   │   │   ├── __init__.py 73 | │   │   │   │   ├── test_unit.py # unit tests 74 | │   │   │   │   ├── test_integration.py # integration tests 75 | │   │   │   │   └── test_models.py # database models tests 76 | │   │   │   └── views.py # logic related to request -> response 77 | │   │   ├── __init__.py # app factory, blueprints, errorhandler and middleware registration 78 | │   │   └── specifications # API specifications, RAML files and JSON schemas 79 | │   │   └── schemas # JSON schemas folder 80 | │   │   └── foss # schemas for a specific blueprint 81 | │   │   ├── create_foss.json # endpoint/view/route schema 82 | │   │   └── update_foss.json # endpoint/view/route schema 83 | │   ├── Dockerfile # Dockerfile for the flask application 84 | │   ├── requirements.txt # application dependencies 85 | │   └── run.py # application creation and running 86 | ├── docker-compose.yml # Dockerfiles manager 87 | ├── Makefile # set of useful tasks (make `targets`) 88 | ├── nginx # nginx docker image related information 89 | │   ├── Dockerfile # Dockerfile for the nginx web server 90 | │   └── sites-enabled 91 | │   └── nginx.conf # nginx configuration 92 | └── README.md 93 | ``` 94 | 95 | ## Prerequisites 96 | 97 | - Python 3.5 98 | - Docker 99 | 100 | ## Installation 101 | 102 | 103 | #### Clone the repository 104 | 105 | ``` 106 | git clone https://github.com/dimmg/flusk.git 107 | ``` 108 | 109 | #### Build and run docker images 110 | 111 | ``` 112 | make dcompose-start 113 | ``` 114 | 115 | #### Run application 116 | 117 | - ##### Development 118 | 119 | SSH into the running `api` container and start the development server 120 | 121 | ``` 122 | docker exec -it flusk_api_1 bash 123 | python run.py 124 | ``` 125 | 126 | By having a running server, execute 127 | 128 | ``` 129 | docker inspect flusk_nginx_1 130 | ``` 131 | 132 | where `IPAddress` it is the address of the running application. 133 | 134 | 135 | 136 | - ##### Production 137 | 138 | Change `docker-compose.yml` file as follows: 139 | 140 | ``` 141 | command: 142 | gunicorn -w 1 -b 0.0.0.0:5000 run:wsgi 143 | # tail -f /dev/null 144 | ``` 145 | 146 | Rebuild the images via 147 | 148 | ``` 149 | make dcompose-restart 150 | ``` 151 | 152 | After rebuilding, the `gunicorn` wsgi server is running in background. 153 | 154 | To get the address of the running web server container run 155 | 156 | ``` 157 | docker inspect flusk_nginx_1 158 | ``` 159 | 160 | #### Migrations 161 | 162 | Migrations are done using the `alembic` migration tool. 163 | 164 | ##### Flow 165 | 166 | 1. make changes to your models when needed 167 | 2. create a migration 168 | 3. check the migration script and modify it as needed 169 | 4. apply the migration 170 | 171 | ##### Commands 172 | 173 | - create migration 174 | ``` 175 | make db-revision msg=<..message..> 176 | ``` 177 | 178 | - apply the last migration 179 | ``` 180 | make db-upgrade 181 | ``` 182 | 183 | - get the raw SQL for the last migration 184 | ``` 185 | make db-upgrade-sql 186 | ``` 187 | 188 | Note that these are the basic migration commands. To get the most from alembic, use the original `$ alembic` runner. 189 | 190 | 191 | 192 | 193 | 194 | --------------------------------------------------------------------------------