The New Utovsky Bolshevik Show

Sat, 05 Nov 2011

A Prompt Piston Primer

At work, we use django-piston to easily provide some RESTful APIs. For those who don't know, Piston is a reusable Django app that allows you to give RESTful access to models by writing simple handlers. These handlers specify the accessible fields, appropriate access methods and suchforth.

In this post, I'll give a quick introduction to Piston (limiting myself to GET APIs only) which I will expand upon in future posts.

Imagine a simple library scenario with the following models:

class Shelf(models.Model):
    location = models.CharField(...)

class Book(models.Model):
    title = models.CharField(...)
    shelf = models.ForeignKey(Shelf, related_name="books")

For some reason (it's an enterprise library, perhaps) we need a REST API on to our books data, so we can easily fetch their title and shelf via HTTP. We'll start by writing a basic handler for books:

from piston.handler import BaseHandler

from models import Book

class BookHandler(BaseHandler):
    model = Book

That's it. Except it isn't quite, as we need to hook it up in our urls.py:

from django.conf.urls.defaults import patterns, include, url
from piston.resource import Resource

from handlers import BookHandler

book_resource = Resource(BookHandler)

urlpatterns = patterns('',
    (r'^book/(?P<id>[^/.])', book_resource)
)

Here we wrap up our handler in a resource (which handles all of the view-like stuff for us) and set /book/<id> to point to it. If you run your server, you should now be able to point at /book/1 and receive a 404. If you create a Book (and a Shelf for it to live on), you should get it back in JSON:

{
    "shelf": {
        "_state": "<django.db.models.base.ModelState object at 0x2be6090>",
        "id": 1,
        "location": "Shelf Location 1"
    },
    "title": "Book 1"
}

Voila! You have the book's title and, even better, you have all of the shelf information included as well. Problem solved! Well, not quite. We don't actually want all of our shelf information included. _state is an internal Django thing [0]. id is an internal identifier which we don't want any consumer of our API to be able to access.

The most obvious option is to specify which fields we include in the API output:

class BookHandler(BaseHandler):
    model = Book
    fields = ('title',
              ('shelf', ('location',))

This syntax is, I hope, relatively obvious. fields contains a list of all the field we want our API output to include. Where we have a related model (shelf in this example) we can use a two-tuple specifying the field name and the list of fields on the related model to include in the output (we only want location). This gives us the following JSON output:

{
    "shelf": {
        "location": "Shelf Location 1"
    },
    "title": "Book 1"
}

This looks exactly as we want. Problem solved! Well, not quite. Let's say that we need to add a field to Book (e.g. page_count). Because we are hard-coding the fields that the API produces, we will have to add it to the fields attribute of our BookHandler. The same is true if we want to add another field to Shelf; we'll have to modify that shelf tuple to include it. There must be a better way.

There is. If we think back to our BookHandler, Piston worked out what fields there were on the Book model all by itself. Also, the Book model has an id field (and probably a _state attribute) and Piston didn't expose those through the API. This tells us two things about Piston handlers:

These are exactly the properties that we are looking for when exposing a Book's Shelf through the API, so let's remove the fields attribute on our BookHandler and write a ShelfHandler:

class ShelfHandler(BaseHandler):
    model = Shelf

class BookHandler(BaseHandler):
    model = Book

If you hit our API now, you will see that the JSON returned is exactly the same as before we made the change. However, if you experiment with modifying the models, you will see that those changes are reflected in the API output with no further modifications by us.

This reflects the last lesson that I want to offer in this post: Piston keeps a registry of handlers for models. If a related model is referenced without any guidelines as to how to display it (as Shelf is implicitly referenced in our most recent BookHandler), Piston will use a handler defined for that model if it is available [1].

Let's quickly review what we've learned:

Whilst writing this post, I've been recording my work in git, you can find the repository at https://github.com/OddBloke/Books-Sample-API.

In a future blog post, I will cover how we handle circular dependencies. I may also write posts about how to get different output formats and, if I do some research, how to use Piston to provide more than a read-only API.

[0]I am using the most recent release of Piston. It would not surprise me if this particular foible had already been fixed in development.
[1]One limitation of this functionality is that the registry only supports a single handler per model, and it will silently overwrite the entry in the registry if you define a second one, which can lead to very confusing behaviour. I will briefly touch on this when I write about circular dependencies.

Posted: Sat, 05 Nov 2011 09:51 | Tags: , , , , | Comments: 4 |

Comments

Hi,

I've been using piston in my work too (brandsexclusive.com.au), but these small bugs and things are annoying me in Piston.

One another library to do the same work is
https://github.com/toastdriven/django-tastypie

Seems better than Piston.
Posted by Luciano Pachco at Sat Nov 5 22:40:36 2011

Hi,

I've been using piston in my work too (brandsexclusive.com.au), but these small bugs and things are annoying me in Piston.

One another library to do the same work is
https://github.com/toastdriven/django-tastypie

Seems better than Piston.
Posted by Luciano Pachco at Sat Nov 5 22:40:39 2011

This is seriously awesome. Thanks for the introduction! (:
Posted by Tim at Sun Feb 5 09:40:46 2012

How /does/ one handle circular dependencies?
Posted by Tim at Sun Feb 5 15:13:52 2012

Name:


E-mail:


URL:


Comment: