Flask Unchained¶
Introducing Flask Unchained¶
The quickest and easiest way to build large web apps and APIs with Flask and SQLAlchemy
Flask Unchained is a fully integrated, declarative, object-oriented web framework for Flask and its optional-batteries-included extension ecosystem. Flask Unchained is powerful, consistent, highly extensible and completely customizable. Flask Unchained stays true to the spirit and API of Flask while simultaneously introducing powerful new building blocks that enable you to rapidly take your Flask apps to the next level:
clean and predictable application structure that encourage good design patterns by organizing code as bundles
no integration headaches between supported libraries and extensions
no plumbing or boilerplate; everything just works straight out-of-the-box
simple and consistent patterns for customizing and/or overriding everything
designed with code reuse in mind; your bundles can be distributed as their own Python packages to automatically integrate with other Flask Unchained apps
Included bundles & integrated extensions (mostly optional)
Controller Bundle: Enhanced class-based views, enhanced blueprints, declarative routing, and Flask WTF for forms and CSRF protection. The only required bundle.
SQLAlchemy Bundle Flask SQLAlchemy and Flask Migrate for database models and migrations, plus some optional “sugar” on top of SQLAlchemy to make the best ORM in existence even quicker and easier to use with SQLAlchemy Unchained.
API Bundle: RESTful APIs with Flask Marshmallow serializers for SQLAlchemy models.
Graphene Bundle: Flask GraphQL with Graphene for SQLAlchemy models.
Security Bundle: Flask Login for authentication and Flask Principal for authorization.
Celery Bundle: Celery distributed tasks queue.
Don’t like the default stack? With Flask Unchained you can bring your own! Flask Unchained is designed to be so flexible that you could even use it to create your own works-out-of-the-box web framework for Flask with an entirely different stack.
Thanks and acknowledgements
The architecture of how Flask Unchained and its bundles works is only possible thanks to Python 3. The concepts and design patterns Flask Unchained introduces are inspired by the Symfony Framework, which is enterprise-proven and awesome, aside from the fact that it isn’t Python ;)
Install Flask Unchained¶
Requires Python 3.7+
pip install "flask-unchained[dev]"
Or, to use asyncio by running atop Quart instead of Flask (experimental!):
pip install "flask-unchained[asyncio,dev]" # Requires Python 3.7+
Attention
This software is somewhere between alpha and beta quality. It works for me, the design patterns are proven and the core is solid, but especially at the edges there will probably be bugs - and possibly some breaking API changes too. Flask Unchained needs you: please file issues on GitHub if you encounter any problems, have any questions, or have any feedback!
Hello World¶
As simple as it gets:
# project-root/app.py
from flask_unchained import AppBundle, Controller route
class App(AppBundle):
pass
class SiteController(Controller):
@route('/')
def index(self):
return 'Hello World from Flask Unchained!'
Running the Development Server¶
And just like that we can run it:
cd project-root
UNCHAINED="app" flask run
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
You can now browse to http://127.0.0.1:5000 to see it in action!
Under the easily accessible hood lives Flask Unchained’s fully customizable App Factory: the good old call to app = Flask(__name__)
and everything else necessary to correctly initialize, register, and run your app’s code. No plumbing, no boilerplate, everything just works.
Testing with pytest¶
Python’s best testing framework comes integrated out-of-the-box:
# project-root/test_app.py
from flask_unchained.pytest import HtmlTestClient
class TestSiteController:
def test_index(self, client: HtmlTestClient):
r = client.get('site_controller.index')
assert r.status_code == 200
assert r.html.count('Hello World from Flask Unchained!') == 1
Tests can be run like so:
cd project-root
UNCHAINED="app" pytest
============================= test session starts ==============================
platform linux -- Python 3.8.6, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: /home/user/dev/project-root
plugins: Faker-4.1.1, flask-1.1.0, Flask-Unchained-0.7.9
collected 1 item
test_app.py . [100%]
============================== 1 passed in 0.05s ===============================
The Production App Factory¶
In development and testing the app factory is automatically used, while in production you call it yourself:
# project-root/wsgi.py
from flask_unchained import AppFactory, PROD
app = AppFactory().create_app(env=PROD)
We’ve just shown how Flask Unchained keeps the minimal simplicity micro-frameworks like Flask are renowned for, but to really begin to grasp the power of using Flask Unchained, we need to go bigger than this simple example!
Hello World for Real¶
Let’s take a peak at some of what this baby can really do to see just how quickly you can start building something more useful:
cd project-root
mkdir -p templates/site
pip install "flask-unchained[dev,sqlalchemy]"
Quotes App¶
We’re going to create a simple app to store authors and quotes in an SQLite database, and to display them to the user in their browser.
# project-root/app.py
from flask_unchained import (FlaskUnchained, AppBundle, BundleConfig,
unchained, injectable, generate_csrf)
from flask_unchained.views import Controller, route, param_converter
from flask_unchained.bundles.sqlalchemy import db, ModelManager
# configuration ---------------------------------------------------------------------
BUNDLES = ['flask_unchained.bundles.sqlalchemy']
class Config(BundleConfig):
SECRET_KEY = 'super-secret-key'
WTF_CSRF_ENABLED = True
SQLALCHEMY_DATABASE_URI = 'sqlite://' # memory
class TestConfig(Config):
WTF_CSRF_ENABLED = False
@unchained.after_request
def set_csrf_token_cookie(response):
if response:
response.set_cookie('csrf_token', generate_csrf())
return response
# database models -------------------------------------------------------------------
class Author(db.Model):
# models get a primary key (id) and created_at/updated_at columns by default
name = db.Column(db.String(length=64))
quotes = db.relationship('Quote', back_populates='author')
class Quote(db.Model):
text = db.Column(db.Text)
author = db.relationship('Author', back_populates='quotes')
author_id = db.foreign_key('Author', nullable=False)
# model managers (dependency-injectable services for database CRUD operations) ------
class AuthorManager(ModelManager):
class Meta:
model = Author
class QuoteManager(ModelManager):
class Meta:
model = Quote
# views (controllers) ---------------------------------------------------------------
class SiteController(Controller):
class Meta:
template_folder = 'site' # the default, auto-determined from class name
# get the app's instance of the QuoteManager service injected into us
quote_manager: QuoteManager = injectable
@route('/')
def index(self):
return self.render('index', quotes=self.quote_manager.all())
@route('/authors/<int:id>')
@param_converter(id=Author) # use `id` in the URL to query that Author in the DB
def author(self, author: Author):
return self.render('author', author=author)
# declare this module (file) is a Flask Unchained Bundle by subclassing AppBundle ---
class App(AppBundle):
def before_init_app(self, app: FlaskUnchained) -> None:
app.url_map.strict_slashes = False
@unchained.inject()
def after_init_app(self,
app: FlaskUnchained,
author_manager: AuthorManager = injectable,
quote_manager: QuoteManager = injectable,
) -> None:
# typically you should use DB migrations and fixtures to perform these steps
db.create_all()
quote_manager.create(
text="Happiness is not a station you arrive at, "
"but rather a manner of traveling.",
author=author_manager.create(name="Margaret Lee Runbeck"))
quote_manager.create(
text="Things won are done; joy's soul lies in the doing.",
author=author_manager.create(name="Shakespeare"))
db.session.commit()
That’s the complete app code right there! Hopefully this helps show what is meant by Flask Unchained minimizing plumbing and boilerplate by being declarative and object-oriented. We just need to add the template files before starting the server:
<!-- project-root/templates/layout.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Flask Unchained Quotes</title>
</head>
<body>
<nav>
<a href="{{ url_for('site_controller.index') }}">Home</a>
</nav>
{% block body %}
{% endblock %}
</body>
</html>
<!-- project-root/templates/site/index.html -->
{% extends "layout.html" %}
{% block body %}
<h1>Flask Unchained Quotes</h1>
{% for quote in quotes %}
<blockquote>
{{ quote.text }}<br />
<a href="{{ url_for('site_controller.author', id=quote.author.id) }}">
{{ quote.author.name }}
</a>
</blockquote>
{% endfor %}
{% endblock %}
<!-- project-root/templates/site/author.html -->
{% extends "layout.html" %}
{% block body %}
<h1>{{ author.name }} Quotes</h1>
{% for quote in author.quotes %}
<blockquote>{{ quote.text }}</blockquote>
{% endfor %}
{% endblock %}
Fire it up:
export UNCHAINED="app"
flask urls
Method(s) Rule Endpoint View
-------------------------------------------------------------------------------------------
GET / site_controller.index app.SiteController.index
GET /authors/<int:id> site_controller.author app.SiteController.author
export UNCHAINED="app"
flask run
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Adding a RESTful API¶
Flask Unchained includes an API Bundle integrating RESTful support atop the Controller Bundle with SQLAlchemy models and Marshmallow serializers. Basic out-of-the-box usage is dead simple.
Install dependencies for the API Bundle:
pip install "flask-unchained[api]"
And add the following code to the bottom of project-root/app.py
:
# append to project-root/app.py
from flask_unchained.bundles.api import ma, ModelResource, ModelSerializer
from flask_unchained.routes import controller, resource, prefix
BUNDLES += ['flask_unchained.bundles.api']
# db model serializers --------------------------------------------------------------
class AuthorSerializer(ModelSerializer):
class Meta:
model = Author
url_prefix = '/authors' # the default, auto-determined from model class name
quotes = ma.Nested('QuoteSerializer', only=('id', 'text'), many=True)
class QuoteSerializer(ModelSerializer):
class Meta:
model = Quote
author = ma.Nested('AuthorSerializer', only=('id', 'name'))
# api views -------------------------------------------------------------------------
class AuthorResource(ModelResource):
class Meta:
model = Author
include_methods = ('get', 'list')
class QuoteResource(ModelResource):
class Meta:
model = Quote
exclude_methods = ('create', 'patch', 'put', 'delete')
# use declarative routing for specifying views with fine-grained control over URLs
routes = lambda: [
controller(SiteController),
prefix('/api/v1', [
resource(AuthorResource),
resource(QuoteResource),
]),
]
We can take a look at the new URLs:
flask urls
Method(s) Rule Endpoint View
-------------------------------------------------------------------------------------------
GET / site_controller.index app.SiteController.index
GET /authors/<int:id> site_controller.author app.SiteController.author
GET /api/v1/authors author_resource.list app.AuthorResource.list
GET /api/v1/authors/<int:id> author_resource.get app.AuthorResource.get
GET /api/v1/quotes quote_resource.list app.QuoteResource.list
GET /api/v1/quotes/<int:id> quote_resource.get app.QuoteResource.get
And run it:
flask run
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Securing the App¶
Flask Unchained also includes the Security Bundle as a foundation for handling authentication and authorization in your apps. It is designed to be extended and customized to your needs - like everything in Flask Unchained! - but it also works out-of-the-box for when all it provides is sufficient for your needs. Let’s set things up to require an authenticated user to use the app’s API.
Install dependencies for the Security Bundle:
pip install "flask-unchained[session,security]"
And add the following to the bottom of your project-root/app.py
:
# append to project-root/app.py
from flask_unchained.bundles.security import SecurityController, auth_required
from flask_unchained.bundles.security import SecurityService, UserManager
from flask_unchained.bundles.security.models import User as BaseUser
from flask_unchained.bundles.sqlalchemy import db
# enable the session and security bundles
BUNDLES += ['flask_unchained.bundles.session',
'flask_unchained.bundles.security']
# configure server-side sessions
Config.SESSION_TYPE = 'sqlalchemy'
Config.SESSION_SQLALCHEMY_TABLE = 'flask_sessions'
# configure security
Config.SECURITY_REGISTERABLE = True # enable user registration
AuthorResource.Meta.decorators = (auth_required,)
QuoteResource.Meta.decorators = (auth_required,)
# want to add fields to the database model for users? no problem!
# just subclass it, keeping the same original class name
class User(BaseUser):
favorite_color = db.Column(db.String)
# add the Security Controller views to our app
routes = lambda: [
controller(SiteController),
controller(SecurityController),
prefix('/api/v1', [
resource('/authors', AuthorResource),
resource('/quotes', QuoteResource),
]),
]
# create a demo user and log them in when the dev server starts
@unchained.before_first_request()
@unchained.inject()
def create_and_login_demo_user(user_manager: UserManager = injectable,
security_service: SecurityService = injectable):
user = user_manager.create(email='demo@example.com',
password='password',
favorite_color='magenta',
is_active=True,
commit=True)
security_service.login_user(user)
By default the Security Bundle only comes with the /login
and /logout
URLs enabled. Let’s confirm we’ve also enabled /register
:
flask urls
Method(s) Rule Endpoint View
-------------------------------------------------------------------------------------------------------------------------------
GET / site_controller.index quotes.SiteController.index
GET /authors/<int:id> site_controller.author quotes.SiteController.author
GET /api/v1/authors author_resource.list quotes.AuthorResource.list
GET /api/v1/authors/<int:id> author_resource.get quotes.AuthorResource.get
GET /api/v1/quotes quote_resource.list quotes.QuoteResource.list
GET /api/v1/quotes/<int:id> quote_resource.get quotes.QuoteResource.get
GET, POST /login security_controller.login flask_unchained.bundles.security.SecurityController.login
GET /logout security_controller.logout flask_unchained.bundles.security.SecurityController.logout
GET, POST /register security_controller.register flask_unchained.bundles.security.SecurityController.register
flask run
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
NOTE: you’ll need to logout the demo user by visiting http://127.0.0.1:5000/logout before the login and register endpoints will work.
Going Big (Project Layout)¶
When you want to expand beyond a single file, Flask Unchained defines a standardized (but configurable) folder structure for you so that everything just works. A typical structure looks like this:
/home/user/dev/project-root
├── unchained_config.py # the Flask Unchained config
├── app # the app bundle Python package
│ ├── admins # Flask-Admin 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 ModelResources
│ ├── __init__.py # your AppBundle subclass
│ ├── config.py # your app config
│ └── routes.py # declarative routes
├── bundles # custom bundles and/or bundle extensions/overrides
│ └── security # a customized/extended Security Bundle
│ ├── models
│ ├── serializers
│ ├── services
│ ├── templates
│ └── __init__.py
├── db
│ └── migrations # migrations generated by Flask-Migrate
├── static # the top-level static assets folder
├── templates # the top-level templates folder
└── tests # your pytest tests
Want to start building now? Check out the Tutorial! There are also some example open source apps available:
Open a PR to add yours!
Features¶
Bundles¶
Bundles are powerful and flexible. They are standalone Python packages that can do anything from integrate Flask extensions to be full-blown apps your app can integrate and extend (like, say, a blog or web store). Conceptually, a bundle is a blueprint, and Flask Unchained gives you complete control to configure not only which views from each bundle get registered with your app and at what routes, but also to extend and/or override anything else you might want to from the bundles you enable.
Some examples of what you can customize from bundles include configuration, controllers, resources, and routes, templates, extensions and services, and models and serializers. Each uses simple and consistent patterns that work the same way across every bundle. Extended/customized bundles can themselves also be distributed as their own projects, and support the same patterns for customization, ad infinitum.
Bundle Structure¶
The example “hello world” app bundle lived in a single file, while a “full” bundle package typically consists of many modules (as shown just above under Project Layout). The module locations for your code are customizable on a per-bundle basis by setting class attributes on your Bundle
subclass, for example:
# your_custom_bundle/__init__.py
from flask_unchained import Bundle
class YourCustomBundle(Bundle):
config_module_name = 'settings'
routes_module_name = 'urls'
views_module_names = ['controllers', 'resources', 'views']
You can see the default module names and the override attribute names to set on your Bundle
subclass by printing the ordered list of hooks that will run for your app using flask unchained hooks
:
flask unchained hooks
Hook Name Default Bundle Module Bundle Module Override Attr
-------------------------------------------------------------------------
register_extensions extensions extensions_module_names
models models models_module_names
configure_app config config_module_name
init_extensions extensions extensions_module_names
services services services_module_names
commands commands commands_module_names
routes routes routes_module_name
bundle_blueprints (None) (None)
blueprints views blueprints_module_names
views views views_module_names
model_serializers serializers model_serializers_module_names
model_resources views model_resources_module_names
celery_tasks tasks celery_tasks_module_names
Bundle Blueprints¶
Bundles are blueprints, so if you want to define request/response functions that should only run for views from a specific bundle, you can do that like so:
from flask_unchained import Bundle, unchained
class YourCoolBundle(Bundle):
name = 'your_cool_bundle' # the default (snake_cased class name)
@unchained.your_cool_bundle.before_request
def this_only_runs_before_requests_to_views_from_your_cool_bundle():
pass
# the other supported decorators are also available:
@unchained.your_cool_bundle.after_request
@unchained.your_cool_bundle.teardown_request
@unchained.your_cool_bundle.context_processor
@unchained.your_cool_bundle.url_defaults
@unchained.your_cool_bundle.url_value_preprocessor
@unchained.your_cool_bundle.errorhandler
The API here is the same as flask.Blueprint
, however, its methods must be accessed via the Unchained
extension. The syntax is @unchained.bundle_name.blueprint_method_name
.
Wait but why?
Sadly, there are some very serious technical limitations with the implementation of flask.Blueprint
such that its direct usage breaks the power and flexibility of Flask Unchained. Under the hood, Flask Unchained does indeed use a blueprint for each bundle - you just never interact with them directly.
You can technically continue using flask.Blueprint
strictly for views in your app bundle, however this support is only kept around for porting purposes. Note that even in your app bundle, views from blueprints unfortunately will not work with declarative routing.
Extending and Overriding Bundles¶
Extending and overriding bundles is pretty simple. All you need to do is subclass the bundle you want to extend in its own Python package, and include that package in your unchained_config.BUNDLES
instead of the original bundle. There is no limit to the depth of the bundle hierarchy (other than perhaps your sanity). So, for example, to extend the Security Bundle, it would look like this:
# project-root/bundles/security/__init__.py
from flask_unchained.bundles.security import SecurityBundle as BaseSecurityBundle
class SecurityBundle(BaseSecurityBundle):
pass
# project-root/unchained_config.py
BUNDLES = [
# ...
'bundles.security',
'app',
]
The App Bundle¶
When defining the app bundle, you must subclass AppBundle
instead of Bundle
:
# project-root/app/__init__.py
from flask_unchained import AppBundle
class App(AppBundle):
pass
Everything about your app bundle is otherwise the same as regular bundles, except the app bundle can extend and/or override anything from any bundle.
The Unchained Extension¶
The “orchestrator” that ties everything together. It handles dependency injection and enables access to much of the public API of flask.Flask
and flask.Blueprint
:
# project-root/app.py
from flask_unchained import unchained, injectable
@unchained.inject()
def print_hello(name: str, hello_service: HelloService = injectable):
print(hello_service.hello_world(name))
@unchained.before_first_request
def runs_once_at_startup():
print_hello("App")
@unchained.app.after_request
def runs_after_each_request_to_an_app_bundle_view(response):
print_hello("Response")
return response
The Unchained
extension also plays a role in the app factory:
The App Factory¶
The AppFactory
discovers all the code from your app and its bundles, and then with it automatically initializes, configures, and “boots up” the Flask app
instance for you. I know that sounds like magic, but it’s actually quite easy to understand, and every step it takes can be customized by you if necessary. In barely-pseudo-code, the app factory looks like this:
from flask import Flask
from flask_unchained import DEV, PROD, STAGING, TEST
class AppFactory:
APP_CLASS = Flask
def create_app(self, env: Union[DEV, PROD, STAGING, TEST]) -> Flask:
# load the Unchained Config and configured bundles
unchained_config = self.load_unchained_config(env)
app_bundle, bundles = self.load_bundles(unchained_config.BUNDLES)
# instantiate the Flask app instance
app = self.APP_CLASS(app_bundle.name, **kwargs_from_unchained_config)
# let bundles configure the app pre-initialization
for bundle in bundles:
bundle.before_init_app(app)
# discover code from bundles and boot the app using hooks
unchained.init_app(app, bundles)
# the Unchained extension runs hooks in their correct order:
# (there may be more hooks depending on which bundles you enable)
RegisterExtensionsHook.run_hook(app, bundles)
ConfigureAppHook.run_hook(app, bundles)
InitExtensionsHook.run_hook(app, bundles)
RegisterServicesHook.run_hook(app, bundles)
RegisterCommandsHook.run_hook(app, bundles)
RegisterRoutesHook.run_hook(app, bundles)
RegisterBundleBlueprintsHook.run_hook(app, bundles)
# let bundles configure the app post-initialization
for bundle in bundles:
bundle.after_init_app(app)
# return the app instance ready to rock'n'roll
return app
The flask
and pytest
CLI commands automatically use the app factory for you, while in production you have to call it yourself:
# project-root/wsgi.py
from flask_unchained import AppFactory, PROD
app = AppFactory().create_app(env=PROD)
For a deeper look check out How Flask Unchained Works.
Controllers, Resources, and Templates¶
The controller bundle includes two base classes that all of your views should extend. The first is Controller
, and the second is Resource
, meant for building RESTful APIs.
Controller¶
Chances are Controller
is the base class you want to extend, unless you’re building a RESTful API. Under the hood, the implementation is actually very similar to flask.views.View
, however, they’re not compatible. Controllers include a bit of magic:
# your_bundle/views.py
from flask_unchained import Controller, route, injectable
class SiteController(Controller):
# all of class Meta is optional (automatic defaults shown)
class Meta:
abstract: bool = False
url_prefix = Optional[str] = '/' # aka no prefix
endpoint_prefix: str = 'site_controller' # snake_cased class name
template_folder: str = 'site' # snake_cased class name prefix
template_file_extension: Optional[str] = '.html'
decorators: List[callable] = ()
# controllers automatically support dependency injection
name_service: NameService = injectable
@route('/foobaz', methods=['GET', 'POST'])
def foo_baz():
return self.render('site/foo_baz.html') # template paths can be explicit
# defaults to @route('/view-one', methods=['GET'])
def view_one():
# or just the filename
return self.render('one') # equivalent to 'site/one.html'
# defaults to @route('/view-two', methods=['GET'])
def view_two():
return self.render('two')
# utility function (gets no route)
def _protected_function():
return 'not a view'
On any subclass of Controller
that isn’t abstract, all public methods are automatically assigned default routing rules. In the example above, foo_baz
has a route decorator, but view_one
and view_two
do not. The undecorated views will be assigned default routing rules of /view-one
and /view-two
respectively (the default is to convert the method name to kebab-case). Protected methods (those prefixed with _
) are not assigned routes.
Templates¶
Flask Unchained uses the Jinja templating language, just like Flask.
By default bundles are configured to use a templates
subfolder. This is customizable per-bundle:
# your_bundle/__init__.py
from flask_unchained import Bundle
class YourBundle(Bundle):
template_folder = 'templates' # the default
Controllers each have their own template folder within Bundle.template_folder
. It defaults to the snake_cased class name, with the suffixes Controller
or View
stripped (if any). You can customize it using Controller.Meta.template_folder
.
The default file extension used for templates is configured by setting TEMPLATE_FILE_EXTENSION
in your app config. It defaults to .html
, and is also configurable on a per-controller basis by setting Controller.Meta.template_file_extension
.
Therefore, the above controller corresponds to the following templates folder structure:
./your_bundle
├── templates
│ └── site
│ ├── foo_baz.html
│ ├── one.html
│ └── two.html
├── __init__.py
└── views.py
Extending and Overriding Templates¶
Templates can be overridden by placing an equivalently named template higher up in the bundle hierarchy (i.e. in a bundle extending another bundle, or in your app bundle).
So for example, the Security Bundle includes default templates for all of its views. They are located at security/login.html
, security/register.html
, and so on. Thus, to override them, you would make a security
folder in your app bundle’s templates
folder and put your customized templates with the same names in it. You can even extend the template you’re overriding, using the standard Jinja syntax (this doesn’t work in regular Flask apps):
{# your_app_or_security_bundle/templates/security/login.html #}
{% extends 'security/login.html' %}
{% block content %}
<h1>Login</h1>
{{ render_form(login_user_form, endpoint='security_controller.login') }}
{% endblock %}
If you encounter problems, you can set the EXPLAIN_TEMPLATE_LOADING
config option to True
to help debug what’s going on.
Resources (API Controllers)¶
The Resource
class extends Controller
to add support for building RESTful APIs. The implementation draws much inspiration from Flask-RSETful (specifically, the Resource and Api classes). Using Resource
adds a bit more magic to controllers around specific methods:
Method name on your Resource subclass |
HTTP Method |
URL Rule |
---|---|---|
list |
GET |
/ |
create |
POST |
/ |
get |
GET |
/<cls.Meta.member_param> |
patch |
PATCH |
/<cls.Meta.member_param> |
put |
PUT |
/<cls.Meta.member_param> |
delete |
DELETE |
/<cls.Meta.member_param> |
If you implement any of these methods, then the shown URL rules will automatically be used.
So, for example:
from http import HTTPStatus
from flask_unchained import Resource, injectable, param_converter, request
from flask_unchained.bundles.security import User, UserManager
class UserResource(Resource):
# class Meta is optional on resources (automatic defaults shown)
class Meta:
url_prefix = '/users'
member_param = '<int:id>'
unique_member_param = '<int:user_id>'
# resources are controllers, so they support dependency injection
user_manager: UserManager = injectable
def list():
return self.jsonify(dict(users=self.user_manager.all()))
# NOTE: returning SQLAlchemy models directly like this is
# only supported by ModelResource from the API Bundle
def create():
data = request.get_json()
user = self.user_manager.create(**data, commit=True)
return self.jsonify(dict(user=user), code=HTTPStatus.CREATED)
@param_converter(id=User)
def get(user):
return self.jsonify(dict(user=user)
@param_converter(id=User)
def patch(user):
data = request.get_json()
user = self.user_manager.update(user, **data, commit=True)
return self.jsonify(dict(user=user))
@param_converter(id=User)
def put(user):
data = request.get_json()
user = self.user_manager.update(user, **data, commit=True)
return self.jsonify(dict(user=user))
@param_converter(id=User)
def delete(user):
self.user_manager.delete(user, commit=True)
return self.make_response('', code=HTTPStatus.NO_CONTENT)
Registered like so:
routes = lambda: [
resource(UserResource),
]
Results in the following routes:
GET /users UserResource.list
POST /users UserResource.create
GET /users/<int:id> UserResource.get
PATCH /users/<int:id> UserResource.patch
PUT /users/<int:id> UserResource.put
DELETE /users/<int:id> UserResource.delete
Declarative Routing¶
Using declarative routing, your app bundle has final say over which views (from all bundles) should get registered with the app, as well as their routing rules. By default, it uses the rules decorated on views:
# project-root/app/routes.py
from flask_unchained import (controller, resource, func, include, prefix,
delete, get, patch, post, put, rule)
from flask_unchained.bundles.security import SecurityController
from .views import SiteController
routes = lambda: [
controller(SiteController),
controller(SecurityController),
]
By running flask urls
, we can verify it does what we want:
flask urls
Method(s) Rule Endpoint View
---------------------------------------------------------------------------------------------------------------------------------
GET / site_controller.index app.views.SiteController.index
GET, POST /login security_controller.login flask_unchained.bundles.security.views.SecurityController.login
GET /logout security_controller.logout flask_unchained.bundles.security.views.SecurityController.logout
Declarative routing can also be much more powerful when you want it to be. For example, to build a RESTful SPA with the Security Bundle, your routes might look like this:
# project-root/app/routes.py
from flask_unchained import (controller, resource, func, include, prefix,
delete, get, patch, post, put, rule)
from flask_unchained.bundles.security import SecurityController, UserResource
from .views import SiteController
routes = lambda: [
controller(SiteController),
controller('/auth', SecurityController, rules=[
get('/reset-password/<token>', SecurityController.reset_password,
endpoint='security_api.reset_password'),
]),
prefix('/api/v1', [
controller('/auth', SecurityController, rules=[
get('/check-auth-token', SecurityController.check_auth_token,
endpoint='security_api.check_auth_token', only_if=True),
post('/login', SecurityController.login,
endpoint='security_api.login'),
get('/logout', SecurityController.logout,
endpoint='security_api.logout'),
post('/send-confirmation-email',
SecurityController.send_confirmation_email,
endpoint='security_api.send_confirmation_email'),
post('/forgot-password', SecurityController.forgot_password,
endpoint='security_api.forgot_password'),
post('/reset-password/<token>', SecurityController.reset_password,
endpoint='security_api.post_reset_password'),
post('/change-password', SecurityController.change_password,
endpoint='security_api.change_password'),
]),
resource('/users', UserResource),
]),
]
Which results in the following:
flask urls
Method(s) Rule Endpoint View
------------------------------------------------------------------------------------------------------------------------------------------------------------------------
GET / site_controller.index app.views.SiteController.index
GET /auth/reset-password/<token> security_api.reset_password flask_unchained.bundles.security.views.SecurityController.reset_password
GET /api/v1/auth/check-auth-token security_api.check_auth_token flask_unchained.bundles.security.views.SecurityController.check_auth_token
POST /api/v1/auth/login security_api.login flask_unchained.bundles.security.views.SecurityController.login
GET /api/v1/auth/logout security_api.logout flask_unchained.bundles.security.views.SecurityController.logout
POST /api/v1/auth/send-confirmation-email security_api.send_confirmation_email flask_unchained.bundles.security.views.SecurityController.send_confirmation_email
POST /api/v1/auth/forgot-password security_api.forgot_password flask_unchained.bundles.security.views.SecurityController.forgot_password
POST /api/v1/auth/reset-password/<token> security_api.post_reset_password flask_unchained.bundles.security.views.SecurityController.reset_password
POST /api/v1/auth/change-password security_api.change_password flask_unchained.bundles.security.views.SecurityController.change_password
POST /api/v1/users user_resource.create flask_unchained.bundles.security.views.UserResource.create
GET /api/v1/users/<int:id> user_resource.get flask_unchained.bundles.security.views.UserResource.get
PATCH /api/v1/users/<int:id> user_resource.patch flask_unchained.bundles.security.views.UserResource.patch
Here is a summary of the functions imported at the top of the routes.py
module:
Function |
Description |
---|---|
Include all of the routes from the specified module at that point in the tree. |
|
Prefixes all of the child routing rules with the given prefix. |
|
Registers a function-based view with the app, optionally specifying the routing rules. |
|
Registers a controller and its views with the app, optionally customizing the routes to register. |
|
Registers a resource and its views with the app, optionally customizing the routes to register. |
|
Define customizations to a controller/resource method’s route rules. |
|
Like |
Dependency Injection and Services¶
Flask Unchained supports dependency injection of services and extensions (by default).
Services¶
For services to be automatically discovered, they must subclass Service
and (by default) live in a bundle’s services
or managers
modules. You can however manually register anything as a “service”, even plain values if you really wanted to, using the unchained.service
decorator and/or the unchained.register_service
method:
from flask_unchained import unchained
@unchained.service(name='something')
class SomethingNotExtendingService:
pass
A_CONST = 'a constant'
unchained.register_service('A_CONST', A_CONST)
Services can request other services be injected into them, and as long as there are no circular dependencies, it will work:
from flask_unchained import Service, injectable
class OneService(Service):
something: SomethingNotExtendingService = injectable
A_CONST: str = injectable
class TwoService(Service):
one_service: OneService = injectable
By setting the default value of a class attribute or function/method argument to the flask_unchained.injectable
constant, you are informing the Unchained
extension that it should inject those arguments.
Important
The names of services must be unique across all of the bundles in your app (by default services are named as the snake_cased class name). If there are any conflicting class names then you will need to use the unchained.service
decorator or the unchained.register_service
method to customize the name the service gets registered under:
from flask_unchained import Service, unchained
@unchained.service('a_unique_name')
class ServiceWithNameConflict(Service):
pass
Automatic Dependency Injection¶
Dependency injection works automatically on all classes extending Service
and Controller
. The easiest way is with class attributes:
from flask_unchained import Controller, injectable
from flask_unchained.bundles.security import Security, SecurityService
from flask_unchained.bundles.sqlalchemy import SessionManager
class SecurityController(Controller):
security: Security = injectable
security_service: SecurityService = injectable
session_manager: SessionManager = injectable
It also works on the constructor, which is functionally equivalent, just more verbose:
class SiteController(Controller):
def __init__(self, security: Security = injectable):
self.security = security
Manual Dependency Injection¶
You can use the unchained.inject
decorator just about anywhere else you want to inject something:
from flask_unchained import unchained, injectable
# decorate a class to use class attributes injection
@unchained.inject()
class Foobar:
some_service: SomeService = injectable
# or you can decorate individual methods
@unchained.inject()
def a_method(self, another_service: AnotherService = injectable):
pass
# it works on regular functions too
@unchained.inject()
def a_function(some_service: SomeService = injectable):
pass
Alternatively, you can also use unchained.get_local_proxy
:
from flask_unchained import unchained
db = unchained.get_local_proxy('db')
Extending and Overriding Services¶
Services are just classes, so they follow the normal Python inheritance rules. All you need to do is name your service the same as the one you want to customize, placed in the services
module higher up in the bundle hierarchy (i.e. in a bundle extending another bundle, or in your app bundle).
Integrating Flask Extensions¶
Extensions that can be used in Flask Unchained bundles have a few limitations. The primary one being, the extension must implement init_app
, and its signature must take a single argument: app
. Some extensions fit this restriction out of the box, but often times you will need to subclass the extension to make sure its init_app
signature matches. You can create new config options to replace arguments that were originally passed into the extension’s constructor and/or init_app
method.
In order for Flask Unchained to actually discover and initialize the extension you want to include, they must be placed in your bundle’s extensions
module. It looks like this:
# your_bundle/extensions.py
from flask_whatever import WhateverExtension
whatever = WhateverExtension()
EXTENSIONS = {
'whatever': whatever,
}
The keys of the EXTENSIONS
dictionary serve as the name that will be used to reference the extension at runtime (and for dependency injection). There can be multiple extensions per bundle, and you can also declare other extensions as dependencies that must be initialized before yours:
EXTENSIONS = {
'whatever': (whatever, ['dep_ext_one', 'dep_ext_two']),
}
Tutorial¶
This tutorial will walk you through creating a basic portfolio application for monitoring your investments. Users will be able to register, log in, create portfolios and manage the stocks in them. You will be able to package and install the application on other computers.
It is assumed you’re already familiar with:
Python 3.7+. The official tutorial is a great way to learn or review.
The Jinja2 templating engine. See the official docs for more info.
Table of Contents
Getting Started¶
Install Flask Unchained¶
Create a new directory and enter it:
mkdir hello-flask-unchained && cd hello-flask-unchained
The tutorial will assume you’re working from the hello-flask-unchained
directory from now on. All commands are assumed to be run from this top-level project directory, and the file names at the top of each code block are also relative to this directory.
Next, let’s create a new virtualenv, install Flask Unchained into it, and activate it:
# create our virtualenv and activate it
python3 -m venv venv && . venv/bin/activate
# install flask-unchained
pip install "flask-unchained[dev]"
# reactivate the virtualenv so that pytest will work correctly
deactivate && . venv/bin/activate
Python Virtual Environments
There are other ways to create virtualenvs for Python, and if you have a different preferred method that’s fine, but you should always use a virtualenv by some way or another.
Project Layout¶
Just like Flask, Flask Unchained apps can be written either as a single file or in multiple files following a (configurable) naming convention. A large project might have a folder structure that looks like this:
/home/user/dev/hello-flask-unchained
├── app # your app bundle package
│ ├── admins # model admins
│ ├── commands # click groups/commands
│ ├── extensions # extension instances
│ ├── models # sqlalchemy models
│ ├── serializers # marshmallow serializers (aka schemas)
│ ├── services # dependency-injectable services
│ ├── tasks # celery tasks
│ ├── templates # jinja templates
│ ├── views # controllers and resources
│ ├── __init__.py
│ ├── config.py # app config
│ └── routes.py # declarative routes
├── assets # static assets to be handled by Webpack
│ ├── images
│ ├── scripts
│ └── styles
├── bundles # third-party bundle extensions/overrides
│ └── security # a customized/extended Security Bundle
│ ├── models
│ ├── serializers
│ ├── templates
│ └── __init__.py
├── db
│ ├── fixtures # sqlalchemy model fixtures (for seeding the dev db)
│ └── migrations # alembic 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
By the end of this tutorial, we’ll have built something very close. But for now, let’s start with the basics.
A Minimal Hello World App¶
The starting project layout of our hello world app is three files:
/home/user/dev/hello-flask-unchained
├── unchained_config.py
├── app.py
└── test_app.py
Let’s create them:
touch unchained_config.py app.py test_app.py
And the code:
1 2 3 4 5 | # unchained_config.py
BUNDLES = [
'app',
]
|
1 2 3 4 5 6 7 8 9 10 11 | # app.py
from flask_unchained import AppBundle, Controller, route
class App(AppBundle):
pass
class SiteController(Controller):
@route('/')
def index(self):
return 'Hello World!'
|
Whenever you create a new app in Flask Unchained, you start by creating a new “app bundle”: This is an overloaded term. The app bundle, conceptually, is your app. Literally, the app bundle is a subclass of AppBundle
that must live in your app bundle’s module root (app.py here).
We can now start the development server with flask run
and you should see your site running at http://localhost:5000:
flask run
* Environment: development
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
Let’s add a quick test before we continue.
# test_app.py
class TestSiteController:
def test_index(self, client):
r = client.get('site_controller.index')
assert r.status_code == 200
assert r.html.count('Hello World!') == 1
Here, we’re using the HTTP client
pytest fixture to request the URL for the endpoint "site_controller.index"
, verifying the response has a status code of 200
, and lastly checking that the string "Hello World!"
is in the response.
Let’s make sure it passes:
pytest
======================== test session starts ========================
platform linux -- Python 3.6.6, pytest-3.6.4, py-1.5.4, pluggy-0.7.1
rootdir: /home/user/dev/hello-flask-unchained, inifile:
plugins: flask-0.10.0, Flask-Unchained-0.8.0
collected 1 item
test_app.py . [100%]
======================== 1 passed in 0.18 seconds ====================
NOTE: If you get any errors, you may need to deactivate and reactivate your virtualenv if you haven’t already since installing pytest
.
If you haven’t already, now would be a good time to initialize a git repo and make our first commit. Before we do that though, let’s add a .gitignore
file to make sure we don’t commit anything that shouldn’t be.
# .gitignore
*.egg-info
*.pyc
.coverage
.cache/
.pytest_cache/
.tox/
__pycache__/
build/
coverage_html_report/
db/*.sqlite
dist/
docs/_build
venv/
Initialize the repo and make our first commit:
git init
git add .
# review to make sure it's not going to do anything you don't want it to:
git status
git commit -m 'initial hello world commit'
OK, everything works, but this is about as basic as it gets. Let’s make things a bit more interesting by moving on to Views, Templates, and Static Assets.
Views, Templates, and Static Assets¶
So far we’ve been returning a raw string from our view function. This works fine for demo purposes, however, in the real world you’ll most often use template files. Let’s create a directory each for our templates and static assets.
mkdir -p templates static \
&& touch templates/layout.html templates/_navbar.html templates/_flashes.html
These directories are the standard locations the Flask
constructor expects, so they will automatically be used. (Note however that if just created either of these directories, then you will need to restart the development server for them to be picked up by it.)
Our site looks pretty weak as it stands. Let’s add Bootstrap to spruce things up a bit:
wget https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/js/bootstrap.min.js -O static/bootstrap-v4.1.2.min.js \
&& wget https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css -O static/bootstrap-v4.1.2.min.css \
&& wget https://code.jquery.com/jquery-3.3.1.slim.min.js -O static/jquery-v3.3.1.slim.min.js \
&& wget https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js -O static/popper-v1.14.3.min.js
Layout Template¶
In order to share as much code as possible between templates, it’s best practice to abstract away the shared boilerplate into templates/layout.html
. Just like vanilla Flask, Flask Unchained uses the Jinja2 templating engine. If you’re unfamiliar with what anything below is doing, I recommend checking out the excellent official Jinja2 documentation.
Now let’s write our templates/layout.html
file:
{# 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 %}Hello Flask Unchained!{% endblock %}
</title>
{% block stylesheets %}
<link rel="stylesheet" href="{{ url_for('static', filename='bootstrap-v4.1.2.min.css') }}">
{% endblock stylesheets %}
{% block extra_head %}
{% endblock extra_head %}
</head>
<body>
{% block header %}
<header>
{% block navbar %}
{% include '_navbar.html' %}
{% endblock %}
</header>
{% endblock%}
{% block body %}
<div class="container">
{% include '_flashes.html' %}
{% block content %}
{% endblock content %}
</div>
{% endblock body %}
{% block javascripts %}
<script src="{{ url_for('static', filename='jquery-v3.3.1.slim.min.js') }}"></script>
<script src="{{ url_for('static', filename='popper-v1.14.3.min.js') }}"></script>
<script src="{{ url_for('static', filename='bootstrap-v4.1.2.min.js') }}"></script>
{% endblock javascripts %}
</body>
</html>
And also the included templates/_flashes.html
and templates/_navbar.html
templates:
{# templates/_flashes.html #}
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
<div class="row flashes">
<div class="col">
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissable fade show" role="alert">
{{ message }}
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endwith %}
{# templates/_navbar.html #}
{% macro nav_link(label) %}
{% set href = kwargs.get('href', url_for(kwargs['endpoint'])) %}
<li class="nav-item {% if kwargs is active %}active{% endif %}">
<a class="nav-link" href="{{ href }}">
{{ label }}
{% if kwargs is active %}
<span class="sr-only">(current)</span>
{% endif %}
</a>
</li>
{% endmacro %}
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" href="{{ url_for('site_controller.index') }}">
Hello Flask Unchained
</a>
<button type="button"
class="navbar-toggler"
data-toggle="collapse"
data-target="#navbarCollapse"
aria-controls="navbarCollapse"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav mr-auto">
{{ nav_link('Home', endpoint='site_controller.index') }}
</ul>
</div>
</nav>
The nav_link
macro perhaps deserves some explanation. This is a small utility function that renders a navigation item in the bootstrap navbar. We do this to make our code more DRY, because every navigation link needs to contain logic to determine whether or not it is the currently active view. The {% if endpoint is active %}
bit is special - Flask Unchained adds the active
template test by default to make this easier.
And now let’s update our app/templates/site/index.html
template to use our new layout template:
{# app/templates/site/index.html #}
{% extends 'layout.html' %}
{% block title %}Hello World!{% endblock %}
{% block content %}
<div class="row">
<div class="col">
<h1>Hello World!</h1>
</div>
</div>
{% endblock %}
Tests should still pass…
pytest
=================================== test session starts ====================================
platform linux -- Python 3.6.6, pytest-3.6.4, py-1.5.4, pluggy-0.7.1
rootdir: /home/user/dev/hello-flask-unchained, inifile:
plugins: flask-0.10.0, Flask-Unchained-0.8.0
collected 1 item
tests/app/test_views.py . [100%]
================================= 1 passed in 0.10 seconds =================================
This seems like a good place to make a commit:
git add .
git status
git commit -m 'refactor templates to extend a base layout template'
Customizing Styles¶
If you take a look at how our new template looks, it’s pretty good, but the h1
tag is now very close to the navbar. Let’s fix that by adding some style customizations:
mkdir static/vendor \
&& mv static/*.min.* static/vendor \
&& touch static/main.css
Let’s update the stylesheets
and javascripts
blocks in our layout template to reference the changed locations of the vendor assets, and our new main.css
stylesheet:
{# templates/layout.html #}
{% block stylesheets %}
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/bootstrap-v4.1.2.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='main.css') }}">
{% endblock stylesheets %}
{% block javascripts %}
<script src="{{ url_for('static', filename='vendor/jquery-v3.3.1.slim.min.js') }}"></script>
<script src="{{ url_for('static', filename='vendor/popper-v1.14.3.min.js') }}"></script>
<script src="{{ url_for('static', filename='vendor/bootstrap-v4.1.2.min.js') }}"></script>
{% endblock javascripts %}
And of course, the custom rule for our h1
tags:
/* static/main.css */
h1 {
padding-top: 0.5em;
margin-top: 0.5em;
}
Let’s commit our changes:
git add .
git status
git commit -m 'add a custom stylesheet'
Adding a Landing Page¶
OK, let’s refactor our views so we have a landing page and a separate page for the hello view. We’re also going to introduce flask_unchained.decorators.param_converter()
here so that we can (optionally) customizable the name we’re saying hello to via the query string:
# app/views.py
from flask_unchained import Controller, route, param_converter
class SiteController(Controller):
@route('/')
def index(self):
return self.render('index')
@route('/hello')
@param_converter(name=str)
def hello(self, name=None):
name = name or 'World'
return self.render('hello', name=name)
The param_converter
converts arguments passed in via the query string to arguments that get passed to the decorated view function. It can make sure you get the right type via a callable (like here), or as we’ll cover later, it can even convert unique identifiers from the URL directly into database models. But that’s getting ahead of ourselves.
Now that we’ve added another view/route, our templates need some work again. Let’s update the navbar, move our existing index.html
template to hello.html
(adding support for the name
template context variable), and lastly add a new index.html
template for the landing page.
{# templates/_navbar.html #}
<ul class="navbar-nav mr-auto">
{{ nav_link('Home', endpoint='site_controller.index') }}
{{ nav_link('Hello', endpoint='site_controller.hello') }} <!-- add this line -->
</ul>
{# app/templates/site/hello.html #}
{% extends 'layout.html' %}
{% block title %}Hello {{ name }}!{% endblock %}
{% block content %}
<div class="row">
<div class="col">
<h1>Hello {{ name }}!</h1>
</div>
</div>
{% endblock %}
{# app/templates/site/index.html #}
{% extends 'layout.html' %}
{% block body %}
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col">
<h1 class="display-3">Hello Flask Unchained!</h1>
</div>
</div>
</div>
</div>
{% endblock %}
We need to update our tests:
# tests/app/test_views.py
class TestSiteController:
def test_index(self, client, templates):
r = client.get('site_controller.index')
assert r.status_code == 200
assert templates[0].template.name == 'site/index.html'
assert r.html.count('Hello Flask Unchained!') == 2
def test_hello(self, client, templates):
r = client.get('site_controller.hello')
assert r.status_code == 200
assert templates[0].template.name == 'site/hello.html'
assert r.html.count('Hello World!') == 2
def test_hello_with_name_parameter(self, client, templates):
r = client.get('site_controller.hello', name='User')
assert r.status_code == 200
assert templates[0].template.name == 'site/hello.html'
assert r.html.count('Hello User!') == 2
A couple things to note here. Most obviously, we added another view, and therefore need to add methods to test it. Also of note is the templates
pytest fixture, which we’re using to verify the correct template got rendered for each of the views.
Let’s make sure they pass:
pytest
=================================== test session starts ===================================
platform linux -- Python 3.6.6, pytest-3.6.4, py-1.5.4, pluggy-0.7.1
rootdir: /home/user/dev/hello-flask-unchained, inifile:
plugins: flask-0.10.0, Flask-Unchained-0.8.0
collected 3 items
tests/app/test_views.py ... [100%]
================================ 3 passed in 0.17 seconds =================================
Cool. You guessed it, time to make a commit!
git add .
git status
git commit -m 'add landing page, parameterize hello view to accept a name'
Adding a Form to the Hello View¶
We’ve parameterized our hello view take a name
argument, however, it’s not exactly discoverable by users (unless perhaps they’re a developer with good variable naming intuition). One way to improve this is by using a form. First, we’ll add a form the old-school way, followed by a refactor to use Flask-WTF form classes.
Let’s update our hello template:
{# app/templates/site/hello.html #}
{% extends 'layout.html' %}
{% block title %}Hello {{ name }}!{% endblock %}
{% block content %}
<div class="row">
<div class="col">
<h1>Hello {{ name }}!</h1>
<h2>Enter your name:</h2>
<form name="hello_form" action="{{ url_for('site_controller.hello') }}" method="POST">
{% if error %}
<ul class="errors">
<li class="error">{{ error }}</li>
</ul>
{% endif %}
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" class="form-control" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
{% endblock %}
And the corresponding view code:
# app/views.py
# import request from flask_unchained
from flask_unchained import Controller, request, route, param_converter
class SiteController(Controller):
# and update the code for our hello view
@route('/hello', methods=['GET', 'POST'])
@param_converter(name=str)
def hello(self, name=None):
if request.method == 'POST':
name = request.form['name']
if not name:
return self.render('hello', error='Name is required.', name='World')
return self.redirect('hello', name=name)
return self.render('hello', name=name or 'World')
A wee styling update to also put some spacing above h2
headers:
/* static/main.css */
h1, h2 {
padding-top: 0.5em;
margin-top: 0.5em;
}
And let’s fix our tests:
# tests/app/test_views.py
# add this import
from flask_unchained import url_for
class TestSiteController:
# and add this method
def test_hello_with_form_post(self, client, templates):
r = client.post('site_controller.hello', data=dict(name='User'))
assert r.status_code == 302
assert r.path == url_for('site_controller.hello')
r = client.follow_redirects(r)
assert r.status_code == 200
assert r.html.count('Hello User!') == 2
# note: when request is a POST, the templates fixture only works after redirecting
assert templates[0].template.name == 'site/hello.html'
Make sure they pass,
pytest
================================== test session starts ===================================
platform linux -- Python 3.6.6, pytest-3.7.1, py-1.5.4, pluggy-0.7.1
rootdir: /home/user/dev/hello-flask-unchained, inifile:
plugins: flask-0.10.0, Flask-Unchained-0.8.0
collected 4 items
tests/app/test_views.py .... [100%]
================================ 4 passed in 0.16 seconds ================================
And commit our changes once satisfied:
git add .
git status
git commit -m 'add a form to the hello view'
Converting to a Flask-WTF Form¶
The above method works, as far as it goes, but both our view code and our template code are very verbose, and the form verification/error handling is awfully manual. Luckily the Flask ecosystem has a solution to this problem, in the awesomely named Flask-WTF
package (it’s installed by default as a dependency of Flask Unchained). With it, our new form looks like this:
touch app/forms.py
# app/forms.py
from flask_unchained.forms import FlaskForm, fields, validators
class HelloForm(FlaskForm):
name = fields.StringField('Name', validators=[
validators.DataRequired('Name is required.')])
submit = fields.SubmitField('Submit')
The updated view code:
# app/views.py
from flask_unchained import Controller, request, route, param_converter
from .forms import HelloForm
class SiteController(Controller):
@route('/')
def index(self):
return self.render('index')
@route('/hello', methods=['GET', 'POST'])
@param_converter(name=str)
def hello(self, name=None):
form = HelloForm(request.form)
if form.validate_on_submit():
return self.redirect('hello', name=form.name.data)
return self.render('hello', hello_form=form, name=name or 'World')
And the updated template:
{# app/templates/site/hello.html #}
{% extends 'layout.html' %}
{% from '_macros.html' import render_form %}
{% block title %}Hello {{ name }}!{% endblock %}
{% block content %}
<div class="row">
<div class="col">
<h1>Hello {{ name }}!</h1>
<h2>Enter your name:</h2>
{{ render_form(hello_form, endpoint='site_controller.hello') }}
</div>
</div>
{% endblock %}
What is this mythical render_form
macro? Well, we need to write it ourselves. But luckily once it’s written, it should work on the majority of FlaskForm
subclasses. Here’s the code for it:
touch templates/_macros.html
{% macro render_form(form) %}
{% set action = kwargs.get('action', url_for(kwargs['endpoint'])) %}
<form name="{{ form._name }}" {% if action %}action="{{ action }}"{% endif %} method="POST">
{{ render_errors(form.errors.get('_error', [])) }}
{% for field in form %}
{{ render_field(field) }}
{% endfor %}
</form>
{% endmacro %}
{% macro render_field(field) %}
{% set input_type = field.widget.input_type %}
{% if input_type == 'hidden' %}
{{ field(**kwargs)|safe }}
{% elif input_type == 'submit' %}
<div class="form-group">
{{ field(class='btn btn-primary', **kwargs)|safe }}
</div>
{% else %}
<div class="form-group">
{% if input_type == 'checkbox' %}
<label for="{{ field.id }}">
{{ field(**kwargs)|safe }} {{ field.label.text }}
</label>
{% else %}
{{ field.label }}
{{ field(class='form-control', **kwargs)|safe }}
{% endif %}
{# always render description and/or errors if they are present #}
{% if field.description %}
<small class="form-text text-muted form-field-description">
{{ field.description }}
</small>
{% endif %}
{{ render_errors(field.errors) }}
</div> {# /.form-group #}
{% endif %}
{% endmacro %}
{% macro render_errors(errors) %}
{% if errors %}
<ul class="errors">
{% for error in errors %}
<li class="error">{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
{% endmacro %}
More complicated forms, for example those with multiple submit buttons or multiple pages, that require more manual control over the presentation can use render_field
directly for each field in the form.
As usual, let’s update our tests and make sure they pass:
# tests/app/test_views.py
class TestSiteController:
# add this method
def test_hello_errors_with_empty_form_post(self, client, templates):
r = client.post('site_controller.hello')
assert r.status_code == 200
assert templates[0].template.name == 'site/hello.html'
assert r.html.count('Name is required.') == 1
pytest
================================== test session starts ===================================
platform linux -- Python 3.6.6, pytest-3.7.1, py-1.5.4, pluggy-0.7.1
rootdir: /home/user/dev/hello-flask-unchained, inifile:
plugins: flask-0.10.0, Flask-Unchained-0.8.0
collected 5 items
tests/app/test_views.py ..... [100%]
================================ 5 passed in 0.19 seconds ================================
Once your tests are passing, it’s time to make commit:
git add .
git status
git commit -m 'refactor hello form to use flask-wtf'
Enabling CSRF Protection¶
By default, CSRF protection is disabled. However, any time you’re using forms or have enabled authentication (covered later), you should also enable CSRF protection. There are two requirements:
The first is to update our configuration:
touch app/config.py
# app/config.py
from flask_unchained import BundleConfig
class Config(BundleConfig):
SECRET_KEY = 'some-secret-key'
WTF_CSRF_ENABLED = True
class TestConfig(Config):
WTF_CSRF_ENABLED = False
And secondly, we need to actually send the CSRF token in the cookie with every response:
# app/__init__.py
from flask_unchained import AppBundle, generate_csrf
class App(AppBundle):
def after_init_app(self, app) -> None:
@app.after_request
def set_csrf_token_cookie(response):
if response:
response.set_cookie('csrf_token', generate_csrf())
return response
Tests should still pass, so it’s time to make commit:
git add .
git status
git commit -m 'enable CSRF protection'
Cool. Let’s move on to Setting up the Database in preparation for installing the Security Bundle.
Setting up the Database¶
Flask Unchained comes integrated with the SQLAlchemy ORM.
Install Dependencies¶
First we need to install SQLAlchemy and related dependencies:
pip install "flask-unchained[sqlalchemy]" py-yaml-fixtures
We also need to update our tests so that they load the pytest fixtures from the SQLAlchemy Bundle:
touch tests/conftest.py
# tests/conftest.py
from flask_unchained.bundles.sqlalchemy.pytest import *
There are two fixtures that it includes: db
and db_session
. They’re both automatically used; db
is scoped session
and will create/drop all necessary tables once per test session while db_session
is function scoped, and thus will run for every test. Its responsibility is to create an isolated session of transactions for each individual test, to make sure that every test starts with a clean slate database without needing to drop and recreate all the tables for each and every test.
Next, enable the SQLAlchemy and Py YAML Fixtures bundles so we can begin using them:
# unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.sqlalchemy',
'py_yaml_fixtures',
'app',
]
Configuration¶
By default, the SQLAlchemy Bundle is configured to use an SQLite database. For the sake of simplicity, we’ll leave the defaults as-is. The SQLite file will be stored at db/<env>.sqlite
.
If you’d like to change the path, it would look like this:
# app/config.py
from flask_unchained import BundleConfig
class Config(BundleConfig):
# ...
SQLALCHEMY_DATABASE_URI = 'sqlite:///db/hello-flask-unchained.sqlite'
If you’re fine using SQLite, continue to Initialize Migrations.
If instead you’d like to use MariaDB/MySQL or PostgreSQL, now would be the time to configure it. For example, to use PostgreSQL with psycopg2
:
# app/config.py
from flask_unchained import BundleConfig
class Config(BundleConfig):
# ...
SQLALCHEMY_DATABASE_URI = '{engine}://{user}:{pw}@{host}:{port}/{db}'.format(
engine=os.getenv('FLASK_DATABASE_ENGINE', 'postgresql+psycopg2'),
user=os.getenv('FLASK_DATABASE_USER', 'hello_fun'),
pw=os.getenv('FLASK_DATABASE_PASSWORD', 'hello_fun'),
host=os.getenv('FLASK_DATABASE_HOST', '127.0.0.1'),
port=os.getenv('FLASK_DATABASE_PORT', 5432),
db=os.getenv('FLASK_DATABASE_NAME', 'hello_fun'))
class TestConfig:
# ...
SQLALCHEMY_DATABASE_URI = '{engine}://{user}:{pw}@{host}:{port}/{db}'.format(
engine=os.getenv('FLASK_DATABASE_ENGINE', 'postgresql+psycopg2'),
user=os.getenv('FLASK_DATABASE_USER', 'hello_fun_test'),
pw=os.getenv('FLASK_DATABASE_PASSWORD', 'hello_fun_test'),
host=os.getenv('FLASK_DATABASE_HOST', '127.0.0.1'),
port=os.getenv('FLASK_DATABASE_PORT', 5432),
db=os.getenv('FLASK_DATABASE_NAME', 'hello_fun_test'))
Or for MariaDB/MySQL, replace the engine
parameter with mysql+mysqldb
and the port
parameter with 3306
.
Note that you’ll probably need to install the relevant driver package, eg:
# for psycopg2
pip install psycopg2-binary
# for mysql
pip install mysqlclient
See the upstream docs on SQLAlchemy dialects for details.
Initialize Migrations¶
The last step is to initialize the database migrations folder:
flask db init
We should commit our changes before continuing:
git add .
git status
git commit -m 'install sqlalchemy and py-yaml-fixtures bundles'
Next, in order to demonstrate using migrations, and also as preparation for installing the Security Bundle, let’s continue to setting up Server Side Sessions using the Session Bundle.
Server Side Sessions¶
By default, Flask will store user session information in a client-side cookie. This works, however, it has some drawbacks. For instance, it doesn’t allow sessions to be terminated by the server, implementation and user details get exposed in the cookie, and logout isn’t fully implemented (Flask asks the browser to delete the cookie, and it will, but if the cookie had been saved/intercepted and later resubmitted, eg by a nefarious party, the cookie would continue to work even though the user thought they logged out). Using server side sessions solves these problems for us, and it’s really easy to set up thanks to Flask-Session.
Install Dependencies¶
pip install "flask-unchained[session]"
Enable the Session Bundle:
# unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.session',
'app',
]
Configuration¶
Let’s configure the Session Bundle to use SQLAlchemy:
# app/config.py
class Config(BundleConfig):
# ...
SESSION_TYPE = 'sqlalchemy'
SESSION_SQLALCHEMY_TABLE = 'flask_sessions'
Because the Session Bundle is just a thin wrapper around Flask Session, configuration options are the same as documented in the official docs.
Database Migrations¶
And now we need to create the table, using a database migration:
flask db migrate -m 'create sessions table'
It’s always a good idea to inspect the migration to make sure it’s going to do exactly what you expect it to do before running it. It should look something about like this:
# db/migrations/versions/[hash]_create_sessions_table.py
"""create sessions table
Revision ID: 17c5247038a6
Revises:
Create Date: 2018-07-30 11:50:19.709960
"""
from alembic import op
import sqlalchemy as sa
import flask_unchained.bundles.sqlalchemy.sqla.types as sqla_bundle
# revision identifiers, used by Alembic.
revision = '17c5247038a6'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('flask_sessions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('session_id', sa.String(length=255), nullable=False),
sa.Column('data', sa.LargeBinary(), nullable=False),
sa.Column('expiry', sa.DateTime(), nullable=True),
sa.Column('created_at', sqla_bundle.DateTime(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sqla_bundle.DateTime(timezone=True),
server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_flask_sessions')),
sa.UniqueConstraint('session_id', name=op.f('uq_flask_sessions_session_id'))
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('flask_sessions')
# ### end Alembic commands ###
Once you’re satisfied, run the migration:
flask db upgrade
That’s it for setting up server side sessions! Let’s make a commit before we continue:
git add .
git status
git commit -m 'install session bundle'
And proceed to Authentication and Authorization using the Security Bundle.
How Flask Unchained Works¶
The Application Factory¶
The app factory might sound like magic, but it’s actually quite easy to understand, and every step it takes is customizable by you, if necessary. The just-barely pseudo code looks like this:
class AppFactory:
def create_app(self, env):
# first load the user's unchained config
unchained_config = self.load_unchained_config(env)
# next load configured bundles
app_bundle, bundles = self.load_bundles(unchained_config.BUNDLES)
# instantiate the Flask app instance
app = Flask(app_bundle.name, **kwargs_from_unchained_config)
# let bundles configure the app pre-initialization
for bundle in bundles:
bundle.before_init_app(app)
# discover code from bundles and boot up the Flask app using hooks
unchained.init_app(app, bundles)
# the Unchained extension runs hooks in their correct order:
RegisterExtensionsHook.run_hook(app, bundles)
ConfigureAppHook.run_hook(app, bundles)
InitExtensionsHook.run_hook(app, bundles)
RegisterServicesHook.run_hook(app, bundles)
RegisterCommandsHook.run_hook(app, bundles)
RegisterRoutesHook.run_hook(app, bundles)
RegisterBundleBlueprintsHook.run_hook(app, bundles)
# (there may be more depending on which bundles you enable)
# let bundles configure the app post-initialization
for bundle in bundles:
bundle.after_init_app(app)
# return the finalized app ready to rock'n'roll
return app
Advanced
You can subclass flask_unchained.AppFactory
if you need to customize any of its behavior. If you want to take things a step further, Flask Unchained can even be used to create your own redistributable batteries-included web framework for Flask using whichever stack of Flask extensions and Python libraries you prefer.
The Unchained Config¶
The first thing the app factory does is to load your “Unchained Config”. The unchained config is used to declare which bundles should be loaded, as well as for passing any optional keyword arguments to the Flask
constructor.
Dedicated Module Mode¶
The most common way to configure Flask Unchained apps is with a module named unchained_config
in the project root:
/home/user/project-root
├── unchained_config.py # your Unchained Config
└── app.py # your App Bundle
The Unchained Config itself doesn’t actually contain a whole lot. The only required setting is the BUNDLES
list:
# project-root/unchained_config.py
import os
# kwargs to pass to the Flask constructor (must be uppercase, all optional)
ROOT_PATH = os.path.dirname(__file__) # determined automatically by default
STATIC_FOLDER = "static" # determined automatically by default
STATIC_URL_PATH = "/static" # determined automatically by default
STATIC_HOST = None # None by default
TEMPLATE_FOLDER = "templates" # determined automatically by default
HOST_MATCHING = False # False by default
SUBDOMAIN_MATCHING = False # False by default
# the ordered list of bundles to load for your app (in dot-module notation)
BUNDLES = [
'flask_unchained.bundles.babel', # always enabled, optional to list here
'flask_unchained.bundles.controller', # always enabled, optional to list here
'app', # your app bundle *must* be last
]
When unchained_config.py
exists in the project-root
directory, exporting UNCHAINED
is not required, and the app can be run like so:
cd project-root
flask run
Single-File App Mode¶
For single-file apps, you can “double purpose” the app
module as your Unchained Config. This is as simple as it gets:
# project-root/app.py
from flask_unchained import AppBundle, Controller, route
class App(AppBundle):
pass
class SiteController(Controller):
@route('/')
def index(self):
return 'hello world'
It can be run like so:
export UNCHAINED="app" # the module where the unchained config resides
flask run
In the above example, we’re essentially telling the app factory, “just use the defaults with my app bundle”. In single-file mode, the app bundle is automatically detected, so there aren’t actually any Unchained Config settings in the above file. To set them looks as you would expect:
# project-root/app.py
import os
from flask_unchained import AppBundle, Controller, route
# kwargs to pass to the Flask constructor (must be uppercase, all optional)
ROOT_PATH = os.path.dirname(__file__) # determined automatically by default
STATIC_FOLDER = "static" # determined automatically by default
STATIC_URL_PATH = "/static" # determined automatically by default
STATIC_HOST = None # None by default
TEMPLATE_FOLDER = "templates" # determined automatically by default
HOST_MATCHING = False # False by default
SUBDOMAIN_MATCHING = False # False by default
# the ordered list of bundles to load for your app (in dot-module notation)
BUNDLES = [
'flask_unchained.bundles.babel', # always enabled, optional to list here
'flask_unchained.bundles.controller', # always enabled, optional to list here
'app', # your app bundle *must* be last
]
class App(AppBundle):
pass
class SiteController(Controller):
@route('/')
def index(self):
return 'hello world'
And once again, just be sure to export UNCHAINED="app"
:
export UNCHAINED="app" # the module where the unchained config resides
flask run
Bundle.before/after_init_app¶
The most obvious place you can hook into the app factory is with your Bundle
subclass, for example:
# project-root/app.py
from flask import Flask
from flask_unchained import AppBundle
class App(AppBundle):
def before_init_app(app: Flask):
app.url_map.strict_slashes = False
def after_init_app(app: Flask):
@app.after_request
def do_stuff(response):
return response
Using the Unchained
extension is another way to plug into the app factory, so let’s look at that next.
The Unchained Extension¶
As an alternative to using Bundle.before_init_app
and Bundle.after_init_app
, the Unchained
extension also acts as a drop-in replacement for some of the public API of Flask
:
from flask_unchained import unchained
@unchained.before_first_request
def called_once_before_the_first_request():
pass
# the other familiar decorators are also available:
@unchained.url_value_preprocessor
@unchained.url_defaults
@unchained.before_request
@unchained.after_request
@unchained.errorhandler
@unchained.teardown_request
@unchained.teardown_appcontext
@unchained.context_processor
@unchained.shell_context_processor
@unchained.template_filter
@unchained.template_global
@unchained.template_tag
@unchained.template_test
These decorators all work exactly the same as if you were using them from the Flask
app
instance itself.
The Unchained
extension first forwards these calls to the Flask
instance itself, and then it calls RunHooksHook.run_hook(app, bundles)
. Hooks are where the real action of actually booting up the app happens.
App Factory Hooks¶
App Factory Hooks are what make sure all of the code from your configured list of bundles gets discovered and registered correctly with both the Flask app
instance and the Unchained extension.
Important
Hooks are what define the patterns to load and customize everything in bundles. By default, to override something, you just place it in your bundle with the same name and in the same location (module) as whatever you want to override, or to extend something, do the same while also subclassing whatever you wish to extend. In other words, you just use standard object-oriented Python while following consistent naming conventions.
Advanced
While it shouldn’t be necessary, you can even extend and/or override hooks themselves if you need to customize their behavior.
These are some of the hooks Flask Unchained includes:
- InitExtensionsHook
Discovers Flask extensions in bundles and initializes them with the app.
- RegisterServicesHook
Discovers services in bundles and registers them with the
Unchained
extension. Both services and extensions are dependency-injectable at runtime into just about anything that can be wrapped with theinject()
decorator.- ConfigureAppHook
Discovers configuration options in bundles and registers them with the app.
- RegisterCommandsHook
Discovers CLI commands in bundles and registers them with the app.
Hooks are also loaded from bundles, for example the Controller Bundle includes these:
- RegisterRoutesHook
Discovers all views/routes in bundles and registers any “top-level” ones with the app.
- RegisterBundleBlueprintsHook
Registers the views/routes in bundles as blueprints with the app. Each bundle gets (conceptually, is) its own blueprint.
- RegisterBlueprintsHook
Discovers legacy Flask Blueprints and registers them with the app.
For our simple “hello world” app, most of these are no-ops, with the exception of the hook to register bundle blueprints. This is the essence of it:
# flask_unchained/bundles/controller/hooks/register_bundle_blueprints_hook.py
from flask_unchained import AppFactoryHook, Bundle, FlaskUnchained
from flask_unchained.bundles.controller.bundle_blueprint import BundleBlueprint
class RegisterBundleBlueprintsHook(AppFactoryHook):
def run_hook(self,
app: FlaskUnchained,
bundles: List[Bundle],
unchained_config: Optional[Dict[str, Any]] = None,
) -> None:
for bundle in bundles:
bp = BundleBlueprint(bundle)
for route in bundle.routes:
bp.add_url_rule(route.full_rule,
defaults=route.defaults,
endpoint=route.endpoint,
methods=route.methods,
**route.rule_options)
app.register_blueprint(bp)
And the result can be seen by running flask urls
:
flask urls
Method(s) Rule Endpoint View
----------------------------------------------------------------------------------------------
GET /static/<path:filename> static flask.helpers.send_static_file
GET / site_controller.index app.SiteController.index
GET /hello site_controller.hello app.SiteController.hello
The Bundle Hierarchy¶
FIXME: Expand on the bundle hierarchy and inheritance concept! Show examples.
Included Bundles¶
Admin Bundle¶
Integrates Flask Admin with Flask Unchained.
Installation¶
Install dependencies:
pip install "flask-unchained[admin]"
Enable the bundle in your unchained_config.py
:
# project-root/unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.admin',
'app',
]
And include the Admin Bundle’s routes:
# project-root/your_app_bundle/routes.py
routes = lambda: [
include('flask_unchained.bundles.admin.routes'),
# ...
]
Config¶
-
class
flask_unchained.bundles.admin.config.
Config
[source]¶ Config class for the Admin bundle. Defines which configuration values this bundle supports, and their default values.
-
ADMIN_NAME
= 'Admin'¶ The title of the admin section of the site.
-
ADMIN_BASE_URL
= '/admin'¶ Base url of the admin section of the site.
-
ADMIN_INDEX_VIEW
= <flask_unchained.bundles.admin.views.dashboard.AdminDashboardView object>¶ The
AdminIndexView
(or subclass) instance to use for the index view.
-
ADMIN_SUBDOMAIN
= None¶ Subdomain of the admin section of the site.
-
ADMIN_BASE_TEMPLATE
= 'admin/base.html'¶ Base template to use for other admin templates.
-
ADMIN_TEMPLATE_MODE
= 'bootstrap4'¶ Which version of bootstrap to use. (bootstrap2, bootstrap3, or bootstrap4)
-
ADMIN_CATEGORY_ICON_CLASSES
= {}¶ Dictionary of admin category icon classes. Keys are category names, and the values depend on which version of bootstrap you’re using.
For example, with bootstrap4:
ADMIN_CATEGORY_ICON_CLASSES = { 'Mail': 'fa fa-envelope', 'Security': 'fa fa-lock', }
-
ADMIN_ADMIN_ROLE_NAME
= 'ROLE_ADMIN'¶ The name of the Role which represents an admin.
-
ADMIN_LOGIN_ENDPOINT
= 'admin.login'¶ Name of the endpoint to use for the admin login view.
-
ADMIN_POST_LOGIN_REDIRECT_ENDPOINT
= 'admin.index'¶ Name of the endpoint to redirect to after the user logs into the admin.
-
ADMIN_LOGOUT_ENDPOINT
= 'admin.logout'¶ Name of the endpoint to use for the admin logout view.
-
ADMIN_POST_LOGOUT_REDIRECT_ENDPOINT
= 'admin.login'¶ Endpoint to redirect to after the user logs out of the admin.
-
API Docs¶
See Admin Bundle API
API Bundle¶
Integrates Marshmallow and APISpec with SQLAlchemy and Flask Unchained.
Installation¶
Install dependencies:
pip install "flask-unchained[api]"
And enable the API bundle in your unchained_config.py
:
# your_project_root/unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.api',
'app',
]
Usage¶
The API bundle includes two extensions, Api
and Marshmallow
. The Api
extension is used for generating OpenAPI documentation and the Marshmallow
extension is used for serialization. These should be imported like so:
from flask_unchained.bundles.api import api, ma
Model Serializers¶
ModelSerializer
is very similar to Flask Marshmallow’s ModelSchema
. There are two differences:
dependency injection is automatically set up, and
we automatically convert field names to/from camel case when dumping/loading (although this is customizable)
Let’s say you have the following model:
# your_bundle/models.py
from flask_unchained.bundles.sqlalchemy import db
class User(db.Model):
name = db.Column(db.String)
email = db.Column(db.String)
password = db.Column(db.String)
A simple serializer for it would look like this:
# your_bundle/serializers.py
from flask_unchained.bundles.api import ma
from .models import User
class UserSerializer(ma.ModelSerializer):
class Meta:
model = User
One gotchya here is that Marshmallow has no way to know that the email column should use an email field. Therefore, we need to help it out a bit:
# your_bundle/serializers.py
from flask_unchained.bundles.api import ma
from .models import User
class UserSerializer(ma.ModelSerializer):
class Meta:
model = User
load_only = ('password',)
email = ma.Email(required=True)
There are three separate contexts for (de)serialization:
standard: dumping/loading a single object
many: dumping/loading multiple objects
create: creating a new object
By default, any serializers you define will be used for all three. This can be customized:
# your_bundle/serializers.py
from flask_unchained.bundles.api import ma
@ma.serializer(many=True)
class UserSerializerMany(ma.ModelSerializer):
# ...
@ma.serializer(create=True)
class UserSerializerCreate(ma.ModelSerializer):
# ...
Let’s make a model resource so we’ll have API routes for it:
Model Resources¶
# your_bundle/views.py
from flask_unchained.bundles.api import ModelResource
from .models import User
class UserResource(ModelResource):
class Meta:
model = User
Add it to your routes:
# your_app_bundle/routes.py
routes = lambda: [
prefix('/api/v1', [
resource('/users', UserResource),
],
]
And that’s it, unless you need to customize any behavior.
Model Resource Meta Options
ModelResource
inherits all of the meta options from Controller
and Resource
, and it adds some options of its own:
meta option name |
description |
default value |
---|---|---|
model |
The model class to use for the resource. |
|
serializer |
The serializer instance to use for (de)serializing an individual model. |
Determined automatically by the model name. Can be set manually to override the automatic discovery. |
serializer_create |
The serializer instance to use for loading data for creation of a new model. |
Determined automatically by the model name. Can be set manually to override the automatic discovery. |
serializer_many |
The serializer instance to use for (de)serializing a list of models. |
Determined automatically by the model name. Can be set manually to override the automatic discovery. |
include_methods |
A list of resource methods to automatically include. |
|
exclude_methods |
A list of resource methods to exclude. |
|
include_decorators |
A list of resource methods for which to automatically apply the default decorators. |
|
exclude_decorators |
A list of resource methods for which to not automatically apply the default decorators. |
|
method_decorators |
This can either be a list of decorators to apply to all methods, or a dictionary of method names to a list of decorators to apply for each method. In both cases, decorators specified here are run before the default decorators. |
|
API Docs¶
See API Bundle API
Babel Bundle¶
The babel bundle provides support for internationalization and localization by integrating Flask BabelEx with Flask Unchained.
Installation¶
The babel bundle comes enabled by default.
Config¶
-
class
flask_unchained.bundles.babel.config.
Config
[source] Default configuration options for the Babel Bundle.
-
LANGUAGES
= ['en'] The language codes supported by the app.
-
BABEL_DEFAULT_LOCALE
= 'en' The default language to use if none is specified by the client’s browser.
-
BABEL_DEFAULT_TIMEZONE
= 'UTC' The default timezone to use.
-
DEFAULT_DOMAIN
= <flask_babelex.Domain object> The default
Domain
to use.
-
DATE_FORMATS
= {'date': 'medium', 'date.full': None, 'date.long': None, 'date.medium': None, 'date.short': None, 'datetime': 'medium', 'datetime.full': None, 'datetime.long': None, 'datetime.medium': None, 'datetime.short': None, 'time': 'medium', 'time.full': None, 'time.long': None, 'time.medium': None, 'time.short': None} A dictionary of date formats.
-
ENABLE_URL_LANG_CODE_PREFIX
= False Whether or not to enable the capability to specify the language code as part of the URL.
-
-
class
flask_unchained.bundles.babel.config.
DevConfig
[source] -
LAZY_TRANSLATIONS
= False Do not use lazy translations in development.
-
-
class
flask_unchained.bundles.babel.config.
ProdConfig
[source] -
LAZY_TRANSLATIONS
= True Use lazy translations in production.
-
-
class
flask_unchained.bundles.babel.config.
StagingConfig
[source] -
LAZY_TRANSLATIONS
= True Use lazy translations in staging.
-
Commands¶
flask babel¶
Babel translations commands.
flask babel COMMAND [<args>...] [OPTIONS]
compile¶
Compile translations into a distributable .mo
file.
flask babel compile [OPTIONS]
Options
-
-d
,
--domain
<domain>
¶
extract¶
Extract newly added translations keys from source code.
flask babel extract [OPTIONS]
Options
-
-d
,
--domain
<domain>
¶
init¶
Initialize translations for a language code.
flask babel init <lang> [OPTIONS]
Options
-
-d
,
--domain
<domain>
¶
Arguments
-
LANG
¶
Required argument
update¶
Update language-specific translations files with new keys discovered by
flask babel extract
.
flask babel update [OPTIONS]
Options
-
-d
,
--domain
<domain>
¶
API Docs¶
See Babel Bundle API
Celery Bundle¶
Integrates Celery with Flask Unchained.
Dependencies¶
A broker of some sort; Redis or RabbitMQ are popular choices.
Installation¶
Install dependencies:
pip install "flask-unchained[celery]" <broker-of-choice>
And enable the celery bundle in your unchained_config.py
:
# your_project_root/unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.celery',
'app',
]
NOTE: If you have enabled the Mail Bundle, and want to send emails asynchronously using celery, then you must list the celery bundle after the mail bundle in BUNDLES
.
Config¶
-
class
flask_unchained.bundles.celery.config.
Config
[source] Default configuration options for the Celery Bundle.
-
CELERY_BROKER_URL
= 'redis://127.0.0.1:6379/0' The broker URL to connect to.
-
CELERY_RESULT_BACKEND
= 'redis://127.0.0.1:6379/0' The result backend URL to connect to.
-
CELERY_ACCEPT_CONTENT
= ('json', 'pickle', 'dill') Tuple of supported serialization strategies.
-
MAIL_SEND_FN
(to=None, template=None, **kwargs) If the celery bundle is listed after the mail bundle in
unchained_config.BUNDLES
, then this configures the mail bundle to send emails asynchronously.
-
Commands¶
flask celery¶
Celery commands.
flask celery COMMAND [<args>...] [OPTIONS]
beat¶
Start the celery beat.
flask celery beat [OPTIONS]
worker¶
Start the celery worker.
flask celery worker [OPTIONS]
API Docs¶
Controller Bundle¶
The controller bundle provides the primary means for defining views and registering their routes with Flask. It is also the controller bundle that makes all of your other bundles blueprints.
Installation¶
The controller bundle comes enabled by default.
Usage¶
Config¶
-
class
flask_unchained.bundles.controller.config.
Config
[source] Default configuration options for the controller bundle.
-
FLASH_MESSAGES
= True Whether or not to enable flash messages.
NOTE: This only works for messages flashed using the
flask_unchained.Controller.flash()
method; using theflask.flash()
function directly will not respect this setting.
-
TEMPLATE_FILE_EXTENSION
= '.html' The default file extension to use for templates.
-
WTF_CSRF_ENABLED
= False Whether or not to enable CSRF protection.
-
Views and Controllers¶
Like stock Flask, Flask Unchained supports using views defined the “standard” way, ie:
# your_app/views.py
from flask import Blueprint, render_template
bp = Blueprint('bp', __name__)
@bp.route('/foo')
def foo():
return render_template('bp/foo.html')
View functions defined this way have a major drawback, however. And that is, as soon as this code gets imported, the view’s route gets registered with the app. Most of the time this is the desired behavior, and it will work as expected with code placed in your app bundle, however bundles meant for distribution to third parties must avoid defining views in this way, because it makes it impossible for the view to be overridden and/or its route to be customized.
The recommended way to define views with Flask Unchained is by using controller classes:
# your_app_bundle/views.py
from flask_unchained import Controller, route
class SiteController(Controller):
@route('/foo')
def foo():
return self.render('foo')
This view will do the same thing as the prior example view, however, using Controller
as the base class has the advantage of making views and their routes easily and independently customizable. Controllers also support automatic dependency injection and include some convenience methods such as flash()
, jsonify()
, redirect()
, and render()
.
Unlike when using the stock Flask decorators to register a view’s routes, in Flask Unchained we must explicitly enable the routes we want:
# your_app_bundle/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),
]
By default this code will register all of the view functions on SiteController
with the app, using the default routing rules as defined on the view methods of the controller.
Declarative Routing¶
Declaration of routes in Flask Unchained can be uncoupled from the definition of the views themselves. Using function- and class-based views is supported. You already saw a simple example above, that would register a single route at /foo
for the SiteController.foo
method. Let’s say we wanted to change it to /site/foobar
:
# your_app_bundle/routes.py
from flask_unchained import (controller, resource, func, include, prefix,
get, delete, post, patch, put, rule)
from .views import SiteController
routes = lambda: [
# this is probably the clearest way to do it:
controller('/site', SiteController, rules=[
get('/foobar', SiteController.foo),
]),
# or you can provide the method name of the controller as a string:
controller('/site', SiteController, rules=[
get('/foobar', 'foo'),
]),
# or use nesting to produce the same result (this is especially useful when
# you want to prefix more than one view/route with the same url prefix)
prefix('/site', [
controller(SiteController, rules=[
get('/foobar', SiteController.foo),
]),
]),
]
Here is a summary of the functions imported at the top of your routes.py
:
Function |
Description |
---|---|
Include all of the routes from the specified module at that point in the tree. |
|
Prefixes all of the child routing rules with the given prefix. |
|
Registers a function-based view with the app, optionally specifying the routing rules. |
|
Registers a controller and its views with the app, optionally customizing the routes to register. |
|
Registers a resource and its views with the app, optionally customizing the routes to register. |
|
Define customizations to a controller/resource method’s route rules. |
|
Defines a controller/resource method as only accepting the HTTP |
|
Defines a controller/resource method as only accepting the HTTP |
|
Defines a controller/resource method as only accepting the HTTP |
|
Defines a controller/resource method as only accepting the HTTP |
|
Defines a controller/resource method as only accepting the HTTP |
Class-Based Views¶
The controller bundle includes two base classes that all of your views should extend. The first is Controller
, which is implemented very similarly to View
, however they’re not compatible. The second is Resource
, which extends Controller
, and whose implementation draws a lot of inspiration from Flask-RSETful (specifically, the Resource and Api classes).
Controller¶
Unless you’re building an API, chances are Controller
is the base class you want to extend. Controllers include a bit of magic that deserves some explanation:
On any Controller subclass that doesn’t specify itself as abstract, all methods not designated as protected by prefixing them with an _
are automatically assigned default routing rules. In the example below, foo_baz
has a route decorator, but foo
and foo_bar
do not. The undecorated views will be assigned default route rules of /foo
and /foo-bar
respectively (the default is to convert the method name to kebab-case).
# your_bundle/views.py
from flask_unchained import Controller, route
class MyController(Controller):
def foo():
return self.render('foo')
def foo_bar():
return self.render('foo_bar')
@route('/foobaz')
def foo_baz():
return self.render('foo_baz')
def _protected_function():
return 'stuff'
Controllers have a few meta options that you can use to customize their behavior:
# your_bundle/views.py
from flask_unchained import Controller, route
class SiteController(Controller):
class Meta:
abstract: bool = False # default is False
decorators: List[callable] = () # default is an empty tuple
template_folder: str = 'site' # see explanation below
template_file_extension: Optional[str] = None # default is None
url_prefix = Optional[str] = None # default is None
endpoint_prefix = 'site_controller' # see explanation below
meta option name |
description |
default value |
---|---|---|
abstract |
Whether or not this controller should be abstract. Abstract controller classes do not get any routes assigned to their view methods (if any exist). |
False |
decorators |
A list of decorators to apply to all views in this controller. |
() |
template_folder |
The name of the folder containing the templates for this controller’s views. |
Defaults to the snake_cased class name (with the |
template_file_extension |
The filename extension to use for templates for this controller’s views. |
Defaults to your app config’s |
url_prefix |
The url prefix to use for all routes from this controller. |
Defaults to |
endpoint_prefix |
The endpoint prefix to use for all routes from this controller. |
Defaults to the snake_cased class name. |
Controllers can be extended or overridden by creating an equivalently named class higher up in the bundle hierarchy. (In other words, either in a bundle that extends another bundle, or in your app bundle.) As an example, the security bundle includes SecurityController
. To extend it, you would simply subclass it like any other class in Python and change what you need to:
# your_app_or_security_bundle/views.py
from flask_unchained.bundles.security import SecurityController as BaseSecurityController
# to extend BaseSecurityController
class SecurityController(BaseSecurityController):
pass
# to completely override it, just use the same name without extending the base class
class SecurityController:
pass
Resource¶
The Resource
class extends Controller
to add support for building RESTful APIs. It adds a bit of magic around specific methods:
HTTP Method |
Resource class method name |
URL Rule |
---|---|---|
GET |
list |
/ |
POST |
create |
/ |
GET |
get |
/<cls.Meta.member_param> |
PATCH |
patch |
/<cls.Meta.member_param> |
PUT |
put |
/<cls.Meta.member_param> |
DELETE |
delete |
/<cls.Meta.member_param> |
If you implement any of these methods, then the shown URL rules will automatically be used.
So, for example:
from http import HTTPStatus
from flask_unchained import Resource, injectable, param_converter, request
from flask_unchained.bundles.security import User, UserManager
class UserResource(Resource):
class Meta:
url_prefix = '/users'
member_param = '<int:id>'
unique_member_param = '<int:user_id>'
user_manager: UserManager = injectable
def list():
return self.jsonify(dict(users=self.user_manager.all()))
def create():
data = request.get_json()
user = self.user_manager.create(**data, commit=True)
return self.jsonify(dict(user=user), code=HTTPStatus.CREATED)
@param_converter(id=User)
def get(user):
return self.jsonify(dict(user=user)
@param_converter(id=User)
def patch(user):
data = request.get_json()
user = self.user_manager.update(user, **data, commit=True)
return self.jsonify(dict(user=user))
@param_converter(id=User)
def put(user):
data = request.get_json()
user = self.user_manager.update(user, **data, commit=True)
return self.jsonify(dict(user=user))
@param_converter(id=User)
def delete(user):
self.user_manager.delete(user, commit=True)
return self.make_response('', code=HTTPStatus.NO_CONTENT)
Registered like so:
routes = lambda: [
resource(UserResource),
]
Results in the following routes:
GET /users UserResource.list
POST /users UserResource.create
GET /users/<int:id> UserResource.get
PATCH /users/<int:id> UserResource.patch
PUT /users/<int:id> UserResource.put
DELETE /users/<int:id> UserResource.delete
Resources have a few extra meta options on top of those that Controller includes:
# your_bundle/views.py
from flask_unchained import Controller, route
class UserResource(Resource):
class Meta:
abstract: bool = False # the default
decorators: List[callable] = () # the default
url_prefix = Optional[str] = '/users' # automatically determined
member_param: str = '<int:id>' # the default
unique_member_parm: str = '<int:user_id>' # automatically determined
meta option name |
description |
default value |
---|---|---|
|
The url prefix to use for all routes from this resource. |
Defaults to the pluralized, kebab-cased class name (without the Resource suffix) |
|
The url parameter rule to use for the special member functions ( |
|
|
The url parameter rule to use for the special member methods ( |
|
Because Resource
is already a subclass of Controller
, overriding resources works the same way as for controllers.
Templating¶
Flask Unchained uses the Jinja templating language, just like stock Flask.
By default bundles are configured to use a templates
subfolder. This is configurable by setting the template_folder
attribute on your Bundle
subclass to a custom path.
Controllers each have their own template folder within Bundle.template_folder
. It defaults to the snake_cased class name, with the suffixes Controller
or View
stripped (if any). You can customize it using flask_unchained.Controller.Meta.template_folder
.
The default file extension used for templates is configured by setting TEMPLATE_FILE_EXTENSION
in your app config. It defaults to .html
, and is also configurable on a per-controller basis by setting flask_unchained.Controller.Meta.template_file_extension
.
Taking the above into account, given the following controller:
class SiteController(Controller):
@route('/')
def index():
return self.render('index')
Then the corresponding folder structure would look like this:
./your_bundle
├── templates
│ └── site
│ └── index.html
├── __init__.py
└── views.py
Extending and Overriding Templates¶
Templates can be overridden by placing an equivalently named template higher up in the bundle hierarchy.
So for example, the Security Bundle includes default templates for all of its views. They are located at security/login.html
, security/register.html
, and so on. Thus, to extend or override them, you would make a security
folder in your app bundle’s templates
folder and put your customized templates with the same names in it. You can even extend the template you’re overriding, using the standard Jinja syntax (this doesn’t work in regular Flask apps):
{# your_app_or_security_bundle/templates/security/login.html #}
{% extends 'security/login.html' %}
{% block content %}
<h1>Login</h1>
{{ render_form(login_user_form, endpoint='security_controller.login') }}
{% endblock %}
If you encounter problems, you can set the EXPLAIN_TEMPLATE_LOADING
config option to True
to help debug what’s going on.
Dependency Injection¶
Controllers are configured with dependency injection set up on them automatically. You can use class attributes or the constructor (or both).
Here’s an example of using class attributes:
from flask_unchained import Controller, injectable
from flask_unchained.bundles.security import Security, SecurityService, SecurityUtilsService
from flask_unchained.bundles.sqlalchemy import SessionManager
class SecurityController(Controller):
security: Security = injectable
security_service: SecurityService = injectable
security_utils_service: SecurityUtilsService = injectable
session_manager: SessionManager = injectable
And here’s what the same thing using the constructor looks like:
from flask_unchained import Controller, injectable
from flask_unchained.bundles.security import Security, SecurityService, SecurityUtilsService
from flask_unchained.bundles.sqlalchemy import SessionManager
class SecurityController(Controller):
def __init__(self,
security: Security = injectable,
security_service: SecurityService = injectable,
security_utils_service: SecurityUtilsService = injectable,
session_manager: SessionManager = injectable):
self.security = security
self.security_service = security_service
self.security_utils_service = security_utils_service
self.session_manager = session_manager
API Docs¶
Graphene Bundle¶
Integrates Flask GraphQL and Graphene-SQLAlchemy with Flask Unchained.
Installation¶
Install dependencies:
pip install "flask-unchained[graphene]"
And enable the graphene bundle in your unchained_config.py
:
# project-root/unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.graphene',
'app',
]
Usage¶
Create a graphene
module in your bundle:
cd your_bundle
mkdir graphene && touch graphene/__init__.py
touch graphene/mutations.py graphene/queries.py graphene/types.py
Let’s say you have some models you want to create a GraphQL schema for:
# your_bundle/models.py
from flask_unchained.bundles.sqlalchemy import db
class Parent(db.Model):
name = db.Column(db.String)
children = db.relationship('Child', back_populates='parent',
cascade='all,delete,delete-orphan')
class Child(db.Model):
name = db.Column(db.String)
parent_id = db.foreign_key('Parent')
parent = db.relationship('Parent', back_populates='children')
Object Types¶
The first step is to define your object types for your models:
# your_bundle/graphene/types.py
import graphene
from flask_unchained.bundles.graphene import SQLAlchemyObjectType
from .. import models
class Parent(SQLAlchemyObjectType):
class Meta:
model = models.Parent
only_fields = ('id', 'name', 'created_at', 'updated_at')
children = graphene.List(lambda: Child)
class Child(SQLAlchemyObjectType):
class Meta:
model = models.Child
only_fields = ('id', 'name', 'created_at', 'updated_at')
parent = graphene.Field(Parent)
Queries¶
Next define the queries for your types:
# your_bundle/graphene/queries.py
import graphene
from flask_unchained.bundles.graphene import QueriesObjectType
from . import types
class Queries(QueriesObjectType):
parent = graphene.Field(types.Parent, id=graphene.ID(required=True))
parents = graphene.List(types.Parent)
child = graphene.Field(types.Child, id=graphene.ID(required=True))
children = graphene.List(types.Child)
When subclassing QueriesObjectType
, it automatically adds default resolvers for you. But these can be overridden if you want, eg:
# your_bundle/graphene/queries.py
import graphene
from flask_unchained import unchained
from flask_unchained.bundles.graphene import QueriesObjectType
from .. import services
from . import types
child_manager: services.ChildManager = unchained.get_local_proxy('child_manager')
class Queries(QueriesObjectType):
# ...
children = graphene.List(types.Child, parent_id=graphene.ID())
def resolve_children(self, info, parent_id=None, **kwargs):
if not parent_id:
return child_manager.all()
return child_manager.filter_by_parent_id(parent_id).all()
Note
Unfortunately, dependency injection does not work with graphene classes. (That said, you can still decorate individual methods with @unchained.inject()
.)
Mutations¶
Graphene mutations, per the RegisterGrapheneMutationsHook
, by default live in the graphene.mutations
module of bundles. This can be customized by setting the graphene_mutations_module_names
attribute on your bundle class.
import graphene
from flask_unchained import unchained, injectable, lazy_gettext as _
from flask_unchained.bundles.graphene import MutationsObjectType, MutationValidationError
from flask_unchained.bundles.security.exceptions import AuthenticationError
from flask_unchained.bundles.security.services import SecurityService, SecurityUtilsService
from flask_unchained.bundles.security.graphene.types import UserInterface
class LoginUser(graphene.Mutation):
class Arguments:
email = graphene.String(required=True)
password = graphene.String(required=True)
success = graphene.Boolean(required=True)
message = graphene.String()
user = graphene.Field(UserInterface)
@unchained.inject('security_service')
def mutate(
self,
info,
email: str,
password: str,
security_service: SecurityService = injectable,
**kwargs,
):
try:
user = LoginUser.validate(email, password)
except MutationValidationError as e:
return LoginUser(success=False, message=e.args[0])
try:
security_service.login_user(user)
except AuthenticationError as e:
return LoginUser(success=False, message=e.args[0])
return LoginUser(success=True, user=user)
@staticmethod
@unchained.inject('security_utils_service')
def validate(
email: str,
password: str,
security_utils_service: SecurityUtilsService = injectable,
):
user = security_utils_service.user_loader(email)
if user is None:
raise MutationValidationError(
_('flask_unchained.bundles.security:error.user_does_not_exist'))
if not security_utils_service.verify_password(user, password):
raise MutationValidationError(
_('flask_unchained.bundles.security:error.invalid_password'))
return user
class LogoutUser(graphene.Mutation):
success = graphene.Boolean(required=True)
@unchained.inject('security_service')
def mutate(self, info, security_service: SecurityService = injectable, **kwargs):
security_service.logout_user()
return LogoutUser(success=True)
class SecurityMutations(MutationsObjectType):
login_user = mutations.LoginUser.Field(description="Login with email and password.")
logout_user = mutations.LogoutUser.Field(description="Logout the current user.")
API Docs¶
Mail Bundle¶
Integrates Flask Mail with Flask Unchained.
Installation¶
Install dependencies:
pip install "flask-unchained[mail]"
And enable the bundle in your unchained_config.py
:
# your_project_root/unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.mail',
'app',
]
NOTE: If you have enabled the Celery Bundle, and want to send emails asynchronously using Celery, then you must list the celery bundle after the mail bundle in BUNDLES
.
Config¶
-
class
flask_unchained.bundles.mail.config.
Config
[source] Default configuration options for the mail bundle.
-
MAIL_SERVER
= '127.0.0.1' The hostname/IP of the mail server.
-
MAIL_PORT
= 25 The port the mail server is running on.
-
MAIL_USERNAME
= None The username to connect to the mail server with, if any.
-
MAIL_PASSWORD
= None The password to connect to the mail server with, if any.
-
MAIL_USE_TLS
= False Whether or not to use TLS.
-
MAIL_USE_SSL
= False Whether or not to use SSL.
-
MAIL_DEFAULT_SENDER
= 'Flask Mail <noreply@localhost>' The default sender to use, if none is specified otherwise.
-
MAIL_SEND_FN
(to: Union[str, List[str], None] = None, template: Optional[str] = None, **kwargs) The function to use for sending emails. Defaults to
_send_mail()
. Any customized send function must implement the same function signature:def send_mail(subject_or_message: Optional[Union[str, Message]] = None, to: Optional[Union[str, List[str]] = None, template: Optional[str] = None, **kwargs): # ...
NOTE: subject_or_message is optional because you can also pass subject as a keyword argument, and to is optional because you can also pass recipients as a keyword argument. These are artifacts of backwards-compatibility with vanilla Flask-Mail.
-
MAIL_DEBUG
= 0 The debug level to set for interactions with the mail server.
-
MAIL_MAX_EMAILS
= None The maximum number of emails to send per connection with the mail server.
-
MAIL_SUPPRESS_SEND
= False Whether or not to actually send emails, or just pretend to. This is mainly useful for testing.
-
MAIL_ASCII_ATTACHMENTS
= False Whether or not to coerce attachment filenames to ASCII.
-
-
class
flask_unchained.bundles.mail.config.
DevConfig
[source] Development-specific config options for the mail bundle.
-
MAIL_DEBUG
= 1 Set the mail server debug level to 1 in development.
-
MAIL_PORT
= 1025 In development, the mail bundle is configured to connect to MailHog.
-
-
class
flask_unchained.bundles.mail.config.
ProdConfig
[source] Production-specific config options for the mail bundle.
-
MAIL_PORT
= 465 In production, the mail bundle is configured to connect using SSL.
-
MAIL_USE_SSL
= True Set use SSL to
True
in production.
-
-
class
flask_unchained.bundles.mail.config.
TestConfig
[source] Test-specific config options for the mail bundle.
-
MAIL_SUPPRESS_SEND
= True Do not actually send emails when running tests.
-
Usage¶
After configuring the bundle, usage is simple:
from flask_unchained.bundles.mail import mail
mail.send_message('hello world', to='foo@bar.com')
mail
is an instance of the Mail
extension, and send_message()
is the only public method on it. Technically, it’s an alias for send()
, which you can also use. (The send()
method is maintained for backwards compatibility with the stock Flask Mail extension, although it has a different but compatible function signature than the original - we don’t require that you manually create Message
instances yourself before calling send()
.)
Commands¶
flask mail¶
Mail commands.
flask mail COMMAND [<args>...] [OPTIONS]
send-test-email¶
Attempt to send a test email to the given email address.
flask mail send-test-email [OPTIONS]
Options
-
--to
<to>
¶ Email address of the recipient.
-
--subject
<subject>
¶ Email subject.
pytest fixtures¶
The mail bundle includes one pytest fixture, outbox()
, that you can use to verify that emails were sent:
def test_something(client, outbox):
r = client.get('endpoint.that.sends.an.email')
assert len(outbox) == 1
assert outbox[0].subject == 'hello world'
assert 'hello world' in outbox[0].html
API Docs¶
See Mail Bundle API
OAuth Bundle¶
Integrates Flask OAuthlib with Flask Unchained. This allows OAuth authentication to any OAuth Provider supported by Flask OAuthlib.
Installation¶
The OAuth Bundle depends on the Security Bundle, as well as a few third-party libraries:
pip install "flask-unchained[oauth,security,sqlalchemy]"
And enable the bundles in your unchained_config.py
:
# your_project_root/unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.sqlalchemy',
'flask_unchained.bundles.security',
'flask_unchained.bundles.oauth',
'app',
]
And set the OAuthController to your app’s routes.py
:
# your_app_bundle_root/routes.py
from flask_unchained.bundles.oauth.views import OAuthController
routes = lambda: [
# ...
controller(OAuthController),
]
Config¶
The OAuth bundle includes support for two remote providers by default:
amazon
github
To configure these, would look like this:
# your_app_bundle_root/config.py
import os
from flask_unchained import BundleConfig
class Config(BundleConfig):
OAUTH_GITHUB_CONSUMER_KEY = os.getenv('OAUTH_GITHUB_CONSUMER_KEY', '')
OAUTH_GITHUB_CONSUMER_SECRET = os.getenv('OAUTH_GITHUB_CONSUMER_SECRET', '')
OAUTH_AMAZON_CONSUMER_KEY = os.getenv('OAUTH_AMAZON_CONSUMER_KEY', '')
OAUTH_AMAZON_CONSUMER_SECRET = os.getenv('OAUTH_AMAZON_CONSUMER_SECRET', '')
You can also add other remote providers, for example to add support for the (made up) abc
provider:
# your_app_bundle_root/config.py
import os
from flask_unchained import BundleConfig
class Config(BundleConfig):
OAUTH_REMOTE_APP_ABC = dict(
consumer_key=os.getenv('OAUTH_ABC_CONSUMER_KEY', ''),
consumer_secret=os.getenv('OAUTH_ABC_CONSUMER_SECRET', ''),
base_url='https://api.abc.com/',
access_token_url='https://abc.com/login/oauth/access_token',
access_token_method='POST',
authorize_url='https://abc.com/login/oauth/authorize'
request_token_url=None,
request_token_params={'scope': 'user:email'},
)
Each remote provider is available at its respective endpoint: /login/<remote-app-name>
- For more information and OAuth config examples see:
Security Bundle¶
Integrates Flask Login and Flask Principal with Flask Unchained. Technically speaking, this bundle is actually a heavily refactored fork of the Flask Security project. As of this writing, it is at approximate feature parity with Flask Security, and supports session and token authentication. (We’ve removed support for HTTP Basic Auth, tracking users’ IP addresses and similar, as well as the experimental password-less login support.)
Installation¶
The Security Bundle depends on the SQLAlchemy Bundle, as well as a few third-party libraries:
pip install "flask-unchained[security,sqlalchemy]"
And enable the bundles in your unchained_config.py
:
# your_project_root/unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.sqlalchemy',
'flask_unchained.bundles.security',
'app',
]
Config¶
-
class
flask_unchained.bundles.security.config.
AuthenticationConfig
[source] Config options for logging in and out.
-
SECURITY_LOGIN_FORM
-
SECURITY_DEFAULT_REMEMBER_ME
= False Whether or not the login form should default to checking the “Remember me?” option.
-
SECURITY_REMEMBER_SALT
= 'security-remember-salt' Salt used for the remember me cookie token.
-
SECURITY_USER_IDENTITY_ATTRIBUTES
= ['email'] List of attributes on the user model that can used for logging in with. Each must be unique.
-
SECURITY_POST_LOGIN_REDIRECT_ENDPOINT
= '/' The endpoint or url to redirect to after a successful login.
-
SECURITY_POST_LOGOUT_REDIRECT_ENDPOINT
= '/' The endpoint or url to redirect to after a user logs out.
-
-
class
flask_unchained.bundles.security.config.
ChangePasswordConfig
[source] Config options for changing passwords
-
SECURITY_CHANGEABLE
= False Whether or not to enable change password functionality.
-
SECURITY_CHANGE_PASSWORD_FORM
alias of
flask_unchained.bundles.security.forms.ChangePasswordForm
-
SECURITY_POST_CHANGE_REDIRECT_ENDPOINT
= None Endpoint or url to redirect to after the user changes their password.
-
SECURITY_SEND_PASSWORD_CHANGED_EMAIL
= False Whether or not to send the user an email when their password has been changed. Defaults to True, and it’s strongly recommended to leave this option enabled.
-
-
class
flask_unchained.bundles.security.config.
EncryptionConfig
[source] Config options for encryption hashing.
-
SECURITY_PASSWORD_SALT
= 'security-password-salt' Specifies the HMAC salt. This is only used if the password hash type is set to something other than plain text.
-
SECURITY_PASSWORD_HASH
= 'bcrypt' Specifies the password hash algorithm to use when hashing passwords. Recommended values for production systems are
argon2
,bcrypt
, orpbkdf2_sha512
. May require extra packages to be installed.
-
SECURITY_PASSWORD_SINGLE_HASH
= False Specifies that passwords should only be hashed once. By default, passwords are hashed twice, first with SECURITY_PASSWORD_SALT, and then with a random salt. May be useful for integrating with other applications.
-
SECURITY_PASSWORD_SCHEMES
= ['argon2', 'bcrypt', 'pbkdf2_sha512', 'plaintext'] List of algorithms that can be used for hashing passwords.
-
SECURITY_PASSWORD_HASH_OPTIONS
= {} Specifies additional options to be passed to the hashing method.
-
SECURITY_DEPRECATED_PASSWORD_SCHEMES
= ['auto'] List of deprecated algorithms for hashing passwords.
-
SECURITY_HASHING_SCHEMES
= ['sha512_crypt'] List of algorithms that can be used for creating and validating tokens.
-
SECURITY_DEPRECATED_HASHING_SCHEMES
= [] List of deprecated algorithms for creating and validating tokens.
-
-
class
flask_unchained.bundles.security.config.
ForgotPasswordConfig
[source] Config options for recovering forgotten passwords
-
SECURITY_RECOVERABLE
= False Whether or not to enable forgot password functionality.
-
SECURITY_FORGOT_PASSWORD_FORM
alias of
flask_unchained.bundles.security.forms.ForgotPasswordForm
-
SECURITY_RESET_PASSWORD_FORM
alias of
flask_unchained.bundles.security.forms.ResetPasswordForm
-
SECURITY_RESET_SALT
= 'security-reset-salt' Salt used for the reset token.
-
SECURITY_RESET_PASSWORD_WITHIN
= '5 days' Specifies the amount of time a user has before their password reset link expires. Always pluralized the time unit for this value. Defaults to 5 days.
-
SECURITY_POST_RESET_REDIRECT_ENDPOINT
= None Endpoint or url to redirect to after the user resets their password.
-
SECURITY_INVALID_RESET_TOKEN_REDIRECT
= 'security_controller.forgot_password' Endpoint or url to redirect to if the reset token is invalid.
-
SECURITY_EXPIRED_RESET_TOKEN_REDIRECT
= 'security_controller.forgot_password' Endpoint or url to redirect to if the reset token is expired.
-
SECURITY_API_RESET_PASSWORD_HTTP_GET_REDIRECT
= None Endpoint or url to redirect to if a GET request is made to the reset password view. Defaults to None, meaning no redirect. Useful for single page apps.
-
SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL
= False Whether or not to send the user an email when their password has been reset. Defaults to True, and it’s strongly recommended to leave this option enabled.
-
-
class
flask_unchained.bundles.security.config.
RegistrationConfig
[source] Config options for user registration
-
SECURITY_REGISTERABLE
= False Whether or not to enable registration.
-
SECURITY_REGISTER_FORM
alias of
flask_unchained.bundles.security.forms.RegisterForm
-
SECURITY_POST_REGISTER_REDIRECT_ENDPOINT
= None The endpoint or url to redirect to after a user completes the registration form.
-
SECURITY_SEND_REGISTER_EMAIL
= False Whether or not send a welcome email after a user completes the registration form.
-
SECURITY_CONFIRMABLE
= False Whether or not to enable required email confirmation for new users.
-
SECURITY_SEND_CONFIRMATION_FORM
alias of
flask_unchained.bundles.security.forms.SendConfirmationForm
-
SECURITY_CONFIRM_SALT
= 'security-confirm-salt' Salt used for the confirmation token.
-
SECURITY_LOGIN_WITHOUT_CONFIRMATION
= False Allow users to login without confirming their email first. (This option only applies when
SECURITY_CONFIRMABLE
is True.)
-
SECURITY_CONFIRM_EMAIL_WITHIN
= '5 days' How long to wait until considering the token in confirmation emails to be expired.
-
SECURITY_POST_CONFIRM_REDIRECT_ENDPOINT
= None Endpoint or url to redirect to after the user confirms their email. Defaults to
SECURITY_POST_LOGIN_REDIRECT_ENDPOINT
.
-
SECURITY_CONFIRM_ERROR_REDIRECT_ENDPOINT
= None Endpoint to redirect to if there’s an error confirming the user’s email.
-
-
class
flask_unchained.bundles.security.config.
TokenConfig
[source] Config options for token authentication.
-
SECURITY_TOKEN_AUTHENTICATION_KEY
= 'auth_token' Specifies the query string parameter to read when using token authentication.
-
SECURITY_TOKEN_AUTHENTICATION_HEADER
= 'Authentication-Token' Specifies the HTTP header to read when using token authentication.
-
SECURITY_TOKEN_MAX_AGE
= None Specifies the number of seconds before an authentication token expires. Defaults to None, meaning the token never expires.
-
-
class
flask_unchained.bundles.security.config.
Config
[source] Config options for the Security Bundle.
-
SECURITY_ANONYMOUS_USER
alias of
flask_unchained.bundles.security.models.anonymous_user.AnonymousUser
-
SECURITY_UNAUTHORIZED_CALLBACK
() This callback gets called when authorization fails. By default we abort with an HTTP status code of 401 (UNAUTHORIZED).
-
SECURITY_DATETIME_FACTORY
() Factory function to use when creating new dates. By default we use
datetime.now(timezone.utc)
to create a timezone-aware datetime.
-
-
class
flask_unchained.bundles.security.config.
TestConfig
[source] Default test settings for the Security Bundle.
-
SECURITY_PASSWORD_HASH
= 'plaintext' Disable password-hashing in tests (shaves about 30% off the test-run time)
-
Commands¶
flask users¶
User model commands.
flask users COMMAND [<args>...] [OPTIONS]
activate¶
Activate a user.
flask users activate <query> [OPTIONS]
Arguments
-
QUERY
¶
Required argument
add-role¶
Add a role to a user.
flask users add-role [OPTIONS]
Options
-
-u
,
--user
<user>
¶ The query to search for a user by. For example, id=5, email=a@a.com or first_name=A,last_name=B.
-
-r
,
--role
<role>
¶ The query to search for a role by. For example, id=5 or name=ROLE_USER.
confirm¶
Confirm a user account.
flask users confirm <query> [OPTIONS]
Arguments
-
QUERY
¶
Required argument
create¶
Create a new user.
flask users create [OPTIONS]
Options
-
--email
<email>
¶ The user’s email address.
-
--password
<password>
¶ The user’s password.
-
--active
,
--inactive
¶
Whether or not the new user should be active.
- Default
False
-
--confirmed-at
<confirmed_at>
¶ The date stamp the user was confirmed at (or enter “now”) [default: None]
-
--send-email
,
--no-email
¶
Whether or not to send the user a welcome email.
- Default
False
deactivate¶
Deactivate a user.
flask users deactivate <query> [OPTIONS]
Arguments
-
QUERY
¶
Required argument
delete¶
Delete a user.
flask users delete <query> [OPTIONS]
Arguments
-
QUERY
¶
Required argument
list¶
List users.
flask users list [OPTIONS]
remove-role¶
Remove a role from a user.
flask users remove-role [OPTIONS]
Options
-
-u
,
--user
<user>
¶ The query to search for a user by. For example, id=5, email=a@a.com or first_name=A,last_name=B.
-
-r
,
--role
<role>
¶ The query to search for a role by. For example, id=5 or name=ROLE_USER.
set-password¶
Set a user’s password.
flask users set-password <query> [OPTIONS]
Options
-
--password
<password>
¶ The new password to assign to the user.
-
--send-email
,
--no-email
¶
Whether or not to send the user a notification email.
- Default
False
Arguments
-
QUERY
¶
Required argument
flask roles¶
Role commands.
flask roles COMMAND [<args>...] [OPTIONS]
create¶
Create a new role.
flask roles create [OPTIONS]
Options
-
--name
<name>
¶ The name of the role to create, eg ROLE_USER.
delete¶
Delete a role.
flask roles delete <query> [OPTIONS]
Arguments
-
QUERY
¶
Required argument
list¶
List roles.
flask roles list [OPTIONS]
API Docs¶
Session Bundle¶
Integrates Flask Session with Flask Unchained. This bundle is only a thin wrapper around Flask Session, and usage in Flask Unchained is as simple as it gets. Enable the bundle, configure SESSION_TYPE
, and you’re off running.
Installation¶
Install dependencies:
pip install "flask-unchained[session]"
And enable the bundle in your unchained_config.py
:
# your_project_root/unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.session',
'app',
]
Config¶
You must configure SESSION_TYPE
, and depending upon what you set it to, any other required options for that type:
SESSION_TYPE |
Extra Required Options |
---|---|
|
(none) |
|
|
|
|
|
|
|
|
|
-
class
flask_unchained.bundles.session.config.
DefaultFlaskConfigForSessions
[source] Default configuration options for sessions in Flask.
-
SESSION_COOKIE_NAME
= 'session' The name of the session cookie.
Defaults to
'session'
.
-
SESSION_COOKIE_DOMAIN
= None The domain for the session cookie. If this is not set, the cookie will be valid for all subdomains of
SERVER_NAME
.Defaults to
None
.
-
SESSION_COOKIE_PATH
= None The path for the session cookie. If this is not set the cookie will be valid for all of
APPLICATION_ROOT
or if that is not set for ‘/’.Defaults to
None
.
-
SESSION_COOKIE_HTTPONLY
= True Controls if the cookie should be set with the
httponly
flag. Browsers will not allow JavaScript access to cookies marked ashttponly
for security.Defaults to
True
.
-
SESSION_COOKIE_SECURE
= False Controls if the cookie should be set with the
secure
flag. Browsers will only send cookies with requests over HTTPS if the cookie is markedsecure
. The application must be served over HTTPS for this to make sense.Defaults to
False
.
-
PERMANENT_SESSION_LIFETIME
= datetime.timedelta(days=31) The lifetime of a permanent session as
datetime.timedelta
object or an integer representing seconds.Defaults to 31 days.
-
SESSION_COOKIE_SAMESITE
= None Restrict how cookies are sent with requests from external sites. Limits the scope of the cookie such that it will only be attached to requests if those requests are “same-site”. Can be set to
'Lax'
(recommended) or'Strict'
.Defaults to
None
.
-
SESSION_REFRESH_EACH_REQUEST
= True Controls the set-cookie behavior. If set to
True
a permanent session will be refreshed each request and get their lifetime extended, if set toFalse
it will only be modified if the session actually modifies. Non permanent sessions are not affected by this and will always expire if the browser window closes.Defaults to
True
.
-
-
class
flask_unchained.bundles.session.config.
Config
[source] Default configuration options for the Session Bundle.
See Flask Session for more information.
-
SESSION_TYPE
= 'null' Specifies which type of session interface to use. Built-in session types:
'null'
:NullSessionInterface
(default)'redis'
:RedisSessionInterface
'memcached'
:MemcachedSessionInterface
'filesystem'
:FileSystemSessionInterface
'mongodb'
:MongoDBSessionInterface
'sqlalchemy'
:SqlAlchemySessionInterface
Defaults to
'null'
.
-
SESSION_PERMANENT
= True Whether use permanent session or not.
Defaults to
True
.
-
SESSION_USE_SIGNER
= False Whether sign the session cookie sid or not. If set to
True
, you have to setSECRET_KEY
.Defaults to
False
.
-
SESSION_KEY_PREFIX
= 'session:' A prefix that is added before all session keys. This makes it possible to use the same backend storage server for different apps.
Defaults to
'session:'
.
-
SESSION_REDIS
= None A
redis.Redis
instance.By default, connect to
127.0.0.1:6379
.
-
SESSION_MEMCACHED
= None A
memcached.Client
instance.By default, connect to
127.0.0.1:11211
.
-
SESSION_FILE_DIR
= '/home/docs/checkouts/readthedocs.org/user_builds/flask-unchained/checkouts/latest/docs/flask_sessions' The folder where session files are stored.
Defaults to using a folder named
flask_sessions
in your current working directory.
-
SESSION_FILE_THRESHOLD
= 500 The maximum number of items the session stores before it starts deleting some.
Defaults to 500.
-
SESSION_FILE_MODE
= 384 The file mode wanted for the session files. Should be specified as an octal, eg
0o600
.Defaults to
0o600
.
-
SESSION_MONGODB
= None A
pymongo.MongoClient
instance.By default, connect to
127.0.0.1:27017
.
-
SESSION_MONGODB_DB
= 'flask_session' The MongoDB database you want to use.
Defaults to
'flask_session'
.
-
SESSION_MONGODB_COLLECT
= 'sessions' The MongoDB collection you want to use.
Defaults to
'sessions'
.
-
SESSION_SQLALCHEMY
= <SQLAlchemyUnchained engine=None> A
SQLAlchemy
extension instance.
-
SESSION_SQLALCHEMY_TABLE
= 'flask_sessions' The name of the SQL table you want to use.
Defaults to
flask_sessions
.
-
SESSION_SQLALCHEMY_MODEL
= None Set this if you need to customize the
BaseModel
subclass used for storing sessions in the database.
-
API Docs¶
SQLAlchemy Bundle¶
Integrates Flask SQLAlchemy Unchained and Flask Migrate with Flask Unchained.
Dependencies¶
Flask SQLAlchemy
Flask Migrate
SQLAlchemy
Alembic
Depending on your database of choice, you might also need a specific driver library.
Installation¶
Install dependencies:
pip install "flask-unchained[sqlalchemy]"
And enable the bundle in your unchained_config.py
:
# your_project_root/unchained_config.py
BUNDLES = [
# ...
'flask_unchained.bundles.sqlalchemy',
'app',
]
Config¶
-
class
flask_unchained.bundles.sqlalchemy.config.
Config
[source] The default configuration options for the SQLAlchemy Bundle.
-
SQLALCHEMY_DATABASE_URI
= 'sqlite:///db/development.sqlite' The database URI that should be used for the connection. Defaults to using SQLite with the database file stored at
ROOT_PATH/db/<env>.sqlite
. See the SQLAlchemy Dialects documentation for more info.
-
SQLALCHEMY_TRANSACTION_ISOLATION_LEVEL
= None Set the engine-wide transaction isolation level.
See the docs for more info.
-
SQLALCHEMY_ECHO
= False If set to
True
SQLAlchemy will log all the statements issued to stderr which can be useful for debugging.
-
SQLALCHEMY_TRACK_MODIFICATIONS
= False If set to
True
, Flask-SQLAlchemy will track modifications of objects and emit signals. The default isFalse
. This requires extra memory and should be disabled if not needed.
-
SQLALCHEMY_RECORD_QUERIES
= None Can be used to explicitly disable or enable query recording. Query recording automatically happens in debug or testing mode. See
get_debug_queries()
for more information.
-
SQLALCHEMY_BINDS
= None A dictionary that maps bind keys to SQLAlchemy connection URIs.
-
SQLALCHEMY_NATIVE_UNICODE
= None Can be used to explicitly disable native unicode support. This is required for some database adapters (like PostgreSQL on some Ubuntu versions) when used with improper database defaults that specify encoding-less databases.
-
SQLALCHEMY_POOL_SIZE
= None The size of the database pool. Defaults to the engine’s default (usually 5).
-
SQLALCHEMY_POOL_TIMEOUT
= None Specifies the connection timeout in seconds for the pool.
-
SQLALCHEMY_POOL_RECYCLE
= None Number of seconds after which a connection is automatically recycled. This is required for MySQL, which removes connections after 8 hours idle by default. Note that Flask-SQLAlchemy automatically sets this to 2 hours if MySQL is used.
Certain database backends may impose different inactive connection timeouts, which interferes with Flask-SQLAlchemy’s connection pooling.
By default, MariaDB is configured to have a 600 second timeout. This often surfaces hard to debug, production environment only exceptions like
2013: Lost connection to MySQL server
during query.If you are using a backend (or a pre-configured database-as-a-service) with a lower connection timeout, it is recommended that you set
SQLALCHEMY_POOL_RECYCLE
to a value less than your backend’s timeout.
-
SQLALCHEMY_MAX_OVERFLOW
= None Controls the number of connections that can be created after the pool reached its maximum size. When those additional connections are returned to the pool, they are disconnected and discarded.
-
SQLALCHEMY_COMMIT_ON_TEARDOWN
= False Whether or not to automatically commit on app context teardown. Defaults to False.
-
ALEMBIC
= {'script_location': 'db/migrations'} Used to set the directory where migrations are stored. ALEMBIC should be set to a dictionary, using the key script_location to set the directory. Defaults to
ROOT_PATH/db/migrations
.
-
ALEMBIC_CONTEXT
= {'render_item': <function render_migration_item>, 'template_args': {'migration_variables': []}} Extra kwargs to pass to the constructor of the Flask-Migrate extension. If you need to change this, make sure to merge the defaults with your settings!
-
-
class
flask_unchained.bundles.sqlalchemy.config.
TestConfig
[source] Default configuration options for testing.
-
SQLALCHEMY_DATABASE_URI
= 'sqlite://' The database URI to use for testing. Defaults to SQLite in memory.
-
Usage¶
Usage of the SQLAlchemy bundle starts with an import:
from flask_unchained.bundles.sqlalchemy import db
From there, usage is extremely similar to stock Flask SQLAlchemy, and aside from a few minor gotchyas, you should just be able to copy your models and they should work (please file a bug report if this doesn’t work!). The gotchyas you should be aware of are:
the automatic table naming has slightly different behavior if the model class name has sequential upper-case characters
any models defined in not-app-bundles must declare themselves as
class Meta: lazy_mapped = True
you must use
back_populates
instead ofbackref
inrelationship
declarations (it may be possible to implement support for backrefs, but honestly, usingback_populates
is more Pythonic anyway, because Zen of Python)many-to-many join tables must be declared as model classes
Furthermore, models in SQLAlchemy Unchained include three columns by default:
column name |
description |
---|---|
|
the primary key |
|
a timestamp of when the model got inserted into the database |
|
a timestamp of the last time the model was updated in the database |
These are customizable by declaring meta options. For example to disable timestamping and to rename the primary key column to pk
, it would look like this:
from flask_unchained.bundles.sqlalchemy import db
class Foo(db.Model):
class Meta:
pk = 'pk'
created_at = None
updated_at = None
Models support the following meta options:
meta option name |
default |
description |
---|---|---|
table |
snake_cased model class name |
The database tablename to use for this model. |
pk |
|
The name of the primary key column. |
created_at |
|
The name of the row creation timestamp column. |
updated_at |
|
The name of the most recent row update timestamp column. |
repr |
|
Column attributes to include in the automatic |
validation |
|
Whether or not to enable validation on the model for CRUD operations. |
mv_for |
|
Used for specifying the name of the model a |
relationships |
|
This is an automatically determined meta option, and is used for determining whether or not a model has the same relationships as its base model. This is useful when you want to override a model from a bundle but change its relationships. The code that determines this is rather experimental, and may not do the right thing. Please report any bugs you come across! |
FIXME: Polymorphic Models
Commands¶
flask db¶
Perform database migrations.
flask db [OPTIONS] COMMAND [ARGS]...
branches¶
Show current branch points
flask db branches [OPTIONS]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
-
-v
,
--verbose
¶
Use more verbose output
current¶
Display the current revision for each database.
flask db current [OPTIONS]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
-
-v
,
--verbose
¶
Use more verbose output
downgrade¶
Revert to a previous version
flask db downgrade [OPTIONS] [REVISION]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
-
--sql
¶
Don’t emit SQL to database - dump to standard output instead
-
--tag
<tag>
¶ Arbitrary “tag” name - can be used by custom env.py scripts
-
-x
,
--x-arg
<x_arg>
¶ Additional arguments consumed by custom env.py scripts
Arguments
-
REVISION
¶
Optional argument
drop¶
Drop database tables.
flask db drop [OPTIONS]
Options
-
--force
¶
edit¶
Edit a revision file
flask db edit [OPTIONS] [REVISION]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
Arguments
-
REVISION
¶
Optional argument
heads¶
Show current available heads in the script directory
flask db heads [OPTIONS]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
-
-v
,
--verbose
¶
Use more verbose output
-
--resolve-dependencies
¶
Treat dependency versions as down revisions
history¶
List changeset scripts in chronological order.
flask db history [OPTIONS]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
-
-r
,
--rev-range
<rev_range>
¶ Specify a revision range; format is [start]:[end]
-
-v
,
--verbose
¶
Use more verbose output
-
-i
,
--indicate-current
¶
Indicate current version (Alembic 0.9.9 or greater is required)
init¶
Creates a new migration repository.
flask db init [OPTIONS]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
-
--multidb
¶
Support multiple databases
merge¶
Merge two revisions together, creating a new revision file
flask db merge [OPTIONS] [REVISIONS]...
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
-
-m
,
--message
<message>
¶ Merge revision message
-
--branch-label
<branch_label>
¶ Specify a branch label to apply to the new revision
-
--rev-id
<rev_id>
¶ Specify a hardcoded revision id instead of generating one
Arguments
-
REVISIONS
¶
Optional argument(s)
migrate¶
Autogenerate a new revision file (Alias for ‘revision –autogenerate’)
flask db migrate [OPTIONS]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
-
-m
,
--message
<message>
¶ Revision message
-
--sql
¶
Don’t emit SQL to database - dump to standard output instead
-
--head
<head>
¶ Specify head revision or <branchname>@head to base new revision on
-
--splice
¶
Allow a non-head revision as the “head” to splice onto
-
--branch-label
<branch_label>
¶ Specify a branch label to apply to the new revision
-
--version-path
<version_path>
¶ Specify specific path from config for version file
-
--rev-id
<rev_id>
¶ Specify a hardcoded revision id instead of generating one
-
-x
,
--x-arg
<x_arg>
¶ Additional arguments consumed by custom env.py scripts
reset¶
Drop database tables and run migrations.
flask db reset [OPTIONS]
Options
-
--force
¶
revision¶
Create a new revision file.
flask db revision [OPTIONS]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
-
-m
,
--message
<message>
¶ Revision message
-
--autogenerate
¶
Populate revision script with candidate migration operations, based on comparison of database to model
-
--sql
¶
Don’t emit SQL to database - dump to standard output instead
-
--head
<head>
¶ Specify head revision or <branchname>@head to base new revision on
-
--splice
¶
Allow a non-head revision as the “head” to splice onto
-
--branch-label
<branch_label>
¶ Specify a branch label to apply to the new revision
-
--version-path
<version_path>
¶ Specify specific path from config for version file
-
--rev-id
<rev_id>
¶ Specify a hardcoded revision id instead of generating one
show¶
Show the revision denoted by the given symbol.
flask db show [OPTIONS] [REVISION]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
Arguments
-
REVISION
¶
Optional argument
stamp¶
‘stamp’ the revision table with the given revision; don’t run any migrations
flask db stamp [OPTIONS] [REVISION]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
-
--sql
¶
Don’t emit SQL to database - dump to standard output instead
-
--tag
<tag>
¶ Arbitrary “tag” name - can be used by custom env.py scripts
Arguments
-
REVISION
¶
Optional argument
upgrade¶
Upgrade to a later version
flask db upgrade [OPTIONS] [REVISION]
Options
-
-d
,
--directory
<directory>
¶ Migration script directory (default is “migrations”)
-
--sql
¶
Don’t emit SQL to database - dump to standard output instead
-
--tag
<tag>
¶ Arbitrary “tag” name - can be used by custom env.py scripts
-
-x
,
--x-arg
<x_arg>
¶ Additional arguments consumed by custom env.py scripts
Arguments
-
REVISION
¶
Optional argument
API Docs¶
Webpack Bundle¶
Commands¶
flask new¶
Generate new code for your Flask Unchained projects.
flask new COMMAND [<args>...] [OPTIONS]
project¶
Create a new Flask Unchained project.
flask new project <dest> [OPTIONS]
Options
-
-a
,
--app-bundle
<app_bundle>
¶ The module name to use for your app bundle.
-
--force
,
--no-force
¶
Whether or not to force creation if project folder is not empty.
- Default
False
-
--prompt
,
--no-prompt
¶
Whether or not to skip prompting and just use the defaults.
- Default
False
-
--dev
,
--no-dev
¶
Whether or not to install development dependencies.
- Default
<function <lambda> at 0x7f4a6aca4950>
-
--admin
,
--no-admin
¶
Whether or not to install the Admin Bundle.
- Default
<function <lambda> at 0x7f4a6aca4a70>
-
--api
,
--no-api
¶
Whether or not to install the API Bundle.
- Default
<function <lambda> at 0x7f4a6aca4b90>
-
--celery
,
--no-celery
¶
Whether or not to install the Celery Bundle.
- Default
<function <lambda> at 0x7f4a6aca4cb0>
-
--graphene
,
--no-graphene
¶
Whether or not to install the Graphene Bundle.
- Default
<function <lambda> at 0x7f4a6aca4dd0>
-
--mail
,
--no-mail
¶
Whether or not to install the Mail Bundle.
- Default
<function <lambda> at 0x7f4a6aca4ef0>
-
--oauth
,
--no-oauth
¶
Whether or not to install the OAuth Bundle.
- Default
<function <lambda> at 0x7f4a6ac9c050>
-
--security
,
--no-security
¶
Whether or not to install the Security Bundle.
- Default
<function <lambda> at 0x7f4a6ac9c170>
-
--session
,
--no-session
¶
Whether or not to install the Session Bundle.
- Default
<function <lambda> at 0x7f4a6ac9c290>
-
--sqlalchemy
,
--no-sqlalchemy
¶
Whether or not to install the SQLAlchemy Bundle.
- Default
<function <lambda> at 0x7f4a6ac9c3b0>
-
--webpack
,
--no-webpack
¶
Whether or not to install the Webpack Bundle.
- Default
<function <lambda> at 0x7f4a6ac9c4d0>
Arguments
-
DEST
¶
Required argument
flask run¶
Run a local development server.
This server is for development purposes only. It does not provide the stability, security, or performance of production WSGI servers.
The reloader and debugger are enabled by default if FLASK_ENV=development or FLASK_DEBUG=1.
flask run [OPTIONS]
Options
-
-h
,
--host
<host>
¶ The interface to bind to.
-
-p
,
--port
<port>
¶ The port to bind to.
-
--cert
<cert>
¶ Specify a certificate file to use HTTPS.
-
--key
<key>
¶ The key file to use when specifying a certificate.
-
--reload
,
--no-reload
¶
Enable or disable the reloader. By default the reloader is active if debug is enabled.
-
--debugger
,
--no-debugger
¶
Enable or disable the debugger. By default the debugger is active if debug is enabled.
-
--eager-loading
,
--lazy-loading
¶
Enable or disable eager loading. By default eager loading is enabled if the reloader is disabled.
-
--with-threads
,
--without-threads
¶
Enable or disable multithreading.
-
--extra-files
<extra_files>
¶ Extra files that trigger a reload on change. Multiple paths are separated by ‘:’.
flask shell¶
Runs a shell in the app context. If IPython
is installed, it will
be used, otherwise the default Python shell is used.
flask shell [OPTIONS]
flask unchained¶
Flask Unchained commands.
flask unchained COMMAND [<args>...] [OPTIONS]
bundles¶
List registered bundles.
flask unchained bundles [OPTIONS]
config¶
Show current app config (or optionally just the options for a specific bundle).
flask unchained config [<bundle_name>] [OPTIONS]
Arguments
-
BUNDLE_NAME
¶
Optional argument
extensions¶
List extensions.
flask unchained extensions [OPTIONS]
hooks¶
List registered hooks (in the order they run).
flask unchained hooks [OPTIONS]
services¶
List services.
flask unchained services [OPTIONS]
flask urls¶
List all URLs registered with the app.
flask urls [OPTIONS]
Options
-
--order-by
<order_by>
¶ Property to order by: methods, rule, endpoint, view, or priority (aka registration order with the app)
flask url¶
Show details for a specific URL.
flask url <url> [OPTIONS]
Options
-
--method
<method>
¶ Method for url to match (default: GET)
Arguments
-
URL
¶
Required argument
flask clean¶
Recursively remove *.pyc and *.pyo files.
flask clean [OPTIONS]
flask lint¶
Run flake8.
flask lint [OPTIONS]
Options
-
-f
,
--fix-imports
¶
Fix imports using isort, before linting
flask qtconsole¶
Starts qtconsole in the app context. !!EXPERIMENTAL!!
Only available if Ipython
, PyQt5
and qtconsole
are installed.
flask qtconsole [OPTIONS]
Testing¶
Included pytest fixtures¶
app¶
maybe_inject_extensions_and_services¶
-
flask_unchained.pytest.
maybe_inject_extensions_and_services
(app, request)[source]¶ Automatically used test fixture. Allows for using services and extensions as if they were test fixtures:
def test_something(db, mail, security_service, user_manager): # assert important stuff
NOTE: This only works on tests themselves; it will not work on test fixtures
cli_runner¶
-
flask_unchained.pytest.
cli_runner
(app)[source]¶ Yields an instance of
FlaskCliRunner
. Example usage:from your_package.commands import some_command def test_some_command(cli_runner): result = cli_runner.invoke(some_command) assert result.exit_code == 0 assert result.output.strip() == 'output of some_command'
client¶
-
flask_unchained.pytest.
client
(app)[source]¶ Yields an instance of
HtmlTestClient
. Example usage:def test_some_view(client): r = client.get('some.endpoint') # r is an instance of :class:`HtmlTestResponse` assert r.status_code == 200 assert 'The Page Title' in r.html
api_client¶
-
flask_unchained.pytest.
api_client
(app)[source]¶ Yields an instance of
ApiTestClient
. Example usage:def test_some_view(api_client): r = api_client.get('some.endpoint.returning.json') # r is an instance of :class:`ApiTestResponse` assert r.status_code == 200 assert 'some_key' in r.json
templates¶
-
flask_unchained.pytest.
templates
(app)[source]¶ Fixture to record which templates (if any) got rendered during a request. Example Usage:
def test_some_view(client, templates): r = client.get('some.endpoint') assert r.status_code == 200 assert templates[0].template.name == 'some/template.html' assert templates[0].context.get('some_ctx_var') == 'expected value'
API Reference¶
Flask Unchained API¶
flask_unchained.app_factory
The Application Factory Pattern for Flask Unchained. |
flask_unchained.app_factory_hook
Base class for hooks. |
flask_unchained.bundles
Like |
|
Base class for bundles. |
flask_unchained.config
Base class for configuration settings. |
flask_unchained.di
Base class for services. |
flask_unchained.flask_unchained
A simple subclass of |
flask_unchained.forms
Base form class extending |
flask_unchained.hooks
Updates |
|
Initializes extensions found in bundles with the current app. |
|
Registers commands and command groups from bundles. |
|
Registers extensions found in bundles with the |
|
Registers services for dependency injection. |
|
An internal hook to discover and run all the other hooks. |
|
Allows configuring bundle views modules. |
flask_unchained.string_utils
|
Right replaces |
|
Converts a string into a url-safe slug. |
|
Returns the plural of a given word, e.g., child => children. |
|
Returns the singular of a given word. |
|
Converts a string to camel case. |
|
Converts a string to class case. |
|
Converts a string to kebab case. |
|
Converts a string to snake case. |
|
Converts a string to title case. |
flask_unchained.unchained
The |
flask_unchained.utils
A dictionary that allows using the dot operator to get and set keys. |
|
Allows extension classes to create properties that proxy to the config value, eg |
|
Use this metaclass to enable config properties on extension classes. |
|
Attempt to import a module from the current working directory. |
|
Converts environment variables to boolean values. |
|
Like |
|
Returns a current timezone-aware |
Constants¶
DEV¶
-
flask_unchained.constants.
DEV
¶ Used to specify the development environment.
PROD¶
-
flask_unchained.constants.
PROD
¶ Used to specify the production environment.
STAGING¶
-
flask_unchained.constants.
STAGING
¶ Used to specify the staging environment.
TEST¶
-
flask_unchained.constants.
TEST
¶ Used to specify the test environment.
injectable¶
-
flask_unchained.di.
injectable
= 'INJECTABLE_PARAMETER'¶ Use this to mark a service parameter as injectable. For example:
class MyService(Service): a_dependency: ADependency = injectable
This allows MyService to be used in two ways:
# 1. using dependency injection with Flask Unchained my_service = MyService() # 2. overriding the dependency injection (or used without Flask Unchained) a_dependency = ADependency() my_service = MyService(a_dependency) # but, if you try to use it without Flask Unchained and without parameters: my_service = MyService() # raises ServiceUsageError
AppFactory¶
-
class
flask_unchained.
AppFactory
[source]¶ The Application Factory Pattern for Flask Unchained.
-
APP_CLASS
¶ alias of
flask_unchained.flask_unchained.FlaskUnchained
-
create_app
(env: Union[development, production, staging, test], bundles: Optional[List[str]] = None, *, _config_overrides: Optional[Dict[str, Any]] = None, _load_unchained_config: bool = True, **app_kwargs) → flask_unchained.flask_unchained.FlaskUnchained[source]¶ Flask Unchained Application Factory. Returns an instance of
APP_CLASS
(by default,FlaskUnchained
).Example Usage:
app = AppFactory().create_app(PROD)
- Parameters
env (str) – Which environment the app should run in. Should be one of “development”, “production”, “staging”, or “test” (you can import them:
from flask_unchained import DEV, PROD, STAGING, TEST
)bundles (List[str]) – An optional list of bundle modules names to use. Overrides
unchained_config.BUNDLES
(mainly useful for testing).app_kwargs (Dict[str, Any]) – keyword argument overrides for the
APP_CLASS
constructor_config_overrides – a dictionary of config option overrides; meant for test fixtures (for internal use only).
_load_unchained_config – Whether or not to try to load unchained_config (for internal use only).
- Returns
The initialized
APP_CLASS
app instance, ready to rock’n’roll
-
static
load_unchained_config
(env: Union[development, production, staging, test]) → module[source]¶ Load the unchained config from the current working directory for the given environment. If
env == "test"
, look fortests._unchained_config
, otherwise check the value of theUNCHAINED
environment variable, falling back to loading theunchained_config
module.
-
get_app_kwargs
(app_kwargs: Dict[str, Any], bundles: List[flask_unchained.bundles.Bundle], env: Union[development, production, staging, test], unchained_config: Dict[str, Any]) → Dict[str, Any][source]¶ Returns
app_kwargs
with default settings applied fromunchained_config
.
-
classmethod
load_bundles
(bundle_package_names: Optional[List[str]] = None, unchained_config_module: Optional[module] = None) → Tuple[Union[None, flask_unchained.bundles.AppBundle], List[flask_unchained.bundles.Bundle]][source]¶ Load bundle instances from the given list of bundle packages. If
unchained_config_module
is given and there was no app bundle listed inbundle_package_names
, attempt to load the app bundle from the unchained config.
-
AppFactoryHook¶
-
class
flask_unchained.
AppFactoryHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Base class for hooks.
Hooks have one entry point,
run_hook()
, which can be overridden to completely customize the behavior of the subclass. The default behavior is to look for objects inbundle_module_names
which pass the result oftype_check()
. These objects are collected from all bundles into a dictionary with keys the result ofkey_name()
, starting from the base-most bundle, allowing bundle subclasses to override objects with the same name from earlier bundles.Subclasses should implement at a minimum
bundle_module_names
,process_objects()
, andtype_check()
. You may also need to set one or both ofrun_before
orrun_after
. Also of interest, hooks can store objects on their bundle’s instance, usingbundle
. Hooks can also modify the shell context usingupdate_shell_context()
.-
name
: str = 'app_factory_hook'¶ The name of this hook. Defaults to the snake_cased class name.
-
run_before
: Union[List[str], Tuple[str, ...]] = ()¶ An optional list of hook names that this hook must run before.
-
run_after
: Union[List[str], Tuple[str, ...]] = ()¶ An optional list of hook names that this hook must run after.
-
bundle_module_name
: Optional[str] = None¶ If
require_exactly_one_bundle_module
is True, only load from this module name in bundles. Should be set toNone
if your hook does not use that default functionality.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = None¶ A list of the default module names this hook will load from in bundles. Should be set to
None
if your hook does not use that default functionality (orrequire_exactly_one_bundle_module
is True).
-
require_exactly_one_bundle_module
: bool = False¶ Whether or not to require that there must be exactly one module name to load from in bundles.
-
bundle_override_module_names_attr
: str = None¶ The attribute name that bundles can set on themselves to override the module(s) this hook will load from for that bundle. The defaults are as follows:
If
require_exactly_one_bundle_module
andbundle_module_name
are set, usef'{YourHook.bundle_module_name}_module_name'
.Otherwise if
bundle_module_names
is set, we use the same f-string, just with the first module name listed inbundle_module_names
.If neither of
bundle_module_name
orbundle_module_names
is set, then this will beNone
.
-
discover_from_bundle_superclasses
: bool = True¶ Whether or not to search the whole bundle hierarchy for objects.
-
limit_discovery_to_local_declarations
: bool = True¶ Whether or not to only include objects declared within bundles (ie not imported from other places, like third-party code).
-
run_hook
(app: flask_unchained.flask_unchained.FlaskUnchained, bundles: List[flask_unchained.bundles.Bundle], unchained_config: Optional[Dict[str, Any]] = None) → None[source]¶ Hook entry point. Override to disable standard behavior of iterating over bundles to discover objects and processing them.
-
process_objects
(app: flask_unchained.flask_unchained.FlaskUnchained, objects: Dict[str, Any]) → None[source]¶ Implement to do stuff with discovered objects (eg, registering them with the app instance).
-
collect_from_bundles
(bundles: List[flask_unchained.bundles.Bundle], *, _initial_objects: Optional[Dict[str, Any]] = None) → Dict[str, Any][source]¶ Collect objects where
type_check()
returnsTrue
from bundles. Discovered names (keys, typically the class names) are expected to be unique across bundle hierarchies, except for the app bundle, which can override anything from other bundles.
-
collect_from_bundle
(bundle: flask_unchained.bundles.Bundle) → Dict[str, Any][source]¶ Collect objects where
type_check()
returnsTrue
from bundles. Bundle subclasses can override objects discovered in superclass bundles.
-
key_name
(name: str, obj: Any) → str[source]¶ Override to use a custom key to determine uniqueness/overriding.
-
type_check
(obj: Any) → bool[source]¶ Implement to determine which objects in a module should be processed by this hook.
-
classmethod
import_bundle_modules
(bundle: flask_unchained.bundles.Bundle) → List[module][source]¶ Safe-import the modules in a bundle for this hook to load from.
-
classmethod
get_module_names
(bundle: flask_unchained.bundles.Bundle) → List[str][source]¶ The list of fully-qualified module names for a bundle this hook should load from.
-
AppBundle¶
Bundle¶
-
class
flask_unchained.
Bundle
[source]¶ Base class for bundles.
Should be placed in your package’s root or its
bundle
module:# your_bundle_package/__init__.py or your_bundle_package/bundle.py class YourBundle(Bundle): pass
-
name
: str = 'bundle'¶ Name of the bundle. Defaults to the snake_cased class name.
-
module_name
: str = 'flask_unchained.bundles'¶ Top-level module name of the bundle (dot notation).
Automatically determined; read-only.
-
root_path
: str = '/home/docs/checkouts/readthedocs.org/user_builds/flask-unchained/envs/latest/lib/python3.7/site-packages/Flask_Unchained-0.9.0-py3.7.egg/flask_unchained/bundles'¶ Root directory path of the bundle’s package.
Automatically determined; read-only.
-
template_folder
¶ Root directory path of the bundle’s template folder. By default, if there exists a folder named
templates
in the bundle packageroot_path
, it will be used, otherwiseNone
.
-
static_folder
¶ Root directory path of the bundle’s static assets folder. By default, if there exists a folder named
static
in the bundle packageroot_path
, it will be used, otherwiseNone
.
-
static_url_path
¶ Url path where this bundle’s static assets will be served from. If
static_folder
is set, this will default to/<bundle.name>/static
, otherwiseNone
.
-
is_single_module
: bool = False¶ Whether or not the bundle is a single module (Python file).
Automatically determined; read-only.
-
default_load_from_module_name
: Optional[str] = None¶ The default module name for hooks to load from. Set hooks’ bundle modules override attributes for the modules you want in separate files.
WARNING - EXPERIMENTAL
Using this feature may cause mysterious exceptions to be thrown!!
Best practice is to organize your code in separate modules.
-
before_init_app
(app: flask_unchained.flask_unchained.FlaskUnchained) → None[source]¶ Override this method to perform actions on the
FlaskUnchained
app instance before theunchained
extension has initialized the application.
-
after_init_app
(app: flask_unchained.flask_unchained.FlaskUnchained) → None[source]¶ Override this method to perform actions on the
FlaskUnchained
app instance after theunchained
extension has initialized the application.
-
BundleConfig¶
-
class
flask_unchained.
BundleConfig
[source]¶ Base class for configuration settings. Allows access to the app-under-construction as it’s currently configured. Example usage:
# your_bundle_root/config.py import os from flask_unchained import BundleConfig class Config(BundleConfig): SHOULD_PRETTY_PRINT_JSON = BundleConfig.current_app.config.DEBUG
FlaskUnchained¶
-
class
flask_unchained.
FlaskUnchained
(import_name: str, static_url_path: Optional[str] = None, static_folder: Optional[str] = 'static', static_host: Optional[str] = None, host_matching: bool = False, subdomain_matching: bool = False, template_folder: Optional[str] = 'templates', instance_path: Optional[str] = None, instance_relative_config: bool = False, root_path: Optional[str] = None)[source]¶ A simple subclass of
flask.Flask
. Overridesregister_blueprint()
andadd_url_rule()
to support automatic (optional) registration of URLs prefixed with a language code.-
config_class
¶ alias of
AttrDictFlaskConfig
-
env
: str = None¶ The environment the application is running in. Will be one of
development
,production
,staging
, ortest
.
-
register_blueprint
(blueprint: flask.blueprints.Blueprint, *, register_with_babel: bool = True, **options: Any) → None[source]¶ The same as
flask.Flask.register_blueprint()
, but ifregister_with_babel
is True, then we also allow the Babel Bundle an opportunity to register language code prefixed URLs.
-
add_url_rule
(rule: str, endpoint: Optional[str] = None, view_func: Optional[function] = None, provide_automatic_options: Optional[bool] = None, *, register_with_babel: bool = False, **options: Any) → None[source]¶ The same as
flask.Flask.add_url_rule()
, but ifregister_with_babel
is True, then we also allow the Babel Bundle an opportunity to register a language code prefixed URL.
-
Service¶
Hooks¶
ConfigureAppHook¶
-
class
flask_unchained.hooks.
ConfigureAppHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Updates
app.config
with the settings from each bundle.-
name
: str = 'configure_app'¶ The name of this hook.
-
bundle_module_name
: Optional[str] = 'config'¶ The default module this hook loads from.
Override by setting the
config_module_name
attribute on your bundle class.
-
run_hook
(app: flask_unchained.flask_unchained.FlaskUnchained, bundles: List[flask_unchained.bundles.Bundle], unchained_config: Optional[Dict[str, Any]] = None) → None[source]¶ For each bundle in
unchained_config.BUNDLES
, iterate through that bundle’s class hierarchy, starting from the base-most bundle. For each bundle in that order, look for aconfig
module, and if it exists, updateapp.config
with the options first from a baseConfig
class, if it exists, and then also if it exists, from an env-specific config class: one ofDevConfig
,ProdConfig
,StagingConfig
, orTestConfig
.
-
InitExtensionsHook¶
-
class
flask_unchained.hooks.
InitExtensionsHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Initializes extensions found in bundles with the current app.
-
name
: str = 'init_extensions'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['extensions']¶ The default module this hook loads from.
Override by setting the
extensions_module_names
attribute on your bundle class.
-
RegisterCommandsHook¶
-
class
flask_unchained.hooks.
RegisterCommandsHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers commands and command groups from bundles.
-
name
: str = 'commands'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['commands']¶ The default module this hook loads from.
Override by setting the
commands_module_names
attribute on your bundle class.
-
run_hook
(app: flask_unchained.flask_unchained.FlaskUnchained, bundles: List[flask_unchained.bundles.Bundle], unchained_config: Optional[Dict[str, Any]] = None) → Dict[str, Union[click.core.Command, click.core.Group]][source]¶ Discover CLI commands and command groups from bundles and register them with the app.
-
RegisterExtensionsHook¶
-
class
flask_unchained.hooks.
RegisterExtensionsHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers extensions found in bundles with the
unchained
extension.-
name
: str = 'register_extensions'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['extensions']¶ The default module this hook loads from.
Override by setting the
extensions_module_names
attribute on your bundle class.
-
RegisterServicesHook¶
-
class
flask_unchained.hooks.
RegisterServicesHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers services for dependency injection.
-
name
: str = 'services'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['services', 'managers']¶ The default modules this hook loads from.
Override by setting the
services_module_names
attribute on your bundle class.
-
RunHooksHook¶
-
class
flask_unchained.hooks.
RunHooksHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ An internal hook to discover and run all the other hooks.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['hooks']¶ The default module this hook loads from.
Override by setting the
hooks_module_names
attribute on your bundle class.
-
run_hook
(app: flask_unchained.flask_unchained.FlaskUnchained, bundles: List[flask_unchained.bundles.Bundle], unchained_config: Optional[Dict[str, Any]] = None) → None[source]¶ Collect hooks from Flask Unchained and the list of bundles, resolve their correct order, and run them in that order to build (boot) the app instance.
-
collect_from_bundle
(bundle: flask_unchained.bundles.Bundle) → Dict[str, flask_unchained.hooks.run_hooks_hook.HookTuple][source]¶ Collect hooks from a bundle hierarchy.
-
collect_unchained_hooks
() → Dict[str, flask_unchained.hooks.run_hooks_hook.HookTuple][source]¶ Collect hooks built into Flask Unchained that should always run.
-
type_check
(obj: Any) → bool[source]¶ Returns True if
obj
is a subclass ofAppFactoryHook
.
-
ViewsHook¶
-
class
flask_unchained.hooks.
ViewsHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Allows configuring bundle views modules.
-
name
: str = 'views'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['views']¶ The default module this hook loads from.
Override by setting the
views_module_names
attribute on your bundle class.
-
Unchained¶
-
class
flask_unchained.
Unchained
(env: Union[development, production, staging, test, None] = None)[source]¶ The
Unchained
extension. Responsible for initializing the app by loading all the things from bundles, keeping references to all of the various discovered bundles and things inside them, and for doing dependency injection. To get access to theunchained
extension instance:from flask_unchained import unchained
Also acts as a replacement for some of the public API of
flask.Flask
. (The part that allows registering url rules, functions to run for handling errors, functions to run during the normal request response cycle, and methods for setting up the Jinja templating environment.)-
get_local_proxy
(name)[source]¶ Returns a
LocalProxy
to the extension or service withname
as registered with the current app.
-
inject
(*args)[source]¶ Decorator to mark a class, method, or function as needing dependencies injected.
Example usage:
from flask_unchained import unchained, injectable # automatically figure out which params to inject @unchained.inject() def my_function(not_injected, some_service: SomeService = injectable): # do stuff # or declare injectables explicitly (makes the ``injectable`` default optional) @unchained.inject('some_service') def my_function(not_injected, some_service: SomeService): # do stuff # use it on a class to set up class attributes injection (and the constructor) @unchained.inject() class MyClass: some_service: SomeService = injectable def __init__(self, another_service: AnotherService = injectable): self.another_service = another_service
-
add_url_rule
(rule, endpoint=None, view_func=None, **options)[source]¶ Register a new url rule. Acts the same as
flask.Flask.add_url_rule()
.
-
before_request
(fn=None)[source]¶ Registers a function to run before each request.
For example, this can be used to open a database connection, or to load the logged in user from the session.
The function will be called without any arguments. If it returns a non-None value, the value is handled as if it was the return value from the view, and further request handling is stopped.
-
before_first_request
(fn=None)[source]¶ Registers a function to be run before the first request to this instance of the application.
The function will be called without any arguments and its return value is ignored.
-
after_request
(fn=None)[source]¶ Register a function to be run after each request.
Your function must take one parameter, an instance of
response_class
and return a new response object or the same (seeprocess_response()
).As of Flask 0.7 this function might not be executed at the end of the request in case an unhandled exception occurred.
-
teardown_request
(fn=None)[source]¶ Register a function to be run at the end of each request, regardless of whether there was an exception or not. These functions are executed when the request context is popped, even if not an actual request was performed.
Example:
ctx = app.test_request_context() ctx.push() ... ctx.pop()
When
ctx.pop()
is executed in the above example, the teardown functions are called just before the request context moves from the stack of active contexts. This becomes relevant if you are using such constructs in tests.Generally teardown functions must take every necessary step to avoid that they will fail. If they do execute code that might fail they will have to surround the execution of these code by try/except statements and log occurring errors.
When a teardown function was called because of an exception it will be passed an error object.
The return values of teardown functions are ignored.
Debug Note
In debug mode Flask will not tear down a request on an exception immediately. Instead it will keep it alive so that the interactive debugger can still access it. This behavior can be controlled by the
PRESERVE_CONTEXT_ON_EXCEPTION
configuration variable.
-
teardown_appcontext
(fn=None)[source]¶ Registers a function to be called when the application context ends. These functions are typically also called when the request context is popped.
Example:
ctx = app.app_context() ctx.push() ... ctx.pop()
When
ctx.pop()
is executed in the above example, the teardown functions are called just before the app context moves from the stack of active contexts. This becomes relevant if you are using such constructs in tests.Since a request context typically also manages an application context it would also be called when you pop a request context.
When a teardown function was called because of an unhandled exception it will be passed an error object. If an
errorhandler()
is registered, it will handle the exception and the teardown will not receive it.The return values of teardown functions are ignored.
-
url_value_preprocessor
(fn=None)[source]¶ Register a URL value preprocessor function for all view functions in the application. These functions will be called before the
before_request()
functions.The function can modify the values captured from the matched url before they are passed to the view. For example, this can be used to pop a common language code value and place it in
g
rather than pass it to every view.The function is passed the endpoint name and values dict. The return value is ignored.
-
url_defaults
(fn=None)[source]¶ Callback function for URL defaults for all view functions of the application. It’s called with the endpoint and values and should update the values passed in place.
-
errorhandler
(code_or_exception)[source]¶ Register a function to handle errors by code or exception class.
A decorator that is used to register a function given an error code. Example:
@app.errorhandler(404) def page_not_found(error): return 'This page does not exist', 404
You can also register handlers for arbitrary exceptions:
@app.errorhandler(DatabaseError) def special_exception_handler(error): return 'Database connection failed', 500
- Parameters
code_or_exception – the code as integer for the handler, or an arbitrary exception
-
template_filter
(arg: Optional[Callable] = None, *, name: Optional[str] = None, pass_context: bool = False, inject: Union[bool, Iterable[str], None] = None, safe: bool = False) → Callable[source]¶ Decorator to mark a function as a Jinja template filter.
- Parameters
name – The name of the filter, if different from the function name.
pass_context – Whether or not to pass the template context into the filter. If
True
, the first argument must be the context.inject – Whether or not this filter needs any dependencies injected.
safe – Whether or not to mark the output of this filter as html-safe.
-
template_global
(arg: Optional[Callable] = None, *, name: Optional[str] = None, pass_context: bool = False, inject: Union[bool, Iterable[str], None] = None, safe: bool = False) → Callable[source]¶ Decorator to mark a function as a Jinja template global (tag).
- Parameters
name – The name of the tag, if different from the function name.
pass_context – Whether or not to pass the template context into the tag. If
True
, the first argument must be the context.inject – Whether or not this tag needs any dependencies injected.
safe – Whether or not to mark the output of this tag as html-safe.
-
template_tag
(arg: Optional[Callable] = None, *, name: Optional[str] = None, pass_context: bool = False, inject: Union[bool, Iterable[str], None] = None, safe: bool = False) → Callable[source]¶ Alias for
template_global()
.- Parameters
name – The name of the tag, if different from the function name.
pass_context – Whether or not to pass the template context into the tag. If
True
, the first argument must be the context.inject – Whether or not this tag needs any dependencies injected.
safe – Whether or not to mark the output of this tag as html-safe.
-
template_test
(arg: Optional[Callable] = None, *, name: Optional[str] = None, inject: Union[bool, Iterable[str], None] = None, safe: bool = False) → Callable[source]¶ Decorator to mark a function as a Jinja template test.
- Parameters
name – The name of the test, if different from the function name.
inject – Whether or not this test needs any dependencies injected.
safe – Whether or not to mark the output of this test as html-safe.
-
string_utils¶
-
flask_unchained.string_utils.
right_replace
(string, old, new, count=1)[source]¶ Right replaces
count
occurrences ofold
withnew
instring
. For example:right_replace('one_two_two', 'two', 'three') -> 'one_two_three'
-
flask_unchained.string_utils.
slugify
(string)[source]¶ Converts a string into a url-safe slug. For example:
slugify('Hello World') -> 'hello-world'
-
flask_unchained.string_utils.
pluralize
(word, pos='NN', custom=None, classical=True)[source]¶ Returns the plural of a given word, e.g., child => children. Handles nouns and adjectives, using classical inflection by default (i.e., where “matrix” pluralizes to “matrices” and not “matrixes”). The custom dictionary is for user-defined replacements.
-
flask_unchained.string_utils.
singularize
(word, pos='NN', custom=None)[source]¶ Returns the singular of a given word.
-
flask_unchained.string_utils.
camel_case
(string)[source]¶ Converts a string to camel case. For example:
camel_case('one_two_three') -> 'oneTwoThree'
-
flask_unchained.string_utils.
class_case
(string)[source]¶ Converts a string to class case. For example:
class_case('one_two_three') -> 'OneTwoThree'
-
flask_unchained.string_utils.
kebab_case
(string)[source]¶ Converts a string to kebab case. For example:
kebab_case('one_two_three') -> 'one-two-three'
NOTE: To generate valid slugs, use
slugify()
utils¶
-
class
flask_unchained.utils.
AttrDict
[source]¶ A dictionary that allows using the dot operator to get and set keys.
-
class
flask_unchained.utils.
ConfigProperty
(key=None)[source]¶ Allows extension classes to create properties that proxy to the config value, eg
app.config[key]
.If key is left unspecified, in will be injected by
ConfigPropertyMetaclass
, defaulting tof'{ext_class_name}_{property_name}'.upper()
.
-
class
flask_unchained.utils.
ConfigPropertyMetaclass
(class_name, bases, clsdict)[source]¶ Use this metaclass to enable config properties on extension classes. I’m not sold on this being a good idea for new extensions, but for backwards compatibility with existing extensions that have silly
__getattr__
magic, I think it’s a big improvement. (NOTE: this only works when the application context is available, but that’s no different than the behavior of what it’s meant to replace.)Example usage:
class MyExtension(metaclass=ConfigPropertyMetaclass): __config_prefix__ = 'MY_EXTENSION' # if __config_prefix__ is unspecified, default is class_name.upper() foobar: Optional[FunctionType] = ConfigProperty() _custom: Optional[str] = ConfigProperty('MY_EXTENSION_CUSTOM') my_extension = MyExtension(app) my_extension.foobar == current_app.config.MY_EXTENSION_FOOBAR my_extension._custom == current_app.config.MY_EXTENSION_CUSTOM
-
flask_unchained.utils.
cwd_import
(module_name)[source]¶ Attempt to import a module from the current working directory.
Raises
ImportError
if not found, or the found module isn’t from the current working directory.
-
flask_unchained.utils.
get_boolean_env
(name, default)[source]¶ Converts environment variables to boolean values. Truthy is defined as:
value.lower() in {'true', 'yes', 'y', '1'}
(everything else is falsy).
-
flask_unchained.utils.
safe_import_module
(module_name)[source]¶ Like
importlib.import_module()
, except it does not raiseImportError
if the requestedmodule_name
is not found.
Admin Bundle API¶
flask_unchained.bundles.admin
The Admin Bundle. |
flask_unchained.bundles.admin.config
Config class for the Admin bundle. |
flask_unchained.bundles.admin.extensions
The Admin extension. |
flask_unchained.bundles.admin.hooks
Registers ModelAdmins with the Admin extension. |
flask_unchained.bundles.admin.forms
Like |
|
An extension of |
flask_unchained.bundles.admin.macro
Replaces |
flask_unchained.bundles.admin.model_admin
Base class for SQLAlchemy model admins. |
flask_unchained.bundles.admin.views
Default admin dashboard view. |
|
Extends |
AdminBundle¶
-
class
flask_unchained.bundles.admin.
AdminBundle
[source]¶ The Admin Bundle.
-
name
: str = 'admin_bundle'¶ The name of the Admin Bundle.
-
after_init_app
(app: flask_unchained.flask_unchained.FlaskUnchained) → None[source]¶ Override this method to perform actions on the
FlaskUnchained
app instance after theunchained
extension has initialized the application.
-
Config¶
-
class
flask_unchained.bundles.admin.config.
Config
[source]¶ Config class for the Admin bundle. Defines which configuration values this bundle supports, and their default values.
-
ADMIN_NAME
= 'Admin'¶ The title of the admin section of the site.
-
ADMIN_BASE_URL
= '/admin'¶ Base url of the admin section of the site.
-
ADMIN_INDEX_VIEW
= <flask_unchained.bundles.admin.views.dashboard.AdminDashboardView object>¶ The
AdminIndexView
(or subclass) instance to use for the index view.
-
ADMIN_SUBDOMAIN
= None¶ Subdomain of the admin section of the site.
-
ADMIN_BASE_TEMPLATE
= 'admin/base.html'¶ Base template to use for other admin templates.
-
ADMIN_TEMPLATE_MODE
= 'bootstrap4'¶ Which version of bootstrap to use. (bootstrap2, bootstrap3, or bootstrap4)
-
ADMIN_CATEGORY_ICON_CLASSES
= {}¶ Dictionary of admin category icon classes. Keys are category names, and the values depend on which version of bootstrap you’re using.
For example, with bootstrap4:
ADMIN_CATEGORY_ICON_CLASSES = { 'Mail': 'fa fa-envelope', 'Security': 'fa fa-lock', }
-
ADMIN_ADMIN_ROLE_NAME
= 'ROLE_ADMIN'¶ The name of the Role which represents an admin.
-
ADMIN_LOGIN_ENDPOINT
= 'admin.login'¶ Name of the endpoint to use for the admin login view.
-
ADMIN_POST_LOGIN_REDIRECT_ENDPOINT
= 'admin.index'¶ Name of the endpoint to redirect to after the user logs into the admin.
-
ADMIN_LOGOUT_ENDPOINT
= 'admin.logout'¶ Name of the endpoint to use for the admin logout view.
-
ADMIN_POST_LOGOUT_REDIRECT_ENDPOINT
= 'admin.login'¶ Endpoint to redirect to after the user logs out of the admin.
-
MAPBOX_MAP_ID
= None¶
-
The Admin Extension¶
-
class
flask_unchained.bundles.admin.
Admin
(app=None, name=None, url=None, subdomain=None, index_view=None, translations_path=None, endpoint=None, static_url_path=None, base_template=None, template_mode=None, category_icon_classes=None)[source]¶ The Admin extension:
from flask_unchained.bundles.admin import admin
RegisterModelAdminsHook¶
-
class
flask_unchained.bundles.admin.hooks.
RegisterModelAdminsHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers ModelAdmins with the Admin extension.
-
name
: str = 'admins'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['admins']¶ The default module this hook loads from.
Override by setting the
admins_module_names
attribute on your bundle class.
-
type_check
(obj)[source]¶ Returns True if
obj
is a subclass ofModelAdmin
.
-
Forms¶
Macro¶
-
flask_unchained.bundles.admin.
macro
(name)[source]¶ Replaces
macro()
, adding support for using macros imported from another file. For example:{# templates/admin/column_formatters.html #} {% macro email(model, column) %} {% set address = model[column] %} <a href="mailto:{{ address }}">{{ address }}</a> {% endmacro %}
class FooAdmin(ModelAdmin): column_formatters = { 'col_name': macro('column_formatters.email') }
Also required for this to work, is to add the following to the top of your master admin template:
{# templates/admin/master.html #} {% import 'admin/column_formatters.html' as column_formatters with context %}
ModelAdmin¶
-
class
flask_unchained.bundles.admin.
ModelAdmin
(model, session, name=None, category=None, endpoint=None, url=None, static_folder=None, menu_class_name=None, menu_icon_type=None, menu_icon_value=None)[source]¶ Base class for SQLAlchemy model admins. More or less the same as
ModelView
, except we set some different defaults.-
form_base_class
¶ alias of
flask_unchained.bundles.admin.forms.ReorderableForm
-
model_form_converter
¶ alias of
flask_unchained.bundles.admin.forms.AdminModelFormConverter
-
action_view
()¶ Mass-model action view.
-
ajax_update
()¶ Edits a single column of a record in list view.
-
create_view
()¶ Create model view
-
delete_view
()¶ Delete model view. Only POST method is allowed.
-
details_view
()¶ Details model view
-
edit_view
()¶ Edit model view
-
index_view
()¶ List view
-
AdminDashboardView¶
AdminSecurityController¶
-
class
flask_unchained.bundles.admin.
AdminSecurityController
[source]¶ Extends
SecurityController
, to customize the template folder to use admin-specific templates.
API Bundle API¶
flask_unchained.bundles.api
The API Bundle. |
flask_unchained.bundles.api.config
Default config settings for the API Bundle. |
flask_unchained.bundles.api.extensions
The Api extension. |
|
The Marshmallow extension. |
flask_unchained.bundles.api.hooks
Registers ModelResources and configures ModelSerializers on them. |
|
Registers ModelSerializers. |
flask_unchained.bundles.api.model_resource
Base class for model resources. |
flask_unchained.bundles.api.model_serializer
Base class for SQLAlchemy model serializers. |
ApiBundle¶
-
class
flask_unchained.bundles.api.
ApiBundle
[source]¶ The API Bundle.
-
name
: str = 'api_bundle'¶ The name of the API Bundle.
-
resources_by_model
= None¶ Lookup of resource classes by class name.
-
serializers
= None¶ Lookup of serializer classes by class name.
-
serializers_by_model
= None¶ Lookup of serializer classes by model class name
-
create_by_model
= None¶ Lookup of serializer classes by model class name, as set by
@ma.serializer(create=True)
(seeserializer()
)
-
many_by_model
= None¶ Lookup of serializer classes by model class name, as set by
@ma.serializer(many=True)
(seeserializer()
)
-
Config¶
-
class
flask_unchained.bundles.api.config.
Config
[source]¶ Default config settings for the API Bundle.
-
API_OPENAPI_VERSION
= '3.0.2'¶
-
API_REDOC_SOURCE_URL
= 'https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js'¶
-
API_TITLE
= None¶
-
API_VERSION
= 1¶
-
API_DESCRIPTION
= None¶
-
API_APISPEC_PLUGINS
= None¶
-
DUMP_KEY_FN
()¶ An optional function to use for converting keys when dumping data to send over the wire. By default, we convert snake_case to camelCase.
-
LOAD_KEY_FN
()¶ An optional function to use for converting keys received over the wire to the backend’s representation. By default, we convert camelCase to snake_case.
-
ACCEPT_HANDLERS
= {'application/json': <function jsonify>}¶ Functions to use for converting response data for Accept headers.
-
Extensions¶
Api¶
-
class
flask_unchained.bundles.api.
Api
[source]¶ The Api extension:
from flask_unchained.bundles.api import api
Allows interfacing with apispec.
-
register_serializer
(serializer, name=None, **kwargs)[source]¶ Method to manually register a
Serializer
with APISpec.- Parameters
serializer –
name –
kwargs –
-
register_model_resource
(resource: flask_unchained.bundles.api.model_resource.ModelResource)[source]¶ Method to manually register a
ModelResource
with APISpec.- Parameters
resource –
-
register_field
(field, *args)[source]¶ Register custom Marshmallow field.
Registering the Field class allows the Schema parser to set the proper type and format when documenting parameters from Schema fields.
- Parameters
field (Field) – Marshmallow Field class
- Param
args: - a pair of the form
(type, format)
to map to - a core marshmallow field type (then that type’s mapping is used)
Examples:
# Map to ('string', 'UUID') api.register_field(UUIDField, 'string', 'UUID') # Map to ('string') api.register_field(URLField, 'string', None) # Map to ('integer, 'int32') api.register_field(CustomIntegerField, ma.fields.Integer)
-
Marshmallow¶
-
class
flask_unchained.bundles.api.
Marshmallow
[source]¶ The Marshmallow extension:
from flask_unchained.bundles.api import ma
Allows decorating a
ModelSerializer
withserializer()
to specify it should be used for creating objects, listing them, or as the fallback.Also provides aliases from the following modules:
flask_unchained.bundles.api
ModelSerializer
(*args, **kwargs)Base class for SQLAlchemy model serializers.
marshmallow.decorators
pre_load
([fn, pass_many])Register a method to invoke before deserializing an object.
post_load
([fn, pass_many, pass_original])Register a method to invoke after deserializing an object.
pre_dump
([fn, pass_many])Register a method to invoke before serializing an object.
post_dump
([fn, pass_many, pass_original])Register a method to invoke after serializing an object.
validates
(field_name)Register a field validator.
validates_schema
([fn, pass_many, …])Register a schema-level validator.
marshmallow.exceptions
ValidationError
(message[, field_name, data, …])Raised when validation fails on a field or schema.
marshmallow.fields
alias of
marshmallow.fields.Boolean
Boolean
(*[, truthy, falsy])A boolean field.
Constant
(constant, **kwargs)A field that (de)serializes to a preset constant.
Date
([format])ISO8601-formatted date string.
DateTime
([format])A formatted datetime string.
NaiveDateTime
([format, timezone])A formatted naive datetime string.
AwareDateTime
([format, default_timezone])A formatted aware datetime string.
Decimal
([places, rounding, allow_nan, as_string])A field that (de)serializes to the Python
decimal.Decimal
type.Dict
([keys, values])A dict field.
Email
(*args, **kwargs)An email field.
Field
(*[, default, missing, data_key, …])Basic field from which other fields should extend.
Float
(*[, allow_nan, as_string])A double as an IEEE-754 double precision string.
Function
([serialize, deserialize])A field that takes the value returned by a function.
alias of
marshmallow.fields.Integer
Integer
(*[, strict])An integer field.
List
(cls_or_instance, **kwargs)A list field, composed with another Field class or instance.
Mapping
([keys, values])An abstract class for objects with key-value pairs.
Method
([serialize, deserialize])A field that takes the value returned by a Schema method.
Nested
(nested, *[, default, only, exclude, …])Allows you to nest a
Schema
inside a field.Number
(*[, as_string])Base class for number fields.
Pluck
(nested, field_name, **kwargs)Allows you to replace nested data with one of the data’s fields.
Raw
(*[, default, missing, data_key, …])Field that applies no formatting.
alias of
marshmallow.fields.String
String
(*[, default, missing, data_key, …])A string field.
Time
([format])A formatted time string.
TimeDelta
([precision])A field that (de)serializes a
datetime.timedelta
object to an integer and vice versa.Tuple
(tuple_fields, *args, **kwargs)A tuple field, composed of a fixed number of other Field classes or instances
UUID
(*[, default, missing, data_key, …])A UUID field.
Url
(*[, relative, schemes, require_tld])An URL field.
alias of
marshmallow.fields.Url
flask_marshmallow.fields
AbsoluteURLFor
(endpoint[, values])Field that outputs the absolute URL for an endpoint.
alias of
flask_marshmallow.fields.URLFor
URLFor
(endpoint[, values])Field that outputs the URL for an endpoint.
Hyperlinks
(schema, **kwargs)Field that outputs a dictionary of hyperlinks, given a dictionary schema with
URLFor
objects as values.flask_marshmallow.sqla
HyperlinkRelated
(endpoint[, url_key, external])Field that generates hyperlinks to indicate references between models, rather than primary keys.
-
serializer
(create=False, many=False)[source]¶ Decorator to mark a
Serializer
subclass for a specific purpose, ie, to be used during object creation or for serializing lists of objects.- Parameters
create – Whether or not this serializer is for object creation.
many – Whether or not this serializer is for lists of objects.
-
Hooks¶
RegisterModelResourcesHook¶
-
class
flask_unchained.bundles.api.hooks.
RegisterModelResourcesHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers ModelResources and configures ModelSerializers on them.
-
name
: str = 'model_resources'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['views']¶ The default module this hook loads from.
Override by setting the
model_resources_module_names
attribute on your bundle class.
-
type_check
(obj)[source]¶ Returns True if
obj
is a subclass ofModelResource
.
-
RegisterModelSerializersHook¶
-
class
flask_unchained.bundles.api.hooks.
RegisterModelSerializersHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers ModelSerializers.
-
name
: str = 'model_serializers'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['serializers']¶ The default module this hook loads from.
Override by setting the
model_serializers_module_names
attribute on your bundle class.
-
process_objects
(app: flask_unchained.flask_unchained.FlaskUnchained, serializers: Dict[str, Type[flask_unchained.bundles.api.model_serializer.ModelSerializer]]) → None[source]¶ Registers model serializers onto the API Bundle instance.
-
type_check
(obj: Any) → bool[source]¶ Returns True if
obj
is a subclass ofModelSerializer
.
-
ModelResource¶
-
class
flask_unchained.bundles.api.
ModelResource
[source]¶ Base class for model resources. This is intended for building RESTful APIs with SQLAlchemy models and Marshmallow serializers.
-
list
(instances)[source]¶ List model instances.
- Parameters
instances – The list of model instances.
- Returns
The list of model instances.
-
create
(instance, errors)[source]¶ Create an instance of a model.
- Parameters
instance – The created model instance.
errors – Any errors.
- Returns
The created model instance, or a dictionary of errors.
-
get
(instance)[source]¶ Get an instance of a model.
- Parameters
instance – The model instance.
- Returns
The model instance.
-
patch
(instance, errors)[source]¶ Partially update a model instance.
- Parameters
instance – The model instance.
errors – Any errors.
- Returns
The updated model instance, or a dictionary of errors.
-
put
(instance, errors)[source]¶ Update a model instance.
- Parameters
instance – The model instance.
errors – Any errors.
- Returns
The updated model instance, or a dictionary of errors.
-
delete
(instance)[source]¶ Delete a model instance.
- Parameters
instance – The model instance.
- Returns
HTTPStatus.NO_CONTENT
-
created
(instance, commit=True)[source]¶ Convenience method for saving a model (automatically commits it to the database and returns the object with an HTTP 201 status code)
-
ModelSerializer¶
-
class
flask_unchained.bundles.api.
ModelSerializer
(*args, **kwargs)[source]¶ Base class for SQLAlchemy model serializers. This is pretty much a stock
flask_marshmallow.sqla.ModelSchema
, except:dependency injection is set up automatically on ModelSerializer
when loading to update an existing instance, validate the primary keys are the same
automatically make fields named
slug
,model.Meta.created_at
, andmodel.Meta.updated_at
dump-only
For example:
from flask_unchained.bundles.api import ModelSerializer from flask_unchained.bundles.security.models import Role class RoleSerializer(ModelSerializer): class Meta: model = Role
Is roughly equivalent to:
from marshmallow import Schema, fields class RoleSerializer(Schema): id = fields.Integer(dump_only=True) name = fields.String() description = fields.String() created_at = fields.DateTime(dump_only=True) updated_at = fields.DateTime(dump_only=True)
-
OPTIONS_CLASS
¶ alias of
ModelSerializerOptionsClass
-
is_create
()[source]¶ Check if we’re creating a new object. Note that this context flag must be set from the outside, ie when the class gets instantiated.
-
load
(data: Mapping, *, many: bool = None, partial: Union[bool, Sequence[str], Set[str]] = None, unknown: str = None, **kwargs)[source]¶ Deserialize a dict to an object defined by this ModelSerializer’s fields.
A
ValidationError
is raised if invalid data is passed.- Parameters
data – The data to deserialize.
many – Whether to deserialize data as a collection. If None, the value for self.many is used.
partial – Whether to ignore missing fields and not require any fields declared. Propagates down to
Nested
fields as well. If its value is an iterable, only missing fields listed in that iterable will be ignored. Use dot delimiters to specify nested fields.unknown – Whether to exclude, include, or raise an error for unknown fields in the data. Use EXCLUDE, INCLUDE or RAISE. If None, the value for self.unknown is used.
- Returns
Deserialized data
-
dump
(obj, *, many: bool = None)[source]¶ Serialize an object to native Python data types according to this ModelSerializer’s fields.
- Parameters
obj – The object to serialize.
many – Whether to serialize obj as a collection. If None, the value for self.many is used.
- Returns
A dict of serialized data
- Return type
-
handle_error
(error: marshmallow.exceptions.ValidationError, data: Any, **kwargs) → None[source]¶ Customize the error messages for required/not-null validators with dynamically generated field names. This is definitely a little hacky (it mutates state, uses hardcoded strings), but unsure how better to do it
Babel Bundle API¶
flask_unchained.bundles.babel
The Babel Bundle. |
|
Return the localized translation of message, based on the language, and locale directory of the domain specified in the translation key (or the current global domain). |
|
Like |
|
Like |
|
Like |
flask_unchained.bundles.babel.config
Default configuration options for the Babel Bundle. |
|
BabelBundle¶
-
class
flask_unchained.bundles.babel.
BabelBundle
[source]¶ The Babel Bundle. Responsible for configuring the correct gettext callables with Jinja, as well as optionally registering endpoints for language-specific URLs (if enabled).
-
name
: str = 'babel_bundle'¶ The name of the Babel Bundle.
-
command_group_names
= ('babel',)¶ Names of the command groups included in this bundle.
-
language_code_key
= 'lang_code'¶ Default Werkzeug parameter name to be used when registering language-specific URLs.
-
before_init_app
(app: flask_unchained.flask_unchained.FlaskUnchained)[source]¶ Override this method to perform actions on the
FlaskUnchained
app instance before theunchained
extension has initialized the application.
-
after_init_app
(app: flask_unchained.flask_unchained.FlaskUnchained)[source]¶ Override this method to perform actions on the
FlaskUnchained
app instance after theunchained
extension has initialized the application.
-
gettext functions¶
-
flask_unchained.
gettext
(*args, **kwargs)[source]¶ Return the localized translation of message, based on the language, and locale directory of the domain specified in the translation key (or the current global domain). This function is usually aliased as
_
:from flask_unchained import gettext as _
-
flask_unchained.
ngettext
(*args, **kwargs)[source]¶ Like
gettext()
, except it supports pluralization. This function is usually aliased as_
:from flask_unchained import ngettext as _
-
flask_unchained.
lazy_gettext
(*args, **kwargs)[source]¶ Like
gettext()
, except lazy. This function is usually aliased as_
:from flask_unchained import lazy_gettext as _
-
flask_unchained.
lazy_ngettext
(*args, **kwargs)[source]¶ Like
ngettext()
, except lazy. This function is usually aliased as_
:from flask_unchained import lazy_ngettext as _
Config¶
-
class
flask_unchained.bundles.babel.config.
Config
[source]¶ Default configuration options for the Babel Bundle.
-
LANGUAGES
= ['en']¶ The language codes supported by the app.
-
BABEL_DEFAULT_LOCALE
= 'en'¶ The default language to use if none is specified by the client’s browser.
-
BABEL_DEFAULT_TIMEZONE
= 'UTC'¶ The default timezone to use.
-
DEFAULT_DOMAIN
= <flask_babelex.Domain object>¶ The default
Domain
to use.
-
DATE_FORMATS
= {'date': 'medium', 'date.full': None, 'date.long': None, 'date.medium': None, 'date.short': None, 'datetime': 'medium', 'datetime.full': None, 'datetime.long': None, 'datetime.medium': None, 'datetime.short': None, 'time': 'medium', 'time.full': None, 'time.long': None, 'time.medium': None, 'time.short': None}¶ A dictionary of date formats.
-
ENABLE_URL_LANG_CODE_PREFIX
= False¶ Whether or not to enable the capability to specify the language code as part of the URL.
-
-
class
flask_unchained.bundles.babel.config.
DevConfig
[source]¶ -
LAZY_TRANSLATIONS
= False¶ Do not use lazy translations in development.
-
Celery Bundle API¶
flask_unchained.bundles.celery
The Celery Bundle. |
flask_unchained.bundles.celery.config
Default configuration options for the Celery Bundle. |
flask_unchained.bundles.celery.extensions
The Celery extension. |
flask_unchained.bundles.celery.hooks
Discovers celery tasks. |
CeleryBundle¶
Config¶
-
class
flask_unchained.bundles.celery.config.
Config
[source]¶ Default configuration options for the Celery Bundle.
-
CELERY_BROKER_URL
= 'redis://127.0.0.1:6379/0'¶ The broker URL to connect to.
-
CELERY_RESULT_BACKEND
= 'redis://127.0.0.1:6379/0'¶ The result backend URL to connect to.
-
CELERY_ACCEPT_CONTENT
= ('json', 'pickle', 'dill')¶ Tuple of supported serialization strategies.
-
MAIL_SEND_FN
(to=None, template=None, **kwargs)¶ If the celery bundle is listed after the mail bundle in
unchained_config.BUNDLES
, then this configures the mail bundle to send emails asynchronously.
-
The Celery Extension¶
-
class
flask_unchained.bundles.celery.
Celery
(*args, **kwargs)[source]¶ The Celery extension:
from flask_unchained.bundles.celery import celery
-
task
(*args, **opts)[source]¶ Decorator to create a task class out of any callable.
See Task options for a list of the arguments that can be passed to this decorator.
- Examples:
@app.task def refresh_feed(url): store_feed(feedparser.parse(url))
with setting extra options:
@app.task(exchange='feeds') def refresh_feed(url): return store_feed(feedparser.parse(url))
- Note:
App Binding: For custom apps the task decorator will return a proxy object, so that the act of creating the task is not performed until the task is used or the task registry is accessed.
If you’re depending on binding to be deferred, then you must not access any attributes on the returned object until the application is fully set up (finalized).
-
DiscoverTasksHook¶
-
class
flask_unchained.bundles.celery.hooks.
DiscoverTasksHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Discovers celery tasks.
-
name
: str = 'celery_tasks'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['tasks']¶ The default module this hook loads from.
Override by setting the
celery_tasks_module_names
attribute on your bundle class.
-
Controller Bundle API¶
flask_unchained.bundles.controller
The Controller Bundle. |
flask_unchained.bundles.controller.config
Default configuration options for the controller bundle. |
flask_unchained.bundles.controller.controller
Base class for views. |
flask_unchained.bundles.controller.decorators
Convert arguments from the URL and/or query string. |
|
Decorator to set default route rules for a view function. |
|
Decorator to mark a |
flask_unchained.bundles.controller.hooks
Registers legacy Flask blueprints with the app. |
|
Registers a bundle blueprint for each bundle with views and/or template/static folders. |
|
Registers routes. |
flask_unchained.bundles.controller.resource
Base class for resources. |
flask_unchained.bundles.controller.route
This is a semi-private class that you most likely shouldn’t use directly. |
flask_unchained.bundles.controller.routes
This function is used to register a controller class’s routes. |
|
This function is used to register a |
|
This function allows to register legacy view functions as routes, eg. |
|
Include the routes from another module at that point in the tree. |
|
Sets a prefix on all of the child routes passed to it. |
|
Used to specify customizations to the route settings of class-based view function. |
|
Like |
|
Like |
|
Like |
|
Like |
|
Like |
flask_unchained.bundles.controller.utils
An improved version of flask’s redirect function |
|
An improved version of flask’s url_for function |
|
Raises an |
|
Generate a CSRF token. |
ControllerBundle¶
-
class
flask_unchained.bundles.controller.
ControllerBundle
[source]¶ The Controller Bundle.
-
name
: str = 'controller_bundle'¶ The name of the Controller Bundle.
-
endpoints
: Dict[str, List[Route]] = None¶ Lookup of routes by endpoint name.
-
controller_endpoints
: Dict[str, List[Route]] = None¶ Lookup of routes by keys: f’{ControllerClassName}.{view_method_name}’
-
bundle_routes
: Dict[str, List[Route]] = None¶ Lookup of routes belonging to each bundle by bundle name.
-
other_routes
: List[Route] = None¶ List of routes not associated with any bundles.
-
Config¶
-
class
flask_unchained.bundles.controller.config.
Config
[source]¶ Default configuration options for the controller bundle.
-
FLASH_MESSAGES
= True¶ Whether or not to enable flash messages.
NOTE: This only works for messages flashed using the
flask_unchained.Controller.flash()
method; using theflask.flash()
function directly will not respect this setting.
-
TEMPLATE_FILE_EXTENSION
= '.html'¶ The default file extension to use for templates.
-
WTF_CSRF_ENABLED
= False¶ Whether or not to enable CSRF protection.
-
Views¶
Controller¶
-
class
flask_unchained.
Controller
[source]¶ Base class for views.
Concrete controllers should subclass this and their public methods will used as the views. By default view methods will be assigned routing defaults with the HTTP method GET and paths as the kebab-cased method name. For example:
from flask_unchained import Controller, injectable, route, no_route from flask_unchained.bundles.sqlalchemy import SessionManager class SiteController(Controller): class Meta: abstract = False # this is the default; no need to set explicitly decorators = () # a list of decorators to apply to all view methods # on the controller (defaults to an empty tuple) template_folder = 'site' # defaults to the snake_cased class name, # minus any Controller/View suffix template_file_extension = app.config.TEMPLATE_FILE_EXTENSION = '.html' url_prefix = None # optional url prefix to use for all routes endpoint_prefix = 'site_controller' # defaults to snake_cased class name # dependency injection works automatically on controllers session_manager: SessionManager = injectable @route('/') # change the default path of '/index' to '/' def index(): return self.render('index') # site/index.html # use the defaults, equivalent to @route('/about-us', methods=['GET']) def about_us(): return self.render('about_us.html') # site/about_us.html # change the path, HTTP methods, and the endpoint @route('/contact', methods=['GET', 'POST'], endpoint='site_controller.contact') def contact_us(): # ... return self.render('site/contact.html.j2') # site/contact.html.j2 @no_route def public_utility_method(): return 'not a view' def _protected_utility_method(): return 'not a view'
How do the calls to render know which template to use? They look in
Bundle.template_folder
for a folder with the controller’sMeta.template_folder
and a file with the passed name andMeta.template_file_extension
. For example:class SiteController(Controller): # these defaults are automatically determined, unless you override them class Meta: template_folder = 'site' # snake_cased class name (minus Controller suffix) template_file_extension = '.html' # from Config.TEMPLATE_FILE_EXTENSION def about_us(): return self.render('about_us') # site/about_us.html def contact(): return self.render('contact') # site/contact.html def index(): return self.render('index') # site/index.html # your_bundle_root ├── __init__.py ├── templates │ └── site │ ├── about_us.html │ ├── contact.html │ └── index.html └── views └── site_controller.py
-
flash
(msg: str, category: Optional[str] = None)[source]¶ Convenience method for flashing messages.
- Parameters
msg – The message to flash.
category – The category of the message.
-
render
(template_name: str, **ctx)[source]¶ Convenience method for rendering a template.
- Parameters
template_name – The template’s name. Can either be a full path, or a filename in the controller’s template folder. (The file extension can be omitted.)
ctx – Context variables to pass into the template.
-
redirect
(where: Optional[str] = None, default: Optional[str] = None, override: Optional[str] = None, **url_kwargs)[source]¶ Convenience method for returning redirect responses.
- Parameters
where – A method name from this controller, a URL, an endpoint, or a config key name to redirect to.
default – A method name from this controller, a URL, an endpoint, or a config key name to redirect to if
where
is invalid.override – Explicitly redirect to a method name from this controller, a URL, an endpoint, or a config key name (takes precedence over the
next
value in query strings or forms)url_kwargs – the variable arguments of the URL rule
_anchor – if provided this is added as anchor to the URL.
_external – if set to
True
, an absolute URL is generated. Server address can be changed viaSERVER_NAME
configuration variable which defaults to localhost._external_host – if specified, the host of an external server to generate urls for (eg https://example.com or localhost:8888)
_method – if provided this explicitly specifies an HTTP method.
_scheme – a string specifying the desired URL scheme. The _external parameter must be set to
True
or aValueError
is raised. The default behavior uses the same scheme as the current request, orPREFERRED_URL_SCHEME
from the app configuration if no request context is available. As of Werkzeug 0.10, this also can be set to an empty string to build protocol-relative URLs.
-
jsonify
(data: Any, code: Union[int, Tuple[int, str, str]] = <HTTPStatus.OK: 200>, headers: Optional[Dict[str, str]] = None)[source]¶ Convenience method to return json responses.
- Parameters
data – The python data to jsonify.
code – The HTTP status code to return.
headers – Any optional headers.
-
errors
(errors: List[str], code: Union[int, Tuple[int, str, str]] = <HTTPStatus.BAD_REQUEST: 400>, key: str = 'errors', headers: Optional[Dict[str, str]] = None)[source]¶ Convenience method to return errors as json.
- Parameters
errors – The list of errors.
code – The HTTP status code.
key – The key to return the errors under.
headers – Any optional headers.
-
Resource¶
-
class
flask_unchained.
Resource
[source]¶ Base class for resources. This is intended for building RESTful APIs. Following the rules shown below, if the given class method is defined, this class will automatically set up the shown routing rule for it.
HTTP Method
Resource class method name
URL Rule
GET
list
/
POST
create
/
GET
get
/<cls.Meta.member_param>
PATCH
patch
/<cls.Meta.member_param>
PUT
put
/<cls.Meta.member_param>
DELETE
delete
/<cls.Meta.member_param>
So, for example:
from flask_unchained import Resource, injectable, param_converter from flask_unchained.bundles.security import User, UserManager class UserResource(Resource): class Meta: member_param: '<string:username>' user_manager: UserManager = injectable def list(): return self.jsonify(dict(users=self.user_manager.all())) # NOTE: returning SQLAlchemy models directly like this is # only supported by ModelResource from the API Bundle def create(): user = self.user_manager.create(**data, commit=True) return self.jsonify(dict(user=user), code=201) @param_converter(username=User) def get(user): return self.jsonify(dict(user=user) @param_converter(username=User) def patch(user): user = self.user_manager.update(user, **data, commit=True) return self.jsonify(dict(user=user)) @param_converter(username=User) def put(user): user = self.user_manager.update(user, **data, commit=True) return self.jsonify(dict(user=user)) @param_converter(username=User) def delete(user): self.user_manager.delete(user, commit=True) return self.make_response('', code=204)
Registered like so:
routes = lambda: [ resource('/users', UserResource), ]
Would register the following routes:
GET /users UserResource.list POST /users UserResource.create GET /users/<string:username> UserResource.get PATCH /users/<string:username> UserResource.patch PUT /users/<string:username> UserResource.put DELETE /users/<string:username> UserResource.delete
See also
ModelResource
from the API bundle.
View Decorators¶
param_converter¶
-
flask_unchained.
param_converter
(*decorator_args, **decorator_kwargs)[source]¶ Convert arguments from the URL and/or query string.
For parsing arguments from the query string, pass their names as keyword argument keys where the value is a lookup (dict, Enum) or callable used to convert the query string argument’s value:
@route('/users/<int:id>') @param_converter(id=User, foo=str, optional=int) def show_user(user, foo, optional=10): # GET /users/1?foo=bar # calls show_user(user=User.query.get(1), foo='bar')
It also supports loading SQLAlchemy models from the database. Call with the url parameter names as keyword argument keys, their values being the model class to convert to.
Models will be looked up by the url param names. If a url param name is prefixed with the snake_cased model name, the prefix will be stripped. If a model isn’t found, abort with a 404.
The view function’s argument names must match the snake_cased model names.
For example:
@route('/users/<int:user_id>/posts/<int:id>') @param_converter(user_id=User, id=Post) def show_post(user, post): # the param converter does the database lookups: # user = User.query.get(id=user_id) # post = Post.query.get(id=id) # and calls the decorated view: show_post(user, post) # or to customize the argument names passed to the view: @route('/users/<int:user_id>/posts/<int:post_id>') @param_converter(user_id={'user_arg_name': User}, post_id={'post_arg_name': Post}) def show_post(user_arg_name, post_arg_name): # do stuff
route¶
-
flask_unchained.
route
(rule=None, blueprint=None, defaults=None, endpoint=None, is_member=False, methods=None, only_if=<py_meta_utils._missing_cls object>, **rule_options)[source]¶ Decorator to set default route rules for a view function. The arguments this function accepts are very similar to Flask’s
route()
, however, theis_member
perhaps deserves an example:class UserResource(ModelResource): class Meta: model = User member_param = '<int:id>' include_methods = ['list', 'get'] @route(is_member=True, methods=['POST']) def set_profile_pic(user): # do stuff # registered like so in your ``app_bundle/routes.py``: routes = lambda: [ resource(UserResource), ] # results in the following routes: # UserResource.list => GET /users # UserResource.get => GET /users/<int:id> # UserResource.set_profile_pic => POST /users/<int:id>/set-profile-pic
- Parameters
rule – The URL rule.
defaults – Any default values for parameters in the URL rule.
endpoint – The endpoint name of this view. Determined automatically if left unspecified.
is_member – Whether or not this view is for a
Resource
member method.methods – A list of HTTP methods supported by this view. Defaults to
['GET']
.only_if – A boolean or callable to dynamically determine whether or not to register this route with the app.
rule_options – Other kwargs passed on to
Rule
.
no_route¶
-
flask_unchained.
no_route
(arg=None)[source]¶ Decorator to mark a
Controller
orResource
method as not a route. For example:class SiteController(Controller): @route('/') def index(): return self.render('index') def about(): return self.render('about', stuff=self.utility_method()) @no_route def utility_method(): return 'stuff' # registered like so in ``your_app_bundle/routes.py`` routes = lambda: [ controller(SiteController), ] # results in the following routes SiteController.index => GET / SiteController.about => GET /about # but without the @no_route decorator, it would have also added this route: SiteController.utility_method => GET /utility-method
NOTE: The perhaps more Pythonic way to accomplish this is to make all non-route methods protected by prefixing them with an underscore, eg
_utility_method
.
Declarative Routing¶
controller¶
-
flask_unchained.
controller
(url_prefix_or_controller_cls: Union[str, Type[flask_unchained.bundles.controller.controller.Controller]], controller_cls: Optional[Type[flask_unchained.bundles.controller.controller.Controller]] = None, *, rules: Optional[Iterable[Union[flask_unchained.bundles.controller.route.Route, Iterable[flask_unchained.bundles.controller.route.Route]]]] = None) → Iterable[flask_unchained.bundles.controller.route.Route][source]¶ This function is used to register a controller class’s routes.
Example usage:
routes = lambda: [ controller(SiteController), ]
Or with the optional prefix argument:
routes = lambda: [ controller('/products', ProductController), ]
Specify
rules
to only include those routes from the controller:routes = lambda: [ controller(SecurityController, rules=[ # these inherit all unspecified kwargs from the decorated view methods rule('/login', SecurityController.login), # methods=['GET', 'POST'] rule('/logout', SecurityController.logout), # methods=['GET'] rule('/sign-up', SecurityController.register), # methods=['GET', 'POST'] ]), ]
- Parameters
url_prefix_or_controller_cls – The controller class, or a url prefix for all of the rules from the controller class passed as the second argument
controller_cls – If a url prefix was given as the first argument, then the controller class must be passed as the second argument
rules – An optional list of rules to limit/customize the routes included from the controller
resource¶
-
flask_unchained.
resource
(url_prefix_or_resource_cls: Union[str, Type[flask_unchained.bundles.controller.resource.Resource]], resource_cls: Optional[Type[flask_unchained.bundles.controller.resource.Resource]] = None, *, member_param: Optional[str] = None, unique_member_param: Optional[str] = None, rules: Optional[Iterable[Union[flask_unchained.bundles.controller.route.Route, Iterable[flask_unchained.bundles.controller.route.Route]]]] = None, subresources: Optional[Iterable[Iterable[flask_unchained.bundles.controller.route.Route]]] = None) → Iterable[flask_unchained.bundles.controller.route.Route][source]¶ This function is used to register a
Resource
’s routes.Example usage:
routes = lambda: [ resource(ProductResource), ]
Or with the optional prefix argument:
routes = lambda: [ resource('/products', ProductResource), ]
Specify
rules
to only include those routes from the resource:routes = lambda: [ resource('/users', UserResource, rules=[ get('/', UserResource.list), get('/<int:id>', UserResource.get), ]), ]
Specify
subresources
to nest resource routes:routes = lambda: [ resource('/users', UserResource, subresources=[ resource('/roles', RoleResource) ]), ]
Subresources can be nested as deeply as you want, however it’s not recommended to go more than two or three levels deep at the most, otherwise your URLs will become unwieldy.
- Parameters
url_prefix_or_resource_cls – The resource class, or a url prefix for all of the rules from the resource class passed as the second argument.
resource_cls – If a url prefix was given as the first argument, then the resource class must be passed as the second argument.
member_param – Optionally override the controller’s member_param attribute.
rules – An optional list of rules to limit/customize the routes included from the resource.
subresources – An optional list of subresources.
func¶
-
flask_unchained.
func
(rule_or_view_func: Union[str, Callable], view_func: Optional[Callable] = <py_meta_utils._missing_cls object>, blueprint: Optional[flask.blueprints.Blueprint] = <py_meta_utils._missing_cls object>, defaults: Optional[Dict[str, Any]] = <py_meta_utils._missing_cls object>, endpoint: Optional[str] = <py_meta_utils._missing_cls object>, methods: Union[List[str], Tuple[str], Set[str], None] = <py_meta_utils._missing_cls object>, only_if: Union[bool, Callable[[flask_unchained.flask_unchained.FlaskUnchained], bool], None] = <py_meta_utils._missing_cls object>, **rule_options) → Iterable[flask_unchained.bundles.controller.route.Route][source]¶ This function allows to register legacy view functions as routes, eg:
@route('/') def index(): return render_template('site/index.html') routes = lambda: [ func(index), ]
It accepts an optional url rule argument:
routes = lambda: [ func('/products', product_list_view), ]
As well as supporting the same kwargs as Werkzeug’s
Rule
, eg:routes = lambda: [ func('/', index, endpoint='home', methods=['GET', 'POST']), ]
- Parameters
rule_or_view_func – The view function, or an optional url rule for the view function given as the second argument
view_func – The view function if passed a url rule as the first argument
only_if – An optional function to decide at runtime whether or not to register the route with Flask. It gets passed the configured app as a single argument, and should return a boolean.
rule_options – Keyword arguments that ultimately end up getting passed on to
Rule
include¶
-
flask_unchained.
include
(url_prefix_or_module_name: str, module_name: Optional[str] = None, *, attr: str = 'routes', exclude: Union[List[str], Tuple[str], Set[str], None] = None, only: Union[List[str], Tuple[str], Set[str], None] = None) → Iterable[flask_unchained.bundles.controller.route.Route][source]¶ Include the routes from another module at that point in the tree. For example:
# project-root/bundles/primes/routes.py routes = lambda: [ controller('/two', TwoController), controller('/three', ThreeController), controller('/five', FiveController), ] # project-root/bundles/blog/routes.py routes = lambda: [ func('/', index), controller('/authors', AuthorController), controller('/posts', PostController), ] # project-root/your_app_bundle/routes.py routes = lambda: [ include('bundles.primes.routes'), # these last two are equivalent include('/blog', 'bundles.blog.routes'), prefix('/blog', [ include('bundles.blog.routes'), ]), ]
- Parameters
url_prefix_or_module_name – The module name, or a url prefix for all of the included routes in the module name passed as the second argument.
module_name – The module name of the routes to include if a url prefix was given as the first argument.
attr – The attribute name in the module, if different from
routes
.exclude – An optional list of endpoints to exclude.
only – An optional list of endpoints to only include.
prefix¶
-
flask_unchained.
prefix
(url_prefix: str, children: Iterable[Union[flask_unchained.bundles.controller.route.Route, Iterable[flask_unchained.bundles.controller.route.Route]]]) → Iterable[flask_unchained.bundles.controller.route.Route][source]¶ Sets a prefix on all of the child routes passed to it. It also supports nesting, eg:
routes = lambda: [ prefix('/foobar', [ controller('/one', OneController), controller('/two', TwoController), prefix('/baz', [ controller('/three', ThreeController), controller('/four', FourController), ]) ]) ]
- Parameters
url_prefix – The url prefix to set on the child routes
children –
rule¶
-
flask_unchained.
rule
(rule: str, cls_method_name_or_view_fn: Union[str, Callable, None] = None, *, defaults: Optional[Dict[str, Any]] = <py_meta_utils._missing_cls object>, endpoint: Optional[str] = <py_meta_utils._missing_cls object>, is_member: Optional[bool] = <py_meta_utils._missing_cls object>, methods: Union[List[str], Tuple[str], Set[str], None] = <py_meta_utils._missing_cls object>, only_if: Union[bool, Callable[[flask_unchained.flask_unchained.FlaskUnchained], bool], None] = <py_meta_utils._missing_cls object>, **rule_options) → Iterable[flask_unchained.bundles.controller.route.Route][source]¶ Used to specify customizations to the route settings of class-based view function. Unspecified kwargs will be inherited from the route decorated on each view. For example:
routes = lambda: [ prefix('/api/v1', [ controller(SecurityController, rules=[ rule('/login', SecurityController.login, endpoint='security_api.login'), # methods=['GET', 'POST'] rule('/logout', SecurityController.logout, endpoint='security_api.logout'), # methods=['GET'] rule('/sign-up', SecurityController.register, endpoint='security_api.register'), # methods=['GET', 'POST'] ]), ], ]
- Parameters
rule – The URL rule.
cls_method_name_or_view_fn – The view function.
defaults – Any default values for parameters in the URL rule.
endpoint – The endpoint name of this view. Determined automatically if left unspecified.
is_member – Whether or not this view is for a
Resource
member method.methods – A list of HTTP methods supported by this view. Defaults to
['GET']
.only_if – A boolean or callable to dynamically determine whether or not to register this route with the app.
rule_options – Other kwargs passed on to
Rule
.
get¶
-
flask_unchained.
get
(rule: str, cls_method_name_or_view_fn: Union[str, Callable, None] = None, *, defaults: Optional[Dict[str, Any]] = <py_meta_utils._missing_cls object>, endpoint: Optional[str] = <py_meta_utils._missing_cls object>, is_member: Optional[bool] = <py_meta_utils._missing_cls object>, only_if: Union[bool, Callable[[flask_unchained.flask_unchained.FlaskUnchained], bool], None] = <py_meta_utils._missing_cls object>, **rule_options) → Iterable[flask_unchained.bundles.controller.route.Route][source]¶ Like
rule()
, except specifically for HTTP GET requests.- Parameters
rule – The url rule for this route.
cls_method_name_or_view_fn – The view function for this route.
is_member – Whether or not this route is a member function.
only_if – An optional function to decide at runtime whether or not to register the route with Flask. It gets passed the configured app as a single argument, and should return a boolean.
rule_options – Keyword arguments that ultimately end up getting passed on to
Rule
patch¶
-
flask_unchained.
patch
(rule: str, cls_method_name_or_view_fn: Union[str, Callable, None] = None, *, defaults: Optional[Dict[str, Any]] = <py_meta_utils._missing_cls object>, endpoint: Optional[str] = <py_meta_utils._missing_cls object>, is_member: Optional[bool] = <py_meta_utils._missing_cls object>, only_if: Union[bool, Callable[[flask_unchained.flask_unchained.FlaskUnchained], bool], None] = <py_meta_utils._missing_cls object>, **rule_options) → Iterable[flask_unchained.bundles.controller.route.Route][source]¶ Like
rule()
, except specifically for HTTP PATCH requests.- Parameters
rule – The url rule for this route.
cls_method_name_or_view_fn – The view function for this route.
is_member – Whether or not this route is a member function.
only_if – An optional function to decide at runtime whether or not to register the route with Flask. It gets passed the configured app as a single argument, and should return a boolean.
rule_options – Keyword arguments that ultimately end up getting passed on to
Rule
post¶
-
flask_unchained.
post
(rule: str, cls_method_name_or_view_fn: Union[str, Callable, None] = None, *, defaults: Optional[Dict[str, Any]] = <py_meta_utils._missing_cls object>, endpoint: Optional[str] = <py_meta_utils._missing_cls object>, is_member: Optional[bool] = <py_meta_utils._missing_cls object>, only_if: Union[bool, Callable[[flask_unchained.flask_unchained.FlaskUnchained], bool], None] = <py_meta_utils._missing_cls object>, **rule_options) → Iterable[flask_unchained.bundles.controller.route.Route][source]¶ Like
rule()
, except specifically for HTTP POST requests.- Parameters
rule – The url rule for this route.
cls_method_name_or_view_fn – The view function for this route.
is_member – Whether or not this route is a member function.
only_if – An optional function to decide at runtime whether or not to register the route with Flask. It gets passed the configured app as a single argument, and should return a boolean.
rule_options – Keyword arguments that ultimately end up getting passed on to
Rule
put¶
-
flask_unchained.
put
(rule: str, cls_method_name_or_view_fn: Union[str, Callable, None] = None, *, defaults: Optional[Dict[str, Any]] = <py_meta_utils._missing_cls object>, endpoint: Optional[str] = <py_meta_utils._missing_cls object>, is_member: Optional[bool] = <py_meta_utils._missing_cls object>, only_if: Union[bool, Callable[[flask_unchained.flask_unchained.FlaskUnchained], bool], None] = <py_meta_utils._missing_cls object>, **rule_options) → Iterable[flask_unchained.bundles.controller.route.Route][source]¶ Like
rule()
, except specifically for HTTP PUT requests.- Parameters
rule – The url rule for this route.
cls_method_name_or_view_fn – The view function for this route.
is_member – Whether or not this route is a member function.
only_if – An optional function to decide at runtime whether or not to register the route with Flask. It gets passed the configured app as a single argument, and should return a boolean.
rule_options – Keyword arguments that ultimately end up getting passed on to
Rule
delete¶
-
flask_unchained.
delete
(rule: str, cls_method_name_or_view_fn: Union[str, Callable, None] = None, *, defaults: Optional[Dict[str, Any]] = <py_meta_utils._missing_cls object>, endpoint: Optional[str] = <py_meta_utils._missing_cls object>, is_member: Optional[bool] = <py_meta_utils._missing_cls object>, only_if: Union[bool, Callable[[flask_unchained.flask_unchained.FlaskUnchained], bool], None] = <py_meta_utils._missing_cls object>, **rule_options) → Iterable[flask_unchained.bundles.controller.route.Route][source]¶ Like
rule()
, except specifically for HTTP DELETE requests.- Parameters
rule – The url rule for this route.
cls_method_name_or_view_fn – The view function for this route.
is_member – Whether or not this route is a member function.
only_if – An optional function to decide at runtime whether or not to register the route with Flask. It gets passed the configured app as a single argument, and should return a boolean.
rule_options – Keyword arguments that ultimately end up getting passed on to
Rule
Route¶
-
class
flask_unchained.bundles.controller.route.
Route
(rule: Optional[str], view_func: Union[str, function], blueprint: Optional[flask.blueprints.Blueprint] = None, defaults: Optional[Dict[str, Any]] = None, endpoint: Optional[str] = None, is_member: bool = False, methods: Union[List[str], Tuple[str, ...], None] = None, only_if: Union[bool, function, None] = <py_meta_utils._missing_cls object>, **rule_options)[source]¶ This is a semi-private class that you most likely shouldn’t use directly. Instead, you should use the public functions in Declarative Routing, and the
route()
andno_route()
decorators.This class is used to store an intermediate representation of route details as an attribute on view functions and class view methods. Most notably, this class’s
rule
andfull_rule
attributes may not represent the final url rule that gets registered withFlask
.Further gotchas with
Controller
andResource
routes include that their view_func must be finalized from the outside usingTheControllerClass.method_as_view
.-
should_register
(app: flask_unchained.flask_unchained.FlaskUnchained) → bool[source]¶ Determines whether or not this route should be registered with the app, based on
only_if
.
-
property
defaults
¶ The URL defaults for this route.
-
property
endpoint
¶ The endpoint for this route.
-
property
is_member
¶ Whether or not this route is for a resource member route.
-
property
method_name
¶ The string name of this route’s view function.
-
property
methods
¶ The HTTP methods supported by this route.
-
property
rule
¶ The (partial) url rule for this route.
-
property
full_rule
¶ The full url rule for this route, including any blueprint prefix.
-
property
full_name
¶ The full name of this route’s view function, including the module path and controller name, if any.
-
Hooks¶
RegisterBlueprintsHook¶
-
class
flask_unchained.bundles.controller.hooks.
RegisterBlueprintsHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers legacy Flask blueprints with the app.
-
name
: str = 'blueprints'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['views']¶ The default module this hook loads from.
Override by setting the
blueprints_module_names
attribute on your bundle class.
-
process_objects
(app: flask_unchained.flask_unchained.FlaskUnchained, blueprints: List[flask.blueprints.Blueprint])[source]¶ Registers discovered blueprints with the app.
-
collect_from_bundles
(bundles: List[flask_unchained.bundles.Bundle]) → List[flask.blueprints.Blueprint][source]¶ Find blueprints in bundles.
-
collect_from_bundle
(bundle: flask_unchained.bundles.Bundle) → Iterable[flask.blueprints.Blueprint][source]¶ Finds blueprints in a bundle hierarchy.
-
type_check
(obj)[source]¶ Returns True if
obj
is an instance offlask.Blueprint
.
-
RegisterBundleBlueprintsHook¶
-
class
flask_unchained.bundles.controller.hooks.
RegisterBundleBlueprintsHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers a bundle blueprint for each bundle with views and/or template/static folders.
-
name
: str = 'bundle_blueprints'¶ The name of this hook.
-
RegisterRoutesHook¶
-
class
flask_unchained.bundles.controller.hooks.
RegisterRoutesHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers routes.
-
name
: str = 'routes'¶ The name of this hook.
-
bundle_module_name
: Optional[str] = 'routes'¶ The default module this hook loads from.
Override by setting the
routes_module_name
attribute on your bundle class.
-
run_hook
(app: flask_unchained.flask_unchained.FlaskUnchained, bundles: List[flask_unchained.bundles.Bundle], unchained_config: Optional[Dict[str, Any]] = None) → None[source]¶ Discover and register routes.
-
process_objects
(app: flask_unchained.flask_unchained.FlaskUnchained, routes: Iterable[flask_unchained.bundles.controller.route.Route])[source]¶ Organize routes by where they came from, and then register them with the app.
-
get_explicit_routes
(bundle: flask_unchained.bundles.Bundle)[source]¶ Collect routes from a bundle using declarative routing.
-
FlaskForm¶
Using forms in Flask Unchained is exactly the same as it is with Flask-WTF.
-
class
flask_unchained.
FlaskForm
(formdata=None, obj=None, prefix='', data=None, meta=None, **kwargs)[source]¶ Base form class extending
flask_wtf.FlaskForm
. Adds support for specifying the field order via afield_order
class attribute.- Parameters
formdata – Used to pass data coming from the end user, usually
request.POST
or equivalent. formdata should be some sort of request-data wrapper which can get multiple parameters from the form input, and values are unicode strings, e.g. a Werkzeug/Django/WebOb MultiDictobj – If
formdata
is empty or not provided, this object is checked for attributes matching form field names, which will be used for field values.prefix – If provided, all fields will have their name prefixed with the value.
data – Accept a dictionary of data. This is only used if
formdata
and obj are not present.meta – If provided, this is a dictionary of values to override attributes on this form’s meta instance.
**kwargs – If formdata is empty or not provided and obj does not contain an attribute named the same as a field, form will assign the value of a matching keyword argument to the field, if one exists.
-
field_order
= ()¶ An ordered list of field names. Fields not listed here will be rendered first.
Utility Functions¶
redirect¶
-
flask_unchained.
redirect
(where: Optional[str] = None, default: Optional[str] = None, override: Optional[str] = None, _anchor: Optional[str] = None, _cls: Union[object, type, None] = None, _external: Optional[bool] = False, _external_host: Optional[str] = None, _method: Optional[str] = None, _scheme: Optional[str] = None, **values) → flask.wrappers.Response[source]¶ An improved version of flask’s redirect function
- Parameters
where – A URL, endpoint, or config key name to redirect to
default – A URL, endpoint, or config key name to redirect to if
where
is invalidoverride – explicitly redirect to a URL, endpoint, or config key name (takes precedence over the
next
value in query strings or forms)values – the variable arguments of the URL rule
_anchor – if provided this is added as anchor to the URL.
_cls – if specified, allows a method name to be passed to where, default, and/or override
_external – if set to
True
, an absolute URL is generated. Server address can be changed viaSERVER_NAME
configuration variable which defaults to localhost._external_host – if specified, the host of an external server to generate urls for (eg https://example.com or localhost:8888)
_method – if provided this explicitly specifies an HTTP method.
_scheme – a string specifying the desired URL scheme. The _external parameter must be set to
True
or aValueError
is raised. The default behavior uses the same scheme as the current request, orPREFERRED_URL_SCHEME
from the app configuration if no request context is available. As of Werkzeug 0.10, this also can be set to an empty string to build protocol-relative URLs.
url_for¶
-
flask_unchained.
url_for
(endpoint_or_url_or_config_key: Optional[str], _anchor: Optional[str] = None, _cls: Union[object, type, None] = None, _external: Optional[bool] = False, _external_host: Optional[str] = None, _method: Optional[str] = None, _scheme: Optional[str] = None, **values) → Optional[str][source]¶ An improved version of flask’s url_for function
- Parameters
endpoint_or_url_or_config_key – what to lookup. it can be an endpoint name, an app config key, or an already-formed url. if _cls is specified, it also accepts a method name.
values – the variable arguments of the URL rule
_anchor – if provided this is added as anchor to the URL.
_cls – if specified, can also pass a method name as the first argument
_external – if set to
True
, an absolute URL is generated. Server address can be changed viaSERVER_NAME
configuration variable which defaults to localhost._external_host – if specified, the host of an external server to generate urls for (eg https://example.com or localhost:8888)
_method – if provided this explicitly specifies an HTTP method.
_scheme – a string specifying the desired URL scheme. The _external parameter must be set to
True
or aValueError
is raised. The default behavior uses the same scheme as the current request, orPREFERRED_URL_SCHEME
from the app configuration if no request context is available. As of Werkzeug 0.10, this also can be set to an empty string to build protocol-relative URLs.
abort¶
-
flask_unchained.
abort
(status: Union[int, Response], *args: Any, **kwargs: Any) → te.NoReturn[source]¶ Raises an
HTTPException
for the given status code or WSGI application.If a status code is given, it will be looked up in the list of exceptions and will raise that exception. If passed a WSGI application, it will wrap it in a proxy WSGI exception and raise that:
abort(404) # 404 Not Found abort(Response('Hello World'))
generate_csrf¶
-
flask_unchained.
generate_csrf
(secret_key=None, token_key=None)[source]¶ Generate a CSRF token. The token is cached for a request, so multiple calls to this function will generate the same token.
During testing, it might be useful to access the signed token in
g.csrf_token
and the raw token insession['csrf_token']
.- Parameters
secret_key – Used to securely sign the token. Default is
WTF_CSRF_SECRET_KEY
orSECRET_KEY
.token_key – Key where token is stored in session for comparison. Default is
WTF_CSRF_FIELD_NAME
or'csrf_token'
.
Graphene Bundle API¶
flask_unchained.bundles.graphene
The Graphene Bundle. |
flask_unchained.bundles.graphene.config
flask_unchained.bundles.graphene.hooks
Registers Graphene Mutations with the Graphene Bundle. |
|
Registers Graphene Queries with the Graphene Bundle. |
|
Creates the root |
|
Registers SQLAlchemyObjectTypes with the Graphene Bundle. |
flask_unchained.bundles.graphene.object_types
Base class for |
|
Base class for |
|
Base class for SQLAlchemy model object types. |
GrapheneBundle¶
Config¶
-
class
flask_unchained.bundles.graphene.config.
Config
[source]¶ -
GRAPHENE_URL
= '/graphql'¶ The URL where graphene should be served from. Set to
None
to disable.
-
GRAPHENE_BATCH_URL
= None¶ The URL where graphene should be served from in batch mode. Set to
None
to disable.
-
GRAPHENE_ENABLE_GRAPHIQL
= False¶ Whether or not to enable GraphIQL.
-
GRAPHENE_PRETTY_JSON
= False¶ Whether or not to pretty print the returned JSON.
-
RegisterGrapheneMutationsHook¶
-
class
flask_unchained.bundles.graphene.hooks.
RegisterGrapheneMutationsHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers Graphene Mutations with the Graphene Bundle.
-
name
: str = 'graphene_mutations'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['graphene.mutations', 'graphene.schema']¶ The default module this hook loads from.
Override by setting the
graphene_mutations_module_names
attribute on your bundle class.
-
process_objects
(app: flask_unchained.flask_unchained.FlaskUnchained, mutations: Dict[str, flask_unchained.bundles.graphene.object_types.MutationsObjectType])[source]¶ Register discovered mutations with the Graphene Bundle.
-
type_check
(obj: Any)[source]¶ Returns True if
obj
is a subclass ofMutationsObjectType
.
-
RegisterGrapheneQueriesHook¶
-
class
flask_unchained.bundles.graphene.hooks.
RegisterGrapheneQueriesHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers Graphene Queries with the Graphene Bundle.
-
name
: str = 'graphene_queries'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['graphene.queries', 'graphene.schema']¶ The default module this hook loads from.
Override by setting the
graphene_queries_module_names
attribute on your bundle class.
-
process_objects
(app: flask_unchained.flask_unchained.FlaskUnchained, queries: Dict[str, flask_unchained.bundles.graphene.object_types.QueriesObjectType])[source]¶ Register discovered queries with the Graphene Bundle.
-
type_check
(obj: Any)[source]¶ Returns True if
obj
is a subclass ofQueriesObjectType
.
-
RegisterGrapheneRootSchemaHook¶
-
class
flask_unchained.bundles.graphene.hooks.
RegisterGrapheneRootSchemaHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Creates the root
graphene.Schema
to register with Flask-GraphQL.-
name
: str = 'graphene_root_schema'¶ The name of this hook.
-
run_hook
(app: flask_unchained.flask_unchained.FlaskUnchained, bundles: List[flask_unchained.bundles.Bundle], unchained_config: Optional[Dict[str, Any]] = None) → None[source]¶ Create the root
graphene.Schema
from queries, mutations, and types discovered by the other hooks and register it with the Graphene Bundle.
-
RegisterGrapheneTypesHook¶
-
class
flask_unchained.bundles.graphene.hooks.
RegisterGrapheneTypesHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Registers SQLAlchemyObjectTypes with the Graphene Bundle.
-
name
: str = 'graphene_types'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['graphene.types', 'graphene.schema']¶ The default module this hook loads from.
Override by setting the
graphene_types_module_names
attribute on your bundle class.
-
QueriesObjectType¶
-
class
flask_unchained.bundles.graphene.
QueriesObjectType
(*args, **kwargs)[source]¶ Base class for
query
schema definitions.graphene.Field
andgraphene.List
fields are automatically resolved (but you can write your own and it will disable the automatic behavior for that field).Example usage:
# your_bundle/graphql/schema.py from flask_unchained.bundles.graphene import QueriesObjectType from . import types class YourBundleQueries(QueriesObjectType): parent = graphene.Field(types.Parent, id=graphene.ID(required=True)) parents = graphene.List(types.Parent) child = graphene.Field(types.Child, id=graphene.ID(required=True)) children = graphene.List(types.Child) # this is what the default resolvers do, and how you would override them: def resolve_child(self, info, **kwargs): return types.Child._meta.model.query.get_by(**kwargs) def resolve_children(self, info, **kwargs): return types.Child._meta.model.query.all()
MutationsObjectType¶
-
class
flask_unchained.bundles.graphene.
MutationsObjectType
(*args, **kwargs)[source]¶ Base class for
mutation
schema definitions.Example usage:
# your_bundle/graphql/mutations.py import graphene from flask_unchained import unchained from flask_unchained.bundles.sqlalchemy import SessionManager, db from graphql import GraphQLError from . import types session_manager: SessionManager = unchained.get_local_proxy('session_manager') class CreateParent(graphene.Mutation): class Arguments: name = graphene.String(required=True) children = graphene.List(graphene.ID) parent = graphene.Field(types.Parent) success = graphene.Boolean() def mutate(self, info, children, **kwargs): if children: children = (session_manager .query(types.Child._meta.model) .filter(types.Child._meta.model.id.in_(children)) .all()) try: parent = types.Parent._meta.model(children=children, **kwargs) except db.ValidationErrors as e: raise GraphQLError(str(e)) session_manager.save(parent, commit=True) return CreateParent(parent=parent, success=True) class DeleteParent(graphene.Mutation): class Arguments: id = graphene.ID(required=True) id = graphene.Int() success = graphene.Boolean() def mutate(self, info, id): parent = session_manager.query(types.Parent._meta.model).get(id) session_manager.delete(parent, commit=True) return DeleteParent(id=id, success=True) class EditParent(graphene.Mutation): class Arguments: id = graphene.ID(required=True) name = graphene.String() children = graphene.List(graphene.ID) parent = graphene.Field(types.Parent) success = graphene.Boolean() def mutate(self, info, id, children, **kwargs): parent = session_manager.query(types.Parent._meta.model).get(id) try: parent.update(**{k: v for k, v in kwargs.items() if v}) except db.ValidationErrors as e: raise GraphQLError(str(e)) if children: parent.children = (session_manager .query(types.Child._meta.model) .filter(types.Child._meta.model.id.in_(children)) .all()) session_manager.save(parent, commit=True) return EditParent(parent=parent, success=True) # your_bundle/graphql/schema.py from flask_unchained.bundles.graphene import MutationsObjectType from . import mutations class YourBundleMutations(MutationsObjectType): create_parent = mutations.CreateParent.Field() delete_parent = mutations.DeleteParent.Field() edit_parent = mutations.EditParent.Field()
SQLAlchemyObjectType¶
-
class
flask_unchained.bundles.graphene.
SQLAlchemyObjectType
(*args, **kwargs)[source]¶ Base class for SQLAlchemy model object types. Acts exactly the same as
graphene_sqlalchemy.SQLAlchemyObjectType
, except we’ve added compatibility with the SQLAlchemy Bundle.Example usage:
# your_bundle/models.py from flask_unchained.bundles.sqlalchemy import db class Parent(db.Model): name = db.Column(db.String) children = db.relationship('Child', back_populates='parent', cascade='all,delete,delete-orphan') class Child(db.Model): name = db.Column(db.String) parent_id = db.foreign_key('Parent') parent = db.relationship('Parent', back_populates='children') # your_bundle/graphql/types.py import graphene from flask_unchained.bundles.graphene import SQLAlchemyObjectType from .. import models class Parent(SQLAlchemyObjectType): class Meta: model = models.Parent only_fields = ('id', 'name', 'created_at', 'updated_at') children = graphene.List(lambda: Child) class Child(SQLAlchemyObjectType): class Meta: model = models.Child only_fields = ('id', 'name', 'created_at', 'updated_at') parent = graphene.Field(Parent)
Mail Bundle API¶
flask_unchained.bundles.mail
The Mail Bundle. |
flask_unchained.bundles.mail.config
Default configuration options for the mail bundle. |
|
Development-specific config options for the mail bundle. |
|
Production-specific config options for the mail bundle. |
|
Inherit settings from production. |
|
Test-specific config options for the mail bundle. |
flask_unchained.bundles.mail.extensions
The Mail extension. |
MailBundle¶
Config¶
-
class
flask_unchained.bundles.mail.config.
Config
[source]¶ Default configuration options for the mail bundle.
-
MAIL_SERVER
= '127.0.0.1'¶ The hostname/IP of the mail server.
-
MAIL_PORT
= 25¶ The port the mail server is running on.
-
MAIL_USERNAME
= None¶ The username to connect to the mail server with, if any.
-
MAIL_PASSWORD
= None¶ The password to connect to the mail server with, if any.
-
MAIL_USE_TLS
= False¶ Whether or not to use TLS.
-
MAIL_USE_SSL
= False¶ Whether or not to use SSL.
-
MAIL_DEFAULT_SENDER
= 'Flask Mail <noreply@localhost>'¶ The default sender to use, if none is specified otherwise.
-
MAIL_SEND_FN
(to: Union[str, List[str], None] = None, template: Optional[str] = None, **kwargs)¶ The function to use for sending emails. Defaults to
_send_mail()
. Any customized send function must implement the same function signature:def send_mail(subject_or_message: Optional[Union[str, Message]] = None, to: Optional[Union[str, List[str]] = None, template: Optional[str] = None, **kwargs): # ...
NOTE: subject_or_message is optional because you can also pass subject as a keyword argument, and to is optional because you can also pass recipients as a keyword argument. These are artifacts of backwards-compatibility with vanilla Flask-Mail.
-
MAIL_DEBUG
= 0¶ The debug level to set for interactions with the mail server.
-
MAIL_MAX_EMAILS
= None¶ The maximum number of emails to send per connection with the mail server.
-
MAIL_SUPPRESS_SEND
= False¶ Whether or not to actually send emails, or just pretend to. This is mainly useful for testing.
-
MAIL_ASCII_ATTACHMENTS
= False¶ Whether or not to coerce attachment filenames to ASCII.
-
-
class
flask_unchained.bundles.mail.config.
DevConfig
[source]¶ Development-specific config options for the mail bundle.
-
MAIL_DEBUG
= 1¶ Set the mail server debug level to 1 in development.
-
MAIL_PORT
= 1025¶ In development, the mail bundle is configured to connect to MailHog.
-
The Mail Extension¶
-
class
flask_unchained.bundles.mail.
Mail
[source]¶ The Mail extension:
from flask_unchained.bundles.mail import mail
-
send_message
(subject_or_message: Union[flask_mail.Message, str, None] = None, to: Union[str, List[str], None] = None, **kwargs)[source]¶ Send an email using the send function as configured by
MAIL_SEND_FN
.- Parameters
subject_or_message – The subject line, or an instance of
flask_mail.Message
.to – The message recipient(s).
kwargs – Extra values to pass on to
Message
-
OAuth Bundle API¶
flask_unchained.bundles.oauth
The OAuth Bundle. |
flask_unchained.bundles.oauth.config
flask_unchained.bundles.oauth.services
flask_unchained.bundles.oauth.views
OAuthBundle¶
Config¶
-
class
flask_unchained.bundles.oauth.config.
Config
[source]¶ -
OAUTH_REMOTE_APP_GITHUB
= {'access_token_method': 'POST', 'access_token_url': 'https://github.com/login/oauth/access_token', 'authorize_url': 'https://github.com/login/oauth/authorize', 'base_url': 'https://api.github.com', 'request_token_params': {'scope': 'user:email'}, 'request_token_url': None}¶
-
OAUTH_REMOTE_APP_AMAZON
= {'access_token_method': 'POST', 'access_token_url': 'https://api.amazon.com/auth/o2/token', 'authorize_url': 'https://www.amazon.com/ap/oa', 'base_url': 'https://api.amazon.com', 'request_token_params': {'scope': 'profile:email'}, 'request_token_url': None}¶
-
OAUTH_REMOTE_APP_GITLAB
= {'access_token_method': 'POST', 'access_token_url': 'https://gitlab.com/oauth/token', 'authorize_url': 'https://gitlab.com/oauth/authorize', 'base_url': 'https://gitlab.com/api/v4/user', 'request_token_params': {'scope': 'openid read_user'}, 'request_token_url': None}¶
-
OAuthService¶
-
class
flask_unchained.bundles.oauth.
OAuthService
[source]¶ -
get_user_details
(provider: flask_oauthlib.client.OAuthRemoteApp) → Tuple[str, dict][source]¶ For the given
provider
, return the user’s email address and any extra data to create the user model with.
Optional callback to add custom behavior upon OAuth authorized.
-
OAuthController¶
Security Bundle API¶
flask_unchained.bundles.security
The Security Bundle. |
flask_unchained.bundles.security.config
Config options for the Security Bundle. |
|
Config options for logging in and out. |
|
Config options for token authentication. |
|
Config options for user registration |
|
Config options for changing passwords |
|
Config options for recovering forgotten passwords |
|
Config options for encryption hashing. |
flask_unchained.bundles.security.extensions
The Security extension. |
flask_unchained.bundles.security.views
The controller for the security bundle. |
|
RESTful API resource for the |
flask_unchained.bundles.security.decorators
Decorator for requiring an authenticated user, optionally with roles. |
|
Decorator for requiring an authenticated user to be the same as the user in the URL parameters. |
|
Decorator requiring that there is no user currently logged in. |
flask_unchained.bundles.security.models
Base user model. |
|
Base role model. |
|
flask_unchained.bundles.security.serializers
Marshmallow serializer for the |
|
Marshmallow serializer for the |
flask_unchained.bundles.security.services
The security service. |
|
The security utils service. |
|
|
|
|
flask_unchained.bundles.security.forms
The default login form. |
|
The default register form. |
|
The default change password form. |
|
The default forgot password form. |
|
The default reset password form. |
|
The default resend confirmation email form. |
SecurityBundle¶
-
class
flask_unchained.bundles.security.
SecurityBundle
[source]¶ The Security Bundle. Integrates Flask Login and Flask Principal with Flask Unchained.
-
name
: str = 'security_bundle'¶ The name of the Security Bundle.
-
command_group_names
= ['users', 'roles']¶ Click groups for the Security Bundle.
-
Config¶
Config options for the Security Bundle. |
|
Config options for logging in and out. |
|
Config options for token authentication. |
|
Config options for user registration |
|
Config options for changing passwords |
|
Config options for recovering forgotten passwords |
|
Config options for encryption hashing. |
General¶
-
class
flask_unchained.bundles.security.config.
Config
[source]¶ Config options for the Security Bundle.
-
SECURITY_ANONYMOUS_USER
¶ alias of
flask_unchained.bundles.security.models.anonymous_user.AnonymousUser
-
SECURITY_UNAUTHORIZED_CALLBACK
()¶ This callback gets called when authorization fails. By default we abort with an HTTP status code of 401 (UNAUTHORIZED).
-
SECURITY_DATETIME_FACTORY
()¶ Factory function to use when creating new dates. By default we use
datetime.now(timezone.utc)
to create a timezone-aware datetime.
-
Authentication¶
-
class
flask_unchained.bundles.security.config.
AuthenticationConfig
[source]¶ Config options for logging in and out.
-
SECURITY_LOGIN_FORM
¶
-
SECURITY_DEFAULT_REMEMBER_ME
= False¶ Whether or not the login form should default to checking the “Remember me?” option.
-
SECURITY_REMEMBER_SALT
= 'security-remember-salt'¶ Salt used for the remember me cookie token.
-
SECURITY_USER_IDENTITY_ATTRIBUTES
= ['email']¶ List of attributes on the user model that can used for logging in with. Each must be unique.
-
SECURITY_POST_LOGIN_REDIRECT_ENDPOINT
= '/'¶ The endpoint or url to redirect to after a successful login.
-
SECURITY_POST_LOGOUT_REDIRECT_ENDPOINT
= '/'¶ The endpoint or url to redirect to after a user logs out.
-
Token Authentication¶
-
class
flask_unchained.bundles.security.config.
TokenConfig
[source]¶ Config options for token authentication.
-
SECURITY_TOKEN_AUTHENTICATION_KEY
= 'auth_token'¶ Specifies the query string parameter to read when using token authentication.
-
SECURITY_TOKEN_AUTHENTICATION_HEADER
= 'Authentication-Token'¶ Specifies the HTTP header to read when using token authentication.
-
SECURITY_TOKEN_MAX_AGE
= None¶ Specifies the number of seconds before an authentication token expires. Defaults to None, meaning the token never expires.
-
Registration¶
-
class
flask_unchained.bundles.security.config.
RegistrationConfig
[source]¶ Config options for user registration
-
SECURITY_REGISTERABLE
= False¶ Whether or not to enable registration.
-
SECURITY_REGISTER_FORM
¶ alias of
flask_unchained.bundles.security.forms.RegisterForm
-
SECURITY_POST_REGISTER_REDIRECT_ENDPOINT
= None¶ The endpoint or url to redirect to after a user completes the registration form.
-
SECURITY_SEND_REGISTER_EMAIL
= False¶ Whether or not send a welcome email after a user completes the registration form.
-
SECURITY_CONFIRMABLE
= False¶ Whether or not to enable required email confirmation for new users.
-
SECURITY_SEND_CONFIRMATION_FORM
¶ alias of
flask_unchained.bundles.security.forms.SendConfirmationForm
-
SECURITY_CONFIRM_SALT
= 'security-confirm-salt'¶ Salt used for the confirmation token.
-
SECURITY_LOGIN_WITHOUT_CONFIRMATION
= False¶ Allow users to login without confirming their email first. (This option only applies when
SECURITY_CONFIRMABLE
is True.)
-
SECURITY_CONFIRM_EMAIL_WITHIN
= '5 days'¶ How long to wait until considering the token in confirmation emails to be expired.
-
SECURITY_POST_CONFIRM_REDIRECT_ENDPOINT
= None¶ Endpoint or url to redirect to after the user confirms their email. Defaults to
SECURITY_POST_LOGIN_REDIRECT_ENDPOINT
.
-
SECURITY_CONFIRM_ERROR_REDIRECT_ENDPOINT
= None¶ Endpoint to redirect to if there’s an error confirming the user’s email.
-
Change Password¶
-
class
flask_unchained.bundles.security.config.
ChangePasswordConfig
[source]¶ Config options for changing passwords
-
SECURITY_CHANGEABLE
= False¶ Whether or not to enable change password functionality.
-
SECURITY_CHANGE_PASSWORD_FORM
¶ alias of
flask_unchained.bundles.security.forms.ChangePasswordForm
-
SECURITY_POST_CHANGE_REDIRECT_ENDPOINT
= None¶ Endpoint or url to redirect to after the user changes their password.
-
SECURITY_SEND_PASSWORD_CHANGED_EMAIL
= False¶ Whether or not to send the user an email when their password has been changed. Defaults to True, and it’s strongly recommended to leave this option enabled.
-
Forgot Password¶
-
class
flask_unchained.bundles.security.config.
ForgotPasswordConfig
[source]¶ Config options for recovering forgotten passwords
-
SECURITY_RECOVERABLE
= False¶ Whether or not to enable forgot password functionality.
-
SECURITY_FORGOT_PASSWORD_FORM
¶ alias of
flask_unchained.bundles.security.forms.ForgotPasswordForm
-
SECURITY_RESET_PASSWORD_FORM
¶ alias of
flask_unchained.bundles.security.forms.ResetPasswordForm
-
SECURITY_RESET_SALT
= 'security-reset-salt'¶ Salt used for the reset token.
-
SECURITY_RESET_PASSWORD_WITHIN
= '5 days'¶ Specifies the amount of time a user has before their password reset link expires. Always pluralized the time unit for this value. Defaults to 5 days.
-
SECURITY_POST_RESET_REDIRECT_ENDPOINT
= None¶ Endpoint or url to redirect to after the user resets their password.
-
SECURITY_INVALID_RESET_TOKEN_REDIRECT
= 'security_controller.forgot_password'¶ Endpoint or url to redirect to if the reset token is invalid.
-
SECURITY_EXPIRED_RESET_TOKEN_REDIRECT
= 'security_controller.forgot_password'¶ Endpoint or url to redirect to if the reset token is expired.
-
SECURITY_API_RESET_PASSWORD_HTTP_GET_REDIRECT
= None¶ Endpoint or url to redirect to if a GET request is made to the reset password view. Defaults to None, meaning no redirect. Useful for single page apps.
-
SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL
= False¶ Whether or not to send the user an email when their password has been reset. Defaults to True, and it’s strongly recommended to leave this option enabled.
-
Encryption¶
-
class
flask_unchained.bundles.security.config.
EncryptionConfig
[source]¶ Config options for encryption hashing.
-
SECURITY_PASSWORD_SALT
= 'security-password-salt'¶ Specifies the HMAC salt. This is only used if the password hash type is set to something other than plain text.
-
SECURITY_PASSWORD_HASH
= 'bcrypt'¶ Specifies the password hash algorithm to use when hashing passwords. Recommended values for production systems are
argon2
,bcrypt
, orpbkdf2_sha512
. May require extra packages to be installed.
-
SECURITY_PASSWORD_SINGLE_HASH
= False¶ Specifies that passwords should only be hashed once. By default, passwords are hashed twice, first with SECURITY_PASSWORD_SALT, and then with a random salt. May be useful for integrating with other applications.
-
SECURITY_PASSWORD_SCHEMES
= ['argon2', 'bcrypt', 'pbkdf2_sha512', 'plaintext']¶ List of algorithms that can be used for hashing passwords.
-
SECURITY_PASSWORD_HASH_OPTIONS
= {}¶ Specifies additional options to be passed to the hashing method.
-
SECURITY_DEPRECATED_PASSWORD_SCHEMES
= ['auto']¶ List of deprecated algorithms for hashing passwords.
-
SECURITY_HASHING_SCHEMES
= ['sha512_crypt']¶ List of algorithms that can be used for creating and validating tokens.
-
SECURITY_DEPRECATED_HASHING_SCHEMES
= []¶ List of deprecated algorithms for creating and validating tokens.
-
The Security Extension¶
-
class
flask_unchained.bundles.security.
Security
[source]¶ The Security extension:
from flask_unchained.bundles.security import security
-
context_processor
(fn)[source]¶ Add a context processor that runs for every view with a template in the security bundle.
- Parameters
fn – A function that returns a dictionary of template context variables.
-
forgot_password_context_processor
(fn)[source]¶ Add a context processor for the
SecurityController.forgot_password()
view.- Parameters
fn – A function that returns a dictionary of template context variables.
-
login_context_processor
(fn)[source]¶ Add a context processor for the
SecurityController.login()
view.- Parameters
fn – A function that returns a dictionary of template context variables.
-
register_context_processor
(fn)[source]¶ Add a context processor for the
SecurityController.register()
view.- Parameters
fn – A function that returns a dictionary of template context variables.
-
reset_password_context_processor
(fn)[source]¶ Add a context processor for the
SecurityController.reset_password()
view.- Parameters
fn – A function that returns a dictionary of template context variables.
-
change_password_context_processor
(fn)[source]¶ Add a context processor for the
SecurityController.change_password()
view.- Parameters
fn – A function that returns a dictionary of template context variables.
-
send_confirmation_context_processor
(fn)[source]¶ Add a context processor for the
SecurityController.send_confirmation_email()
view.- Parameters
fn – A function that returns a dictionary of template context variables.
-
Views¶
Decorators¶
-
flask_unchained.bundles.security.
auth_required
(decorated_fn=None, **role_rules)[source]¶ Decorator for requiring an authenticated user, optionally with roles.
Roles are passed as keyword arguments, like so:
@auth_required(role='REQUIRE_THIS_ONE_ROLE') @auth_required(roles=['REQUIRE', 'ALL', 'OF', 'THESE', 'ROLES']) @auth_required(one_of=['EITHER_THIS_ROLE', 'OR_THIS_ONE'])
Either of the role or roles kwargs can also be combined with one_of:
@auth_required(role='REQUIRED', one_of=['THIS', 'OR_THIS'])
Aborts with
HTTP 401: Unauthorized
if no user is logged in, orHTTP 403: Forbidden
if any of the specified role checks fail.
-
flask_unchained.bundles.security.
auth_required_same_user
(*args, **kwargs)[source]¶ Decorator for requiring an authenticated user to be the same as the user in the URL parameters. By default the user url parameter name to lookup is
id
, but this can be customized by passing an argument:@auth_require_same_user('user_id') @bp.route('/users/<int:user_id>/foo/<int:id>') def get(user_id, id): # do stuff
Any keyword arguments are passed along to the @auth_required decorator, so roles can also be specified in the same was as it, eg:
@auth_required_same_user('user_id', role='ROLE_ADMIN')
Aborts with
HTTP 403: Forbidden
if the user-check fails.
SecurityController¶
-
class
flask_unchained.bundles.security.
SecurityController
[source]¶ The controller for the security bundle.
-
check_auth_token
(**kwargs)[source]¶ View function to check a token, and if it’s valid, log the user in.
Disabled by default; must be explicitly enabled in your
routes.py
.
-
send_confirmation_email
()[source]¶ View function which sends confirmation token and instructions to a user.
-
confirm_email
(token)[source]¶ View function to confirm a user’s token from the confirmation email send to them. Supports html and json requests.
-
forgot_password
()[source]¶ View function to request a password recovery email with a reset token. Supports html and json requests.
-
UserResource¶
Models and Managers¶
User¶
-
class
flask_unchained.bundles.security.
User
(**kwargs)[source]¶ Base user model. Includes
email
,password
,is_active
, andconfirmed_at
columns, and a many-to-many relationship to theRole
model via the intermediaryUserRole
join table.
UserManager¶
-
class
flask_unchained.bundles.security.
UserManager
[source]¶ ModelManager
for theUser
model.
Role¶
RoleManager¶
-
class
flask_unchained.bundles.security.
RoleManager
[source]¶ ModelManager
for theRole
model.
UserRole¶
Serializers¶
UserSerializer¶
RoleSerializer¶
Services¶
SecurityService¶
-
class
flask_unchained.bundles.security.
SecurityService
(mail: Optional[flask_unchained.bundles.mail.extensions.mail.Mail] = None)[source]¶ The security service.
Contains shared business logic that doesn’t belong in controllers, but isn’t so low level that it belongs in
SecurityUtilsService
.-
login_user
(user: flask_unchained.bundles.security.models.user.User, remember: Optional[bool] = None, duration: Optional[datetime.timedelta] = None, force: bool = False, fresh: bool = True) → bool[source]¶ Logs a user in. You should pass the actual user object to this. If the user’s is_active property is
False
, they will not be logged in unless force isTrue
.This will return
True
if the log in attempt succeeds, andFalse
if it fails (i.e. because the user is inactive).- Parameters
user (object) – The user object to log in.
remember (bool) – Whether to remember the user after their session expires. Defaults to
False
.duration (
datetime.timedelta
) – The amount of time before the remember cookie expires. IfNone
the value set in the settings is used. Defaults toNone
.force (bool) – If the user is inactive, setting this to
True
will log them in regardless. Defaults toFalse
.fresh (bool) – setting this to
False
will log in the user with a session marked as not “fresh”. Defaults toTrue
.
-
logout_user
()[source]¶ Logs out the current user and cleans up the remember me cookie (if any).
Sends signal identity_changed (from flask_principal). Sends signal user_logged_out (from flask_login).
-
register_user
(user, allow_login=None, send_email=None, _force_login_without_confirmation=False)[source]¶ Service method to register a user.
Sends signal user_registered.
Returns True if the user has been logged in, False otherwise.
-
change_password
(user, password, send_email=None)[source]¶ Service method to change a user’s password.
Sends signal password_changed.
- Parameters
user – The
User
’s password to change.password – The new password.
send_email – Whether or not to override the config option
SECURITY_SEND_PASSWORD_CHANGED_EMAIL
and force either sending or not sending an email.
-
reset_password
(user, password)[source]¶ Service method to reset a user’s password. The same as
change_password()
except we this method sends a different notification email.Sends signal password_reset.
- Parameters
user –
password –
- Returns
-
send_email_confirmation_instructions
(user)[source]¶ Sends the confirmation instructions email for the specified user.
Sends signal confirm_instructions_sent.
- Parameters
user – The user to send the instructions to.
-
send_reset_password_instructions
(user)[source]¶ Sends the reset password instructions email for the specified user.
Sends signal reset_password_instructions_sent.
- Parameters
user – The user to send the instructions to.
-
SecurityUtilsService¶
-
class
flask_unchained.bundles.security.
SecurityUtilsService
[source]¶ The security utils service. Mainly contains lower-level encryption/token handling code.
-
get_hmac
(password)[source]¶ Returns a Base64 encoded HMAC+SHA512 of the password signed with the salt specified by
SECURITY_PASSWORD_SALT
.- Parameters
password – The password to sign.
-
verify_password
(user, password)[source]¶ Returns
True
if the password is valid for the specified user.Additionally, the hashed password in the database is updated if the hashing algorithm happens to have changed.
- Parameters
user – The user to verify against
password – The plaintext password to verify
-
hash_password
(password)[source]¶ Hash the specified plaintext password.
It uses the configured hashing options.
- Parameters
password – The plaintext password to hash
-
verify_hash
(hashed_data, compare_data)[source]¶ Verify a hash in the security token hashing context.
-
use_double_hash
(password_hash=None)[source]¶ Return a bool indicating whether a password should be hashed twice.
-
generate_confirmation_token
(user)[source]¶ Generates a unique confirmation token for the specified user.
- Parameters
user – The user to work with
-
confirm_email_token_status
(token)[source]¶ Returns the expired status, invalid status, and user of a confirmation token. For example:
expired, invalid, user = confirm_email_token_status('...')
- Parameters
token – The confirmation token
-
generate_reset_password_token
(user)[source]¶ Generates a unique reset password token for the specified user.
- Parameters
user – The user to work with
-
reset_password_token_status
(token)[source]¶ Returns the expired status, invalid status, and user of a password reset token. For example:
expired, invalid, user, data = reset_password_token_status('...')
- Parameters
token – The password reset token
-
get_token_status
(token, serializer, max_age=None, return_data=False)[source]¶ Get the status of a token.
- Parameters
token – The token to check
serializer – The name of the serializer. Can be one of the following:
confirm
,login
,reset
max_age – The name of the max age config option. Can be one of the following:
SECURITY_CONFIRM_EMAIL_WITHIN
orSECURITY_RESET_PASSWORD_WITHIN
-
Forms¶
LoginForm¶
-
class
flask_unchained.bundles.security.forms.
LoginForm
(*args, **kwargs)[source]¶ The default login form.
-
validate
()[source]¶ Validate the form by calling
validate
on each field. ReturnsTrue
if validation passes.If the form defines a
validate_<fieldname>
method, it is appended as an extra validator for the field’svalidate
.- Parameters
extra_validators – A dict mapping field names to lists of extra validator methods to run. Extra validators run after validators passed when creating the field. If the form has
validate_<fieldname>
, it is the last extra validator.
-
RegisterForm¶
ChangePasswordForm¶
-
class
flask_unchained.bundles.security.forms.
ChangePasswordForm
(*args, **kwargs)[source]¶ The default change password form.
-
validate
()[source]¶ Validate the form by calling
validate
on each field. ReturnsTrue
if validation passes.If the form defines a
validate_<fieldname>
method, it is appended as an extra validator for the field’svalidate
.- Parameters
extra_validators – A dict mapping field names to lists of extra validator methods to run. Extra validators run after validators passed when creating the field. If the form has
validate_<fieldname>
, it is the last extra validator.
-
ForgotPasswordForm¶
ResetPasswordForm¶
SendConfirmationForm¶
-
class
flask_unchained.bundles.security.forms.
SendConfirmationForm
(*args, **kwargs)[source]¶ The default resend confirmation email form.
-
validate
()[source]¶ Validate the form by calling
validate
on each field. ReturnsTrue
if validation passes.If the form defines a
validate_<fieldname>
method, it is appended as an extra validator for the field’svalidate
.- Parameters
extra_validators – A dict mapping field names to lists of extra validator methods to run. Extra validators run after validators passed when creating the field. If the form has
validate_<fieldname>
, it is the last extra validator.
-
Validators¶
-
flask_unchained.bundles.security.forms.
password_equal
(form, field)¶ Compares the values of two fields.
- Parameters
fieldname – The name of the other field to compare to.
message – Error message to raise in case of a validation error. Can be interpolated with %(other_label)s and %(other_name)s to provide a more helpful error.
-
flask_unchained.bundles.security.forms.
new_password_equal
(form, field)¶ Compares the values of two fields.
- Parameters
fieldname – The name of the other field to compare to.
message – Error message to raise in case of a validation error. Can be interpolated with %(other_label)s and %(other_name)s to provide a more helpful error.
Session Bundle API¶
flask_unchained.bundles.session
The Session Bundle. |
flask_unchained.bundles.session.config
Default configuration options for sessions in Flask. |
|
Default configuration options for the Session Bundle. |
flask_unchained.bundles.session.hooks
If using |
SessionBundle¶
-
class
flask_unchained.bundles.session.
SessionBundle
[source]¶ The Session Bundle. Integrates Flask Session with Flask Unchained.
Config¶
-
class
flask_unchained.bundles.session.config.
DefaultFlaskConfigForSessions
[source]¶ Default configuration options for sessions in Flask.
-
SESSION_COOKIE_NAME
= 'session'¶ The name of the session cookie.
Defaults to
'session'
.
-
SESSION_COOKIE_DOMAIN
= None¶ The domain for the session cookie. If this is not set, the cookie will be valid for all subdomains of
SERVER_NAME
.Defaults to
None
.
-
SESSION_COOKIE_PATH
= None¶ The path for the session cookie. If this is not set the cookie will be valid for all of
APPLICATION_ROOT
or if that is not set for ‘/’.Defaults to
None
.
-
SESSION_COOKIE_HTTPONLY
= True¶ Controls if the cookie should be set with the
httponly
flag. Browsers will not allow JavaScript access to cookies marked ashttponly
for security.Defaults to
True
.
-
SESSION_COOKIE_SECURE
= False¶ Controls if the cookie should be set with the
secure
flag. Browsers will only send cookies with requests over HTTPS if the cookie is markedsecure
. The application must be served over HTTPS for this to make sense.Defaults to
False
.
-
PERMANENT_SESSION_LIFETIME
= datetime.timedelta(days=31)¶ The lifetime of a permanent session as
datetime.timedelta
object or an integer representing seconds.Defaults to 31 days.
-
SESSION_COOKIE_SAMESITE
= None¶ Restrict how cookies are sent with requests from external sites. Limits the scope of the cookie such that it will only be attached to requests if those requests are “same-site”. Can be set to
'Lax'
(recommended) or'Strict'
.Defaults to
None
.
-
SESSION_REFRESH_EACH_REQUEST
= True¶ Controls the set-cookie behavior. If set to
True
a permanent session will be refreshed each request and get their lifetime extended, if set toFalse
it will only be modified if the session actually modifies. Non permanent sessions are not affected by this and will always expire if the browser window closes.Defaults to
True
.
-
-
class
flask_unchained.bundles.session.config.
Config
[source]¶ Default configuration options for the Session Bundle.
See Flask Session for more information.
-
SESSION_TYPE
= 'null'¶ Specifies which type of session interface to use. Built-in session types:
'null'
:NullSessionInterface
(default)'redis'
:RedisSessionInterface
'memcached'
:MemcachedSessionInterface
'filesystem'
:FileSystemSessionInterface
'mongodb'
:MongoDBSessionInterface
'sqlalchemy'
:SqlAlchemySessionInterface
Defaults to
'null'
.
-
SESSION_PERMANENT
= True¶ Whether use permanent session or not.
Defaults to
True
.
-
SESSION_USE_SIGNER
= False¶ Whether sign the session cookie sid or not. If set to
True
, you have to setSECRET_KEY
.Defaults to
False
.
-
SESSION_KEY_PREFIX
= 'session:'¶ A prefix that is added before all session keys. This makes it possible to use the same backend storage server for different apps.
Defaults to
'session:'
.
-
SESSION_REDIS
= None¶ A
redis.Redis
instance.By default, connect to
127.0.0.1:6379
.
-
SESSION_MEMCACHED
= None¶ A
memcached.Client
instance.By default, connect to
127.0.0.1:11211
.
-
SESSION_FILE_DIR
= '/home/docs/checkouts/readthedocs.org/user_builds/flask-unchained/checkouts/latest/docs/flask_sessions'¶ The folder where session files are stored.
Defaults to using a folder named
flask_sessions
in your current working directory.
-
SESSION_FILE_THRESHOLD
= 500¶ The maximum number of items the session stores before it starts deleting some.
Defaults to 500.
-
SESSION_FILE_MODE
= 384¶ The file mode wanted for the session files. Should be specified as an octal, eg
0o600
.Defaults to
0o600
.
-
SESSION_MONGODB
= None¶ A
pymongo.MongoClient
instance.By default, connect to
127.0.0.1:27017
.
-
SESSION_MONGODB_DB
= 'flask_session'¶ The MongoDB database you want to use.
Defaults to
'flask_session'
.
-
SESSION_MONGODB_COLLECT
= 'sessions'¶ The MongoDB collection you want to use.
Defaults to
'sessions'
.
-
SESSION_SQLALCHEMY
= <SQLAlchemyUnchained engine=None>¶ A
SQLAlchemy
extension instance.
-
SESSION_SQLALCHEMY_TABLE
= 'flask_sessions'¶ The name of the SQL table you want to use.
Defaults to
flask_sessions
.
-
SESSION_SQLALCHEMY_MODEL
= None¶ Set this if you need to customize the
BaseModel
subclass used for storing sessions in the database.
-
RegisterSessionModelHook¶
-
class
flask_unchained.bundles.session.hooks.
RegisterSessionModelHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ If using
sqlalchemy
as the SESSION_TYPE, register theSession
model with the SQLAlchemy Bundle.-
name
: str = 'register_session_model'¶ The name of this hook.
-
run_hook
(app: flask_unchained.flask_unchained.FlaskUnchained, bundles: List[flask_unchained.bundles.Bundle], unchained_config: Optional[Dict[str, Any]] = None) → None[source]¶ If using
sqlalchemy
as theSESSION_TYPE
, register theSession
model with the SQLAlchemy Bundle.
-
SQLAlchemy Bundle API¶
flask_unchained.bundles.sqlalchemy
The SQLAlchemy Bundle. |
flask_unchained.bundles.sqlalchemy.config
The default configuration options for the SQLAlchemy Bundle. |
|
Default configuration options for testing. |
flask_unchained.bundles.sqlalchemy.extensions
The SQLAlchemyUnchained extension. |
flask_unchained.bundles.sqlalchemy.hooks
Discovers SQLAlchemy models. |
flask_unchained.bundles.sqlalchemy.services
The database session manager service. |
|
Base class for database model manager services. |
flask_unchained.bundles.sqlalchemy.sqla
Overridden to make nullable False by default |
|
Class decorator for SQLAlchemy models to attach listeners for class methods decorated with |
|
Class method decorator for SQLAlchemy models. |
|
Class decorator to specify a field to slugify. |
|
Helper method to add a foreign key column to a model. |
flask_unchained.bundles.sqlalchemy.forms
Base class for SQLAlchemy model forms. |
SQLAlchemyBundle¶
-
class
flask_unchained.bundles.sqlalchemy.
SQLAlchemyBundle
[source]¶ The SQLAlchemy Bundle. Integrates SQLAlchemy and Flask-Migrate with Flask Unchained.
-
name
: str = 'sqlalchemy_bundle'¶ The name of the SQLAlchemy Bundle.
-
command_group_names
= ['db']¶ Click groups for the SQLAlchemy Bundle.
-
models
= None¶ A lookup of model classes keyed by class name.
-
Config¶
-
class
flask_unchained.bundles.sqlalchemy.config.
Config
[source]¶ The default configuration options for the SQLAlchemy Bundle.
-
SQLALCHEMY_DATABASE_URI
= 'sqlite:///db/development.sqlite'¶ The database URI that should be used for the connection. Defaults to using SQLite with the database file stored at
ROOT_PATH/db/<env>.sqlite
. See the SQLAlchemy Dialects documentation for more info.
-
SQLALCHEMY_TRANSACTION_ISOLATION_LEVEL
= None¶ Set the engine-wide transaction isolation level.
See the docs for more info.
-
SQLALCHEMY_ECHO
= False¶ If set to
True
SQLAlchemy will log all the statements issued to stderr which can be useful for debugging.
-
SQLALCHEMY_TRACK_MODIFICATIONS
= False¶ If set to
True
, Flask-SQLAlchemy will track modifications of objects and emit signals. The default isFalse
. This requires extra memory and should be disabled if not needed.
-
SQLALCHEMY_RECORD_QUERIES
= None¶ Can be used to explicitly disable or enable query recording. Query recording automatically happens in debug or testing mode. See
get_debug_queries()
for more information.
-
SQLALCHEMY_BINDS
= None¶ A dictionary that maps bind keys to SQLAlchemy connection URIs.
-
SQLALCHEMY_NATIVE_UNICODE
= None¶ Can be used to explicitly disable native unicode support. This is required for some database adapters (like PostgreSQL on some Ubuntu versions) when used with improper database defaults that specify encoding-less databases.
-
SQLALCHEMY_POOL_SIZE
= None¶ The size of the database pool. Defaults to the engine’s default (usually 5).
-
SQLALCHEMY_POOL_TIMEOUT
= None¶ Specifies the connection timeout in seconds for the pool.
-
SQLALCHEMY_POOL_RECYCLE
= None¶ Number of seconds after which a connection is automatically recycled. This is required for MySQL, which removes connections after 8 hours idle by default. Note that Flask-SQLAlchemy automatically sets this to 2 hours if MySQL is used.
Certain database backends may impose different inactive connection timeouts, which interferes with Flask-SQLAlchemy’s connection pooling.
By default, MariaDB is configured to have a 600 second timeout. This often surfaces hard to debug, production environment only exceptions like
2013: Lost connection to MySQL server
during query.If you are using a backend (or a pre-configured database-as-a-service) with a lower connection timeout, it is recommended that you set
SQLALCHEMY_POOL_RECYCLE
to a value less than your backend’s timeout.
-
SQLALCHEMY_MAX_OVERFLOW
= None¶ Controls the number of connections that can be created after the pool reached its maximum size. When those additional connections are returned to the pool, they are disconnected and discarded.
-
SQLALCHEMY_COMMIT_ON_TEARDOWN
= False¶ Whether or not to automatically commit on app context teardown. Defaults to False.
-
ALEMBIC
= {'script_location': 'db/migrations'}¶ Used to set the directory where migrations are stored. ALEMBIC should be set to a dictionary, using the key script_location to set the directory. Defaults to
ROOT_PATH/db/migrations
.
-
ALEMBIC_CONTEXT
= {'render_item': <function render_migration_item>, 'template_args': {'migration_variables': []}}¶ Extra kwargs to pass to the constructor of the Flask-Migrate extension. If you need to change this, make sure to merge the defaults with your settings!
-
The SQLAlchemy Extension¶
-
class
flask_unchained.bundles.sqlalchemy.
SQLAlchemyUnchained
(app=None, use_native_unicode=True, session_options=None, metadata=None, query_class=<class 'flask_sqlalchemy_unchained.BaseQuery'>, model_class=<class 'flask_unchained.bundles.sqlalchemy.base_model.BaseModel'>)[source]¶ The SQLAlchemyUnchained extension:
from flask_unchained.bundles.sqlalchemy import db
Provides aliases for common SQLAlchemy stuffs:
sqlalchemy.schema: Columns & Tables
Represents a column in a database table.
Defines a generated column, i.e.
A plain default value on a column.
A DDL-specified DEFAULT column value.
A marker for a transparent database-side default.
Defines a dependency between two columns.
A table-level INDEX.
Represents a named database sequence.
Represent a table in a database.
sqlalchemy.schema: Constraints
A table- or column-level CHECK constraint.
A table-level SQL constraint.
A table-level FOREIGN KEY constraint.
A table-level PRIMARY KEY constraint.
A table-level UNIQUE constraint.
sqlalchemy.types: Column types
A type for bigger
int
integers.A bool datatype.
A type for
datetime.date()
objects.A type for
datetime.datetime()
objects.Generic Enum Type.
Type representing floating point types, such as
FLOAT
orREAL
.A type for
int
integers.A type for
datetime.timedelta()
objects.A type for large binary byte data.
A type for fixed precision numbers, such as
NUMERIC
orDECIMAL
.Holds Python objects, which are serialized using pickle.
A type for smaller
int
integers.The base for all string and character types.
A variably sized string type.
A type for
datetime.time()
objects.Allows the creation of types which add additional functionality to an existing type.
A variable length Unicode string type.
An unbounded-length Unicode string type.
relationship helpers
Return a Python property implementing a view of a target attribute which references an attribute on members of the target.
declared_attr
Mark a class-level method as representing the definition of a mapped property or special declarative member name.
Helper method to add a foreign key column to a model.
A decorator which allows definition of a Python object method with both instance-level and class-level behavior.
A decorator which allows definition of a Python descriptor with both instance-level and class-level behavior.
Provide a relationship between two mapped classes.
sqlalchemy.types: SQL types
Represent a SQL Array type.
The SQL BIGINT type.
The SQL BINARY type.
The SQL BLOB type.
The SQL BOOLEAN type.
The SQL CHAR type.
The CLOB type.
The SQL DATE type.
The SQL DATETIME type.
The SQL DECIMAL type.
The SQL FLOAT type.
alias of
sqlalchemy.sql.sqltypes.INTEGER
The SQL INT or INTEGER type.
Represent a SQL JSON type.
The SQL NCHAR type.
The SQL NUMERIC type.
The SQL NVARCHAR type.
The SQL REAL type.
The SQL SMALLINT type.
The SQL TEXT type.
The SQL TIME type.
The SQL TIMESTAMP type.
The SQL VARBINARY type.
The SQL VARCHAR type.
sqlalchemy.schema
A literal DDL statement.
A collection of
_schema.Table
objects and their associated schema constructs.A MetaData variant that presents a different
bind
in every thread.sqlalchemy.sql.expression
Return an
_expression.Alias
object.Produce an ALL expression.
Produce a conjunction of expressions joined by
AND
.Produce an ANY expression.
Produce an ascending
ORDER BY
clause element.Produce a
BETWEEN
predicate clause.Produce a “bound expression”.
Produce a
CASE
expression.Produce a
CAST
expression.Return the clause
expression COLLATE collation
.Produce a
ColumnClause
object.Construct
_expression.Delete
object.Produce a descending
ORDER BY
clause element.Produce an column-expression-level unary
DISTINCT
clause.Return an
EXCEPT
of multiple selectables.Return an
EXCEPT ALL
of multiple selectables.Construct a new
_expression.Exists
against an existing_expression.Select
object.Return a
Extract
construct.Return a
False_
construct.Generate SQL function expressions.
Produce a
FunctionFilter
object against a function.Construct an
_expression.Insert
object.Return an
INTERSECT
of multiple selectables.Return an
INTERSECT ALL
of multiple selectables.Produce a
_expression.Join
object, given two_expression.FromClause
expressions.Return a
_expression.Lateral
object.Return a literal clause, bound to a bind parameter.
Produce a
ColumnClause
object that has the :paramref:`_expression.column.is_literal` flag set to True.Return a negation of the given clause, i.e.
Return a constant
Null
construct.nullsfirst
Produce the
NULLS FIRST
modifier for anORDER BY
expression.nullslast
Produce the
NULLS LAST
modifier for anORDER BY
expression.Produce a conjunction of expressions joined by
OR
.Return an
OUTER JOIN
clause element.Create an ‘OUT’ parameter for usage in functions (stored procedures), for databases which support them.
Produce an
Over
object against a function.Construct a new
_expression.Select
.subquery
Return an
_expression.Alias
object derived from a_expression.Select
.Produce a new
_expression.TableClause
.Return a
_expression.TableSample
object.Construct a new
_expression.TextClause
clause, representing a textual SQL string directly.Return a constant
True_
construct.Return a
Tuple
.Associate a SQL expression with a particular type, without rendering
CAST
.Return a
UNION
of multiple selectables.Return a
UNION ALL
of multiple selectables.Construct an
_expression.Update
object.Produce a
WithinGroup
object against a function.
RegisterModelsHook¶
-
class
flask_unchained.bundles.sqlalchemy.hooks.
RegisterModelsHook
(unchained: flask_unchained.unchained.Unchained, bundle: Optional[flask_unchained.bundles.Bundle] = None)[source]¶ Discovers SQLAlchemy models.
-
name
: str = 'models'¶ The name of this hook.
-
bundle_module_names
: Union[List[str], Tuple[str, ...], None] = ['models']¶ The default module this hook loads from.
Override by setting the
models_module_names
attribute on your bundle class.
-
Services¶
ModelForm¶
-
class
flask_unchained.bundles.sqlalchemy.forms.
ModelForm
(*args, **kwargs)[source]¶ Base class for SQLAlchemy model forms.
-
validate
()[source]¶ Validate the form by calling
validate
on each field. ReturnsTrue
if validation passes.If the form defines a
validate_<fieldname>
method, it is appended as an extra validator for the field’svalidate
.- Parameters
extra_validators – A dict mapping field names to lists of extra validator methods to run. Extra validators run after validators passed when creating the field. If the form has
validate_<fieldname>
, it is the last extra validator.
-
SQLAlchemy¶
Column¶
foreign_key¶
-
flask_unchained.bundles.sqlalchemy.sqla.
foreign_key
(*args, fk_col: Optional[str] = None, primary_key: bool = False, nullable: bool = False, ondelete: Optional[str] = None, onupdate: Optional[str] = None, **kwargs) → flask_unchained.bundles.sqlalchemy.sqla.column.Column[source]¶ Helper method to add a foreign key column to a model.
For example:
class Post(db.Model): category_id = db.foreign_key('Category') category = db.relationship('Category', back_populates='posts')
Is equivalent to:
class Post(db.Model): category_id = db.Column(db.BigInteger, db.ForeignKey('category.id'), nullable=False) category = db.relationship('Category', back_populates='posts')
Customizing all the things:
class Post(db.Model): _category_id = db.foreign_key('category_id', # db column name db.String, # db column type 'categories', # foreign table name fk_col='pk') # foreign key col name
Is equivalent to:
class Post(db.Model): _category_id = db.Column('category_id', db.String, db.ForeignKey('categories.pk'), nullable=False)
- Parameters
args –
foreign_key()
takes up to three positional arguments.
Most commonly, you will only pass one argument, which should be the model name, the model class, or table name you’re linking to. If you want to customize the column name the foreign key gets stored in the database under, then it must be the first string argument, and you must also supply the model name, class or table name. You can also customize the column type (eg
sa.Integer
orsa.String(36)
) by passing it as an arg.- Parameters
fk_col (str) – The column name of the primary key on the opposite side of the relationship (defaults to
sqlalchemy_unchained.ModelRegistry.default_primary_key_column
).primary_key (bool) – Whether or not this
Column
is a primary key.nullable (bool) – Whether or not this
Column
should be nullable.kwargs – Any other kwargs to pass the
Column
constructor.
events¶
-
flask_unchained.bundles.sqlalchemy.sqla.
on
(*args, **listen_kwargs)[source]¶ Class method decorator for SQLAlchemy models. Must be used in conjunction with the
attach_events()
class decoratorUsage:
@attach_events class Post(Model): uuid = Column(String(36)) post_tags = relationship('PostTag', back_populates='post') # m2m # instance event (only one positional argument, the event name) # kwargs are passed on to the sqlalchemy.event.listen function @on('init', once=True) def generate_uuid(self, args, kwargs): self.uuid = str(uuid.uuid4()) # attribute event (two positional args, field name and event name) @on('post_tags', 'append') def set_tag_order(self, post_tag, initiating_event): if not post_tag.order: post_tag.order = len(self.post_tags) + 1
-
flask_unchained.bundles.sqlalchemy.sqla.
attach_events
(*args)[source]¶ Class decorator for SQLAlchemy models to attach listeners for class methods decorated with
on()
Usage:
@attach_events class User(Model): email = Column(String(50)) @on('email', 'set') def lowercase_email(self, new_value, old_value, initiating_event): self.email = new_value.lower()
-
flask_unchained.bundles.sqlalchemy.sqla.
slugify
(field_name, slug_field_name=None, mutable=False)[source]¶ Class decorator to specify a field to slugify. Slugs are immutable by default unless mutable=True is passed.
Usage:
@slugify('title') def Post(Model): title = Column(String(100)) slug = Column(String(100)) # pass a second argument to specify the slug attribute field: @slugify('title', 'title_slug') def Post(Model) title = Column(String(100)) title_slug = Column(String(100)) # optionally set mutable to True for a slug that changes every time # the slugified field changes: @slugify('title', mutable=True) def Post(Model): title = Column(String(100)) slug = Column(String(100))
Webpack Bundle API¶
flask_unchained.bundles.webpack
The Webpack Bundle. |
flask_unchained.bundles.webpack.config
Default configuration options for the Webpack Bundle. |
|
Default production configuration options for the Webpack Bundle. |
|
Inherit production settings. |
flask_unchained.bundles.webpack.extensions
The Webpack extension. |
WebpackBundle¶
Config¶
-
class
flask_unchained.bundles.webpack.config.
Config
[source]¶ Default configuration options for the Webpack Bundle.
-
WEBPACK_MANIFEST_PATH
= None¶ The full path to the
manifest.json
file generated by Webpack Manifest Plugin.
-
Extensions¶
CHANGELOG¶
v0.9.0 (2021/06/07)¶
fix security bundle salt configuration for itsdangerous 2.0+
fix security bundle redirect vulnerability
add shell readline completion (from Flask PR 3960)
fix
BundleBlueprint.register
to work with Flask 2.0+fix compatibility with click 8.0+
bump required flask-sqlalchemy-unchained version
API bundle fixes and improvements
v0.8.1 (2021/01/17)¶
Features¶
upgrade Flask-Admin templates to bootstrap4
add Admin-specific post login/logout redirect endpoints
add default Model Admins for the User and Role models
Bug Fixes¶
fix default config settings for
ADMIN_LOGIN_ENDPOINT
andADMIN_LOGOUT_ENDPOINT
defer initialization of the Admin extension to fix template overriding
do not register duplicate templates folder for single-module app bundle
Breaking Changes¶
rename
User.active
toUser.is_active
for compatibility with Flask-Login v0.5
v0.8.0 (2020/12/20)¶
Features¶
major improvements to
AppFactory
andAppFactoryHook
support single-file app bundles (just export the app bundle as
UNCHAINED
)support using a custom subclass of
FlaskUnchained
usingAppFactory.APP_CLASS
support using a custom subclass of
AppFactory
support passing all kwargs to
Flask
by setting the same names upper-cased inunchained_config
support automatic defaults for the Flask app kwargs
root_path
,template_folder
,static_folder
, andstatic_url_path
support extending and overriding hooks with the same consistent object-oriented patterns
support using a custom module name for
unchained_config
by setting theUNCHAINED
environment variablemake it possible to define multiple modules hooks should load from (excluding config and routes, as those only make sense to live inside a single module within bundles)
very experimental: add
Bundle.default_load_from_module_name
to ease migration from single-file app bundles to individual modules for different types (ie grouped by base class)
set up automatic dependency injection on commands (use
from flask_unchained.cli import cli, click
and define command groups for your commands using@cli.group()
)add
flask unchained config
command for listing the current config (optionally filtered by bundle)add
flask unchained extensions
command for listing extensions discovered by the appadd
flask unchaiend services
command for listing services discovered by the appalias
flask.abort
(werkzeug.exceptions.abort
) asflask_unchained.abort
alias
flask_wtf.csrf.generate_csrf
asflask_unchained.generate_csrf
alias
flask.Request
andflask.Response
intoflask_unchained
support
Accept
headers for handling responses in the API bundleallow customizing the endpoint prefix for controllers using
Controller.Meta.endpoint_prefix
General Improvements¶
document the rest of SQLAlchemy’s config options
automatically discover services in the
services
andmanagers
modules of bundlesbump sqlalchemy-unchained to v0.11.0
add compatibility with pytest 5
Bug Fixes¶
fix grouping routes by which bundle they’re from
fix registration of resource method routes so the order is deterministic
fix
ConfigureAppHook
to load configs from every bundle in the hierarchy, not just the top-most onefix resolving extension initiation order to only happen once instead of twice
fix passing explicit rule overrides to
routes.resource
fix automatic endpoint names for resource routes using the default implementations for create/list/get/delete/patch/put
fix using default url rule from view function when no explicit rule passed to
func
fix
flask urls
command when no URLs foundmake sure hooks don’t resolve local proxies
fix
ModelResource.Meta.url_prefix
to useMeta.model.__name__
instead of the resource’s class nameallow using
AnonymousUser
as if it were a SQLAlchemy model in queries
Breaking Changes¶
rename
flask_unchained.BaseService
toflask_unchained.Service
rename
PROJECT_ROOT
toROOT_PATH
for consistency with upstreamFlask
rename
Bundle.folder
toBundle.root_path
for consistency withFlask
rename
Controller.Meta.template_folder_name
toController.Meta.template_folder
for consistency withFlask
AppFactory
is now aSingleton
that must be instantiated (ie changeAppFactory.create_app(env)
toAppFactory().create_app(env)
inwsgi.py
)no longer automatically set up dependency injection on all the methods from classes (you can still decorate them manually with
unchained.inject()
, but the preferred approach is to use class attributes to define what to inject into classes)default endpoint name for simple view functions is now just the function name
rename Resource method name constants to reduce confusion with HTTP method names
remove
AppBundleConfig.ROOT_PATH
andAppBundleConfig.APP_ROOT
as they didn’t always work correctly (useBundleConfig.current_app.root_path
instead)moved
flask_unchained.commands.utils.print_table
toflask_unchained.cli.print_table
if using the API bundle, require
marshmallow>=3.0
,marshmallow-sqlalchemy>=0.23
, andflask-marshmallow>=0.12
if using the SQLAlchemy bundle, require
sqlalchemy-unchained>=0.10
CSRF protection is no longer enabled by default. To re-enable it:
from flask_unchained import BundleConfig, unchained, generate_csrf
class Config(BundleConfig):
SECRET_KEY = 'some-secret-key'
WTF_CSRF_ENABLED = True
class TestConfig(Config):
WTF_CSRF_ENABLED = False
@unchained.after_request
def set_csrf_token_cookie(response):
if response:
response.set_cookie('csrf_token', generate_csrf())
return response
customizing bundle module locations changed:
class YourBundle(Bundle):
extensions_module_name = 'custom' # before
extensions_module_names = ['custom'] # after
services_module_name = 'custom' # before
services_module_names = ['custom'] # after
commands_module_name = 'custom' # before
commands_module_names = ['custom'] # after
blueprints_module_name = 'custom' # before
blueprints_module_names = ['custom'] # after
models_module_name = 'custom' # before
models_module_names = ['custom'] # after
admins_module_name = 'custom' # before
admins_module_names = ['custom'] # after
resources_module_name = 'custom' # before
model_resources_module_names = ['custom'] # after
serializers_module_name = 'custom' # before
model_serializers_module_names = ['custom'] # after
celery_tasks_module_name = 'custom' # before
celery_tasks_module_names = ['custom'] # after
graphene_queries_module_name = 'custom' # before
graphene_queries_module_names = ['custom'] # after
graphene_mutations_module_name = 'custom' # before
graphene_mutations_module_names = ['custom'] # after
graphene_types_module_name = 'custom' # before
graphene_types_module_names = ['custom'] # after
Internals¶
move
Bundle
andAppBundle
into theflask_unchained.bundles
modulemove
BundleBlueprint
into theflask_unchained.bundles.controller.bundle_blueprint
modulemove
_DeferredBundleFunctions
intoflask_unchained.unchained
, rename it toDeferredBundleFunctions
make a bunch more protected internal classes public
make
_has_views
,_blueprint_names
,_static_folders
,is_top_bundle
and_has_hierarchy_name_conflicts
methods onBundle
propertiesrename double-negative
reverse_mro
parameter forBundle._iter_class_hierarchy
tomro
warn when identical routes are registered
make
AppFactory
methodsload_bundles
,load_bundle
andis_bundle
classmethodsadd a noop
ViewsHook
to consolidate logic for defining and loadingviews_module_names
move
param_converter
into the controller bundle
v0.7.9 (2019/05/19)¶
compatibility with
sqlalchemy-unchained >= 0.7.6
v0.7.8 (2019/04/21)¶
bump required
alembic
version to 1.0.9, fixesimmutabledict is not defined
error
v0.7.7 (2019/04/11)¶
bump requirements
change behavior of
flask new project
command to use defaults unless--prompt
is givenNOTE: broken with sqlalchemy bundle, must install alembic using:
pip install git+https://github.com/sqlalchemy/alembic.git@d46de05b8b3281a85e6b107ef3f3407e232eb9e9#egg=alembic
v0.7.6 (2019/03/24)¶
NOTE: broken with sqlalchemy bundle, must install alembic using:
pip install git+https://github.com/sqlalchemy/alembic.git@d46de05b8b3281a85e6b107ef3f3407e232eb9e9#egg=alembic
v0.7.5 (2019/03/24)¶
NOTE: broken with sqlalchemy bundle, must install alembic using:
pip install git+https://github.com/sqlalchemy/alembic.git@d46de05b8b3281a85e6b107ef3f3407e232eb9e9#egg=alembic
v0.7.4 (2019/03/17)¶
support injecting current app config into services
extend the
string
url parameter converter to supportupper=True/False
add
ModelForm.make_instance
convenience methodfix
ModelForm.name
to returnbytes
add
.gitignore
toflask new project
commandimprove error message when no
config
module found in the app bundle
v0.7.3 (2019/02/26)¶
do not generate
celery_app.py
for new projects without the celery bundle enabledimprove user warnings when mail bundle is enabled but lxml or beautifulsoup isn’t installed
bump required versions of py-meta-utils and sqlalchemy-unchained
v0.7.2 (2019/02/25)¶
fix the project’s registered name on PyPI so it doesn’t contain spaces
v0.7.1 (2019/02/04)¶
Bug fixes¶
support multiple routing rules with the same endpoint per view function
fix type error in dependency injection when comparing parameter values with string
v0.7.0 (2019/01/30)¶
Features¶
- fire:OAuth:fire
support with the new OAuth Bundle (many thanks to @chriamue!)
- fire:GraphQL:fire
support with the new Graphene Bundle
add support for specifying parameters to inject into classes as class attributes
when using
unchained.inject()
on a class, or subclassing a class that supports automatic dependency injection, all non-dunderscore methods now support having dependencies injectedthe
include
function used inroutes.py
now supports specifying the url prefix as the first argumentsupport distributing and loading database fixture files with/from bundles
implement proper support for
ModelForm
(it now adds fields for columns by default)
Configuration Improvements¶
add a way for bundle configs to get access to the current app-under-construction
make options in
app.config
accessible as attributes, egapp.config.SECRET_KEY
is now the same asapp.config['SECRET_KEY']
apply any settings from the app bundle config not already present in
app.config
as defaults before loading bundles
General¶
improve documentation of how Flask Unchained works
update to py-meta-utils 0.7.4 and sqlalchemy-unchained 0.7.0
update to marshmallow 2.16
update to marshmallow-sqlalchemy 0.15
Breaking Changes¶
move database fixture loading code into the
py_yaml_fixtures
package (which is now a bundle as of v0.4.0)consolidate
unchained.get_extension_local_proxy
andunchained.get_service_local_proxy
into a single function,unchained.get_local_proxy
rename
AppConfig
toBundleConfig
rename the
SQLAlchemy
extension class toSQLAlchemyUnchained
rename
flask_unchained.bundles.sqlalchemy.model_form
toflask_unchained.bundles.sqlalchemy.forms
rename the Graphene Bundle’s
QueryObjectType
toQueriesObjectType
andMutationObjectType
toMutationsObjectType
rename the Security Bundle’s
SecurityUtilsService.verify_and_update_password
method toverify_password
(internal) descriptors, metaclasses, meta options, and meta option factories are now protected
(internal) rename the
flask_unchained.app_config
module toflask_unchained.config
(internal) remove the
Bundle.root_folder
descriptor as it made no sense (Bundle.folder
is the bundle package’s root folder)(internal) rename
ConfigPropertyMeta
toConfigPropertyMetaclass
Bug fixes¶
fix the
Api
extension so it only generates docs for model resources that are registered with the appfix setting of
Route._controller_cls
when controllers extend another concrete controller with routesfix
Bundle.static_url_path
descriptorspecify required minimum package versions in
setup.py
, and pin versions inrequirements.txt
fix the
UnchainedModelRegistry.reset
method so it allows using factory_boy fromconftest.py
fix the
flask celery
commands so that they gracefully terminate instead of leaving zombie processes runningfix
param_converter
to allowing converting models from optional query parametersadd support to graphene for working with SQLAlchemy BigInteger columns
v0.6.6 (2018/10/09)¶
ship
_templates
folder with the distribution so that theflask new <tempate>
command works when Flask Unchained gets installed viapip
v0.6.0 - v0.6.5 (2018/10/09)¶
IMPORTANT: these releases are broken, use v0.6.6
export
get_boolean_env
from coreflask_unchained
packageexport
param_converter
from coreflask_unchained
packagefix discovery of user-app
tests._unchained_config
improve the output of commands that display their information in a table
improve the output of custom sqlalchemy types in generated migrations files
improve the output of the Views column from the
flask urls
commandimprove the
--order-by
option of theflask urls
commandrename command
flask db import_fixtures
toflask db import-fixtures
add a
FlaskForm
base class extendingFlaskForm
that adds support for specifying the rendered field orderautomatically set the
csrf_token
cookie on responsesoverride the
click
module to also support documenting arguments withhelp
also make the default help options
-h
and--help
instead of just--help
refactor the hook store to be a class attribute of the bundle the hook(s) belong to
add an
env
attribute on theFlaskUnchained
app instancemake the
bundles
attribute on theUnchained
extension anAttrDict
bundles are now instantiated instead of passing the classes around directly
add default config options, making
DEBUG
andTESTING
unnecessary to set manuallyadd a
_name
attribute toFlaskForm
to automatically name forms when rendering them programmaticallyadd
get_extension_local_proxy
andget_service_local_proxy
methods to theUnchained
extensionadd support for overriding static files from bundles
minor refactor of the declarative routing for
Controller
andResource
classesconsolidate default route rule generation into the
Route
classmake it possible to override the
member_param
of aResource
with theresource
routes function
add a
TEMPLATE_FILE_EXTENSION
option toAppConfig
that controllers will respect by default. Controllers can still set theirtemplate_file_extension
attribute to override the application-wide default.implement missing
delete
routing functionpreliminary support for customizing the generated unique member param
fix setting of
Route._controller_cls
to automatically always happenrefactor the SQLAlchemy Bundle to split most of it out into its own package, so that it can be used on its own (without Flask).
fix the resource url prefix descriptor to convert to kebab-case instead of snake-case
rename
Controller.template_folder
toController.template_folder_name
add
Controller.make_response
as an alias forflask.make_response
convert attributes on
Controller
,Resource
, andModelResource
to beclass Meta
optionsrename
_meta
toMeta
per py-meta-utils v0.3rename
ModelManager.find_all
toModelManager.all
andModelManager.find_by
toModelManager.filter_by
for consistency with theQuery
apimove instantiation of the
CSRFProtect
extension from the security bundle into the controller bundle, where it belongs, so that it always gets usedimprove registration of request cycle functions meant to run only for a specific bundle blueprint
update
BaseService
to use a MetaOptionsFactorymake the
ModelManager.model
class attribute a meta optionrename the
flask db drop --drop
option toflask db drop --force
to skip promptingrename the
flask db reset --reset
option toflask db reset --force
to skip promptingadd
no_autoflush
toSessionManager
v0.5.1 (2018/07/25)¶
include html templates in the distribution
add bundles to the shell context
v0.5.0 (2018/07/25)¶
export
FlaskUnchained
from the root packageexport Flask’s
current_app
from the root packagenever register static assets routes with babel bundle
integrate the admin bundle into the
flask_unchained
packageintegrate the api bundle into the
flask_unchained
packageintegrate the celery bundle into the
flask_unchained
packageintegrate the mail bundle into the
flask_unchained
packageintegrate the sqlalchemy bundle into the
flask_unchained
packageintegrate the webpack bundle into the
flask_unchained
package
v0.4.2 (2018/07/21)¶
fix tests when babel_bundle isn’t loaded
v0.4.1 (2018/07/20)¶
fix infinite recursion error when registering urls and blueprints with babel
v0.4.0 (2018/07/20)¶
make
tests._unchained_config
optional ifunchained_config
existsfix discovery of bundle views to include any bundle in the hierarchy with views
subclass Flask to improve handling of adding blueprints and url rules in conjunction with the babel bundle
rename
unchained.BUNDLES
tounchained.bundles
v0.3.2 (2018/07/16)¶
fix naming of bundle static endpoints
v0.3.1 (2018/07/16)¶
support loading bundles as the app bundle for development
refactor the babel commands to work with both app and regular bundles
fix discovery of tests._unchained_config module
v0.3.0 (2018/07/14)¶
add
flask qtconsole
commandrename Bundle.iter_bundles to Bundle.iter_class_hierarchy
add
cli_runner
pytest fixture for testing click commandsfix register commands hook to support overriding groups and commands
support registering request hooks, template tags/filters/tests, and context processors via deferred decorators on the Unchained and Bundle classes
ship the controller bundle, session bundle, and babel bundles as part of core
the babel and controller bundles are now mandatory, and will be included automatically
v0.2.2 (2018/04/08)¶
bugfix: Bundle.static_url_prefix renamed to Bundle.static_url_path
v0.2.1 (2018/04/08)¶
bugfix: check for
FunctionType
inset_up_class_dependency_injection
v0.2.0 (2018/04/06)¶
rename
BaseConfig
toConfig
add utilities for dealing with optional dependencies:
OptionalClass
: generic base class that can also be used as a substitute for extensions that have base classes defined as attributes on themoptional_pytest_fixture
: allows to conditionally register test fixtures
hooks now declare their dependencies by hook name, as opposed to using an integer priority
v0.1.x¶
early releases