Introduction to Flask

Ned Jackson Lovely

njl@njl.us

@nedjl

Python Code Referenced In This Talk

Django

Whatever wasn't the Lawrence Journal‑World was Django

WSGI

Web Server Gateway Interface
PEP-333/PEP-3333
Werkzeug is a collection of tools for WSGI

Jinja2

Fast, Django-inspired Python Templates

Jinja2 + Werkzeug April Fools Joke

Flask

More "toolkit" than "framework"

Werkzeug + Jinja2 + Some Glue = Flask

Hello World

Trivial Hello World

#!/usr/bin/env python
from flask import Flask
app = Flask(__name__)

@app.route('/')
def splash():
    return '<h1>Hello World!</h1>'

if __name__ == '__main__':
    app.run(debug=True, port=4001)
    

Flask Object

    #!/usr/bin/env python
    from flask import Flask
    app = Flask(__name__)

    """
    .
    .
    .
    """

    if __name__ == '__main__':
            app.run(debug=True, port=4001)
    

Production

    gunicorn -w 4 -b 127.0.0.1:4000 hello_world_one:app
    

Binding Functions to URLs

    @app.route('/')
    def splash():
        return '

Hello World!

'

Variables in the Path

    @app.route('/post/<int:post_id>')
    def show_post(post_id):
        pass

    @app.route('/user/<username>')
    def show_user_profile(username):
        pass
    

Reverse with url_for()

        url_for('show_post', post_id=27)
        url_for('show_user_profile', username="Bob")
        url_for('splash')
    

Can Filter By Action

    @app.route('/login')
    def login_on_default_get():
        pass

    @app.route('/login', methods=['POST'])
    def login_on_post():
        pass
    

Can Return Different Things

  • str / unicode
  • (response, status, headers)
  • flask.Response / current_app.response_class
  • WSGI Function

JSON API Responses

    #from flask import jsonify
    from json import dumps
    from flask import current_app

    def jsonify(**kwargs):
        C = current_app.response_class
        return C(dumps(kwargs), mimetype='application/json')

#!/usr/bin/env python
from flask import Flask
app = Flask(__name__)

@app.route('/')
def splash():
    return '<h1>Hello World!</h1>'

if __name__ == '__main__':
    app.run(debug=True, port=4001)
    

Tests Let Me Sleep

from hello_world_one import app

def test_splash_page():
    client = app.test_client()
    response = client.get('/')
    assert response.status_code == 200
    assert 'Hello' in response.data
    
#!/usr/bin/env python
from flask import Flask
app = Flask(__name__)

def bad_function():
    x = 27
    raise Exception("Error over here")

@app.route('/whoops')
def whoops():
    bad_function()
    return 'Hooray!'

@app.route('/')
def splash():
    return '<a href="/whoops">here</a>'

if __name__ == '__main__':
    app.run(debug=True, port=4005)
    

Templating

The Template

<!DOCTYPE html>
<html>
    <head><title>Hello!</title></head>
    <body>
    {% if user %}
    <h1>Hello, {{user|replace("e", "3")}}!</h1>
    {% else %}
        <form>
            <input name="user" placeholder="Your Name">
        </form>
    {% endif %}
    </body>
</html>
    
    > ls -l
    -rwxr-xr-x  1 n  staff   197 Mar 23 19:40 hello_world_two.py
    drwxr-xr-x  2 n  staff    68 Mar 24 18:51 static
    drwxr-xr-x  5 n  staff   170 Mar 23 19:55 templates

    

Template Hello World

#!/usr/bin/env python
from flask import Flask, render_template, request
app = Flask(__name__)

@app.route('/')
def splash():
    user = request.values.get('user')
    return render_template('hello.html', user=user)


if __name__ == '__main__':
    app.run(debug=True, port=4002)
    

Some Tests

from hello_world_two import app

def test_splash_page():
    client = app.test_client()
    response = client.get('/')
    assert response.status_code == 200
    assert 'form' in response.data

def test_name_submission():
    client = app.test_client()
    for name in [u'Ned', u"\u2603"]:
        response = client.get(u'/?user={}'.format(name))
        assert response.status_code == 200
        data = response.data.decode('utf8')
        assert u'Hello,' in data
        assert name.replace('e', '3') in data
        assert 'form' not in response.data
    

Thread-Local Proxies

    from flask import request, session
    from flask import g, current_app
    
    request.form    #Form values
    request.args    #Query string
    request.values  #Combined
    request.data    #Raw form data
    request.files
    request.headers
    request.path    #(url, script_root, base_url, url_root)
    request.get_json()
    "...."
    
    app.secret_key = 'SOMETHING LONG AND RANDOM'

    session['key'] = value

    #tricky:
    session['thing']['other_thing'] = 'new value'
    session.modified = True

    session.new
    session.modified
    session.permanent
    
    @app.before_request
    def decorate_request():
        uid = session.get('user_id')
        if uid:
            g.user = some_function(uid)
        else:
            g.user = None
    

"Flash" Hello World

#!/usr/bin/env python
from flask import (Flask, render_template, request,
                    flash, url_for, redirect)
app = Flask(__name__)
app.secret_key = "Something unique and secret"

@app.route('/')
def splash():
    return render_template('flash.html')

