Boilerplate

master
informatic 2020-03-26 09:15:59 +01:00
commit 0477400a74
17 changed files with 363 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.pyc
.ropeproject

30
Dockerfile Normal file
View File

@ -0,0 +1,30 @@
FROM alpine:3.11.3@sha256:ddba4d27a7ffc3f86dd6c2f92041af252a1f23a8e742c90e6e1297bfa1bc0c45
EXPOSE 5000
WORKDIR /usr/src/app
RUN apk add --no-cache \
uwsgi-python3 \
python3 \
libpq git
# psycopg2 needs some extra build tools and headers. Install them and build in a
# single step in order not to pollute Docker layers
RUN apk add --no-cache --virtual .build-deps gcc python3-dev musl-dev postgresql-dev libffi-dev && \
pip3 install --no-cache-dir psycopg2==2.8.4 pycparser==2.20 cffi==1.14.0 bcrypt==3.1.7 && \
apk del --no-cache .build-deps
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
ENV FLASK_APP formity
ENV FLASK_ENV production
COPY . .
STOPSIGNAL SIGINT
CMD flask db upgrade && exec uwsgi --http-socket 0.0.0.0:5000 \
--processes 4 \
--uid uwsgi \
--plugins python3 \
--wsgi formity.wsgi:application \
--touch-reload formity/wsgi.py

19
docker-compose.yml Normal file
View File

@ -0,0 +1,19 @@
version: "3"
services:
postgres:
# postgres:9.6.17-alpine
image: postgres@sha256:c48c87e19b1c9bdc9d1de8a0f53fa1c7f91f887ecc06d0c2efd3f3425090b6c0
volumes:
- pgdata:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=secret
backend:
build: .
ports:
- 5000:5000
volumes:
- .:/usr/src/app
volumes:
pgdata:

24
formity/__init__.py Normal file
View File

@ -0,0 +1,24 @@
import flask
from formity.extensions import db, migrate, admin
def create_app():
app = flask.Flask(
__name__,
template_folder='../templates',
static_folder='../static',
)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config.from_object('formity.settings.%s' % app.env)
db.init_app(app)
migrate.init_app(app, db)
admin.init_app(app)
import formity.admin
import formity.views
import formity.models
app.register_blueprint(formity.views.bp)
return app

0
formity/admin.py Normal file
View File

28
formity/extensions.py Normal file
View File

@ -0,0 +1,28 @@
from flask import current_app
import flask_sqlalchemy
import flask_migrate
import flask_admin
from flask_admin.contrib.sqla import ModelView as BaseModelView
from flask_login import current_user
class SecurityMixin:
def is_accessible(self):
return current_user.is_authenticated
def inaccessible_callback(self, name, **kwargs):
# redirect to login page if user doesn't have access
return current_app.login_manager.unauthorized()
class IndexView(SecurityMixin, flask_admin.AdminIndexView):
pass
class ModelView(SecurityMixin, BaseModelView):
pass
db = flask_sqlalchemy.SQLAlchemy()
migrate = flask_migrate.Migrate()
admin = flask_admin.Admin(template_mode='bootstrap3', index_view=IndexView())

1
formity/models.py Normal file
View File

@ -0,0 +1 @@
from formity.extensions import db

View File

View File

@ -0,0 +1,8 @@
from environs import Env
env = Env()
env.read_env()
FLASK_ADMIN_FLUID_LAYOUT = True
SECRET_KEY = env.str('SECRET_KEY', default='randomstring')
SQLALCHEMY_DATABASE_URI = env.str('DATABASE_URI', default='postgresql+psycopg2://postgres:secret@postgres')

49
formity/utils.py Normal file
View File

@ -0,0 +1,49 @@
import re
import unicodedata
from sqlalchemy.orm import exc
from werkzeug.exceptions import abort
def get_object_or_404(model, *criterion):
try:
rv = model.query.filter(*criterion).one()
except (exc.NoResultFound, exc.MultipleResultsFound):
abort(404)
else:
return rv
def strip_accents(text):
"""
Strip accents from input String.
:param text: The input string.
:type text: String.
:returns: The processed String.
:rtype: String.
"""
try:
text = unicode(text, 'utf-8')
except (TypeError, NameError): # unicode is a default on python 3
pass
text = unicodedata.normalize('NFD', text)
text = text.encode('ascii', 'ignore')
text = text.decode("utf-8")
return str(text)
def text_to_id(text):
"""
Convert input text to id.
:param text: The input string.
:type text: String.
:returns: The processed String.
:rtype: String.
"""
text = strip_accents(text.lower())
text = re.sub('[ ]+', '_', text)
text = re.sub('[^0-9a-zA-Z_-]', '', text)
return text

3
formity/views.py Normal file
View File

@ -0,0 +1,3 @@
from flask import Blueprint
bp = Blueprint('main', __name__)

3
formity/wsgi.py Normal file
View File

@ -0,0 +1,3 @@
import formity
application = formity.create_app()

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration.

45
migrations/alembic.ini Normal file
View File

@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

96
migrations/env.py Normal file
View File

@ -0,0 +1,96 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option(
'sqlalchemy.url',
str(current_app.extensions['migrate'].db.engine.url).replace('%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

30
requirements.txt Normal file
View File

@ -0,0 +1,30 @@
alembic==1.4.2
blinker==1.4
certifi==2019.11.28
chardet==3.0.4
click==7.1.1
environs==7.3.1
Flask==1.1.1
Flask-Admin==1.5.5
Flask-Login==0.5.0
Flask-Migrate==2.5.3
Flask-OAuthlib==0.9.5
git+https://code.hackerspace.pl/informatic/flask-spaceauth#egg=Flask-SpaceAuth
Flask-SQLAlchemy==2.4.1
idna==2.9
itsdangerous==1.1.0
Jinja2==2.11.1
Mako==1.1.2
MarkupSafe==1.1.1
marshmallow==3.5.1
oauthlib==2.1.0
python-dateutil==2.8.1
python-dotenv==0.12.0
python-editor==1.0.4
requests==2.23.0
requests-oauthlib==1.3.0
six==1.14.0
SQLAlchemy==1.3.15
urllib3==1.25.8
Werkzeug==1.0.0
WTForms==2.2.1