Flask Arch is a template for building organized Flask applications. It offers flexibility for simple and complex apps.
from flask import Flask
from .config import (
    configure_app_env, configure_extensions, configure_error_handler,
    configure_jinja_env, configure_modules
)


def create_app() -> Flask:
    app = Flask(__name__)

    configure_app_env(app)
    configure_error_handler(app)
    configure_extensions(app)
    configure_jinja_env(app)
    configure_modules(app)

    return app

flask-arch/src/app/__init__.py
davidsantana06/flask-arch
from .config_app_env import configure_app_env
from .config_error_handler import configure_error_handler
from .config_extensions import configure_extensions
from .config_jinja_env import configure_jinja_env
from .config_modules import configure_modules

flask-arch/src/app/config/__init__.py
davidsantana06/flask-arch
from datetime import timedelta
from dotenv import load_dotenv
from flask import Flask
from os import environ, path

from app.constants import APP_DIRECTORY, ROOT_DIRECTORY


ENV_FILE_PATH = path.join(ROOT_DIRECTORY, '..', '.env')
load_dotenv(ENV_FILE_PATH)


def configure_app_env(app: Flask) -> None:
    app.static_folder = path.join(APP_DIRECTORY, 'static')
    app.template_folder = path.join(APP_DIRECTORY, 'templates')
    app.config.from_mapping({
        'SECRET_KEY': environ.get('SECRET_KEY'),
        'SQLALCHEMY_DATABASE_URI': environ.get('SQLALCHEMY_DATABASE_URI'),
        'SQLALCHEMY_TRACK_MODIFICATIONS': False,
        'PERMANENT_SESSION_LIFETIME': timedelta(days=1)
    })

flask-arch/src/app/config/config_app_env.py
davidsantana06/flask-arch
from flask import Flask
from http import HTTPStatus
from werkzeug.exceptions import HTTPException

from app.misc import render_template


ERROR_MESSAGE = {
    400: 'Invalid request.',
    401: 'Unauthorized access.',
    403: 'Forbidden acess.',
    404: 'Page not found.',
    405: 'HTTP method not allowed.',
    500: 'Internal server error.'
}
GENERIC_MESSAGE = 'An unexpected error has occurred.'


def error_handler(e: Exception):
    error_code = e.code if (isinstance(e, HTTPException)) else 500
    description = ERROR_MESSAGE.get(error_code, GENERIC_MESSAGE)
    http_status = HTTPStatus(error_code)
    data = {
        'error': {
            'code': error_code,
            'description': description
        }
    }

    return (render_template('error-handler', data), http_status)


def configure_error_handler(app: Flask) -> None:
    app.register_error_handler(Exception, error_handler)

flask-arch/src/app/config/config_error_handler.py
davidsantana06/flask-arch
from flask import Flask

from app.extensions import database, bcrypt, csrf, login_manager, socket_io
from app.models import User


def load_user(user_id: str) -> User:
    return User.find_by_id(int(user_id))


def configure_extensions(app: Flask) -> None:
    # database.init_app(app)
    bcrypt.init_app(app)
    csrf.init_app(app)

    login_manager.init_app(app)
    login_manager.login_view = 'auth.login'
    login_manager.user_loader(load_user)

    socket_io.init_app(app, cors_allowed_origins='*')

flask-arch/src/app/config/config_extensions.py
davidsantana06/flask-arch
from flask import Flask
from app.misc import dt_now, format_dt, template


def configure_jinja_env(app: Flask) -> None:
    app.jinja_env.globals.update({
        'template': template,
        'dt_now': dt_now,
        'format_dt': format_dt
    })

flask-arch/src/app/config/config_jinja_env.py
davidsantana06/flask-arch
from flask import Flask, Blueprint
from importlib import import_module
from os import listdir

from app.constants import MODULES_DIRECTORY


MODULE_DIRECTORY = MODULES_DIRECTORY + '\\{module_name}'
MODULE_PATH = 'app.modules.{module_name}'