@app.route('/', methods=['POST'])
def add_name():
    user = request.form.get('user', '')
    flash('Hello, {}!'.format(user))
    return redirect(url_for('splash'))

if __name__ == '__main__':
    app.run(debug=True, port=4003)
    
<!DOCTYPE html>
<html>
    <head><title>Hello!</title></head>
    <body>
    {% with messages = get_flashed_messages() %}
    {% if messages %}
        {% for msg in messages %}
        <h1>{{msg}}</h1>
        {% endfor %}
    {% endif %}
    {% endwith %}

    <form method="POST">
        <input name="user" placeholder="Your Name">
    </form>
    </body>
</html>
    

Wall Application

Some Logic

import redis, json

r = redis.Redis()

MAX_LENGTH = 20
KEY = 'MESSAGES'

def add_message(user, msg):
    r.lpush(KEY, json.dumps(dict(user=user, msg=msg)))
    r.ltrim(KEY, 0, MAX_LENGTH-1)

def get_messages():
    rv = [json.loads(x) for x in r.lrange(KEY, 0, -1)]
    return rv
    
app = Flask(__name__)
app.secret_key = 'SOMETHING LONG AND RANDOM'

@app.route('/')
def splash():
    msgs = logic.get_messages()
    return render_template('home.html', msgs=msgs)

@app.route('/login', methods=['POST'])
def login():
    session['user'] = request.values.get('user', 'A. Non')
    return redirect(url_for('splash'))

@app.route('/add_post', methods=['POST'])
def add_post():
    msg = request.values.get('msg', "")
    logic.add_message(session['user'], msg)
    return redirect(url_for('splash'))

    
<!DOCTYPE html>
<html><head></head>
    <body>
        <ul>
        {% for msg in msgs %}
            <li><strong>{{msg.user}}</strong>: {{msg.msg}}</li>
        {% endfor %}
        </ul>
        {% if session.user %}
        <form method="POST" action="{{url_for('add_post')}}">
            <input name="msg" placeholder="Your Message">
        </form>
        {% else %}
        <form method="POST" action="{{url_for('login')}}">
            <input name="user" placeholder="Your Name">
        </form>
        {% endif %}
    </body>
</html>
    
import board

def test_login():
    with board.app.test_client() as c:
        r = c.get('/')
        assert "Your Name" in r.data
        assert "/login" in r.data

        r = c.post('/login', data=dict(user='Bob'),
                    follow_redirects=True)
        assert "Your Message" in r.data
        assert "/login" not in r.data
        assert "/add_post" in r.data

        r = c.post('/add_post', 
                    data=dict(msg='Hello World!'),
                    follow_redirects=True)
        assert 'Bob' in r.data
        assert 'Hello World!' in r.data
        assert '/add_post' in r.data
    

WTForms

    class PastebinEdit(Form):
        language = SelectField(u'Programming Language',
                               choices=PASTEBIN_LANGUAGES)
        code = TextAreaField()

    

SQLAlchemy

    class User(Base):
        __tablename__ = 'users'

        id = Column(Integer, primary_key=True)
        name = Column(String)
        fullname = Column(String)
        password = Column(String)

        def __repr__(self):
            return "<User(name='%s', fullname='%s', password='%s')>" % (
                        self.name, self.fullname, self.password)

    

Configuration

    app = Flask(__name__)
    app.config.from_object('yourapplication.default_settings')
    app.config.from_envvar('YOURAPPLICATION_SETTINGS')
    

Configuration

    $ export YOURAPPLICATION_SETTINGS=/path/to/settings.cfg
    $ ./run-app.py
      * Running on http://127.0.0.1:5000/
      * Restarting with reloader...
    

Getting Bigger

from flask import Blueprint, redirect, url_for
from redis import Redis

bp = Blueprint('counter', __name__)
r = Redis()
KEY = 'counter'

@bp.route('/')
def show_counter():
    rv = '''<h1>{}</h1><a href="up">Up</a>
            <br><a href="down">Down</a>'''
    return rv.format(r.get(KEY))

@bp.route('/up')
def counter_up():
    r.incr(KEY)
    return redirect(url_for('.show_counter'))

@bp.route('/down')
def counter_down():
    r.incr(KEY, -1)
    return redirect(url_for('.show_counter'))
    
#!/usr/bin/env python
from flask import Flask, url_for
from counter import bp

app = Flask(__name__)

app.register_blueprint(bp, url_prefix='/counter')

@app.route('/')
def splash():
    url = url_for('counter.show_counter')
    return 'Good stuff <a href="{}">here</a>'.format(url)

if __name__ == '__main__':
    app.run(debug=True, port=4005)
    

API

import heli
from flask import Flask, request, jsonify

app = Flask(__name__)
h = heli.Heli()

_ARGS = ('yaw', 'pitch', 'throttle', 'trim',)

@app.route('/api', methods=['POST'])
def api():
    for k,v in request.values.items():
        if k not in _ARGS:
            continue
        setattr(h, k, int(v))
    h.send()
    return jsonify(**{k:getattr(h,k) for k in _ARGS})

Toolkit vs. Framework

Thank You!

Ned Jackson Lovely

njl@njl.us

@nedjl