Tornado is a simple web framework...
...around a fast HTTP server...
...around a low-level event system.
It can be used for...
...building web apps...
...HTTP REST APIs...
...authentication frameworks...
...even custom TCP-based services.
from tornado.web import Application, RequestHandler
from tornado.ioloop import IOLoop
class HomeHandler(RequestHandler):
def get(self):
self.set_header("Content-type", "text/plain")
self.write("Hello, world!")
if __name__ == "__main__":
app = Application([("/", HomeHandler)])
app.listen(8080)
IOLoop.instance().start()
I'm going to talk through how Tornado works.
This means some libraries you might not use.
Tornado is actually very simple.
Tornado selects the best library for your platform.
For Linux, this means epoll.
For Mac, this means kqueue.
For Windows, this means old / boring select.
These are not the codes you are looking for.
The IOLoop abstracts those polling libraries away:
from tornado.ioloop import IOLoop
ioloop = IOLoop.instance()
# watch for events on a socket / file descriptor
ioloop.add_handler(sock.fileno(), callback, ioloop.READ)
# add a callback to run on the next "cycle"
ioloop.add_callback(callback)
# add a callback to run at (or after) a specific time
ioloop.add_timeout(time.time() + 60, callback)
Singletons -- great until they murder you
Use as default, but allow passing it in
class TCPServer(object):
def __init__(self, ioloop=None):
self._ioloop = ioloop or IOLoop.instance()
def send(self, message):
#...
All you need unless you're writing a TCP client / server.
Use these to split up blocking operations, defer tasks, etc.
# blocking operation...
results = DB.slow_find()
def parse_results(results):
for result in results:
# do something
# let other things work for a while
IOLoop.instance().add_callback(parse_results, results)
If you ARE writing a server, you need to:
# add a callback for a file descriptor on certain events
ioloop.add_handler(sock.fileno(), callback, ioloop.READ)
# update for new events
ioloop.update_handler(sock.fileno(), ioloop.WRITE)
# remove file descriptors when finished
ioloop.remove_handler(sock.fileno())
...but stayed tuned...
IOStream wraps sockets:
ioloop = IOLoop.instance()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
stream = IOStream(sock, io_loop=ioloop)
stream.connect(("google.com", 80), connect_callback)
stream.read_bytes(4096, read_callback)
# etc...
It provides plenty of helpers:
# read until exact string match
stream.read_until("\n\n", header_callback)
# read until a regular expression is matched
stream.read_until_regex("BYTES: \d+\s+", bytes_callback)
# read until the socket closes -- optional streaming
stream.read_until_close(finish_callback, streaming_callback)
# write out to the socket
stream.write("Foobar!", write_callback)
# if a socket is closed, call a callback
stream.set_close_callback(on_close)
stream.closed() or stream.reading() or stream.writing()
Use IOStream when someone else hasn't done it already.
(For instance, your favorite new NoSQL database.)
Don't use IOStream for HTTP clients -- that's coming up.
HTTPServer is (generally) invisible.
It leverages IOStream, etc. to support HTTP/1.1.
Runs implicitly when Application.listen() is called.
You probably won't need to touch it, but it's there.
Uses HTTPRequest.
HTTPRequest is used heavily in the request handlers.
It wraps information / helpers for client connections:
request.body # the request body string, if present
request.headers # dictionary of headers
request.cookies # dictionary of cookies
request.path # /foobar/path
request.query # ?key=value
request.remote_ip # actual request ip
request.scheme # actual request scheme (http[s])
tornado.web
contains the real meat of Tornado.
Most work (in web apps) is done here.
Encapsulates the "V" and "C" of "MVC"
from tornado.web import RequestHandler, Application
class Controller(RequestHandler):
# regex groups in route passed to method
def get(self, username):
ip = self.request.remote_ip # request on handler
# tornado includes its own templating system
self.render(
"template.html", ip=ip, username=username)
app = Application([
# routes are regex passed into Application
("/(\w+)", Controller)
])
{% extends "base.html" %}
{% block content %}
<h1>Hello, {{ username.uppercase() }}.</h1>
{# it's mostly just python. #}
{% if ip == "127.0.0.1" %}
<p>You must still be working on this.</p>
{% else %}
<p>You are accessing us from {{ ip }}.</p>
{% end if %}
{% end block %}
{# You can even... #}
{% import requests %}
{{ requests.get("http://www.google.com").text }}
{# ...but don't. That's really, really stupid. #}
It has just enough for what you need...
...with enough rope to hang yourself twice.
So be smart.
class Users(RequestHandler):
def post(self):
content = json.loads(self.request.body)
# application is available from the handler
id = self.application.settings["database"].save(content)
# a dictionary passed to write == JSON
self.write({
"id": "%s" % (id),
"name": content["name"]
})
app = Application([("/users", Users)],
debug=True, # for automatic reloading
database=pymongo.Connection()["mydatabase"])
from tornado.wsgi import WSGIApplication
app = WSGIApplication([("/", Handler)])
# for giggles...
server = gevent.pywsgi.WSGIServer(('', 8080), app)
server.serve_forever()
Can't do Tornado's async stuff, though.
class BaseHandler(RequestHandler):
@property
def db(self):
return self.application.settings["db"]
def get_current_user(self):
token = self.get_secure_cookie("token")
user = self.db.users.find_one({"token": token})
return user or None
class LoginHandler(BaseHandler):
def post(self):
if self.current_user:
self.redirect("/dashboard")
username = self.get_argument("username")
password = self.get_argument("password")
user = self.db.find_one({"username": username})
if not user or user.password != password:
return self.redirect("/login")
self.set_secure_cookie("token", user.token)
self.redirect("/dashboard")
class Dashboard(BaseHandler):
@tornado.web.authenticated
def get(self):
self.render("dashboard.htm", user=self.current_user)
class AsyncHandler(RequestHandler):
@tornado.web.asynchronous
def get(self):
service.fetch("/other/thing", callback=self._callback)
# request stays open...
def _callback(self, response):
if response:
self.write(response)
else:
raise HTTPError(500, "We couldn't find it.")
# without finish, the request never closes
self.finish()
There are lots of other helpers in the web stack.
(Websockets, error helpers, etc.)
Dig around in the documentation.
from tornado.httpclient import AsyncHTTPClient
def callback(response):
print response.code
print response.body
client = AsyncHTTPClient()
client.fetch("http://foo.com/some/resource", callback)
def fetch_callback(response):
print response.headers
print response.body
client = AsyncHTTPClient()
client.fetch("https://foo.com/some/resource",
method="POST", body=json.dumps({"key": "value"}),
headers={"Content-type": "application/json"},
auth_username="foo", auth_password="bar",
validate_cert=False, callback=fetch_callback)
You'll probably use this a lot.
It's especially useful when used with auth.
(seeggwwaaayyy...)
Tornado includes lots of third party auth.
This fits nicely with the async nature of Tornado.
Also, you can be lazy about your auth system.
from tornado.auth import TwitterMixin
class TwitterAuthHandler(RequestHandler, TwitterMixin):
@tornado.web.asynchronous
def get(self):
if self.get_argument("oauth_token", None):
return self.get_authenticated_user(self._on_auth)
self.authorize_redirect()
def _on_auth(self, user):
if not user:
raise HTTPError(500, "Couldn't auth.")
user = self.db.create({"username": user["username"]})
self.set_secure_cookie("token", user.token)
self.redirect("/dashboard")
Deployment
To Async or Not?
Testing
Libraries of Import
@joshmarshall on the Twitters.
@joshmarshall on the GitHub.