Grok's songlist application
I've been following with interest a number of posts that talk about creating a simple REST-based web application that persists the number of plays various songs have had. Here's the history:
A pure WSGI application: http://www.eflorenzano.com/blog/post/writing-blazing-fast-infinitely-scalable-pure-wsgi/
A easier to read but slower CherryPy application: http://blog.dowski.com/2009/01/08/http-utilities-with-cherrypy/
An app using RESTish: http://dev.timparkin.co.uk/2009/01/happy-medium-from-wsgi-to-cherrypy.html
And a followup using Werkzeug: http://dev.timparkin.co.uk/2009/01/werkzeug-and-werkzeugish-example.html
First a few comments on the protocol: the RESTfulness of this protocol could definitely be improved. As was remarked by a comment on the original post, REST-based apps return lists of URLs in overviews and the overview in this app doesn't. I'd also modify the way new songs get registered with the app and make that a POST request on the song container, instead of implicity creating such resources by the mere act of traversing. I haven't made any such modifications to the protocol even though the last improvement would simplify my code as it'd let me get rid of the
I thought it'd be interesting to implement the same using Grok (1.0a1). Without more ado, here it is:
import grok from zope.app.publication.interfaces import IBeforeTraverseEvent class App(grok.Application, grok.Container): def traverse(self, id): if id not in self: song = self[id] = Song() return song return self[id] @grok.subscribe(App, IBeforeTraverseEvent) def applySkin(obj, event): # make rest layer the default if necessary if not IRESTLayer.providedBy(event.request): grok.util.applySkin(event.request, IRESTLayer, grok.IRESTSkinType) class IRESTLayer(grok.IRESTLayer): grok.restskin('main') class AppREST(grok.REST): grok.context(App) grok.layer(IRESTLayer) def GET(self): return ','.join(['%s=%s' % (k, v.count) for k, v in self.context.items()]) def DELETE(self): for key in list(self.context.keys()): del self.context[key] class Song(grok.Model): def __init__(self): self.count = 0 class SongREST(grok.REST): grok.context(Song) grok.layer(IRESTLayer) def GET(self): return str(self.context.count) def POST(self): self.context.count += 1 return str(self.context.count)
Before I go into the good news (Grok gives you two important features here that the other frameworks examples don't have), first the bad news.
It's about 10 lines longer than the CherryPy and Restish examples (the Werkzeug example is shorter still but rather low-level).
Performance-wise it's the slowest of the bunch, on my machine, which is comparable to the machines of the others (in my case an Intel Core 2 Duo 2400 MHz Linux box) I get about 580 requests per second (not too shabby):
Concurrency Level: 1 Time taken for tests: 17.39500 seconds Complete requests: 10000 Failed requests: 0 Write errors: 0 Total transferred: 2500000 bytes HTML transferred: 10000 bytes Requests per second: 586.87 [#/sec] (mean) Time per request: 1.704 [ms] (mean) Time per request: 1.704 [ms] (mean, across all concurrent requests) Transfer rate: 143.26 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.0 0 0 Processing: 1 1 1.1 1 61 Waiting: 0 1 1.0 1 60 Total: 1 1 1.1 1 61
Now to the good news. The other examples all store their information in a global variable in the form of a dictionary. Gulp. This application actually features true persistence. When you restart your server, the counted information is still there - it's in the database. This means that the benchmark actually includes database access (to the ZODB).
You may have noted that there isn't much database access code there. That's because the ZODB allows transparent persistence of Python objects. This actually made it trivially easy to write this application with true persistence.
The other has more to do with framework power. This is not low-level code, and that shouldn't be underestimated. We have available to us a framework that offers a ton of features, both out of the box and as extensions. I'll talk about some out-of-the-box features here.
Grok's REST system allows you to extend existing (persistent) objects in your application with RESTful behavior. These objects can retain their original UI entirely. If I actually left out the applySkin code above, the RESTful URLs would look like this:
and the normal URLs to the objects would look like this:
This means that you could give your app both a normal UI as well as REST-based access. In the example I've used the applySkin line to consolidate them into a single URL space however.
In addition, Grok's REST support also features a powerful and built-in security system. You can give each access a permission by adding the line
@grok.require('some.permission') def GET(self): ...
Grok also takes care of URL management for you out of the box. The objects in the app all have a URL automatically. Should I want to display the URL of each song object in addition to its count I'd change the GET line of
SongREST to this:
def GET(self): return grok.url(self.request, self.context) + ' ' + str(self.context.count)
If you want to see a much bigger REST-ful app I've written with Grok for a customer (ID-StudioLab at the Technical University of Delft), please check out imageSTORE (it comes with a lot of doctests). It's a RESTful persistent storage of image information.