Rhino Documentation

Introduction

Rhino is a python microframework for building RESTful web services.

It aims to ...

Rhino is MIT licensed and runs under Python 2.6+. The Source code and bug tracker are hosted at github.

Note: This is a beta release. APIs may change at any time.

Installation

From pypi:

$ pip install rhino

From a git checkout:

$ git clone https://github.com/trendels/rhino.git
$ cd rhino
$ python setup.py install

To run the test suite, clone the repository as shown above, and run:

$ pip install -r requirements.txt
$ make test

Getting started

"Hello, World"

from rhino import Mapper, get

@get
def hello(request):
    return "hello, world!"

app = Mapper()
app.add('/', hello)

if __name__ == '__main__':
    app.start_server('localhost', 9000)

Run this code and visit http://localhost:9000/ in your browser. You should see the text "hello, world!".

Resources and Mappers

Creating Resources

Resources are the building blocks for RESTful web services. In Rhino, we create resources using instances of the Resource class:

from rhino import Resource, Mapper, ok

TODOS = ["Read the Rhino documentation", "???", "Profit!"]

todo_list = Resource()

# Register a handler for HTTP GET requests
@todo_list.get
def list_todos(request):
    return ok("\n".join(TODOS))

# Register a handler for HTTP POST requests
@todo_list.post
def add_todo(request):
    TODOS.append(request.body)
    return redirect(request.url_for(todo_list))

app = Mapper()
app.add('/todos', todo_list)

if __name__ == '__main__':
    app.start_server()

The resource's methods are used as decorators to register handler functions. A resource can have handlers for multiple methods, and even multiple handlers for the same method (see Content Negotiation). A handler is a regular Python function that receives the request as its first argument and produces an HTTP response.

Resources are registered with a Mapper under a URL template. The mapper is the entry point for the application, and dispatches incoming requests to resources based on the request URL.

The mapper knows the URLs of all resources, and can also construct URLs. This functionality is exposed through the Request.url_for() method, as seen in the example above. This avoids having hardcoded URLs in different places of the code that are hard to change later.

Named Routes

When building URLs, it may not always be possible or practical to refer to resources directly. For this reason, routes can be assigned a name in Mapper.add() by which they can be referred to later:

mapper.add('/todos', todo_list, name='todos')

# Now we can refer to the route like this:
request.url_for(todo_list)

# As well as this:
request.url_for('todos')

These special route names are predefined:

  • request.url_for('.') refers to the current route itself.

  • request.url_for('/') refers to the root level of the app, which may not be identical with the root level of the server, depending on how the app is hosted.

URL Templates

URL templates used with the mapper can contain parameters and ranges:

from rhino.errors import NotFound

todo_item = Resource()

@todo_item.get
def show_item(request, item_id):
    try:
        item = TODOS[int(item_id)]
    except IndexError:
        raise NotFound("Todo item with id %s not found." % item_id)
    return ok("Information about todo item '%s'." % item)

app.add('/todos/{item_id:digits}', todo_item)

In this example, the part of the URL path after '/todos/' will be extracted and passed as a named argument item_id to the show_item handler. It is also required to contain only digits or the route won't match. See rhino.mapper for a list of default range names.

When building a URL for a route who's template contains URL parameters, all required parameters must be provided as keyword arguments to Request.url_for(). The arguments don't need to be strings, but their string representation must match the parameter's range (digits, in this case):

request.url_for(todo_item, item_id=1)      # -> http://localhost/items/1
request.url_for(todo_item, item_id='foo')  # raises InvalidArgumentError

For appending a query string to the generated URL, use the special _query parameter:

request.url_for(todo_list, _query={'foo': 1}) # -> http://localhost/items?foo=1

Validating URL parameters

In resources which expect URL parameters, the parameter often refers to a internal object that is required for answering the request, such as a row in a database. Instead of fetching the object in each handler and exiting with an error if it doesn't exist, you can register a special from_url handler to do this in one place:

@todo_item.from_url
def item_from_url(request, item_id):
    try:
        item = TODOS[int(item_id)]
    except IndexError:
        raise NotFound("Todo item with id %s not found." % item_id)
    return {'item': item}

The handler is called before the actual request handler, and must return a new dictionary, which replaces the URL parameters passed to the handler. Alternatively it can stop further processing by raising an HTTPException (See HTTP Exceptions).

In resources supporting many request methods, this simplifies handlers considerably:

# Continued from above ...

@todo_item.get
def show_item(request, item):  # Instead of item_id, we now get an item
    return ok("Information about todo item %s" item)

@todo_item.put
def update_item(request, item):
    # update item ...

@todo_item.delete
def delete_item(request, item):
    # delete item ...

Simple Resources

As the "Hello, world" example shows, there are some common shortcuts:

  • If a resource only handles one HTTP method (usually GET), which is quite common, you can create a standalone handler function using one of the decorators get(), post(), put(), patch(), delete() or options().

  • Instead of a Response, you can also return an arbitrary object that will be sent as the response body, using a default status code of "200 OK".

Accessing request data

The request data from the WSGI environment is stored in a Request instance.

Query parameters

Request parameters are stored as a QueryDict in the query property:

# Example: given a query string of 'a=b&id=1&id=2'
request.query['a']                   # -> 'a'
request.query.get('name', 'default') # -> 'default'
request.query.get_all('id')          # -> ['1', '2']

Form fields

The form property holds form fields as a QueryDict. It is parsed when it is first accessed during a request using the stdlib's cgi.FieldStorage class.

File upload fields from multipart forms have the extra attributes 'filename', 'type' and 'file':

# Accessing a regular form field
username = request.form['username']

# Accessing a file upload field
client_filename = request.form['upload'].filename
content_type = request.form['upload'].type
file_data = request.form['upload'].file.read()

Request headers

The headers property contains a dict-like object that provides case-insensitive access to the request headers. Also, accessing a non-existing request header returns None instead of raising KeyError:

# The following are equivalent:
request.headers['user-agent']
request.headers['USER-AGENT']
request.headers.get('User-Agent')

For more request properties, see the API documentation for Request.

Sending a response

Building HTTP responses

To return a response from a handler function, use one of the helper functions (ok(), created(), no_content(), redirect() or response()), or construct a Response instance directly. The helper functions allow passing headers as keyword arguments, and accept non-string values for etag, last_modified, and expires (See response() for details).

from rhino import response, ok, redirect

# Common response codes
ok('hello')                          # 200 OK
redirect('/foo/bar')                 # 302 Found
redirect(code=303, location='/quux') # 303 See Other

# Custom responses
response(code=418, body="I'm a teapot!", x_tea="Earl Grey")

Setting response headers:

# Create a response with header 'Content-Type: application/json'
ok(json.dumps({'status': 'green'}), content_type='application/json')

# Create a response with headers:
#   Expires: <http date 5 minutes in the future>
#   X-Powered-By: my-app-version
ok('hello, world!', expires=300, x_powered_by='my-app-version')

Entities

Instead of manually specifying a Content-Type header, it is often easier to have the content-producing function return an Entity instead, which represents a response body with entity headers, but no status code:

from rhino import Entity

def json_entity(obj):
    return Entity(body=json.dumps(obj), content_type='application/json')

@get
def get_status(request):
    return json_entity({'status': 'supergreen'})

The Entity constructor accepts the same keyword arguments as response().

When used as a response body, the entity body and headers will be used to populate the response. Headers passed to the response directly take precedence and can override entity headers if needed:

@get
def get_legacy_status(request):
    return ok(json_entity({'status': 'green'}), content_type='text/x-json')

Streaming responses

Returning an iterator will cause the response to be streamed using chunked transfer encoding, if implemented by the WSGI server:

@get
def chunked_response(request):
    return ok(chunk for chunk in ['one', 'two', 'three'])

When paired with an asynchronous WSGI server, such as gevent.wsgi.WSGIServer, this can be used for long-polling handlers or Server-Sent Events:

import gevent
from rhino.util import sse_event

