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.
render_json() function which converts an object to JSON and does all the right stuff to return it via WSGI.**struct. As if luck had anything to do with it. 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.
2006-10-09