Exposing REST services with Python and django-piston
The ability to build service oriented applications has been an indispensable tool for the modern developer for some time. Nonetheless, when considering the architecture for a new project we're often overwhelmed by the myriad ways expose our services to the world. I recently began work on a python/django project that needed to expose a highly scalable API. REST has become and industry buzzword lately, Twitter, Facebook, Flickr and many other household names employ REST API's to expose their data to the world.
It should be mentioned as early as possible that RESTful services aren't a defined standard in the way SOAP based services are. Rather, REST is a design pattern meant to guide developers that want to provide consistent, well organized access to sets of data. Though the full REST pattern is beyond the scope of this post, developers should remember that the goal of a REST service is to use the "GET"," PUT", "POST", and "DELETE" HTTP methods to retrieve, change, create and remove data from a service. Hereis a great overview of the REST architecture. If, for some reason, you dislike yourself and don't value your time you can read Roy Fielding's original description of REST services in his doctoral dissertation (It's actually quite useful).
When I sat down to build my first python REST service the first thing I did was look for an existing framework to build my API on top of. I immediately found the django-rest-interface library. I really like how easy it was to setup an API for your existing django model using this framework. Below is a quick example from the frameworks homepage of a model that has been exposed with django-rest-interface all from within the urls.py module:
from django_restapi.model_resource import Collection
queryset = MyModel.objects.all(),
responder = XMLResponder()
)
urlpatterns = patterns ('',
# ...
url(r'^xml/mymodel/(.*?)/?$', mymodel_resource)
)
# ...
url(r'^xml/mymodel/(.*?)/?$', mymodel_resource)
)
This code exposes the Collection resource at the URL: http://yourhost/xml/mymodel/.>
Though I enjoyed how easy this is. The django-rest-interface is a very light library that neglects more complex requirements. To be specific. I want to authenticate clients of my API with Oauth instead of basic authentication. An overview of the Oauth authentication pattern can be found here. Without too much effort, I discovered django-piston. django-piston offered all the functionality of django-rest-interface plus the ability to authenticate clients with Oauth. Below is an example of a model exposed with django-piston.
The first part of this code snippet defines the urls.py module. In this module we create views that implement resource handlers. Resource Handlers are described lower in this post.
from django.conf.urls.defaults import *
from piston.resource import Resource
from piston.authentication import HttpBasicAuthenticationfrom myapp.handlers import BlogPostHandler, ArbitraryDataHandlerauth = HttpBasicAuthentication(realm="My Realm")
ad = { 'authentication': auth }blogpost_resource = Resource(handler=BlogPostHandler, **ad)
arbitrary_resource = Resource(handler=ArbitraryDataHandler, **ad)urlpatterns += patterns('',
url(r'^posts/(?P<post_slug>[^/]+)/$', blogpost_resource),
url(r'^other/(?P<username>[^/]+)/(?P<data>.+)/$', arbitrary_resource),
)
from piston.resource import Resource
from piston.authentication import HttpBasicAuthenticationfrom myapp.handlers import BlogPostHandler, ArbitraryDataHandlerauth = HttpBasicAuthentication(realm="My Realm")
ad = { 'authentication': auth }blogpost_resource = Resource(handler=BlogPostHandler, **ad)
arbitrary_resource = Resource(handler=ArbitraryDataHandler, **ad)urlpatterns += patterns('',
url(r'^posts/(?P<post_slug>[^/]+)/$', blogpost_resource),
url(r'^other/(?P<username>[^/]+)/(?P<data>.+)/$', arbitrary_resource),
)
The following code exists within the handlers.py class. This code defines the logic that will be executed when a message handler is called with specific http methods. I particularly liked approach because it allows one to create resources that more tightly follow the REST pattern.
import refrom piston.handler import BaseHandler
from piston.utils import rc, throttlefrom myapp.models import Blogpostclass BlogPostHandler(BaseHandler):
allowed_methods = ('GET', 'PUT', 'DELETE')
fields = ('title', 'content', ('author', ('username', 'first_name')), 'content_size')
exclude = ('id', re.compile(r'^private_'))
model = Blogpost @classmethod
def content_size(self, blogpost):
return len(blogpost.content) def read(self, request, post_slug):
post = Blogpost.objects.get(slug=post_slug)
return post @throttle(5, 10*60) # allow 5 times in 10 minutes
def update(self, request, post_slug):
post = Blogpost.objects.get(slug=post_slug) post.title = request.PUT.get('title')
post.save() return post def delete(self, request, post_slug):
post = Blogpost.objects.get(slug=post_slug) if not request.user == post.author:
return rc.FORBIDDEN # returns HTTP 401 post.delete() return rc.DELETED # returns HTTP 204class ArbitraryDataHandler(BaseHandler):
methods_allowed = ('GET',) def read(self, request, username, data):
user = User.objects.get(username=username) return { 'user': user, 'data_length': len(data) }
from piston.utils import rc, throttlefrom myapp.models import Blogpostclass BlogPostHandler(BaseHandler):
allowed_methods = ('GET', 'PUT', 'DELETE')
fields = ('title', 'content', ('author', ('username', 'first_name')), 'content_size')
exclude = ('id', re.compile(r'^private_'))
model = Blogpost @classmethod
def content_size(self, blogpost):
return len(blogpost.content) def read(self, request, post_slug):
post = Blogpost.objects.get(slug=post_slug)
return post @throttle(5, 10*60) # allow 5 times in 10 minutes
def update(self, request, post_slug):
post = Blogpost.objects.get(slug=post_slug) post.title = request.PUT.get('title')
post.save() return post def delete(self, request, post_slug):
post = Blogpost.objects.get(slug=post_slug) if not request.user == post.author:
return rc.FORBIDDEN # returns HTTP 401 post.delete() return rc.DELETED # returns HTTP 204class ArbitraryDataHandler(BaseHandler):
methods_allowed = ('GET',) def read(self, request, username, data):
user = User.objects.get(username=username) return { 'user': user, 'data_length': len(data) }
This post is meant to introduce you to building REST services with Python and Django using django-piston. For a more thorough overview of django-piston spend some time on the wiki.
