Andrew Wilkinson

Random Ramblings on Programming

Posts Tagged ‘restful

Building Better Web Services With Django (Part 2)

with 2 comments

In the first part I talked about using the Content-Type and Accept HTTP headers to allow a single website to be use both by humans and programs.

In the previous part I gave a decorator which can be used to make working with JSON very easy. For our use though this isn’t great because a view decorated in this way only accepts JSON as the POST body and only returns JSON, regardless of the HTTP headers.

The decorator given below relies on a django snippet to decode the Accept header for us so don’t forget to added it to your middleware.

def content_type(func, common=None, json_in=None, json_out=None, form_in=None):
    def wrapper(req, *args, **kwargs):
        # run the common function, if we have one
        if common is not None:
            args, kwargs = common(req, *args, *kwargs), {}
            if isinstance(args, HttpResponse): return args
        content_type = req.META.get("content_type", "")
        if content_type == "application/json":
            args, kwargs = json_in(req, json.loads(req.raw_post_data), *args, *kwargs), {}
        elif content_type == "application/x-www-form-urlencoded":
            args, kwargs = json_in(req, req.POST, *args, *kwargs), {}
        else:
             return HttpResponse(status=415, "Unsupported Media Type")

        if isinstance(args, HttpResponse): return args

        for (media_type, q_value) in req.accepted_types:
            if media_type == "text/html":
                return func(req, args, kwargs)
            else:
                r = json_out(req, args, kwargs)
                if isinstance(r, HttpResponse):
                    return r
                else:
                    return HttpResponse(json.dumps(r), mimetype="application/json")
         return func(req, args, kwargs)
    return wrapper

So, how can we use this decorator? Let’s imagine we’re creating a blog and we have a view which displays a post on that blog. If they user posts it should create a new comment. Firstly we create a function, common, which gets the blog object and returns a 404 if it doesn’t exist. The return of this function is passed onto all other functions as their arguments.

def common(req, blog_id):
    try:
        return (get_post_by_id(int(blog_id)), )
    except ValueError:
        return HttpResponse(status=404)

Next we write two functions to handle the cases where the users POSTs a form encoded body, or some JSON. The return values of these functions are passed onto the chosen output function as the arguments.

def json_in(req, json, blog_post):
    # process json
    return (blog_post ,)

def form_in(req, form, blog_post):
    # process form
    return (blog_post, )

The JSON output function doesn’t need to return an HttpResponse object like a normal Django view because the output is automatically encoded as a string and wrapped in a response object.

def json_out(req, blog_post):
    return blog_post.to_json()

Finally we come to the HTML output function. This function is also called if not mime type in Accept is suitable.

@content_type(common=common, json_in=json_in, json_out=json_out, form_in=form_in)
def blog_post(req, blog_post):
    return render_to_template("post.html", {"post": blog_post})

This decorator is really little more than a sketch. Many more content types could be supported, but hopefully it gives a good example of how you can write a very flexible webservice and still reduce code duplication as much as possible.

Advertisements

Written by Andrew Wilkinson

April 23, 2009 at 12:28 pm

Building Better Web Services With Django (Part 1)

with 4 comments

Building a RESTful webservice is pretty straight-forward with Django, but in many cases you want to have both a human readable website and a machine readable api. A lot of websites solve this problem by using http://www.x.com as the human site, an api.x.com as the machine site. They also will typically have different structures to support the different usecases.

Unless your documentation is really excellent and the person writing the client to your service actually reads it building a client for the service is an error prone process. In an ideal world the developer would be able to browse the website and use the same urls in their client program. Fortunately HTTP has two headers which make it possible to do just that, Content-Type and Accept.

The Content-Type header describes the type of data that is included in the body of the HTTP request. Typically this will be values such as text/html, application/json or application/x-www-form-urlencoded. A content type is sent by the client when POSTing or PUTing data, and whenever the webserver includes some data in its response. The Accept header is sent by a client to specify what content types it can accept in the response. This header has a more complicated format that Content-Type because it can used to specify a number of different content types and to give a weighting to each.

When combined these two headers can be used to allow a normal user to browse the site and to allow a robot to make api calls on the same site, using the same urls. This makes it easier both for the creator of the programmer accessing your site and for you because you can easily share code between the site and your api.

I’m going to outline a decorator that will let write a webservice such as this, that will support HTML and JSON output, and JSON and form encoded data as inputs.

First we’ll create a decorator that parses any post data as JSON and passes it the view as the second parameter (after the request object). It will also JSON encode any return value that’s not an HTTPResponse object.

import simplejson as json

from django.http import HttpResponse

def json_view(func):
    def wrap(req, *args, **kwargs):
        try:
            j = json.loads(req.raw_post_data)
        except ValueError:
            j = None

        resp = func(req, j, *args, **kwargs)

        if isinstance(resp, HttpResponse):
            return resp

        return HttpResponse(json.dumps(resp), mimetype="application/json")

    return wrap

This decorator should be pretty easy follow, but here is an example to illustrate its use.

@json_view
def view(req, json, arg1, arg2):
    obj = get_obj(arg1, arg2)
    if req.method == "POST" and json is not None:
        # process json here
        return {"status": "ok"}
    else:
        return {"status": "failed"}

This really cuts down on the code you need to write, but this view only handles JSON as its input and output. Next we need to parse the Accept headers and return an ordered list of content types so we can choose the preferred option. No need to reinvent the wheel, so we just pull some code from djangosnippets.org.

All the parts are in place now, and in my next post we’ll create a decorator which takes these parts ands puts them together.

Written by Andrew Wilkinson

April 8, 2009 at 12:21 pm