def configure_modules(app: Flask) -> None:
    for module_name in listdir(MODULES_DIRECTORY):
        module_directory = MODULE_DIRECTORY.format(
            module_name=module_name
        )

        if '__init__.py' in listdir(module_directory):
            module = import_module(
                MODULE_PATH.format(module_name=module_name)
            )

            for _, item in module.__dict__.items():
                if isinstance(item, Blueprint):
                    app.register_blueprint(item)
                    break

flask-arch/src/app/config/config_modules.py
davidsantana06/flask-arch
from os import path


ROOT_DIRECTORY = path.abspath(
    path.join(
        path.dirname(__file__),
        '..', '..'
    )
)
OUTPUT_DIRECTORY = path.join(ROOT_DIRECTORY, 'output')
UPLOADS_DIRECTORY = path.join(ROOT_DIRECTORY, 'uploads')
SRC_DIRECTORY = path.join(ROOT_DIRECTORY, 'src')
APP_DIRECTORY = path.join(SRC_DIRECTORY, 'app')
MODULES_DIRECTORY = path.join(APP_DIRECTORY, 'modules')

flask-arch/src/app/constants.py
davidsantana06/flask-arch
from flask_bcrypt import Bcrypt
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from flask_socketio import SocketIO
from flask_wtf import CSRFProtect


database = SQLAlchemy()
bcrypt = Bcrypt()
csrf = CSRFProtect()
login_manager = LoginManager()
socket_io = SocketIO()

flask-arch/src/app/extensions.py
davidsantana06/flask-arch
from .data_transfer import dict_to_form, form_to_obj, obj_to_form
from .datetime_handler import dt_now, format_dt
from .jinja_helpers import render_template, template

flask-arch/src/app/misc/__init__.py
davidsantana06/flask-arch
from flask_wtf import FlaskForm
from typing import Dict, List


def dict_to_form(data: Dict[str, object], form: FlaskForm) -> None:
    for field in form:
        name = field.name

        if name in data:
            value = data[name]
            field.data = value
            data.pop(name)


def obj_to_form(obj: object, form: FlaskForm, reserverd_attrs: List[str] = []) -> None:
    for field in form:
        name = field.name

        if (hasattr(obj, name)) and (name not in reserverd_attrs):
            field.data = getattr(obj, name)


def form_to_obj(form: FlaskForm, obj: object, reserverd_attrs: List[str] = []) -> None:
    for field in form:
        name = field.name

        if name not in reserverd_attrs:
            field.populate_obj(obj, name)

flask-arch/src/app/misc/data_transfer.py
davidsantana06/flask-arch
from datetime import date, datetime


