Skip to main content

The best way to build Flask apps

Project description

Flask Unchained

The best way to build Flask apps

What is it?

Flask Unchained is a work-in-progress fully integrated, optional-batteries-included web framework built on top of Flask and its extension ecosystem. Flask Unchained aims to be a fresh, modern take on building web apps and APIs with Flask - while trying to stay as true as possible to the spirit and API of Flask.

Why use it?

  • designed to be easy to start with and even easier to grow your app
  • documented with real-world-usage in mind (some sections are still a work-in-progress)
  • clean, predictable application structure that encourages good design patterns (no circular imports!)
  • simple and consistent patterns for customizing, extending, and/or overriding almost everything (e.g. configuration, views/controllers/resources, routes, templates, services, extensions, ...)
    • your customizations are easily distributable as a standalone bundle (Python package), which itself then supports the same patterns for customization, ad infinitum.
  • no integration headaches between supported libraries
    • testing with pytest integrated out-of-the-box
    • SQLAlchemy Bundle
      • integrates the SQLAlchemy Database ORM and Alembic for migrations
      • unified API for creating, querying, updating and deleting models
      • enhanced declarative models (configurable):
        • automatic primary keys unless you define one yourself
        • automatic timestamping when models are created and updated
        • much simplified polymorphic model inheritance
    • Security Bundle
      • a heavily cleaned up and refactored fork of Flask-Security
      • currently supports the same session and token authentication methods
      • includes optional HTML/JSON/GraphQL endpoints for:
        • login/logout
        • registration with optional email confirmation
        • forgot password / reset password
        • change password
    • OAuth Bundle
      • integrates with the Security Bundle, adding support for OAuth 1.0 and OAuth 2.0
    • Celery Bundle
      • integrates the Celery distributed task queue
    • Mail Bundle
      • email-sending support via Flask-Mail
      • optionally integrates with the Celery Bundle for automatic asynchronous emails
    • Session Bundle
    • API Bundle
      • work-in-progress RESTful API framework integrating SQLAlchemy with Marshmallow serializers
      • work-in-progress support for OpenAPI (aka Swagger) docs
    • GraphQL Bundle

How does it work?

Flask Unchained implements the Application Factory Pattern, utilizing a standardized (but configurable) way to organize "bundles" of code, such that they become easily distributable, reusable, and customizable across multiple independent projects. All of the code within bundles is automatically discovered and registered with the app. You can think of bundles as an enhanced replacement for both Flask blueprints and extensions. Bundles are somewhat comparable to Django's "apps", but I think you'll find bundles are more powerful and flexible.

The architecture is inspired by Symfony, which is awesome, aside from the fact that it isn't Python ;) If you've heard of Laravel, that framework is built on top of the Symfony Components - with Flask Unchained, the existing Python/Flask ecosystems are our "components."[*]

[*] Don't like the default choices? Flask Unchained is almost completely customizable. The core architecture offers you the potential to use an entirely different stack of libraries, or swap out only certain components - the power to choose is yours.

Useful Links

Table of Contents

Features

  • Python 3.6+

  • improved class-based views with the Controller, Resource, and ModelResource base classes

  • declarative routing

  • dependency injection of services and extensions

  • includes out-of-the-box (mostly optional) integrations with:

    • Flask-Login (user authentication and sessions management) and Flask-Principal (user authorization with permissions and roles)

      • both session and token authentication are currently supported
      • includes optional support for registration (with optional required email confirmation before account activation)
      • optional change password and forgot password functionality
    • (distributed task queue, optional)

    • Flask-Admin (admin interface, optional)

    • Flask-BabelEx (translations, always enabled but optional)

    • pytest and factory_boy (testing framework)

Quickstart

# (create a virtual environment)
pip install flask-unchained[dev]
flask new project <your-project-folder-name>

# (answer the questions and `cd` into the new directory)
pip install -r requirements-dev.txt
flask run

NOTE: If you enabled the SQLAlchemy Bundle, then you may need to run migrations before running the development server:

flask db init
flask db migrate -m 'create initial tables'
flask db upgrade

What does it look like?

Application Structure and Project Layout

Unlike stock Flask, Flask Unchained apps cannot be written in a single file. Instead, Flask Unchained's bundles define a (configurable) folder convention that must be followed for Flask Unchained to be able to correctly discover all of your code. A large application structure might look about like this:

/home/user/dev/project-root
├── app                 # your app bundle package
│   ├── admins          # model admins
│   ├── commands        # Click CLI groups/commands
│   ├── extensions      # Flask extensions
│   ├── models          # SQLAlchemy models
│   ├── fixtures        # SQLAlchemy model fixtures (for seeding the dev db)
│   ├── serializers     # Marshmallow serializers (aka schemas)
│   ├── services        # dependency-injectable services
│   ├── tasks           # Celery tasks
│   ├── templates       # Jinja2 templates
│   ├── views           # Controllers, Resources and views
│   └── __init__.py
│   └── config.py       # app config
│   └── routes.py       # declarative routes
├── assets              # static assets to be handled by Webpack
│   ├── images
│   ├── scripts
│   └── styles
├── bundles             # custom bundles and/or bundle extensions/overrides
│   └── security        # a customized/extended Security Bundle
│       ├── models
│       ├── serializers
│       ├── services
│       ├── templates
│       └── __init__.py
├── db
│   └── migrations      # Alembic (SQLAlchemy) migrations (generated by Flask-Migrate)
├── static              # static assets (Webpack compiles to here, and Flask
│                       #  serves this folder at /static (by default))
├── templates           # the top-level templates folder
├── tests               # your pytest tests
├── webpack             # Webpack configs
└── unchained_config.py # the Flask Unchained config

To learn how to build such a larger example application, check out the official tutorial.

A minimal application structure looks about like this:

/home/user/dev/hello-flask-unchained
├── app
│   ├── templates
│   │   └── site
│   │       └── index.html
│   ├── __init__.py
│   ├── config.py
│   ├── forms.py
│   ├── models.py
│   ├── routes.py
│   ├── services.py
│   └── views.py
└── unchained_config.py

Let's built it!

# (create a virtualenv and activate it)
pip install flask-unchained[dev,sqlalchemy] \
   && mkdir -p hello-flask-unchained/app && cd hello-flask-unchained \
   && touch unchained_config.py && cd app \
   && mkdir -p templates/site && touch templates/site/index.html \
   && touch __init__.py config.py forms.py models.py routes.py services.py views.py

Bundles

The first step is to create an app bundle module in your project root - we're calling ours app here - with an AppBundle subclass in it:

# hello-flask-unchained/app/__init__.py

from flask_unchained import AppBundle, FlaskUnchained


class App(AppBundle):
    # you only need to subclass AppBundle; everything below is optional. for example, these
    # attributes can be set to customize which bundle module to load from (defaults shown)
    config_module_name = 'config'
    models_module_name = 'models'
    routes_module_name = 'routes'
    services_module_name = 'services'

    # these two methods are optional callbacks which the app factory will call
    def before_init_app(self, app: FlaskUnchained):
        pass

    def after_init_app(self, app: FlaskUnchained):
        pass

Configuration

The configuration for Flask Unchained itself is pretty minimal, for example:

# hello-flask-unchained/unchained_config.py

import os

PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))

def folder_or_none(folder_name):
    folder = os.path.join(PROJECT_ROOT, folder_name)
    return folder if os.path.exists(folder) else None

# these get passed to the :class:`FlaskUnchained` constructor
TEMPLATE_FOLDER = folder_or_none('templates')
STATIC_FOLDER = folder_or_none('static')
STATIC_URL_PATH = '/static' if STATIC_FOLDER else None

# declare which bundles Flask Unchained should load
BUNDLES = [
    'flask_unchained.bundles.sqlalchemy',
    'app',  # your app bundle *must* be last
]

And add the required app configuration to your app bundle:

# hello-flask-unchained/app/config.py

import os

from flask_unchained import AppBundleConfig


class Config(AppBundleConfig):
    SECRET_KEY = os.getenv('FLASK_SECRET_KEY', 'change-me-to-a-secret-key')


# the following env-specific config classes are all optional

class DevConfig(Config):
    pass

class ProdConfig(Config):
    pass

class StagingConfig(ProdConfig):
    pass

Models

# hello-flask-unchained/app/models.py

