AN INTRODUCTION TO TORNADO

Gavin M. Roy CTO myYearbook.com

pyCon 2011 Atlanta, GA

TORNADO AT MYYEARBOOK.COM
• Currency

Connect

• Marketing Website, Portal, RESTful API • Redirect • Nerve • Staplr • Image

Engine

2 Upload Service

WHAT IS TORNADO?
•A

scalable, non-blocking web server and micro-framework in Python 2.5 & 2.6.
• Python

3 port underway at FriendFeed and open-sourced by Facebook

• Developed • Similar

to web.py in use requests/sec backend*

• Fast: ~1,500
* Your milage will vary

FEATURES
• • • • • •

Small barrier to entry to quickly developing applications Third Party Authentication via OpenID, OAuth Mixins Light-weight template system Auto-magical cross-site forgery protection WSGI && Google App Engine Support Develop using debug mode and automatically reload code and templates when changed on disk

class Application(object):     """A collection of request handlers that make up a web application. Instances of this class are callable and can be passed directly to HTTPServer to serve the application: application = web.Application([ (r"/", MainPageHandler), ]) http_server = httpserver.HTTPServer(application) http_server.listen(8080) ioloop.IOLoop.instance().start() The constructor for this class takes in a list of URLSpec objects or (regexp, request_class) tuples. When we receive requests, we iterate over the list in order and instantiate an instance of the first request class whose regexp matches the request path. Each tuple can contain an optional third element, which should be a dictionary if it is present. That dictionary is passed as keyword arguments to the contructor of the handler. This pattern is used for the StaticFileHandler below: application = web.Application([ (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}), ])

CLEAN, WELL DOCUMENTED CODE

WHAT TORNADO ISN’T
•A

full stack framework like Django on Twisted is an unmaintained port, Tornado on Twisted the Cyclone project

• Based

• There

• Influenced •A

replacement for a front-end web server behind a reverse proxy http server (nginx, Cherokee)

• Run

TORNADO VS TWISTED
• •

Tornado doesn’t have to be asynchronous It doesn’t have as many asynchronous drivers

Can introduce blocking behaviors

• • •

The Tornado code is smaller and very easy to understand Less mature than Twisted You don’t need to buy into a development methodology

Write Python not Twisted

KEY MODULES
Take only what you need

TORNADO.WEB
• Most

development is focused around this module classes used in a web application decorators function: @tornado.web.asynchronous Required: @tornado.web.authenticated

• Multiple • Includes

• Asynchronous • Authentication

TORNADO APPLICATION
• tornado.web.Application: Main • Canonical Tornado

controller class

Hello World:

import tornado.httpserver import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def get(self): self.write("Hello, world") if __name__ == "__main__": application = tornado.web.Application([ (r"/", MainHandler), ]) http_server = tornado.httpserver.HTTPServer(application) http_server.listen(8888) tornado.ioloop.IOLoop.instance().start()

REQUEST HANDLERS
• tornado.web.RequestHandler • Extend

RequestHandler for larger web apps Handling Connections

• Session

• Database, Cache • Localization • Implement

for your Application

REQUEST HANDLERS
• Classes

implementing define functions for processing

• get, head, post, delete, put, options • Hooks

on Initialization, Prepare, Close for setting HTTP Status, Headers, Cookies, Redirects

• Functions

and more

REQUEST HANDLER EXAMPLE
import redis import tornado.web class MyRequestHandler(tornado.web.RequestHandler): def initialize(self): host = self.application.settings['Redis']['host'] port = self.application.settings['Redis']['port'] self.redis = redis.Redis(host, port) class Homepage(MyRequestHandler): @tornado.web.asynchronous def get(self): content = self.redis.get('homepage') self.write(content) self.finish()

TORNADO.TEMPLATE
• Not

required to other engines python exposure in template

• Similar

• Limited

• Fast, extensible • Built-in • Adds

support in the RequestHandler class

cache busting static content delivery

REQUESTHANDLER.RENDER
Code
class Home(RequestHandler): def get(self): self.render('home.html', username='Leeroy Jenkins');

Template

<html> <body> Hi {{username}}, welcome to our site. </body> </html>

BASE TEMPLATE
<html> <head> <title>My Site :: {% block title %}Unextended Template{% end %}</title> <link rel="stylesheet" type="text/css" href="{{ static_url('css/site.css') }}" /> <script type="text/javascript" src="{{ static_url('javascript/site.js') }}"></script> {% if not current_user %} <script type="text/javascript" src="http://api.recaptcha.net/js/recaptcha_ajax.js"> </script> {% end %} </head> <body{% if current_user %} class="authenticated"{% end %}> {% include "header.html" %} {% if request.uri not in ['/', ''] and current_user %} {{ modules.MemberBar() }} {% end %} <div id="content"> {% block content %} No Content Specified {% end %} </div> <ul id="footer"> <li><a href="/terms">{{_("Terms and Conditions")}}</a></li> <li class="center">{{_("Version")}}: {{ handler.application.settings['version'] }}</li> <li class="right">{{_("Copyright")}} &copy; {{ datetime.date.today().year }}</li> </ul> </body> </html>

CONTENT TEMPLATE

{% extends "base.html" %} {% block title %}{{_("Error Title")}}{% end %} {% block content %} <h1>{{_("Error Title")}}</h1> <img src="/static/images/sad_robot.png" class="error_robot" /> <p class="error_message">{{_("Error Message")}}</p> <h2 class="error">{{status_code}} - {{exception}}</h2> {% end %}

TEMPLATE XSRF EXAMPLE

<form action="/login" method="post"> {{ xsrf_form_html() }} <div>Username: <input type="text" name="username"/></div> <div>Password: <input type="password" name="password"/></div> <div><input type="submit" value="Sign in"/></div> </form>

No additional work required.

UI MODULES
• Extend

templates with reusable widgets across the site import assigned when Application is created to RequestHandler in behavior

• One

• Similar

UIMODULE EXAMPLE

UIMODULE EXAMPLE
Embed
 <div>{{ modules.HTTPSCheck() }}</div> class HTTPSCheck(tornado.web.UIModule):

UIModule Class

def render(self): if 'X-Forwarded-Ssl' not in self.request.headers or \ self.request.headers['X-Forwarded-Ssl'] != 'on': return self.render_string("modules/ssl.html") return ''

<div class="information"> <a href="https://{{request.host}}{{request.uri}}"> {{_("Click here to use a secure connection")}} </a> </div>

Template

TORNADO.LOCALE

Locale files in one directory
• •

In a csv format Named as locale.csv, e.g. en_US.csv

tornado.locale.load_translations (path)

Pass path where files are located

Invoked as _ method in templates

ADDING LOCALIZATION
import tornado.locale as locale import tornado.web class RequestHandler(tornado.web.RequestHandler): def get_user_locale(self): # Fake user object has a get_locale() function user_locale = self.user.get_locale() # If our locale is supported return it if user_locale in locale.get_supported_locales(None): return user_locale # Defaults to Accept-Language header if supported return None

USING LOCALIZATION
<html> <body> {{_("Welcome to our site.")}} </body> </html>

LOCALE FILE EXAMPLE: DE_DE
"New","Neu" "Donate","Spenden" "New Paste","Neuer Paste" "Secure, Private Pasting","Sicheres Pasten" "Unclaimed Hostname","Sie benutzen einen offenen Hostnamen. Klicken Sie heir für weitere Informationen." "Paste Options","Paste Optionen" "Formatting","Formatierung" "No Formatting","Keine Formatierung" "Line Numbers","Zeilennummern" "On","An" "Off","Aus" "Minutes","Minuten" "Hour","Stunde" "Day","Tag" "Week","Woche" "Year","Jahr" "Expire Paste","Wann soll der Paste gelöscht werden?" "Encryption","Verschlüsselung" "Encryption Key","Passwort-Verschlüsselung" "Encryption Algorithm","Algorithm-Verschlüsselung" "Save Paste","Paste speichern" "All Rights Reserved","Alle Rechte vorbehalten"

TEMPLATE EXAMPLE AGAIN
<html> <head> <title>My Site :: {% block title %}Unextended Template{% end %}</title> <link rel="stylesheet" type="text/css" href="{{ static_url('css/site.css') }}" /> <script type="text/javascript" src="{{ static_url('javascript/site.js') }}"></script> {% if not current_user %} <script type="text/javascript" src="http://api.recaptcha.net/js/recaptcha_ajax.js"> </script> {% end %} </head> <body{% if current_user %} class="authenticated"{% end %}> {% include "header.html" %} {% if request.uri not in ['/', ''] and current_user %} {{ modules.MemberBar() }} {% end %} <div id="content"> {% block content %} No Content Specified {% end %} </div> <ul id="footer"> <li><a href="/terms">{{_("Terms and Conditions")}}</a></li> <li class="center">{{_("Version")}}: {{ handler.application.settings['version'] }}</li> <li class="right">{{_("Copyright")}} &copy; {{ datetime.date.today().year }}</li> </ul> </body> </html>

TORNADO.AUTH
• Built •

in Mixins for OpenID, OAuth, OAuth2

Google, Twitter, Facebook, Facebook Graph, Friendfeed

• Use

RequestHandler to extend your own login functions with the mixins if wanted asynchronous supported in WSGI and Google App Engine

• Is

• Not

USING TORNADO.AUTH
- [/login/form, site.auth_reg.LoginForm] - [/login/friendfeed, site.auth_reg.LoginFriendFeed]

class LoginFriendFeed(RequestHandler, tornado.auth.FriendFeedMixin): @tornado.web.asynchronous def get(self): if self.get_argument("oauth_token", None): self.get_authenticated_user(self.async_callback(self._on_auth)) return self.authorize_redirect() def _on_auth(self, ffuser): if not ffuser: raise tornado.web.HTTPError(500, "FriendFeed auth failed") return username = ffuser['username']

TORNADO.IOLOOP
• Protocol

independent uses ioloop.IOLoop

• tornado.httpserver.HTTPServer • RabbitMQ • Built

driver Pika uses ioloop.IOLoop

in timer functionality

• tornado.ioloop.add_timeout • tornado.ioloop.PeriodicCallback

TORNADO.IOLOOP EXAMPLE
class MyClient(object): def connect(self, host, port): # Create our socket self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) self.sock.connect((host, port)) self.sock.setblocking(0) self.io_loop = tornado.ioloop.IOLoop.instance() # Append our handler to tornado's ioloop for our socket events = tornado.ioloop.IOLoop.READ | \ tornado.ioloop.IOLoop.ERROR self.io_loop.add_handler(self.sock.fileno(), self._handle_events, events)

https://github.com/pika/pika/blob/master/pika/adapters/tornado_connection.py

OTHER MODULES OF NOTE

tornado.database

tornado.options

MySQL wrapper

Similar to optparse

tornado.escape

tornado.testing

Misc escape functions

Test support classes

tornado.httpclient

tornado.websocket

Async HTTP client

Websocket Support

tornado.iostream

Non-blocking TCP helper class

ASYNC DRIVERS
• Memcache • MongoDB • PostgreSQL • RabbitMQ • Redis

FIN
• Follow

me on Twitter @crad

• Blog: http://gavinroy.com • Pika: http://github.com/pika • Async • We’re

RabbitMQ/AMQP Support for Tornado

hiring at myYearbook.com me an email: gmr@myyearbook.com

• Drop

IMAGE CREDITS
• Lego

by Craig A. Rodway http://www.flickr.com/photos/m0php/530526644/ Clipper X courtesy of NASA

• Delta

• United

Nations San Francisco Conference by Yould http://www.flickr.com/photos/un_photo/3450033473/