Rhino is a python microframework for building RESTful web services.
It aims to ...
make building small web services as frictionless as possible, while providing features such as conditional request handling, content negotiation and generating URLs out of the box.
make it easy to extend applications via a standard extension mechanism, to allow building more advanced web applications while keeping the framework core small and simple.
encourage code re-use by structuring services into small, self-contained applications that can be nested within each other or run as standalone services.
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.
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
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 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.
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 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
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 ...
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".
The request data from the WSGI environment is stored in a Request instance.
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']
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()
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.
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')
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')
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)
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.
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)
rhino.ext.jinja2
-- Extension for rendering jinja templates.rhino.ext.session
-- Cookie-based session support, and other backends provided by beaker.session.rhino.ext.sqlalchemy
-- Provides a lazily initialized sqlalchemy Session object via the context.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.
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')
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
ETag
and Last-Modified
HeadersIf 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.
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:
Content-Negotiation on the basis of the Accept
header is only enabled if all handlers of a resource that are taken into consideration (i.e. with the matching HTTP method) are annotated with the mime-type they provide (via provides
). For other handlers, there is no way for the framework to predict what mime-type they might produce. In this case, the framework will only consider the handler(s) that are not annotated to be present.
If a handler has a provides
annotation, and no Content-Type is explicitly set on the response, the value of the provides
parameter will be used by default.
A handler that has no accepts
annotation will be treated as if it accepts */*
(i.e. anything).
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
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.
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.
The Context can be extended via the Mapper.add_ctx_property() method.
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.
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 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.