TIME_PATTERN = '%I:%M %p'
DATE_PATTERN = '{month} {day}, {year}'
MONTH = [
    'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]


def dt_now() -> datetime:
    return datetime.now()


def format_dt(dt: datetime, reference_date: date) -> str:
    formatted_dt = ''

    if dt.date() == reference_date:
        formatted_dt = dt.strftime(TIME_PATTERN)
    else:
        month_idx = dt.month - 1
        formatted_dt = DATE_PATTERN.format(
            month=MONTH[month_idx], day=dt.day, year=dt.year
        )

    return formatted_dt

flask-arch/src/app/misc/datetime_handler.py
davidsantana06/flask-arch
from flask import render_template as flask_render_template
from typing import Dict


TEMPLATE_EXTENSION = '.html.j2'


def complete_template_name(template_name: str) -> str:
    if not template_name.endswith(TEMPLATE_EXTENSION):
        template_name += TEMPLATE_EXTENSION

    return template_name


def render_template(template_name: str, data: Dict[str, object] = {}) -> str:
    return flask_render_template(complete_template_name(template_name), **data)


def template(template_name: str) -> str:
    return complete_template_name(template_name)

flask-arch/src/app/misc/jinja_helpers.py
davidsantana06/flask-arch
from flask_login import UserMixin
from flask_sqlalchemy.model import Model
from sqlalchemy import (
    Column, ColumnElement, DateTime, Integer, String, UnaryExpression,
    and_
)
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.sql import func
from typing import List

from .extensions import database


class CRUDMixin(Model):
    @declared_attr
    def __tablename__(cls):
        return cls.__name__

    @declared_attr
    def created_at(cls):
        return Column(DateTime, nullable=False, default=func.now())

    @declared_attr
    def updated_at(cls):
        return Column(DateTime, nullable=False, default=func.now())

    @declared_attr
    def status(cls):
        return Column(Integer, nullable=False, default=1)

    @staticmethod
    def add(model: Model) -> None:
        try:
            database.session.add(model)
            database.session.commit()
        except Exception as e:
            database.session.rollback()
            raise e

    @staticmethod
    def delete(model: Model) -> None:
        try:
            database.session.delete(model)
            database.session.commit()
        except Exception as e:
            database.session.rollback()
            raise e

    @classmethod
    def find_first(cls, filters: List[ColumnElement[bool]]) -> Model:
        return cls.query.filter(
            and_(*filters)
        ).first()

    @classmethod
    def find_all(
        cls, filter_clauses: List[ColumnElement[bool]] = None, order_clauses: List[UnaryExpression] = None
    ) -> List[Model]:
        query = cls.query

        if filter_clauses:
            query = query.filter(and_(*filter_clauses))

        if order_clauses:
            query = query.order_by(*order_clauses)

        return query.all()


class User(database.Model, CRUDMixin, UserMixin):
    id = Column(Integer, primary_key=True)
    name = Column(String(50), nullable=False)
    email = Column(String(50), unique=True, nullable=False)
    username = Column(String(30), unique=True, nullable=False)
    password = Column(String(60), nullable=False)

    @classmethod
    def find_by_id(cls, id: int) -> 'User':
        return cls.find_first([User.id == id])

    @classmethod
    def find_by_username(cls, username: str) -> 'User':
        return cls.find_first([User.username == username])

    @classmethod
    def find_all_by_name(cls, name: str) -> List['User']:
        return cls.find_all([User.name.icontains(name)])

flask-arch/src/app/models.py
davidsantana06/flask-arch
from flask import Blueprint

auth = Blueprint(
    'auth', __name__, static_folder='static', static_url_path=f'/{__name__}', template_folder='templates'
)

from .views import *

flask-arch/src/app/modules/auth/__init__.py
davidsantana06/flask-arch
from flask_wtf import FlaskForm
from wtforms import PasswordField, StringField, SubmitField
from wtforms.validators import DataRequired, Length


class LoginForm(FlaskForm):
    username = StringField(
        label='Username',
        validators=[DataRequired(), Length(1, 30)]
    )
    password = PasswordField(
        label='Password',
        validators=[DataRequired(), Length(1, 255)]
    )
    submit = SubmitField('Login')

flask-arch/src/app/modules/auth/forms.py
davidsantana06/flask-arch
from http import HTTPStatus

from app.misc import render_template

from . import auth
from .forms import LoginForm


@auth.route('/login', methods=['GET', 'POST'])
def login():
    data = {
        'form': LoginForm()
    }
    return (render_template('login', data), HTTPStatus.OK)

flask-arch/src/app/modules/auth/views.py
davidsantana06/flask-arch
from flask import Blueprint

home = Blueprint(
    'home', __name__, static_folder='static', static_url_path=f'/{__name__}', template_folder='templates'
)

from .events import *
from .views import *

flask-arch/src/app/modules/home/__init__.py
davidsantana06/flask-arch
from flask_socketio import emit
from app.extensions import socket_io


@socket_io.on('message')
def handle_message(message):
    print('Received message:', message)
    emit('message', message)

flask-arch/src/app/modules/home/events.py
davidsantana06/flask-arch
from flask_login import login_required
from http import HTTPStatus

from app.misc import render_template

from . import home


@home.get('/')
def index():
    return (render_template('index'), HTTPStatus.OK)


@home.get('/login-test')
@login_required
def login_test():
    return (render_template('index'), HTTPStatus.OK)


@home.get('/socket')
def socket():
    return (render_template('socket'), HTTPStatus.OK)

flask-arch/src/app/modules/home/views.py
davidsantana06/flask-arch