from flask_unchained.bundles.sqlalchemy import db


class NameSubmission(db.Model):
    class Meta:
        repr = ('id', 'name')

    name = db.Column(db.String(128))

    # the following primary key and timestamp columns are automatically added to models
    # (if necessary/not customized)
    # id = db.Column(db.Integer, primary_key=True)
    # created_at = db.Column(db.DateTime, server_default=sqlalchemy.func.now())
    # updated_at = db.Column(db.DateTime, server_default=sqlalchemy.func.now(),
    #                        onupdate=sqlalchemy.func.now())

Services

# hello-flask-unchained/app/services.py

from flask_unchained.bundles.sqlalchemy import ModelManager

from . import models


class NameSubmissionManager(ModelManager):
    class Meta:
        model = models.NameSubmission

    def create(self, name, commit: bool = False, **kwargs) -> models.NameSubmission:
        return super().create(name=name, commit=commit, **kwargs)

Forms

# hello-flask-unchained/app/forms.py

from flask_unchained.bundles.sqlalchemy.forms import ModelForm

from . import models


class NameSubmissionForm(ModelForm):
    class Meta:
        model = models.NameSubmission

Views

A hello world view:

# hello-flask-unchained/app/views.py

from flask_unchained import Controller, route, request, injectable, param_converter

from . import forms, models, services


class SiteController(Controller):
    name_submission_manager: services.NameSubmissionManager = injectable

    @route('/', methods=['GET', 'POST'])
    @param_converter(id=models.NameSubmission)  # converts the `id` query param to a model
    def index(self, name_submission=None):
        form = forms.NameSubmissionForm(request.form)
        if form.validate_on_submit():
            name_submission = self.name_submission_manager.create(name=form.name.data,
                                                                  commit=True)
            return self.redirect('index', id=name_submission.id)
        return self.render('index', form=form, name_submission=name_submission)

Templates

{# hello-flask-unchained/app/templates/layout.html #}

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>{% block title %}Flask Unchained Hello World{% endblock %}</title>

    {% block stylesheets %}
    {% endblock stylesheets %}
  </head>

  <body>
    {% block body %}
      <div class="container">
        {% block content %}
        {% endblock content %}
      </div>
    {% endblock body %}

    {% block javascripts %}
    {% endblock javascripts %}
  </body>
</html>
{# hello-flask-unchained/app/templates/site/index.html #}

{% extends 'layout.html' %}

{% set name = name_submission.name | default('World') %}

{% block title %}Hello {{ name }}{% endblock %}

{% block content %}
  <h1>Hello {{ name }}</h1>

  <form name="{{ form._name }}" action="{{ url_for('site_controller.index') }}" method="POST">
    {{ form.hidden_tag() }}
    {{ form.name.label('Enter your name:') }}
    {{ form.name() }}
    <button type="submit">Submit</button>
  </form>
{% endblock %}

Routes

Now we can register the controller with our routes:

# hello-flask-unchained/app/routes.py

from flask_unchained import (controller, resource, func, include, prefix,
                             get, delete, post, patch, put, rule)

from .views import SiteController


routes = lambda: [
    controller(SiteController),
]

Commands

And run it:

flask run
 * Environment: development
 * Debug mode: on
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Now you should be able to browse to http://localhost:5000 to view your new site!

Contributing

Contributions are more than welcome! This is a big project with a lot of different things that need doing. There's a TODO file in the project root, or if you've got an idea, open an issue or a PR and let's chat.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Files for Flask-Unchained, version 0.7.0
Filename, size File type Python version Upload date Hashes
Filename, size Flask_Unchained-0.7.0-py3-none-any.whl (420.2 kB) File type Wheel Python version py3 Upload date Hashes View hashes
Filename, size Flask Unchained-0.7.0.tar.gz (296.7 kB) File type Source Python version None Upload date Hashes View hashes

Supported by

Elastic Elastic Search Pingdom Pingdom Monitoring Google Google BigQuery Sentry Sentry Error logging AWS AWS Cloud computing DataDog DataDog Monitoring Fastly Fastly CDN SignalFx SignalFx Supporter DigiCert DigiCert EV certificate StatusPage StatusPage Status page