Flask
Table of Contents
1 WSGI
Web Server Gateway Interface
def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/html')]) return '<h1>Hello, world!</h1>'
- environ: [dict] http request
- start_response: a function to send response
- return body(html)
2 Context
| Variable name | Context | Description |
|---|---|---|
| current_app | Application context | The application instance for the active application |
| g | Application context | An object can use for temporary storage. reset with each request |
| request | Request context | The request object, which encapsulates a http request |
| session | Request context | The user session, a dictionary storage shared by requests within session |
3 Request hooks
hook decorators
- before_first_request: register a function to run before the first request
- before_request: register a function to run before each request
- after_request: register a function to run after each request if no unhandled exceptions occurred
- teardown_request: register a function to run after each request even if unhandled exceptions occurred
4 Response
4.1 tuple
- Form: (html, status_code, headerdict)
4.2 response object
from flask import make_response @app.route('/') def index(): response = make_response('<h1>This document carries a cookie!</h1>') response.set_cookie('answer', '42') return response
4.3 redirect
return redirect('http://www.example.com')
4.4 abort
used for error handling
- abort does not return control back to the function that calls it but gives control back to the web server by raising an exception.
abort(404)
5 Jinja2 template engine
5.1 Variable
- Form:
{{ name }} - Jinja2 recognizes variables of any typ
<p>A value from a dictionary: {{ mydict['key'] }}.</p>
<p>A value from a list: {{ mylist[3] }}.</p>
<p>A value from a list, with a variable index: {{ mylist[myintvar] }}.</p>
<p>A value from an object's method: {{ myobj.somemethod() }}.</p>
5.2 Filters
| Filter name | Description |
|---|---|
| safe | Renders the value without applying escaping |
| capitalize | Converts the first char to uppercase |
| lower | |
| upper | |
| title | |
| trim | Removes leading and trailing whitespace |
| striptags | Removes any HTML tags |
<h1>Hello, {{ name|capitalize }}!</h1>
5.2.1 safe
- Never use the safe filter on values that aren’t trusted, such as text entered by users on web forms.
5.3 Control structures
5.3.1 if
{% if user %}
Hello, {{ user }}!
{% else %}
Hello, Stranger!
{% endif %}
5.3.2 for loop
<ul>
{% for comment in comments %}
<li>{{ comment }}</li>
{% endfor %}
</ul>
5.3.3 macro
macro are similar to functions in Python code
{% macro render_comment(comment) %}
<li>{{ comment }}</li>
{% endmacro %}
<ul>
{% for comment in comments %}
{{ render_comment(comment) }}
{% endfor %}
</ul>
import macro from standalone macro file
{% import 'macros.html' as macros %}
<ul>
{% for comment in comments %}
{{ macros.render_comment(comment) }}
{% endfor %}
</ul>
5.3.4 include common file
{% include 'common.html' %}
5.4 Inheritance
Block tags define elements that a derived template can change.
5.4.1 base template
<html>
<head>
{% block head %}
<title>{% block title %}{% endblock %} - My Application</title>
{% endblock %}
</head>
<body>
{% block body %}
{% endblock %}
</body>
</html>
5.4.2 extendsion template
If the application needs to add its own content to a block that already has some content, then Jinja2’s super() function must be used.
{% extends "base.html" %}
{% block title %}Index{% endblock %}
{% block head %}
{{ super() }} <!--super() to retain the original contents -->
<style>
</style>
{% endblock %}
{% block body %}
<h1>Hello, World!</h1>
{% endblock %}
5.5 Custom Error Pages
@app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 @app.errorhandler(500) def internal_server_error(e): return render_template('500.html'), 500
5.6 Links
url_for() function to generate dynamic URLs from the imformation stored in the app's URL map.
url_for('user', name='john', _external=True) # return http://localhost:5000/user/john
- arg1: view function name
- args: view function args
_external: return an absolute URL
5.7 Static files
Flask looks for static files in a subdirectory called static located in the application’s root folder.
url_for('static', filename='css/styles.css', _external=True)
6 Web forms
Form data from clients is in request.form (POST)
6.1 flask_wtf
flask_wtf wraps the WTForms packages, handles two things:
- generate HTML code for forms
- validate the submitted form data
6.1.1 CSRF protection
flask_wtf uses token to verify the authenticity of requests with form data
app = Flask(__name__) app.config['SECRET_KEY'] = 'hard to guess string'
- the SECRET_KEY configuration is aslo used by Flask and other third-party extendsions
- For added security, the secret key should be stored in an environment variable
instead of being embedded in the code.
6.1.2 Form class
from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import Required class NameForm(Form): name = StringField('What is your name?', validators=[Required()]) submit = SubmitField('Submit')
- the StringField class represents an <input> element with a type="text" attribute
- the SubmitField class represents an <input> element with a type="submit" attribute
Fields
- Text field: StringField, TextAreaField, PasswordField, HiddenField, DateField, DateTimeField,
IntegerField, DecimalField, FloatField
- BooleanField: Checkbox with True and False values
- RadioField: List of radio buttons
- SelectField: Drop-down list of choices
- SelectMultipleField: Drop-down list of choices with multiple selection
- FileField: File upload field
- SubmitField: Form submission button
- FormField: Embed a form as a field in a container form
- FieldList: List of fields of a given type
Validators
Email, IPAddress, Length, NumberRange, Optional, Required, Regexp, URL, AnyOf, NoneOf
- EqualTo: useful when requesting a password to be entered twice for confirmation
Template
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
View function
@app.route('/', methods=['GET', 'POST']) def index(): name = None form = NameForm() if form.validate_on_submit(): name = form.name.data form.name.data = '' return render_template('index.html', form=form, name=name)
- methods argument register the view function as a handler for GET and POST requests, default GET only.
- validate_on_submit is True when the form was submitted and the data has been accepted by all the field validators
6.2 redirect issue
Browsers repeat the last request they have sent when they are asked to refresh the page. When the last request sent is a POST request with form data, a refresh would cause a duplicate form submission.
- Good practice: never leave a POST request as a last request sent by the browser. Respond to POST requests with a redirect instead of a normal response.
- The trick is known as the Post/Redirect/Get pattern.
6.3 Sessions
from flask import Flask, render_template, session, redirect, url_for @app.route('/', methods=['GET', 'POST']) def index(): form = NameForm() if form.validate_on_submit(): session['name'] = form.name.data return redirect(url_for('index')) return render_template('index.html', form=form, name=session.get('name'))
6.4 flash
- To give the user a confirmation message after a request is completed
from flask import flash old_name = session.get('name') if old_name is not None and old_name != form.name.data: flash('Looks like you have changed your name!')
6.4.1 render messages
The best place to render flashed messages is the base template.
use get_flashed_messages() to retrieve the messages and render them
{% for message in get_flashed_messages() %} <div class="alert alert-warning"> <button type="button" class="close" data-dismiss="alert">×</button> {{ message }} </div> {% endfor %}
7 Databases
7.1 ORMs/ODMs
- SQLAlchemy
- MongoEngine
7.2 flask_sqlalchemy
from flask.ext.sqlalchemy import SQLAlchemy basedir = os.path.abspath(os.path.dirname(__file__)) app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] =\ 'sqlite:///' + os.path.join(basedir, 'data.sqlite') app.config['SQLALCHEMY_COMMIT_ON_TEARDOWN'] = True db = SQLAlchemy(app)
7.2.1 Model
class Role(db.Model): __tablename__ = 'roles' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True) users = db.relationship('User', backref='role', laze='dynamic') def __repr__(self): return '<Role %r>' % self.name class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True, index=True) role_id = db.Column(db.Integer, db.ForeignKey('roles.id')) def __repr__(self): return '<User %r>' % self.username
7.2.2 Relationship
SQLAlchemy relationship options: backref, primaryjoin, lazy, uselist, order_by, secondary, secondaryjoin
7.2.3 Ops
Creating
db = SQLAlchemy(app) db.drop_all() db.create_all()
Inserting
admin_role = Role(name='Admin') user_john = User(username='john', role=admin_role) db.session.add(admin_role) db.session.add(user_john)
or
db.session.add_all([admin_role, user_john])
commit
db.session.commit()check
print(admin_role.id)
- db.session.rollback()
Modifying
admin_role.name = 'Administrator' db.session.add(admin_role) db.session.commit()
Deleting
db.session.delete(mod_role) db.session.commit()
Querying
Role.query.all() User.query.filter_by(role=admin_role).all() str(User.query.filter_by(role=user_role)) # check SQL query
7.2.4 Integration with the Python Shell
from flask_script import Shell from flask.ext.script import Manager manager = Manager(app) def make_shell_context(): return dict(app=app, db=db, User=User, Role=Role) manager.add_command("shell", Shell(make_context=make_shell_context))
7.2.5 Database Migrations
use flask-migrate
from flask_migrate import Migrate, MigrateCommand migrate = Migrate(app, db) manager.add_command('db', MigrateCommand)
python main.py db init
python main.py db migrate -m "initial migration"
python main.py db upgrade
8 Application Structure
├── __init__.py
├── main/
├── __init__.py
├── errors.py
├── forms.py
└── views.py
├── static/
├── templates/
├── email.py
├── models.py
├── migrations/
├── venv/
├── tests/
├── requirements.txt
├── config.py
└── manage.py launches the application and other application tasks.
8.1 Configuration Example
import os basedir = os.path.abspath(os.path.dirname(__file__)) class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string' SQLALCHEMY_COMMIT_ON_TEARDOWN = True FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]' FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>' FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN') @staticmethod def init_app(app): pass class DevelopmentConfig(Config): DEBUG = True MAIL_SERVER = 'smtp.googlemail.com' MAIL_PORT = 587 MAIL_USE_TLS = True MAIL_USERNAME = os.environ.get('MAIL_USERNAME') MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \ 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite') class TestingConfig(Config): TESTING = True SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \ 'sqlite:///' + os.path.join(basedir, 'data-test.sqlite') class ProductionConfig(Config): SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 'sqlite:///' + os.path.join(basedir, 'data.sqlite') config = { 'development': DevelopmentConfig, 'testing': TestingConfig, 'production': ProductionConfig, 'default': DevelopmentConfig }
8.2 Dynamic App Creation
- app/__init__.py
from flask import Flask, render_template from flask_bootstrap import Bootstrap from flask_mail import Mail from flask_moment import Moment from flask_sqlalchemy import SQLAlchemy from config import config bootstrap = Bootstrap() mail = Mail() moment = Moment() db = SQLAlchemy() def create_app(config_name): app = Flask(__name__) app.config.from_object(config[config_name]) config[config_name].init_app(app) bootstrap.init_app(app) mail.init_app(app) moment.init_app(app) db.init_app(app) from main import main as main_blueprint # see below app.register_blueprint(main_blueprint) return app
Blueprint
A blueprint is similar to an application in that it can also define routes
- app/main/__init__.py
from flask import Blueprint main = Blueprint('main', __name__) from . import views, errors
- app/main/error.py
from flask import render_template from . import main @main.app_errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 @main.app_errorhandler(500) def internal_server_error(e): return render_template('500.html'), 500
8.3 Launch Script
app/manage.py
#!/usr/bin/env python import os from app import create_app, db from app.models import User, Role from flask_script import Manager, Shell from flask_migrate import Migrate, MigrateCommand app = create_app(os.getenv('FLASK_CONFIG') or 'default') manager = Manager(app) migrate = Migrate(app, db) def make_shell_context(): return dict(app=app, db=db, User=User, Role=Role) manager.add_command("shell", Shell(make_context=make_shell_context)) manager.add_command('db', MigrateCommand) if __name__ == '__main__': manager.run()
add test command
@manager.command def test(): """Run the unit tests.""" import unittest tests = unittest.TestLoader().discover('tests') unittest.TextTestRunner(verbosity=2).run(tests)
the function’s docstring is displayed in the help messages
8.4 Project Generator
8.4.1 cookiecutter
- cookiecutter-flask
- cookiecutter-flask-restful
9 RESTful API
9.1 characteristics
9.1.1 Stateless
A client request must contain all the information that is necessary to carry it out. The server must not store any state about the client that persists from one request to the next.
9.1.2 Cache
Responses from the server can be labeled as cacheable or noncacheable so that clients (or intermediaries between clients and servers) can use a cache for optimization purposes.
9.1.3 Uniform Interface
often HTTP, HTTPs
9.1.4 Layered System
Proxy servers, caches, or gateways can be inserted between clients and servers as necessary to improve performance, reliability, and scalability.
9.1.5 Code-on-Demand
Clients can optionally download code from the server to execute in their context.
9.2 Request Methods
| Request method | Target | Description | HTTP status code |
|---|---|---|---|
| GET | Individual resource URL | Obtain the resource | 200 |
| GET | Resource collection URL | Obtain the collection of resources | 200 |
| POST | Resource collection URL | Create a new resource and add it to the collection | 201 |
| PUT | Individual resource URL | Create/Modify an existing resource | 200 |
| DELETE | Individual resource URL | Delete a resource | 200 |
| DELETE | Resource collection URL | Delete all resources in the collection | 200 |
9.3 URL
/api/v1.0/[collection]/[individual]
9.4 Error Handling
- 200: OK
- 201: Created
- 400: Bad request
- 403: Forbidden
- 404: Not found
- 405: Method not allowed
- 500: internal server error
9.4.1 rehandle 404, 500
404/500 are generated by Flask on its own and will usually return an HTML response
@main.app_errorhandler(404) def page_not_found(e): if request.accept_mimetypes.accept_json and \ not request.accept_mimetypes.accept_html: response = jsonify({'error': 'not found'}) response.status_code = 404 return response return render_template('404.html'), 404
implement rest of errors
# app/api/errors.py: API error handler for status code 403 def forbidden(message): response = jsonify({'error': 'forbidden', 'message': message}) response.status_code = 403 return response
9.5 Authentication
9.5.1 flask-httpauth Usage
from flask_httpauth import HTTPBasicAuth auth = HTTPBasicAuth() @auth.verify_password def verify_password(email, password): if email == '': g.current_user = AnonymousUser() return True user = User.query.filter_by(email = email).first() if not user: return False g.current_user = user return user.verify_password(password)
- protect a route with the auth.login_required decorator
9.5.2 Token-based Authentication
class User(db.Model): # ... def generate_auth_token(self, expiration): s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration) return s.dumps({'id': self.id}) @staticmethod def verify_auth_token(token): s = Serializer(current_app.config['SECRET_KEY']) try: data = s.loads(token) except: return None return User.query.get(data['id'])
Verification
- app/api_1_0/authentication.py
@auth.verify_password def verify_password(email_or_token, password): if email_or_token == '': g.current_user = AnonymousUser() return True if password == '': g.current_user = User.verify_auth_token(email_or_token) g.token_used = True return g.current_user is not None user = User.query.filter_by(email=email_or_token).first() if not user: return False g.current_user = user g.token_used = False return user.verify_password(password) @api.route('/token') def get_token(): if g.current_user.is_anonymous() or g.token_used: return unauthorized('Invalid credentials') return jsonify({'token': g.current_user.generate_auth_token( expiration=3600), 'expiration': 3600})
9.5.3 Serializing
Model should implement to_json method and @staticmethod from_json
9.5.4 Pagination of Large Resource
Example:
@api.route('/posts/') def get_posts(): page = request.args.get('page', 1, type=int) pagination = Post.query.paginate( page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False) posts = pagination.items prev = None if pagination.has_prev: prev = url_for('api.get_posts', page=page-1, _external=True) next = None if pagination.has_next: next = url_for('api.get_posts', page=page+1, _external=True) return jsonify({ 'posts': [post.to_json() for post in posts], 'prev': prev, 'next': next, 'count': pagination.total })
9.5.5 Use httpie to test API
http --json --auth : GET http://127.0.0.1:5000/api/v1.0/posts/
10 Extensions
10.1 flask_script
Command-Line Options
10.2 flask_bootstrap
base template blocks
- doc, html_attribs, html, head, title, metas, styles, body_attribs, body, navbar, content, scripts
10.3 flask_moment
- Localization of dates and times
client-side moment.js do the localization. flask_moment module integrates moment.js into Jinja2 templates.
10.4 flask_mail
from flask_mail import Mail mail = Mail(app) msg = Message('test subject', sender='you@example.com', recipients=['you@example.com']) msg.body = 'text body' msg.html = '<b>HTML</b> body' with app.app_context(): # send() uses current_app, so it needs to be executed with an activated application context. mail.send(msg)
10.4.1 Simplify Mail Sending
from flask_mail import Message app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]' #application-specific configuration app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@example.com>' # application-specific configuration def send_email(to, subject, template, **kwargs): msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject, sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to]) msg.body = render_template(template + '.txt', **kwargs) # content can be rendered! msg.html = render_template(template + '.html', **kwargs) mail.send(msg)
10.4.2 Configuration
MAIL_HOSTNAME, MAIL_PORT, MAIL_USE_TLS, MAIL_USE_SSL, MAIL_USERNAME, MAIL_PASSWORD
10.4.3 Asynchronous Sending
thr = Thread(target=send_async_email, args=[app, msg]) thr.start()