# Send an event with 'data: ping' every five seconds.
@get(provides='text/event-stream')
def event_stream(request):
    while True:
        yield sse_event(data='ping')
        gevent.sleep(5)

HTTP Exceptions

Sometimes it is useful to stop further processing of a request from a place where you cannot simply return a response, such as a callback or a from_url handler. For this reason, responses that are errors or redirects (3xx, 4xx, 5xx) can also be generated by raising subclasses of HTTPException:

from rhino.errors import NotFound

@get
def get_user(request, name):
    user = get_user_from_database(name=name)
    if not user:
        raise NotFound("The user could not be found.")
    return ok("Information about user %s" %  user.name)

Some common HTTP Exceptions are defined in the rhino.errors module, as well as base classes for the different types of errors that can be subclassed to create new kinds.

Note: By convention, the Rhino documentation and code examples use exceptions for all HTTP errors (4xx and 5xx), even if they occur in a request handler, and regular response objects for everything else.

Cookies

Incoming cookies are stored as a dictionary in the Request.cookies attribute. Outgoing cookies can be set and deleted using the Response.set_cookie() and Response.delete_cookie() methods.

# Accessing a request cookie
request.cookies.get('user-font-size', '12px')

# Setting a cookie
resp = ok()
resp.set_cookie('color', 'green', max_age=3600, httponly=True)

Note that only strings can be used as cookie values. For anything but the most simple use cases, you should probably use the rhino.ext.session extension instead.

Extensions and the Context

The primary extension mechanism is the Context. This is an object that is created for each request, and has the same lifetime as the Request object.

The context can be requested in a handler using dependency injection. To receive the context, add an argument named ctx to the handler function's signature (by convention, this should be the 2nd argument after the request, although unlike the request, the context is always passed in by name).

@get
def hello_with_ctx(request, ctx):
    # This handler has access to the context ...

By default, the context is empty, and only serves as an interface to install Callbacks. However, it can be extended by using the Mapper.add_ctx_property() method:

@get
def hello_with_ctx(request, ctx):
    return ok("The app name is %s" % ctx.app_name)

app = Mapper()
app.add_ctx_property('app_name', "My Application")
app.add('/', hello_with_ctx)

In this example, all context instances within this Mapper will have an 'app_name' attribute containing the string "My Application". Usually, the second argument to add_ctx_property will be a factory function to provide a lazily initialized property. See Extending Rhino Applications for details.

