Dave Kuhlman
http://www.rexx.com/~dkuhlman
Email: [email protected]
Copyright (c) 2003 Dave Kuhlman
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
REST is an architecture for the design of Web applications and Web services.
This document describes a specific application delivery model, REST, and techniques for implementing Web applications that follow the REST model on top of AOLserver, PyWX, and Quixote. Much of this document should be meaningful and useful for those who intend to develop using Quixote on top of other Web servers, e.g. Apache.
What is REST? -- REST is a set of guidelines for constructing Web applications. Those guidelines are mostly restrictions. This (restricting our options) is appropriate because one of our goals is to develop Web applications that are more simple and more understandable both by the developers of the application and by its clients and users.
A central document on REST is Architectural Styles and the Design of Network-based Software Architectures.
And, here are several other links that will lead you to plenty of additional information on REST:
And, here are some of those guidelines or restrictions:
And here are some less important guidelines:
Conceptually, what's the big deal anyway? Is it a big deal? I believe the shift from standard Web application to a RESTful application is a significant shift. Let me try to show why.
In a number of respects, PyWX is an especially strong platform for developing applications guided by the REST architecture. In particular:
Summary -- PyWX (and Python) provide strengths in both (1) access to resources and (2) formatting representations.
This section provides some vague guidelines for implementing RESTful applications on top of PyWX (and possibly Quixote). The guidelines are vague for at least two reasons:
With those reservations, here are a few guidelines:
Quixote provides (at least) two capabilities that are of value in developing REST servers:
The simple answer is to write a Quixote application, but to eliminate the dependence on session information. (All state information is carried on the client, not the server, remember.)
A more complete answer would involve the following steps, most of which are standard REST strategy:
import Ns import updateformsub def update(request, name): conn = Ns.GetConn() server = conn.Server() pool = Ns.DbPoolDefault(server) dbHandle = Ns.DbHandle(pool) response = request.response response.set_header('Content-type', 'text/xml; charset=iso-8859-1') # The PyWX way to get request content. #buff = PyWX_buffer.GetBuff() # The Quixote way to get request content. buff = request.stdin content = buff.read() root = updateformsub.parseString(content) name = root.getName() descrip = root.getDescription() # Update the database. updateDB(dbHandle, name, descrip) if descrip == '[delete]': root.setDescription('[deleted]') contentstream = StringIO.StringIO() root.export(contentstream, 0) content = contentstream.getvalue() contentstream.close() return content
A note about PTL/templates vs. vanilla Python functions -- In some cases Quixote templates will enable you to write cleaner code than plain Python functions, although Python makes it very easy to generate text, also. I'd look to using PTL and templates whenever (1) there is a high ratio of boilerplate to calculated text or (2) when you can make things convenient by composing templates. For example, here is a template that composes a header, a body, and a footer:
template genHeader(request, name): """<html> ... """ template genFooter(request, name): """ ... </html> """ template genContent(request, name): genHeader(request, name) genBody(request, name) genFooter(request, name)
where each of genHeader, genBody, and genFooter return its own bit of content. For more on this, see the comments on refactoring in Greg Ward's article: http://www.linuxjournal.com/article.php?sid=6178.
The following server-side examples are available in rest_examples.zip. These examples have been tested with AOLserver 3.5.1, PyWX 1.0b2, and Quixote 0.5.1.
Handler scripts are included for AOLserver and PyWX. Look under directory handlers. These won't mean much to you if you do not use AOLserver.
If you attempt to use these examples with Apache, you will most likely need to create your own Quixote driver scripts.
This section provides explanation and commentary on these examples.
DrillDown is a standard REST pattern. This example shows a list of recipes and enables the user to ``drill down'' by selecting a recipe for display. The user can also select and display either (1) the ingredients or (2) the instructions for a recipe.
# data.py recipes = { 'greensalad': { 'name': "Green Salad", 'ingredients': ['lettuce', 'parsley', 'bellpepper', 'radish', 'dressing'], 'instructions': [ 'Wash all ingredients.', 'Cut up lettuce.', 'Chop parsley, bellpepper, and radish.', 'Add dressing.', 'Toss', ] }, 'fruitsalad': { 'name': "Fruit Salad", 'ingredients': ['apples', 'oranges', 'banana', 'plum', 'pomegranate'], 'instructions': [ 'Wash apples and plums. Cut into chunks.', 'Peel oranges. Break in sections and cut in half.', 'Shell pomegranates, submerging under water to avoid splatters and 'Mix and serve', ] } }
Comments:
# __init__.py _q_exports = ['recipelist', 'recipe', 'details'] def _q_index(request): return IndexMsg IndexMsg = """\ <html> <head><title>Index</title></head> <body> <div align="center"><h1>Index</h1></div> <p><a href="recipelist">Show list of recipes</a></p> </body> </html> """
Comments:
# recipelist.ptl from data import recipes _q_exports = [] template _q_index(request): """\ <html> <head><title>Recipe List</title></head> <body> <p>A list of recipes:</p> <table border="1"> <tr> <th>Recipe</th> <th>Ingredients</th> <th>Instructions</th> """ for key in recipes.keys(): ' <tr>\n' ' <td><a href="/DrillDown/recipe/%s">%s</a></td>\n' % \ (key, recipes[key]['name']) ' <td><a href="/DrillDown/details/ingredients/%s">%s</a></td>\n' % \ (key, recipes[key]['name']) ' <td><a href="/DrillDown/details/instructions/%s">%s</a></td>\n' % \ (key, recipes[key]['name']) ' </tr>\n' """\ </table> <hr> <p><a href="/DrillDown/">Return to index</a></p> </body> </html> """
# recipe.py import string from data import recipes _q_exports = [] def _q_getname(request, name): if not name.lower() in recipes.keys(): return NosuchrecipeMsg % name return showRecipe(request, name) NosuchrecipeMsg = """\ <html> <head><title>Error</title></head> <body> <div align="center"><h1>Error</h1></div> <p>No such recipe: %s</p> </body> </html> """ def showRecipe(request, name): recipe = recipes[name] rname = recipe['name'] doc = ['<html>', '<head><title>Recipe -- %s</title><head>' % rname, '<body>' '<div align="center"><h1>Recipe -- %s</h1></div>' % rname, '<h2>Ingredients</h2>', '<ul>' ] for item in recipe['ingredients']: doc.append(' <li>%s</li>' % item) doc.append('</ul>') doc.append('<hr>') doc.append('<h2>Instructions</h2>') doc.append(' <ol>') for item in recipe['instructions']: doc.append(' <li>%s</li>' % item) doc.append(' </ol>') doc.append(' <hr>') doc.append(' <p><a href="/DrillDown/recipelist">Return to recipe list</a></p>') doc.append(' <p><a href="/DrillDown/">Return to index</a></p>') doc.append('</body>') doc.append('</html>') content = string.join(doc, '\n') return content
Comments:
# details.py import string from quixote.errors import TraversalError from data import recipes _q_exports = [] def _q_getname(request, name): if name == 'ingredients': return Ingredients() elif name == 'instructions': return Instructions() else: raise TraversalError('No such detail: %s' % name) class Ingredients: _q_exports = [] def __init__(self): pass def show(self, request, name): recipe = recipes[name] rname = recipe['name'] doc = [ '<html>' '<head><title>Drilldown - Details - Ingredientss</title></head>', '<body>', ' <div align="center"><h1>Recipe -- %s</h1></div>' % rname, ' <h2>Ingredients</h2>', ' <ol>' ] for item in recipe['ingredients']: doc.append(' <li>%s</li>' % item) doc.append(' </ol>') doc.append(' <hr>') doc.append(' <p><a href="/DrillDown/recipelist">Return to recipe list</a></p>') doc.append(' <p><a href="/DrillDown/">Return to index</a></p>') doc.append('</body>') doc.append('</html>') content = string.join(doc, '\n') return content def _q_getname(self, request, name): if not recipes.has_key(name): raise TraversalError('No such recipe: %s' % name) return self.show(request, name) class Instructions: _q_exports = [] def __init__(self): pass def show(self, request, name): recipe = recipes[name] rname = recipe['name'] doc = [ '<html>' '<head><title>Drilldown - Details - Instructions</title></head>', '<body>' '<div align="center"><h1>Recipe -- %s</h1></div>' % rname, '<h2>Instructions</h2>', '<ol>' ] for item in recipe['instructions']: doc.append(' <li>%s</li>' % item) doc.append('</ol>') doc.append(' <hr>') doc.append(' <p><a href="/DrillDown/recipelist">Return to recipe list</a></p>') doc.append(' <p><a href="/DrillDown/">Return to index</a></p>') doc.append('</body>') doc.append('</html>') content = string.join(doc, '\n') return content def _q_getname(self, request, name): if not recipes.has_key(name): raise TraversalError('No such recipe: %s' % name) return self.show(request, name)
Comments:
ListAndDetail is another ``drill down'' example. This one retrieves its list of items from a relational database.
This example shows the combination of features from Quixote and PyWX:
The database itself is stored in PostgreSQL. The plants database table has two columns: (1) plant name and (2) plant description.
Here is the implementation along with some explanation and commentary.
# __init__.py _q_exports = ['index', 'list', 'detail'] from index import index def _q_index(request): return index(request)
# index.ptl def index(request): accept = request.get_header('accept', 'text/html') if accept.find('text/html') >= 0: return index_html(request) else: return index_xml(request) template index_html(request): """<html> <head><title>ListAndDetails - Index</title></head> <body> <div align="center"><h1>ListAndDetails</h1></div> <p><a href="/ListAndDetails/list/">List of plants</a></p> </body> </html> """ template index_xml(request): """\ <?xml version="1.0"?> <plants> <link>/ListAndDetails/list/</link> </plants> """
Comments:
# list.ptl import string import Ns _q_exports = [] def _q_index(request): conn = Ns.GetConn() server = conn.Server() pool = Ns.DbPoolDefault(server) dbHandle = Ns.DbHandle(pool) accept = request.get_header('accept', 'text/html') if accept.find('text/html') >= 0: return plants_list_html(request, dbHandle) else: return plants_list_xml(request, dbHandle) template plants_list_html(request, dbHandle): """<html> <head><title>ListAndDetails - Plant List</title></head> <body> <div align="center"><h1>Plant List</h1></div> <p><a href="/ListAndDetails/">Return to index</a></p> <table border="1"> """ generate_plants_html(request, dbHandle) """ </table> </body> </html> """ def generate_plants_html(request, dbHandle): rowSet = dbHandle.Select('select * from plants order by name') doc = [] idx = 0 while dbHandle.GetRow(rowSet) == Ns.OK: idx += 1 values = rowSet.values() name = values[0] doc.append(' <tr>') doc.append(' <td>%d</td>' % idx) doc.append(' <td>%s</td>' % name) doc.append(' <td><a href="/ListAndDetails/detail/%s">%s</a></td>' % \ (name, name)) doc.append(' </tr>') content = string.join(doc, '\n') return content template plants_list_xml(request, dbHandle): """\ <?xml version="1.0"?> <plants> """ generate_plants_xml(request, dbHandle) """ </plants> """ def generate_plants_xml(request, dbHandle): rowSet = dbHandle.Select('select * from plants order by name') doc = [] idx = 0 while dbHandle.GetRow(rowSet) == Ns.OK: idx += 1 values = rowSet.values() name = values[0] descrip = values[1] doc.append(' <index>%d</index>' % idx) doc.append(' <name>%s</name>' % name) doc.append(' <link>/ListAndDetails/detail/%s</link>' % name) doc.append(' </plant>') content = string.join(doc, '\n') return content
Comments:
Ns
).
# detail.ptl import Ns import quixote _q_exports = [] def _q_getname(request, name): conn = Ns.GetConn() server = conn.Server() pool = Ns.DbPoolDefault(server) dbHandle = Ns.DbHandle(pool) query = "select * from plants where name='%s'" % name rowSet = dbHandle.Select(query) if dbHandle.GetRow(rowSet) != Ns.OK: raise quixote.errors.TraversalError("No such plant: %r" % name) values = rowSet.values() descrip = values[1] if request.get_header('accept', 'text/html').find('text/html') >= 0: return detail_html(name, descrip) else: return detail_xml(name, descrip) template detail_html(name, descrip): """<html> <head><title>ListAndDetails - Plant Detail</title></head> <body> <div align="center"><h1>Plant Detail</h1></div> <p>Plant: %s</p> <p>Description: %s</p> <hr> <p><a href="/ListAndDetails/list/">Return to list of plants</a></p> <p><a href="/ListAndDetails/">Return to index</a></p> </body> </html> """ % (name, descrip) template detail_xml(name, descrip): """\ <?xml version="1.0"?> <plantdetail> <name>%s</name> <description>%s</description> </plantdetail> """ % (name, descrip) def _q_exception_handler(request, exc): if request.get_header('accept', 'text/html').find('text/html') >= 0: return quixote.errors.default_exception_handler(request, exc) else: return """\ <?xml version="1.0"?> <errormsg> <msg>%s</msg> </errormsg> """ % exc
Comments:
This section describes a small amount of support for developing REST clients in Python.
For the purposes of this discussion, a REST client:
What to build upon:
Assumptions:
Client development strategy -- Here are a few steps that you can follow::
import plantlistsub def showlist(line): host = 'warbler:8081' url = '/UpdateRecord/recordlist/' headers = {'Accept': 'text/xml'} params = {} try: conn = httplib.HTTPConnection(host) req = conn.request('GET', url, params, headers) except socket.error: print "Can't connect to server: warbler:8081" return res = conn.getresponse() status = res.status content = res.read() conn.close() if status != 200: print "Can't get plant list. status: %d" % status return root = plantlistsub.parseString(content) print 'Plant list:' for plant in root.getPlant(): print ' Plant # %s:' % plant.getIndex() print ' Name: %s' % plant.getName() print ' Description: "%s"' % plant.getDescription() print ' Record link: %s' % plant.getRecordlink() print ' Update link: %s' % plant.getUpdatelink()
root = plantlistsub.parseString(content)
for plant in root.getPlant(): print ' Plant # %s:' % plant.getIndex() print ' Name: %s' % plant.getName() print ' Description: "%s"' % plant.getDescription() print ' Record link: %s' % plant.getRecordlink() print ' Update link: %s' % plant.getUpdatelink()
newdescrip = raw_input('New description: ') if newdescrip != '[quit]': root.setDescription(newdescrip) contentstream = StringIO.StringIO() root.export(contentstream, 0) content = contentstream.getvalue() contentstream.close() url = root.getSubmitlink() length = len(content) conn = httplib.HTTPConnection(host) req = conn.request('POST', url, content, headers) res = conn.getresponse() status = res.status content = res.read() conn.close()
Here is a more complete (though still trivial) example which combines the fragments above into a client program.
#!/usr/bin/env python import sys import cmd import httplib, socket import updateformsub import plantlistsub import StringIO HELP_TEXT = """\ Commands: showlist -- Show list of plant names. show <name> -- Show plant <name>. update <name> -- Update description for plant <name>. quit -- Exit. help -- Show this help. """ class RestCmd(cmd.Cmd): prompt = '>>> ' intro = "Type '?' for help." def __init__(self): cmd.Cmd.__init__(self) def do_help(self, line): print HELP_TEXT def do_quit(self, line): sys.exit(1) def emptyline(self): print "Enter a command or type '?' for help." def request_http(self, function, host, url, params, headers): try: conn = httplib.HTTPConnection(host) req = conn.request(function, url, params, headers) except socket.error: print "Can't connect to server: %s" % host return res = conn.getresponse() status = res.status content = None if status == 200: content = res.read() conn.close() return (status, content) def do_showlist(self, line): host = 'warbler:8081' url = '/UpdateRecord/recordlist/' headers = {'Accept': 'text/xml'} params = {} status, content = self.request_http('GET', host, url, params, headers) if status != 200: print "Can't get plant list. status: %d" % status return root = plantlistsub.parseString(content) print 'Plant list:' for plant in root.getPlant(): print ' Plant # %s:' % plant.getIndex() print ' Name: %s' % plant.getName() print ' Description: "%s"' % plant.getDescription() print ' Record link: %s' % plant.getRecordlink() print ' Update link: %s' % plant.getUpdatelink() def do_show(self, line): name = None if line: args = line.split() if len(args) > 0: name = args[0] if not name: print '*** Missing plant name.' return host = 'warbler:8081' url = '/UpdateRecord/updateform/%s' % name headers = {'Accept': 'text/xml'} params = {} status, content = self.request_http('GET', host, url, params, headers) if status != 200: print "Can't get description for name: %s" % name root = updateformsub.parseString(content) descrip = root.getDescription() print 'Name: %s' % name print 'Description: %s' % descrip def do_update(self, line): name = None if line: args = line.split() if len(args) > 0: name = args[0] if not name: print '*** Missing plant name.' return host = 'warbler:8081' url = '/UpdateRecord/updateform/%s' % name headers = {'Accept': 'text/xml'} params = {} status, content = self.request_http('GET', host, url, params, headers) if status != 200: print "Can't get description for name: %s" % name root = updateformsub.parseString(content) olddescrip = root.getDescription() print 'Old description: %s' % olddescrip print 'Enter new description ("[quit]" to skip update).' newdescrip = raw_input('New description: ') if newdescrip != '[quit]': root.setDescription(newdescrip) contentstream = StringIO.StringIO() root.export(contentstream, 0) content = contentstream.getvalue() contentstream.close() url = root.getSubmitlink() status, content = self.request_http('POST', host, url, \ content, headers) print 'status: %d' % status USAGE_TEXT = """ Usage: python client1.py """ def usage(): print USAGE_TEXT sys.exit(-1) def main(): args = sys.argv[1:] if len(args) != 0: usage() interp = RestCmd() interp.cmdloop() if __name__ == '__main__': main() #import pdb #pdb.run('main()')
Comments and explanation:
Titus Brown, Brent Fulgham, Michael Haggerty -- The developers of PyWX. Titus is currently actively supporting PyWX and makes this possible.
And, thanks to the implementors of Quixote for producing an exceptionally usable application server that is so well suited for REST.
See Also:
This document was generated using the LaTeX2HTML translator.
LaTeX2HTML is Copyright © 1993, 1994, 1995, 1996, 1997, Nikos Drakos, Computer Based Learning Unit, University of Leeds, and Copyright © 1997, 1998, Ross Moore, Mathematics Department, Macquarie University, Sydney.
The application of LaTeX2HTML to the Python documentation has been heavily tailored by Fred L. Drake, Jr. Original navigation icons were contributed by Christopher Petrilli.