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: 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 traverse method. I thought it'd be interesting to implement the same using Grok (1.0a1). Without more ado, here it is: 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): 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: 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: 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.
(6) Sat Jan 10 2009 14:29 Grok's songlist application:
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)
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
http://localhost:8080/++rest++main/song/1
http://localhost:8080/main/song/1
@grok.require('some.permission')
def GET(self):
...
def GET(self):
return grok.url(self.request, self.context) + ' ' + str(self.context.count)
- Comments:
Posted by Matt Goodall at Sat Jan 10 2009 16:09
re restful interfaces ... you're correct that the list should contain URLs but POSTing to the song container to register a new song is surely wrong. A song needs a known name for a service like this to work.Sure, POST'ing to a song resource identified by a number is daft too, but that's because the original example is (presumably) deliberately simple in order to demonstrate implementing a simple service right down at the HTTP level. Still, it's a lot better than allocating a new ID every time a song is played ;-).A better solution would probably be analyse the song, build a unique id (one that would be the same for everyone!), and POST to /song/ to add to the play count. See http://en.wikipedia.org/wiki/MusicDNS, for instance.I'm not sure POSTing to a resource that might not strictly exist yet is very typical but I don't see a problem with it either.As for the example code, I agree that using a framework has advantages but then I would do - I'm the primary author of restish, http://pypi.python.org/pypi/restish ;-).I'm not going to compare grok with anything else, and I don't care enough about relatively small performance differences of such a simple service right now (plain WSGI will always win on speed and arguably lose on maintainability), but what's with the ++rest++ stuff in the URL? What's wrong with one set of URLs and content negotiation?
Posted by Martijn Faassen at Sat Jan 10 2009 20:19
Matt, you bring up a lot of good points here, mostly to do with REST.When I designed the imageSTORE protocol I studied the O'Reilly "RESTful Web Services" book a lot. It's not the ultimate dogma of REST of course - people's opinions differ a lot, but they have thought about this stuff. The creation pattern is often to POST to a factory resource, that then returns with a Location header in the response that has the URL of the new location in it. That factory resource can be the song container but indeed it doesn't have to be.Giving the client the responsibility of id creation may not be the right thing. Instead the idea might be to POST some information to the factory resource and let it figure out the proper id itself. That's one reason why the factory pattern is so useful.For an example of a restful protocol that uses POSTing to a collection URI for resource creation look at the atompub specification.This parallels web applications where you are frequently not in charge of assigning the id of a new information item either (though of course in some cases you are).I deliberately avoid content negotation for RESTful services, as I like to be able to browse through my RESTful service with a web browser (at least the read-only part of it) without having to specify manual headers. The "RESTful Web Services" book also recommends separate URLs for separate representation as well (even though it does consider content negotation as "restful" as well). The idea is to put as much as possible in the URL and as little as possible in the metadata. Metadata can get lost more easily as URIs get passed around a lot. With content negotation, you couldn't pass your REST URL to a web-based validator nor could you bookmark.Note that the ++rest++ pattern is not actually used in the example I posted above; in that case I collapse the namespace into a single one and there's no ++rest++ stuff in the URLs. I do the same in the imageSTORE, though there I do define a derivative ++rest++flash protocol that has some hacks in it that work around the badly broken HTTP implementation in Flash (you have to tunnel PUT and DELETE over POST for instance). This way the core REST protocol stays clean but Flash clients can at least use the hacked version by specifying ++rest++flash.I'm the primary author of Grok's REST support, by the way. :)
Posted by Christian Wyglendowski at Sat Jan 10 2009 22:11
Good post. It really has been interesting to see how this simple application has been implemented across the various frameworks/methodologies. Shortly after I posted my example I noticed that I had my CherryPy app logging to stdout in addition to the logging that spawning was already doing. I turned that off and it made a nice difference in the speed. It brought it up to par with the restish example at least, though still falling quite short of the pure WSGI version.Anyhow, lies, damn lies and benchmarks aside, this has still been interesting. The more frameworky solutions being slower but providing more things that you would actually use in production (logging in CherryPy, the persistence in your Grok example, etc) and the nice raw speed in the pure WSGI example.Out of curiosity, what server did you use to benchmark your application?
Posted by Martijn Faassen at Sat Jan 10 2009 22:23
Hm, the server, this changed very recently in Grok 1.0a1 from zserver to something WSGI-based. I think it's the paste HTTP server, which in turns hooks up to Zope 3's WSGI support, but don't shoot me if I get it wrong. :)
Posted by Christian Wyglendowski at Sun Jan 11 2009 05:19
Ah, ok. I know that Eric, Tim and I all used spawning* in our benchmarks which I am guessing might perform better than the Paste http server.* http://pypi.python.org/pypi/Spawning/0.7
Posted by Alec Munro at Mon Jan 12 2009 23:45
Fun!It's tremendously fascinating to me to see this discussion take place right now, as I think I am on the cusp of putting together a whole slew of web services. I'm most familiar with Grok, out of all of them, so I'm glad to see that represented, and it helps me have a basis for comparison.