Some default extensions are included to add common functionality. These usually require additional python modules to be installed (those are listed at the top of the extension's documentation)

Here is an example app using jinja2 templates and cookie-based sessions. Save the following code to a file named webapp.py:

from rhino import Mapper, get
from rhino.ext.jinja2 import JinjaRenderer
from rhino.ext.session import CookieSession

app = Mapper()
app.add_ctx_property('render_template', JinjaRenderer('.'))
app.add_ctx_property('session', CookieSession(secret='my-session-secret'))

@get
def index(request, ctx):
    counter = ctx.session.get('counter', 0)
    ctx.session['counter'] = counter + 1
    return ctx.render_template('index.html', visits=counter)

app.add('/', index)

if __name__ == '__main__':
    app.start_server()

And the index.html template:

<!DOCTYPE html>
<html>
  <body>
    <h1>Welcome!</h1>
    <p>You have visited this site {{ visits }} times.</p>
  </body>
</html>

To test the application, first make sure the prerequisites are installed:

$ pip install beaker jinja2

Then start the development server:

$ python webapp.py
Server listening at http://localhost:9000/

Point your browser at http://localhost:9000/ and reload the page a few times, and you should see the counter go up.

Serving static files

To serve static files, you can use the StaticDirectory and StaticFile resources:

from rhino import Mapper, StaticDirectory, StaticFile

app = Mapper()
app.add('/favicon.ico', StaticFile('./favicon.ico'))
app.add('/static/{path:any}', StaticDirectory('./static/'), name='static')

Using a WSGI Server

For production uses, you should run the app using a WSGI server of your choice.

The Mapper.wsgi() method is the WSGI entry point. For example, to run the "hello, world" example using the gunicorn WSGI server, save the code to a file named hello.py and run

$ gunicorn --bind=localhost:8000 hello:app.wsgi

Advanced Topics

Conditional Requests

ETag and Last-Modified Headers

If you add an ETag or Last-Modified header to a response, Rhino will automatically handle conditional GET and HEAD requests. The easiest way to do this is to use response() or related functions:

@get
def resource_with_etag(request):
    return ok("hello, world", etag="hello-v1")

from datetime import datetime

@get
def resource_with_last_modified(request):
    return ok("hello, world", last_modified=datetime.now())

Note that you don't have to put quotes around the ETag value, as they will be added automatically (but you can if you want). In most cases, ETag is preferable to Last-Modified, since the HTTP date format only has 1-second precision.

Generating ETags automatically

An easy way to generate ETags is to compute a hash over the entire response body, e.g. an HTML page. This way you can be sure the ETag changes whenever anything on the page has changed. There is a shortcut to do this in Rhino, by passing a callable to the etag argument of response(). The callable will be called immediately with the request body to produce an ETag.

from hashlib import sha1

def sha1_etag(s):
    return sha1(s.encode('utf-8')).hexdigest()

@get
def resource_with_sha1_etag(request):
    return ok("hello, world!", etag=sha1_etag)

ETags and lazy response bodies

Simply adding ETag headers to outgoing responses can already save considerable bandwidth, but with the examples we've seen so far, the entire response is still generated on the server for each request. If a representation is expensive to produce, it can be beneficial to pre-compute ETags, and try to handle conditional requests on the basis of the ETag header alone. This way, the response body is only produced when it actually needs to be sent over the wire.

For this reason, Rhino allows you to wrap any valid response body in a callable, that will be called only if needed:

@get
def resource_with_precomputed_etag(request):
    etag = get_etag()  # Cheap: e.g a cache lookup
    def body():  # Will only be called for response codes != "304 Not Modified"
        return expensive_operation()
    return ok(body, etag=etag)

Content Negotiation

If a resource is able to produce representations in different formats, or different versions of the same format, you can use Content-Negotiation. All handler decorators (get(), Resource.get()), etc. allow you to specify provides and accepts arguments for this purpose.

The value of provides should be a single, fully-qualified mime-type, (e.g. text/plain, application/json), that will be matched with the Accept header sent by the client to find the best representation available.

The value of accepts can be a media-range, such as text/*, or a mime-type like provides.

import json

greeting = Resource()

@greeting.get(provides='text/plain; charset=utf-8')
def greeting_text(request):
    return ok("hello, world")

@greeting.get(provides='application/json')
def greeting_json(request):
    return ok(json.dumps({"message": "hello, world"}))

A few important things to note:

Representations

Going a step further than only selecting among handlers, Content-Negotiation can also be combined with automatic seralization/deserialization in the form of "Representations". In Rhino, a representation is any object that has "provides" and "serialize" attributes (if used for output), and/or "accepts" and "deserialize" (if used for input). Representations are used with the produces and consumes arguments to handler decorators. They provide Content-Negotiation, just like accepts/provides, but also methods to serialize the response body or deserialize the request body on demand.

Here is an example of a generic JSON representation:

import json

class json_repr(object):
    accepts = 'application/json'
    provides = 'application/json'

    @staticmethod
    def serialize(obj):
        return json.dumps(obj, sort_keys=True)

    @staticmethod
    def deserialize(f):
        return json.loads(f.read())

And a resource which produces and consumes this representation:

greeting = Resource()
greeting.message = "hello, world!"

@greeting.get(produces=json_repr)
def get_json(request):
    # After the handler has returned, the response body will be serialized
    # using json_repr.serialize()
    return {'message': greeting.message}

@greeting.put(consumes=json_repr, produces=json_repr)
def put_json(request):
    try:
        # Accessing the request body will cause it to be deserialized first
        # using json_repr.deserialize()
        greeting.message = request.body['text']
    except ValueError:
        raise BadReqeust
    return {'message': greeting.message}

Representations are also useful if you want to provide different versions of a custom content-type (Content-Type versioning):

class v1_repr(object):
    produces = 'application/vnd.myorg.document+json;v=1'
    # ...

class v2_repr(object):
    produces = 'application/vnd.myorg.document+json;v=2'
    # ...

@get(produces=v1_repr)
@get(produces=v2_repr)
def document_json(request):
    doc = get_document()
    return doc

Extending Rhino Applications

Callbacks

Callbacks are functions that can be installed on the Context and that are called by the framework at different stages during request processing. See Context.add_callback() for more information.

Wrappers

A wrapper is a kind of middleware that wraps a mapper and has full control over how request processing proceeds. It can inspect the incoming request, decide whether or not to pass it on to the wrapped mapper, and modify the response. See Mapper.add_wrapper() for details.

Context Properties

The Context can be extended via the Mapper.add_ctx_property() method.

Resource Views

Views are an optional feature that allows you to have a single resource act in different ways depending on which route the request came from:

import json

message = Resource()
message.text = "hello, world!"

# By default, the resource supports GET and returns plain text.
@message.get
def get_message(request):
    return message.text

# The 'json' view supports GET and returns json
@message.get('json')
def get_message_json(request):
    return ok(json.dumps({'text': message.text}),
              content_type='application/json')

# The 'edit-form' view supports GET and POST and returns an HTML form
@message.get('edit-form')
def message_edit_form(request):
    return ok('''
        <form method="post" action="">
          <input type="text" name="text">
          <input type="submit" value="Update>
        </form>
        ''', content_type='text/html')

@message.post('edit-form')
def edit_message(request):
    message.text = request.form['text']
    return redirect(message)

app = Mapper()

# Each route to the resource is associated with a different view (the part
# of the name after the ':')
app.add('/message', message, 'message')               # The default view
app.add('/message.json', message, 'message:json')     # The 'json' view
app.add('/message/edit', message, name=':edit-form')  # The 'edit-form' view

All handler decorators accept a view argument that associates the handler with this view. A route is associated with a view when the name includes a colon (:). The part after the : is the view name. When the resource is accessed via a route matching a view, it will act as if only the handlers associated with that view are present. Thus, each view acts effectively as a separate resource.

The example above shows two use cases for views:

Compared to implementing each view as a separate resource, views allow common infrastructure like from_url handlers to be shared between related resources.

Nesting Mappers

To build re-usable applications, or to structure larger applications into smaller parts, mappers can be nested:

from rhino import Mapper

import app_one
import app_two

main_app = Mapper()
main_app.add('/one|', app_one, name='one')
main_app.add('/two|', app_two, name='two')

The | marker at the end of the pattern causes the pattern to only be matched against the beginning of PATH_INFO. The rest of the path will be preserved and consumed by nested mappers.

Building URLs for nested resources.

Building URLs for routes in other mappers that are nested under a common root is possible if all routes on the way have a name.

@get
def hello1(request):
    return 'hello 1'

@get
def hello2(request):
    return 'hello 2'

red_mapper = Mapper()
red_mapper.add('/hello', hello1, name='hello')

green_mapper = Mapper()
green_mapper.add('/hello', hello2, name='hello')

red_mapper.add('/green|', green_mapper, name='green')

root_mapper = Mapper()
root_mapper.add('/red|', red_mapper, name='red')

This creates the following URL structure (only resources are shown):

URL Resource
/red/hello hello1
/red/green/hello hello2

From anywhere within the app, a URL the hello1 and hello2 resources can be can be created like this, using the absolute path starting at root_mapper:

request.url_for('/red.hello')        # for hello1
request.url_for('/red.green.hello')  # for hello2

Using a relative path, we can build a URL for hello2 from within hello1 like this:

request.url_for('green.hello')

And from within hello2, the URL for hello1 like this:

request.url_for('..hello')

The last two cases would keep working even if red_mapper is run as a standalone app, or nested under a different mapper. See Request.url_for() for more information.

API Documentation

Module Index