RESTful JSON Server

Joe Gregorio

Now I've already introduced robaccia and wsgicollection so we'll build on top of those pieces. (Yes, I know, robaccia is my throw away web framework that I can't seem to throw away. Oh well.)

If you remember robaccia we have several parts; urls.py, view.py, model.py, and templates. In addition to the usual parts of robaccia we will need one additional piece, a library to serialize and deserialize JSON.

The model.py is very simple, not that it has to be, but just to keep the noise level down so we can concentrate on the important stuff:

from sqlalchemy import *
import dbconfig

recipe = Table('recipe', dbconfig.metadata,
             Column('id', Integer, primary_key=True),
             Column('title', String(200)),
             Column('instructions', String(30000)),
         )

And the urls.py is also simple since we are using wsgicollection:

import selector
import view

urls = selector.Selector()

urls.add('/cookbook/json/[{id}][;{noun}]', _ANY_=view.JSONCollection())

Note that we pick out both the 'id' and the 'noun' out of the URI matching even though we don't use the 'noun' at this point.

Now we can get to the meat of the program, our view.py. There are several things to note. The first is that 'recipe' is imported as 'table'. I did this to stress that the rest of the code really isn't specific to our 'recipe' table and could be used with any table.

import robaccia
import simplejson
from sqlalchemy import desc
from model import recipe as table
from wsgicollection import Collection

primary_key = table.primary_key.columns[0]

def _load_json(environ):
    entity = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH']))
    struct = simplejson.loads(entity)
    return dict([(k.encode('us-ascii'), v) for k,v in struct.iteritems()])


class JSONCollection(Collection):

    # GET /cookbook/json/ 
def list(self, environ, start_response):
        result = table.select().execute()    #1 
        struct = {
            "members":[{'href': "%d" % row.id, 
               'title': row.title} for row in result.fetchall()],
            "next": None}
        return robaccia.render_json(start_response, struct) #2
# POST /cookbook/json/ 
def create(self, environ, start_response):
        struct = _load_json(environ)        #3
        table.insert().execute(**struct)    #4

        start_response("201 Created", [])   #5
return []

    # GET /cookbook/json/{id}
def retrieve(self, environ, start_response):
        id = environ['selector.vars']['id']
        result = table.select(primary_key==id).execute()
        struct = dict(zip(result.keys, result.fetchone())) #6
return robaccia.render_json(start_response, struct)

    # PUT /cookbook/json/{id}
def update(self, environ, start_response):
        struct = _load_json(environ)
        id = environ['selector.vars']['id']
        table.update(primary_key==id).execute(struct) #7 

        start_response("200 OK", [])
        return []

    # DELETE /cookbook/json/{id}
def delete(self, environ, start_response):
        id = environ['selector.vars']['id']
        table.delete(primary_key==id).execute()

        start_response("200 OK", [])
        return []

Here are some notes on what's going on in this file. These correspond to the #N comments in the code above.

  1. Here we optimistically select everything from the database. We should do the paging if the number of entries is long.
  2. Here is the one addition to robaccia, the render_json() function which converts an object to JSON and does all the right stuff to return it via WSGI.
  3. We use our utility function to convert the incoming JSON entity into a native Python data struture. Note the asymmetry with 'render_json'. I don't like that asymmetry. Bah.
  4. We luck out and the dictionary that we use as a structure maps perfectly into what our database interface was expecting, that is we turn our dictionary into named parameters to the function call, thus the simple **struct. As if luck had anything to do with it.
  5. Bug, I don't return a Location: header like I said I would. Oops.
  6. Convert the results of the 'select' into a dictionary that we can then render to JSON. I told you luck had nothing to do with it.
  7. Again with the dictionary.

Can you believe that's it? Actually, for completeness, one more file to determine the exact database mapping via dbconfig.py.

from sqlalchemy import *

metadata = BoundMetaData('sqlite:///cookbook.db')

Now that's really all of it. We don't even have any templates since we are just serializing and deserializing data structures. To create the database run:

$ python manage.py create

And now we can access our application via main.cgi if we are running under CGI or:

$ python manage.py run

to test it on the local machine.

Next time we'll get into more client-side work, but for now here is a simple Python client that uses our new web service and runs over all the recipes and appends "on a stick" to each title, thus turning our cookbook into one more appropriate for the State Fair. I haven't done any fancy abstraction here, just a simple application of httplib2:

import httplib2
import simplejson
import urlparse

h = httplib2.Http(".cache")

BASE = "http://bitworking.org/projects/jep/cookbook/main.cgi/cookbook/json/"
# Get the collection
(resp, content) = h.request(BASE)
struct = simplejson.loads(content)

# Iterate over the members in the collection
# Update each member's title
for member in struct['members']:
    abs_member = urlparse.urljoin(BASE, member['href'])
    (resp, content) = h.request(abs_member)
    struct = simplejson.loads(content)
    title = struct['title'].split(" on a stick")[0]
    struct['title'] = title + " on a stick"
    (resp, content) = h.request(abs_member, 
       method="PUT", body=simplejson.dumps(struct))

# Add a new recipe to the collection
new_member = {
    "title": "Some random recipe",
    "instructions": "First, get a ..."
}

(resp, content) = h.request(BASE, 
    method="POST", body=simplejson.dumps(new_member))

The one thing to note with the code is that it carefully trims off any trailing "on a stick" from the title, thus allowing you to run the client more than once.

All the code here can be downloaded from http://bitworking.org/projects/jep/cookbook/. And yes, just in case you're wondering, I am loving the bzr. It dramatically reduces the distance between writing and sharing code. Add in a simple .htacces to pretty up the Apache directory listing and you're in business.

As promised, next time we'll get into some better client code.

comments powered by Disqus