Edit (November 2, 2012): This is horribly outdated. Use class-based views or tastypie.
Firstly, I’ve been missing in action for a few months and I apologize to you, my loyal reader, for that. Without making excuses (here comes the excuses), work has been picking up, my girlfriend moved from about 15 miles away to only about 8 blocks away and Starcraft II is in beta. Regardless, I’m back in the Python action. WoooHooo!
REST interfaces & Django
This post is somewhat of a follow-up on my post on RESTful Django web services because I didn’t really talk in my previous post about Piston. Piston (sometimes django-piston) is a library for creating RESTful services in Django and it supports some of the features that I spoke about in my previous post such as good caching support with Django’s cache framework, different output formats (eg. XML & JSON) via what Piston calls emitters, and the ability but not the requirement to use Django models as REST resources. I don’t know how I missed Piston before, but people blog (*) about it and it has made the rounds on the Django User’s list. However, even after looking closely at it, I decided not to go with it. In this post I’m going to talk about what I did and did not like and why I rolled my own REST micro-framework. That almost sounds like I’m giving myself too much credit given that my micro-framework is only ~30 lines.
(*) BTW, Despite the fact that Eric updates his blog somewhat infrequently (sounds familiar) it is well worth a read.
Piston: the good
Piston ships with quite a bit of good documentation and allegedly is used to power some of BitBucket’s services — lending to its credibility. Specifically, I liked the fact that it plugged directly into Django models. You simply write a short Handler for your model explaining what fields to expose and you’re mostly done.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import re from piston.handler import BaseHandler from myapp.models import Blogpost class BlogPostHandler(BaseHandler): allowed_methods = ('GET') fields = ('title', 'content', ('author', ('username', 'first_name'))) exclude = ('id', re.compile(r'^private_')) model = Blogpost def read(self, request, post_slug): post = Blogpost.objects.get(slug=post_slug) return post |
It effectively wraps up your handler and does all the JSON/XML/YAML serialization for you while still giving you the ability to customize it. On top of this, it plugs in nicely with Django’s form validation and allows you to do some other nice features like throttling requests based on which user does it.
Piston: the bad & the ugly
I started to look at Piston, but because I wasn’t using throttling, using OAuth, outputting anything other than JSON and I wasn’t tying to models I didn’t think that Piston bought me anything. In reality, it wasn’t doing anything my me other than properly returning HttpResponseNotAllowed. My other issue is that this project involved different outputs based on HTTP headers. For example, a GET on a certain URL would return JSON formatted data (a read in the CRUD world) if an HTTP header was present and an HTML page presenting that data if it wasn’t. Piston uses different emitters based on a request parameter format (eg. /path/resource/?format=JSON). Piston gets you up and running quickly, but it didn’t fit my use case.
Also, this is a little nitpicky, but when I see something like:
1 |
return rc.FORBIDDEN # returns HTTP 401 |
I cringe a little bit considering that status code 403 is the correct status code for Forbidden. There’s a ticket for this already. Why did Piston define constants for returning various status codes anyway when that functionality is already built into Django. Is rc.DELETED so much easier than HttpResponse(status_code=204)? Perhaps it’s a little clearer and Django really should have HttpResponse subclasses for even the less common responses, but I think this definitely involves repeating yourself (and Django’s mantra is don’t repeat yourself).
The solution
I always wondered why Django didn’t allow for routing URLs based on the HTTP method: It seems like such a common use case. The developers discussed it back in 2006, but in the end it was decided that building only the simple case was best as it yielded a relatively clean urls.py. Building off of that thread, the example in the Django book (search for “method_splitter”) and another blog post, I rolled a little framework to meet my needs instead of using something like Piston.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
## utils/dispatcher.py from django.http import HttpResponseNotAllowed # see rfc 2616 - http://www.ietf.org/rfc/rfc2616.txt s9.2 - s9.9 HTTP_METHODS = ('GET', 'POST', 'PUT', 'HEAD', 'TRACE', 'DELETE', 'OPTIONS', 'CONNECT') def service_dispatcher(request, *args, **kwargs): """ Routes requests to the correct view method based on the HTTP method """ # loop over all possible HTTP methods and find the appropriate service allowed_methods = [] appropriate_service = None for method in HTTP_METHODS: service_view = kwargs.pop(method, None) if service_view is not None: # store legal HTTP methods in case we need to return a 405 allowed_methods.append(method) # found the correct service method if request.method == method: appropriate_service = service_view # if the correct service was found, call it # otherwise return a 405 - method not allowed - error if appropriate_service is not None: return appropriate_service(request, *args, **kwargs) else: return HttpResponseNotAllowed(allowed_methods) ## urls.py from django.conf.urls.defaults import * from myapp.utils.dispatcher import service_dispatcher from myapp.blog import services urlpatterns = patterns('', url(r'^/myapp/blog/$', service_dispatcher, {'GET': services.blog_get, 'POST': services.blog_post}), ) |
I found this to be a much simpler and easily extensible. The argument against this is that urls.py becomes bigger, but in a lot of ways I found this to be clearer. From reading the urlpatterns, I can quickly tell exactly what gets called in each case. In addition, routing differently based on HTTP headers, cookies, the source or anything else becomes as simple as adding a parameter and a little code to service_dispatcher.
In the end, it’s wasn’t that I didn’t like Piston, it’s just that I didn’t need it.