Slide 1

Slide 1 text

Advanced Flask Patterns PyCon Russia 2013 — a presentation by Armin Ronacher @mitsuhiko

Slide 2

Slide 2 text

-1 Who am I?

Slide 3

Slide 3 text

That's me ✤ Armin Ronacher ✤ @mitsuhiko ✤ Creator of Flask/Werkzeug/Jinja2

Slide 4

Slide 4 text

0 Focus & Caveats

Slide 5

Slide 5 text

Interrupt me ✤ Assumes some sense of Flask knowledge ✤ If too fast, interrupt me ✤ If not detailed enough, let me know

Slide 6

Slide 6 text

1 State Management

Slide 7

Slide 7 text

Flask States ✤ Setup State ✤ Application Context Bound ✤ Request Context Bound

Slide 8

Slide 8 text

Setup State >>> from flask import g >>> g.foo = 42 Traceback (most recent call last): File "", line 1, in RuntimeError: working outside of application context

Slide 9

Slide 9 text

Application Bound >>> ctx = app.app_context() >>> ctx.push() >>> g.foo = 42 >>> g.foo 42 >>> from flask import request >>> request.args Traceback (most recent call last): File "", line 1, in RuntimeError: working outside of request context

Slide 10

Slide 10 text

Request Bound >>> ctx = app.test_request_context() >>> ctx.push() >>> request.url 'http://localhost/' >>> g.foo = 42 >>> g.foo 42

Slide 11

Slide 11 text

Lifetimes ✤ flask.current_app ⇝ application context ✤ flask.g ⇝ application context (as of 0.10) ✤ flask.request ⇝ request context ✤ flask.session ⇝ request context

Slide 12

Slide 12 text

Quick Overview ✤ Application contexts are fast to create/destroy ✤ Pushing request context pushes new application context ✤ Flask 0.10 binds g to the application context ✤ Bind resources to the application context

Slide 13

Slide 13 text

2 Resource Management

Slide 14

Slide 14 text

Basic Guide ✤ Create/Destroy Application Context == Task ✤ Bind resources task wise ✤ Resources: claimed database connections, caches

Slide 15

Slide 15 text

Teardown Illustrated >>> from flask import Flask >>> app = Flask(__name__) >>> @app.teardown_appcontext ... def called_on_teardown(error=None): ... print 'Tearing down, error:', error ... >>> ctx = app.app_context() >>> ctx.push() >>> >>> ctx.pop() Tearing down, error: None >>> with app.app_context(): ... 1/0 ... Tearing down, error: integer division or modulo by zero Traceback (most recent call last): File "", line 2, in ZeroDivisionError: integer division or modulo by zero

Slide 16

Slide 16 text

Resource Management def get_database_connection(): con = getattr(g, 'database_connection', None) if con is None: g.con = con = connection_pool.get_connection() return con @app.teardown_appcontext def return_database_connection(error=None): con = getattr(g, 'database_connection', None) if con is not None: connection_pool.release_connection(con)

Slide 17

Slide 17 text

Responsive Resources @app.teardown_appcontext def return_database_connection(error=None): con = getattr(g, 'database_connection', None) if con is None: return if error is None: con.commit() else: con.rollback() connection_pool.release_connection(con)

Slide 18

Slide 18 text

Per-Task Callbacks def after_commit(f): callbacks = getattr(g, 'on_commit_callbacks', None) if callbacks is None: g.on_commit_callbacks = callbacks = [] callbacks.append(f) return f

Slide 19

Slide 19 text

Per-Task Callbacks @app.teardown_appcontext def return_database_connection(error=None): con = getattr(g, 'database_connection', None) if con is None: return if error is None: con.commit() callbacks = getattr(g, 'on_commit_callbacks', ()) for callback in callbacks: callback() else: con.rollback() connection_pool.release_connection(con)

Slide 20

Slide 20 text

Per-Task Callbacks Example def purchase_product(product, user): user.purchased_products.append(product) @after_commit def send_success_mail(): body = render_template('mails/product_purchased.txt', user=user, product=product ) send_mail(user.email_address, 'Product Purchased', body)

Slide 21

Slide 21 text

3 Response Creation

Slide 22

Slide 22 text

Response Object Passing ✤ One request object: read only ✤ Potentially many response objects, passed down a stack ✤ … can be implicitly created ✤ … can be replaced by other response objects ✤ there is no flask.response!

Slide 23

Slide 23 text

Implicit Response Creation @app.route('/') def index(): return render_template('index.html')

Slide 24

Slide 24 text

Explicit Creation from flask import make_response @app.route('/') def index(): body = render_template('index.html') response = make_response(body) response.headers['X-Powered-By'] = 'Not-PHP/1.0' return response

Slide 25

Slide 25 text

Customized Creation from flask import Flask, jsonify class CustomFlask(Flask): def make_response(self, rv): if hasattr(rv, 'to_json'): return jsonify(rv.to_json()) return Flask.make_response(self, rv)

Slide 26

Slide 26 text

Customized Creation Example class User(object): def __init__(self, id, username): self.id = id self.username = username def to_json(self): return { 'id': self.id, 'username': self.username } app = CustomFlask(__name__) @app.route('/') def index(): return User(42, 'john')

Slide 27

Slide 27 text

4 Server Sent Events

Slide 28

Slide 28 text

Basic Overview ✤ Open Socket ✤ Sends "data: \r\n\r\n" packets ✤ Good idea for gevent/eventlet, bad idea for kernel level concurrency

Slide 29

Slide 29 text

Subscribing from redis import Redis from flask import Response, stream_with_context redis = Redis() @app.route('/streams/interesting') def stream(): def generate(): pubsub = redis.pubsub() pubsub.subscribe('interesting-channel') for event in pubsub.listen(): if event['type'] == 'message': yield 'data: %s\r\n\r\n' % event['data'] return Response(stream_with_context(generate()), direct_passthrough=True, mimetype='text/event-stream')

Slide 30

Slide 30 text

Publishing from flask import json, redirect, url_for @app.route('/create-something', methods=['POST']) def create_something(): create_that_thing() redis.publish('interesting-channel', json.dumps({ 'event': 'created', 'kind': 'something' })) return redirect(url_for('index'))

Slide 31

Slide 31 text

Don't be Afraid of Proxying ✤ gunicorn/uwsgi blocking for main app ✤ gunicorn gevent for SSE ✤ nginx for unification

Slide 32

Slide 32 text

5 Worker Separation

Slide 33

Slide 33 text

supervisor config [program:worker-blocking] command=gunicorn -w 4 yourapplication:app -b 0.0.0.0:8000 [program:worker-nonblocking] command=gunicorn -k gevent -w 4 yourapplication:app -b 0.0.0.0:8001

Slide 34

Slide 34 text

nginx config server { listen 80; server_name example.com; location /streams { proxy_set_header Host $http_host; proxy_pass http://localhost:8001/streams; } location / { proxy_set_header Host $http_host; proxy_pass http://localhost:8000/; } }

Slide 35

Slide 35 text

6 Signing Stuff

Slide 36

Slide 36 text

Basic Overview ✤ Use itsdangerous for signing information that roundtrips ✤ Saves you from storing information in a database ✤ Especially useful for small pieces of information that need to stay around for long (any form of token etc.)

Slide 37

Slide 37 text

User Activation Example from flask import abort import itsdangerous serializer = itsdangerous .URLSafeSerializer(secret_key=app.config['SECRET_KEY']) ACTIVATION_SALT = '\x7f\xfb\xc2(;\r\xa8O\x16{' def get_activation_link(user): return url_for('activate', code=serializer.dumps(user.user_id, salt=ACTIVATION_SALT)) @app.route('/activate/') def activate(code): try: user_id = serializer.loads(code, salt=ACTIVATION_SALT) except itsdangerous.BadSignature: abort(404) activate_the_user_with_id(user_id)

Slide 38

Slide 38 text

7 Customization

Slide 39

Slide 39 text

Simple Cache Busting from hashlib import md5 import pkg_resources ASSET_REVISION = md5(str(pkg_resources.get_distribution( 'Package-Name').version)).hexdigest())[:14] @app.url_defaults def static_cache_buster(endpoint, values): if endpoint == 'static': values['_v'] = ASSET_REVISION

Slide 40

Slide 40 text

Disable Parsing from flask import Flask, Request class SimpleRequest(Request): want_form_data_parsed = False data = None app = Flask(__name__) app.request_class = SimpleRequest

Slide 41

Slide 41 text

8 Secure Redirects

Slide 42

Slide 42 text

Redirect Back from urlparse import urlparse, urljoin def is_safe_url(target): ref_url = urlparse(request.host_url) test_url = urlparse(urljoin(request.host_url, target)) return test_url.scheme in ('http', 'https') and \ ref_url.netloc == test_url.netloc def is_different_url(url): this_parts = urlparse(request.url) other_parts = urlparse(url) return this_parts[:4] != other_parts[:4] and \ url_decode(this_parts.query) != url_decode(other_parts.query) def redirect_back(fallback): next = request.args.get('next') or request.referrer if next and is_safe_url(next) and is_different_url(next): return redirect(next) return redirect(fallback)

Slide 43

Slide 43 text

Q&A http://fireteam.net/ — Armin Ronacher — @mitsuhiko