|
|
||||||||||||||||||
|
|
||||||||||||||||||
![]() |
![]() |
Issue 5 - Revision 7 / April 20, 2004
|
|||
|
REST, Quixote, and Python - - - - - - - - - - - - By Dave Kuhlman | February 3, 2003 Representational State Transfer (REST) REST is an architectural style for the design of message-passing systems; most notably, it's the architectural style expressed by HTTP and the World Wide Web. Knowledge of and familiarity with REST priniciples is useful 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 (a Python plug-in for AOLserver), and Quixote (a Python web application framework). But most of this article will apply equally well to Quixote applications deployed on other web servers, such as Apache or medusa. The guidance REST offers to building web applications comes mostly in the form of restrictions, which is appropriate because one of the 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. 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:
In a number of respects, PyWX is an especially strong platform for developing applications guided by the REST architecture. In particular, PyWX allows manipulation of resources and representations. PyWX provides especially strong access to resources. And REST is all about delivering representations of resources. Relational databases are supported through the database API provided by AOLserver and exposed through PyWX. And, Python is an excellent glue language, providing access to C libraries, XML parcers, XML-RPC, and much more. Python provides exceptionally strong features, both in terms of the logic and formatting capabilities, needed to generate representations. We will also see below how Quixote adds even stronger formatting capabilities for producing representations. This section provides some vague guidelines for implementing RESTful applications on top of PyWX (and possibly Quixote). REST is an architectural style. It is not, for example, a toolset or framework that would force you to iron clad set of rules. The web application you will construct will be more or less RESTful, depending on how closely you follow REST principles. And if you already know the tools you will build on (AOLserver, PyWX, and Quixote, for example), then building your application in the REST style will be mostly obvious. With those reservations in mind, consider these guidelines: Identify your significant resources. REST is all about delivering resources, about making those resources accessible across the Web. Your first step should be to identify the resources that you wish to expose and to specify which information in those resources you are attempting to make visible. Implement the Python code that provides access to the resources. You should also explore the AOLserver API that is exposed by PyWX, looking for the access to you resources that it provides. And, remember, PyWX also exposes AOLserver's Tcl API. Implement the Python code that generates the representations of the resources. Factoring your code into reusable Python modules and classes is a good way to go here. Try to think in terms of user interface classes: for example, a class that encapsulates a resource and contains methods that produce representations of different kinds. Quixote has three elements that help to develop RESTful applications.
So how do you develop a RESTful application with Quixote?
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. I use PTL and templates whenever there is a high ratio of boilerplate to generated text, or 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. Some Simple ExamplesThis section provides explanation and commentary on these examples. DrillDown is a standard REST pattern. This example shows a list of food 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. The recipes are stored in the file data.py from which they can be imported:
# 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',
]
}
}
A set of recipes is are stored in a dictionary. The keys in the dictionary are names of recipes. The values are recipes. Each recipe is a dictionary with three entries: (1) the name of the recipe; (2) the ingredients; and (3) instructions for preparing the recipe. The Quixote dispatcher is in __init__.py. It exports the modules that serve up a list of recipes (module recipelist), (2) serves up a recipe (module recipe), and (3) provides recipe details (ingredients and instructions).
# __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>
"""
The __init__ module dispatches requests to submodules by exporting recipelist and recipe in the magic variable _q_exports. Quixote does the rest. The index page is produced by function _q_index, which is automatically exposed to Quixote and which Quixote uses as the default page. The recipelist module is a Quixote template file, with a ptl extension. It displays a list of recipes and URIs for each one. The URIs are used to drill down to the recipe itself, to a list of ingredients, or to a list of instructions.
# 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>
"""
Since _q_index is a template, there is no need to explicitly return its content. Any text in a Quixote template is automatically returned. Embedded in the template is a Python for-loop that inserts the recipe name and URIs to be used to access the recipe, the ingredient list, and the instruction list. The recipe module displays a single recipe.
# 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
recipe provides a _q_getname function that takes the last (right-most) component of the URI and uses it to select a recipe. This function, in effect, dispatches to individual recipes. showRecipe() formats and displays the recipe. It creates a list of lines of text, then joins them into a single string (inserting a newline between each line). Using list append and join is faster that string append. Other alternatives would be to use either module StringIO or module cStringIO from the Python standard library. The details module is a Python file that displays either> (1) the ingredients or (2) the instructions for a recipe. In effect, it drills down into either ingredients or instructions.
# 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:
For the purposes of this discussion, a REST client is written in Python; communicates with a server via HTTP; sends and receives XML messages, that is, the server passes a representation of a resource in XML and the client passes content to the server by filling in an XML "form" and returning the XML form as the content of a POST request. The client's building blocks include:
I'm assuming several things. First, that the server responds to HTTP requests with XML content. Second, that the implementor of the server (application) provides an W3C XML Schema definition of the XML messages passed between server and client.These XML Schema definitions are compatible with generateDS.py. In order to build a client, given these assumptions, take the following steps.
Because of its flexibility and expressiveness, Python is a good fit for building web services and applications. Using Quixote is a particularly good fit for building them in a RESTful way.
|
||||||||||||||||||||||||||||||||||||||||
|
Py is committed to bringing you great Python Articles. | |||||||||||||||||||||||||||||||||||||||||
![]() |
Reproduction of material from any of PyZine's pages without prior written permission is strictly prohibited. Copyright 2003 - 2005 PyZine |
|