Easy REST API with django-tastypie

One day at work I had to implement JSON response on simple GET request. I was just going to code another RESTish bicycle as my project manager told me to use django-tastypie. I'm sure he knows what he is talking about so I opened google to find what tastypie is.

When spec for my task was being written, author didn't think about what tool will be used for implementation. On the other side tastypie authors didn't know what people will do with their lib. So my task was to make this two worlds meet in one place. On github project page there's a simple example with very basic things showed. The real world is usually more complicated. I'll try to show you how I worked with tastypie.


Let's take an example. For this post I created tine django project which you can find at github.
We have some working web application and we need to add REST API to it. Our main models are: Cabinet, Folder and File. Cabinets stand in some office. Every Cabinet has Folders inside, which contains Files of different types.

The task is to implement such response for request GET api/cabinet/list
{
   "cabinets":[
      {
         "added":"2011-06-17",
         "color":"black",
         "floor":2,
         "folders":[
            {
               "files":[
                  {
                     "archive_num":"221122",
                     "content":"Lorem ipsum dolor sit amet
consectetur adipisicing elit",
                     "type":"Evidence"
                  }
               ],
               "name":"XFiles",
               "secrecy":10
            },
            {
               "files":[
                  {
                     "archive_num":"1111",
                     "content":"sunt in culpa qui officia deserunt mollit 
anim id est laborum",
                     "type":"Document"
                  },
                  {
                     "archive_num":"333",
                     "content":"$1000",
                     "type":"Bill"
                  }
               ],
               "name":"Foldy folder",
               "secrecy":6
            }
         ],
         "height":2,
         "width":10
      },
      {
         "added":"2011-06-17",
         "color":"white",
         "floor":10,
         "folders":[
            {
               "files":[
                  {
                     "archive_num":"87878",
                     "content":"Funny picture",
                     "type":"Picture"
                  }
               ],
               "name":"Fun pics",
               "secrecy":0
            },
            {
               "files":[
                  {
                     "archive_num":"23423424",
                     "content":"Photo of some big room",
                     "type":"Photo"
                  }
               ],
               "name":"Old folder",
               "secrecy":3
            }
         ],
         "height":1,
         "width":4
      }
   ]
}

This means that we need whole tree of entries, gathered in one root called 'Cabinets'.

Installing is easy and usual - pip install django-tastypie.

After reading docs we create first resource that looks like:
from tastypie.resources import ModelResource

from app.models import Cabinet


class CabinetResource(ModelResource):
    class Meta:
        queryset = Cabinet.objects.all()

Docs say that we should put this in api.py file in app folder.

Then we should create url handler:
from app.api import CabinetResource

cabinet_resource = CabinetResource()

urlpatterns = patterns('',
    ...
    url(r'^api/', include(cabinet_resource.urls))
    ...
)

This creates a resource at /api/cabinet which returns:
{
   "meta":{
      "limit":20,
      "next":null,
      "offset":0,
      "previous":null,
      "total_count":2
   },
   "objects":[
      {
         "added":"2011-06-17",
         "color":"black",
         "floor":2,
         "height":2,
         "id":"1",
         "resource_uri":"/api/cabinet/1/",
         "width":10
      },
      {
         "added":"2011-06-17",
         "color":"white",
         "floor":10,
         "height":1,
         "id":"2",
         "resource_uri":"/api/cabinet/2/",
         "width":4
      }
   ]
}

Good start but we need more!

We got Meta object, that we don't want to get :), we have resource_uri and id, which we didn't ask either, and most important - we want elements, connected to our Cabinets.

But first let's deal with URI of our resource. Our task is clear - URI must be api/cabinet/list. Resource Meta class has field resource_name, which is clearly described in docs. By default resource name is generated from resource class name by removing word Resource and lowercasing it. But you can specify your own resource name and nothing keeps you from making it as custom as you want, for example - list/cabinet (api part is defined at urls.py), right what we need!

While adding subelements to response I tried some weird hacks. But it turned out to be quite simple. tastypie doesn't make connections automatically. Authors follow the principle "Explicit is better then implicit". So every connection of your resource must be done manually.
Connections are established between resources, not models. So here's a resource for Cabinet's child class - Folder:
class FolderResource(ModelResource):
    class Meta:
        queryset = Folder.objects.all()

Now we need to add field to CabinetResource, what result in something like:
class CabinetResource(ModelResource):
    folders = fields.ToManyField('app.api.FolderResource',
                                 'folders', full=True)
    class Meta:
        .....   

Optional parameter full is False by default. In this state folders subelement will only contain links to it's respective Folder resources. To work this way you need to extend your urlhandlers with FolderResource urls. This is described quite well in official docs. But in our case we need to show not just a link, but whole object's content. To achieve it we set full=True. Here's little trick I've noticed while was writing this post. If you don't specify urlhandlers for subelements (Folder in our case) resource_uri parameter for subelements will be empty. So nobody would have a way to get your objects (via this API, I mean)

Having always empty parameter isn't cool. So I wanted to turn them off. First I tried hackish way, deleting this parameter manually on response generation step. But then I noticed a parameter include_resource_uri which prevents adding resource_uri parameter to object.

And at last we would like to remove field id. Showing to the world inner data like ID of object in database is potentially dangerous. This could be done with fields or excludes parameters, that might be familiar to you from Django Forms, that use the same principle. To hide id field we add excludes = ['id'] to our Meta classes.

So now our api.py file is:
from tastypie.resources import ModelResource
from tastypie import fields

from app.models import Cabinet, Folder


class FolderResource(ModelResource):
    class Meta:
        queryset = Folder.objects.all()
        excludes = ['id']
        include_resource_uri = False


class CabinetResource(ModelResource):
    folders = fields.ToManyField('app.api.FolderResource',
                                 'folders', full=True)

    class Meta:
        queryset = Cabinet.objects.all()
        resource_name = 'cabinet/list'
        excludes = ['id']
        include_resource_uri = False

And response at /api/cabinet/list is
{
   "meta":{
      "limit":20,
      "next":null,
      "offset":0,
      "previous":null,
      "total_count":2
   },
   "objects":[
      {
         "added":"2011-06-17",
         "color":"black",
         "floor":2,
         "folders":[
            {
               "name":"XFiles",
               "secrecy":10
            },
            {
               "name":"Foldy folder",
               "secrecy":6
            }
         ],
         "height":2,
         "width":10
      },
      {
         "added":"2011-06-17",
         "color":"white",
         "floor":10,
         "folders":[
            {
               "name":"Fun pics",
               "secrecy":0
            },
            {
               "name":"Old folder",
               "secrecy":3
            }
         ],
         "height":1,
         "width":4
      }
   ]
}

Next step is to add subelements of Folders - Files. We already know how to do it, so let's make it quick.

Adding a resource:
class FileResource(ModelResource):
    class Meta:
        queryset = File.objects.all()
        excludes = ['id']
        include_resource_uri = False

And a field to Folder resource:
class FolderResource(ModelResource):
    files = fields.ToManyField('app.api.FileResource',
                               'files', full=True)

    class Meta:
       ...

And now we have a response like this (excerpt):
....
"objects":[
      {
         "added":"2011-06-17",
         "color":"black",
         "floor":2,
         "folder":[
            {
               "file":[
                  {
                     "archive_num":"221122",
                     "content":"Lorem ipsum dolor sit amet, consectetur"
                  }
               ],
               "name":"XFiles",
               "secrecy":10
            },
            {
               "file":[
                  {
                     "archive_num":"1111",
                     "content":"Duis aute irure dolor in reprehenderit
in voluptate velit esse"
                  },
                  {
                     "archive_num":"333",
                     "content":"$1000"
                  }
               ],
               "name":"Foldy folder",
               "secrecy":6
            }
         ],
         "height":2,
         "width":10
      },
....

Almost there. One issue left about Files model. It has field FileType that tells us quite useful information. It is implemented as ForeighKey, but our specification tells us to integrate it flat, without creating another sublevel. What we can do with it? Let's first create regular subresource.
class FileTypeResource(ModelResource):
    class Meta:
        queryset = FileType.objects.all()


class FileResource(ModelResource):
    type = fields.ToOneField('app.api.FileTypeResource',
                             'type', full=True)

    class Meta:
        ...

That responses with
    ...
"files":[
    {
        "archive_num":"221122",
        "content":"Lorem ipsum dolor sit amet, consectetur adipisicing elit",
        "type":{
            "id":"4",
            "resource_uri":"",
            "type":"Evidence"
        }
    }
 ],
...

We have
"type":{
    "id":"4",
    "resource_uri":"",
    "type":"Evidence"
 }

but we want only "type":"Evidence".

Hopefully tastypie has quite a lot of handy hooks that you can use to modify default behaviour. What we need now is dehydrate method. It is called every time when object is prepared for serialization. dehydrate receives two parameters:
  • self - obviously, current resource object link
  • bundle - object of Bundle class. It has two main fields:
    • obj - a link to object that is being serialized
    • data - dictionary with content of obj prepared for serialization
By default dehydrate simply returns bundle, which is afterwards serialized into JSON dictionary (or other structure with subelements if response is not JSON). So, basically, everything we need is dehydrate to return one bit of data - FileType.type
class FileTypeResource(ModelResource):
    class Meta:
        queryset = FileType.objects.all()

    def dehydrate(self, bundle):
        return bundle.data['type']

dehydrate is the right place to manipulate with data you're going to return to your client. Let's deal with what is left! Default response consist object meta. It is generally useful to have some meta data attached to response. Without it things can become confusing when using offsets or other request modifiers. Nonetheless, sometimes we just have to get rid of it. There's a method for that! Method is called alter_list_data_to_serialize and it starts after all dehydrations, right before serialization. We have to add it to our root CabinetResource. :
    def alter_list_data_to_serialize(self, request, data_dict):
        if isinstance(data_dict, dict):
            if 'meta' in data_dict:
                # Get rid of the "meta".
                del(data_dict['meta'])

        return data_dict

To be clear, dehydrate works on per-resource level, when alter_list_data_to_serialize works with results of dehydrate — generated python lists and dicts. And the last thing. Now root element is called 'objects' and I don't like it. To rename we add following to alter_list_data_to_serialize
    ...
    def alter_list_data_to_serialize(self, request, data_dict):
        if isinstance(data_dict, dict):
            if 'meta' in data_dict:
                # Get rid of the "meta".
                del(data_dict['meta'])
                # Rename the objects.
                data_dict['cabinets'] = copy.copy(data_dict['objects'])
                del(data_dict['objects'])
        return data_dict

Renaming is simple, though I still think there must be some option to set it without doing not-so-good things like this. That's it. There's a ton of features I didn't cover. This post is already much larger then I expected. I have to stop here or I will never publish it :)

No comments:

Post a Comment

Thanks for your comment!
Come back and check response later.

Post a Comment