====================
Patterns for Twisted
====================
:author: Dave Kuhlman
:address: dkuhlman@rexx.com
http://www.rexx.com/~dkuhlman
:revision: 1.0c
:date: Sept. 22, 2003
:copyright: Copyright (c) 2003 Dave Kuhlman. This documentation is
under The Artistic License:
http://www.opensource.org/licenses/artistic-license.
:abstract: This document describes and discusses several design
patterns for the development of Twisted Web applications.
.. sectnum:: :depth: 4
.. contents::
Introduction
============
This document contains notes and examples that describe several
programming patterns for Twisted, including patterns for deferred
operations, for database access, for a client of a text search
engine, and for an XML-RPC client.
I've called these "design patterns", although they are also
"implementation patterns" in an attempt to suggest that they are a
bit more abstract. My intent is to encourage you to use these
patterns both in the design of your Twisted Web application as
well as in implementing it (possibly by copying and editing the
examples providing).
These patterns may seem to have a `REST (REpresentational State
Transfer)`_ cast, and that is intentional. My aims are to help
those attempting to treat the Web as a resource, in contrast to
dealing with the Web as an extended computer using some style of
RPC (e.g. XML-RPC or SOAP). However, I'm also hoping that the
patterns described in this document are generic enough so that
they will be helpful in other styles of Web application as well.
.. _`REST (REpresentational State Transfer)`:
http://internet.conveyor.com/RESTwiki/moin.cgi/RestFaq
I've read the Twisted documentation on deferreds as closely as I
could, but I do not feel that I am experienced enough to have
good intuitions here, so any suggestions and corrections would be
appreciated.
Application Goals
=================
Here are some goals for applications developed with the use of
these patterns:
- REST-ful applications.
- Business logic through finite state machines (FSM).
- Support for the development of clients.
- Support for down-loadable clients and for clients that can be
updated automatically from the server, e.g. by down-loading and
installing scripts.
- Delivery on top of Twisted.
- Tailored support for several specific application architectures,
for example:
+ Database search, listings, and update (add, modify, delete).
+ Text repository search, display, update. Use of a search
engine, e.g. Glimpse.
+ Use of XML-RPC as a client to access back-end resources
exposed by an XML-RPC server.
+ Multi-step, interactive, processes with complex logic
implemented on top of a finite state machine (FSM).
The Design Patterns
===================
These are the patterns that we describe in this document. We hope
to also provide some support for implementing with these patterns.
We are interested in the following development patterns, which are
described in more detail below:
- The dispatcher pattern
- The database update pattern
- The FSM multi-step pattern
- The text repository search and update pattern
- The XML-RPC pattern
The Dispatcher Pattern
======================
This pattern takes the URL and translates it into a call to the
Python method intended to implement the request. It does this by
inspecting the path on the URL.
Example
-------
The brute-force pattern
.......................
Here is an example of a class that dispatches requests::
class ResourceDispatcher(resource.Resource):
isLeaf = True
def render(self, request):
done = False
if len(request.postpath) == 1 and request.postpath[0]:
if request.postpath[0] == 'show_plants':
dbaccess = dbmod.DBAccess(registry)
dbaccess.show_plants(request)
done = True
elif request.postpath[0] == 'show_plants_by_rating':
dbaccess = dbmod.DBAccess(registry)
dbaccess.show_plants_by_rating(request)
done = True
elif request.postpath[0] == 'get_add_form':
dbaccess = dbmod.DBAccess(registry)
dbaccess.show_add_form(request)
done = True
elif request.postpath[0] == 'get_delete_form':
dbaccess = dbmod.DBAccess(registry)
dbaccess.show_delete_form(request)
done = True
elif request.postpath[0] == 'add_plant':
dbaccess = dbmod.DBAccess(registry)
dbaccess.add_plant(request)
done = True
elif request.postpath[0] == 'delete_plant':
dbaccess = dbmod.DBAccess(registry)
dbaccess.delete_plant(request)
done = True
if done:
return NOT_DONE_YET
else:
content = Content_Dispatch % (
request.postpath,
request.args,
time.ctime(),
)
request.write(content)
request.finish()
Comments and explanation:
- Setting the class variable ``isLeaf`` to ``True`` tells Twisted
not to perform further processing on the URL path.
- The list ``request.postpath`` contains un-digested parts of
the URL path.
The name-prefix pattern
.......................
For some purposes, this pattern is a bit simpler, although it may
not give you as much control over individual cases::
class ResourceDispatcher(resource.Resource):
isLeaf = True
def render(self, request):
if len(request.postpath) == 1 and request.postpath[0]:
name = request.postpath[0]
dbaccess = dbmod.DBAccess(registry)
try:
method = getattr(dbaccess, 'webrequest_' + name)
except AttributeError:
pass
else:
method(request)
return NOT_DONE_YET
content = Content_Dispatch % (
request.postpath,
request.args,
time.ctime(),
)
request.write(content)
request.finish()
Explanation:
- This dispatcher retrieves an attribute (the method) from a class
given a request name on the right end of the URL.
- If your implementation is spread across several modules and/or
classes, then you might still have to do a bit of if-else stuff.
Or, you might consider using a dictionary that maps names to
modules and classes.
The Database Update Pattern
===========================
This pattern uses Twisted deferred objects to request database
operations. Here is a description:
- A database class with one method for each database operation.
Note that if the "slow" resource is something else (XML-RPC, for
example), then this would be a class for that resource. I'm
guessing that various slow resources are different enough so
that no single interface (API, set of public methods) could be
defined, but that would be nice.
- A resource class with one operation for each request or resource
type. A dispatcher (a Twisted .rpy page) parses the request URL
and dispatches/calls the appropriate method in this class. This
class would also contain application logic.
- For each step in the sequence, a (private) method in the
resource class that performs that step by calling the
appropriate method in the database class and (for all but the
last step) by providing as the callback function, the method
that implements the next step in the process.
- My example code uses an PostgreSQL database, but other databases
that have Python support could also be used.
Strategy
--------
- Separate the deferring operations (for example, requests to the
PostgreSQL database) from the code that calls them and that also
contains application logic.
- Execute operations in sequence. Operation n+1 must not begin
operation n until is complete. Implement this by chaining
callbacks.
- Provide simple Web page and form definitions. Enable editing
and delivery of simple HTML documents.
.. Data fill-in via Quixote template language or other template
and string interpolation language.
Example
-------
The application logic class
...........................
And, here is the resource class that requests the slow operations
and provides a callback to be called when the slow operation is
complete. This class also provides application logic. For each
operation that requires multiple sequential operations, it
provides multiple methods that each calls a database operation and
provides a callback (except, of course, for the last step)::
class DBAccess:
def __init__(self, registry):
self.registry = registry
get_globals(registry)
o
o
o
#
# Delete a row.
#
def delete_plant(self, request):
self.request = request
self.db = PlantDatabase("pyPgSQL.PgSQL", CONNECTION_ARGS)
self.content = Content_ShowPlants
self.plant_name = request.args.get('plant_name')
if self.plant_name and len(self.plant_name) > 0:
self.plant_name = self.plant_name[0]
self.plant_desc = request.args.get('plant_desc')
if self.plant_desc and len(self.plant_desc) > 0:
self.plant_desc = self.plant_desc[0]
self.plant_rating = request.args.get('plant_rating')
if self.plant_rating and len(self.plant_rating) > 0:
self.plant_rating = self.plant_rating[0]
self.delete_plant_step_1()
return NOT_DONE_YET
def delete_plant_step_1(self):
self.db.checkPlant(self.plant_name).addCallbacks(
self.delete_plant_step_2,
self.db.operationError)
def delete_plant_step_2(self, resultlist):
if resultlist:
deferred = self.db.deletePlant(
self.plant_name)
deferred.addCallbacks(self.delete_plant_step_3,
self.db.operationError)
else:
self.content = Content_Missing
self.content = self.content % self.plant_name
self.request.write(self.content)
self.request.finish()
def delete_plant_step_3(self, resultlist):
self.db.getPlants().addCallbacks(self.gotPlants,
self.db.operationError)
#
# Show the plants in the database.
#
def gotPlants(self, resultlist):
plantlist = []
plantlist.append('
')
plantlist.append('Plant DB')
plantlist.append('| Name | Description | Rating |
')
for plant in resultlist:
plantlist.append('')
s1 = '| %s | %s | %s | ' % \
(plant[0], plant[1], plant[2])
plantlist.append(s1)
plantlist.append('
')
plantlist.append('
')
plantstr = '\n'.join(plantlist)
content = self.content % (self.request.args, plantstr)
self.request.write(content)
self.request.finish()
The following second pattern uses a single method implemented as a
state machine, rather than using separate methods for each step
(as in the previous example). Whereas the previous example,
deleted a plant, this example adds a plant if it is not already in
the database, or updates the plant if it is in the database::
STEP_1 = 1
STEP_2 = 2
STEP_3 = 3
class DBAccess:
o
o
o
#
# Add or update a row.
# This uses a state machine pattern.
#
def add_plant(self, request):
self.request = request
self.state = STEP_1
self.add_plant_machine()
def add_plant_machine(self, *args):
if self.state == STEP_1:
self.db = PlantDatabase("pyPgSQL.PgSQL", CONNECTION_ARGS)
self.content = Content_ShowPlants
self.plant_name = self.request.args.get('plant_name')
if self.plant_name and len(self.plant_name) > 0:
self.plant_name = self.plant_name[0]
self.plant_desc = self.request.args.get('plant_desc')
if self.plant_desc and len(self.plant_desc) > 0:
self.plant_desc = self.plant_desc[0]
self.plant_rating = self.request.args.get('plant_rating')
if self.plant_rating and len(self.plant_rating) > 0:
self.plant_rating = self.plant_rating[0]
self.state = STEP_2
self.db.checkPlant(self.plant_name).addCallbacks(
self.add_plant_machine,
self.db.operationError)
return NOT_DONE_YET
elif self.state == STEP_2:
resultlist = args[0]
# Is the plant already in the database?
if resultlist:
self.state = STEP_3
deferred = self.db.updatePlant(
self.plant_name, self.plant_desc, self.plant_rating)
deferred.addCallbacks(self.add_plant_machine,
self.db.operationError)
else:
self.state = STEP_3
deferred = self.db.addPlant(
self.plant_name, self.plant_desc, self.plant_rating)
deferred.addCallbacks(self.add_plant_machine,
self.db.operationError)
return NOT_DONE_YET
elif self.state == STEP_3:
self.db.getPlants().addCallbacks(self.gotPlants,
self.db.operationError)
return NOT_DONE_YET
Explanation:
- This state machine is sequential: it executes step 1, 2, and 3
in order.
- Each state is implemented by a clause in the if-elif statement.
- Each state sets the variable ``self.state`` to indicate the next
state to be executed.
- Each state sets this method itself as the next callback.
Comparison and analysis:
- The difference between the above two patterns is that the first
uses separate methods for each step whereas the second pattern
uses a single method with separate states (each implemented by a
clause in an if-elif statement.
- Once you learn to recognize each pattern and its structure,
either is understandable and usable. The choice between them may
be a personal preference.
- A good recommendation is to pick one of the above two patterns
and to use it consistently. What you want is code that is
recognizable and clear, and that you can understand and debug.
And, here is yet one more pattern for the same task. This one
attempts to simplify things by replacing the ``if-elif-...``
statement. It implements each state as a separate method, and
provides a dispatcher to these methods::
class DBAccess:
o
o
o
#
# Add or update a row.
# This method uses a state machine pattern.
# It also has a dispatcher that retrieves and calls the
# method that implements the current state.
#
def add_plant(self, request):
self.request = request
self.state = 'step_1'
self.add_plant_machine()
#
# This method is the dispatcher.
#
def add_plant_machine(self, *args):
method = getattr(self, 'add_plant_machine_' + self.state)
method(*args)
def add_plant_machine_step_1(self, *args):
self.db = PlantDatabase("pyPgSQL.PgSQL", CONNECTION_ARGS)
self.content = Content_ShowPlants
self.plant_name = self.request.args.get('plant_name')
if self.plant_name and len(self.plant_name) > 0:
self.plant_name = self.plant_name[0]
self.plant_desc = self.request.args.get('plant_desc')
if self.plant_desc and len(self.plant_desc) > 0:
self.plant_desc = self.plant_desc[0]
self.plant_rating = self.request.args.get('plant_rating')
if self.plant_rating and len(self.plant_rating) > 0:
self.plant_rating = self.plant_rating[0]
self.state = 'step_2'
self.db.checkPlant(self.plant_name).addCallbacks(
self.add_plant_machine,
self.db.operationError)
return NOT_DONE_YET
def add_plant_machine_step_2(self, *args):
resultlist = args[0]
if resultlist:
self.state = 'step_3'
deferred = self.db.updatePlant(
self.plant_name, self.plant_desc, self.plant_rating)
deferred.addCallbacks(self.add_plant_machine,
self.db.operationError)
else:
self.state = 'step_3'
deferred = self.db.addPlant(
self.plant_name, self.plant_desc, self.plant_rating)
deferred.addCallbacks(self.add_plant_machine,
self.db.operationError)
return NOT_DONE_YET
def add_plant_machine_step_3(self, *args):
self.db.getPlants().addCallbacks(self.gotPlants,
self.db.operationError)
return NOT_DONE_YET
Explanation:
- Note the method ``add_plant_machine``. It acts as a dispatcher
for our state machine. It takes the current state (a string:
"step_1", "step_2, "step_3"), concatenates it to
"add_plant_machine\_", then retrieves and calls the method by
that name.
- Each method that implements a state (except the last), sets
``self.state`` to the next state, and adds ``add_plant_machine``
as a callback.
Repository class
................
Here is an example of the *repository access* class or database
class -- Each method implements a single operation that may cause
a delay, then returns a deferred::
class PlantDatabase(adbapi.ConnectionPool):
"""Update and retrieve from the Plant_DB database.
"""
def checkPlant(self, name):
deferred = self.runQuery("select * from Plant_DB where p_name = '%s'" % name)
return deferred
def updatePlant(self, plant_name, plant_desc, plant_rating):
sql = "update Plant_DB set p_desc='%s', p_rating='%s' where p_name='%s'" % \
(plant_desc, plant_rating, plant_name)
deferred = self.runOperation(sql)
return deferred
def addPlant(self, plant_name, plant_desc, plant_rating):
sql = "insert into Plant_DB values ('%s', '%s', '%s')" % \
(plant_name, plant_desc, plant_rating)
deferred = self.runOperation(sql)
return deferred
def deletePlant(self, plant_name):
sql = "delete from Plant_DB where p_name='%s'" % \
(plant_name,)
deferred = self.runOperation(sql)
return deferred
def getPlants(self):
sql = "SELECT * FROM Plant_DB order by p_name"
deferred = self.runQuery(sql)
return deferred
def operationError(self, error):
log.msg("%s Operation Failed: %s" % (reflect.qual(self.__class__), error))
log.err(error)
Comments and explanation:
- There is one function in this class for each type of request. In
our example these are check for existence, update, add, delete,
and get all.
- Each of these operations is a "slow" operation. To prevent
blocking, it requests the operation and immediately returns a
deferred. (Note: We will learn how to implement a slow
operation without blocking the in the text repository pattern.
- By inheriting from ``twisted.enterprise.adbapi.ConnectionPool``,
we get get the ability to run database queries that use the
connection pool. In order to do so, we pass to the constructor
for class ``PlantDatabase`` the name of the database API and the
connection args. See the application logic class.
Connection pooling
..................
This section provides an example that shows both:
1. How to share values across sessions and
2. How to create and uses a database connection pool.
``ConnectionPool`` saves the time needed to create a database
connection for each session.
Here is code that shares a single instance of the database
connection pool across all sessions::
class Globals:
def __init__(self):
self.dbpool = None
def getDbpool(self):
return self.dbpool
def setDbpool(self, dbpool):
self.dbpool = dbpool
def get_globals(registry):
globalContainer = registry.getComponent(globalvalues.Globals)
if not globalContainer:
globalContainer = globalvalues.Globals()
dbpool = PlantDatabase("pyPgSQL.PgSQL",
"localhost:5432:test:postgres:flicker")
globalContainer.setDbpool(dbpool)
registry.setComponent(globalvalues.Globals, globalContainer)
else:
dbpool = globalContainer.getDbpool()
return dbpool
Explanation:
- Class ``Globals`` is our container for global values. In this
example, the only value stored in it is the database connection
pool.
- The function ``get_globals`` attempts to get the existing,
shared globals container. If the globals container exists, it
returns the DB connection pool (an instance of
``PlantDatabase``) from that container. If it does not exist,
it creates a container and a DB connection pool, puts the DB
connection pool in the container, saves it, and returns the new
DB connection pool.
- In order to use the database connection pool, we create an
instance of a class that subclasses
``twisted.enterprise.adbapi.ConnectionPool`` (in our example
code this class is ``PlantDatabase``) passing to the constructor
the database connection parameters. The query functions provided
by that superclass know how to use the database connection pool.
See the "Repository class" example.
Questions and discussion
------------------------
Here are a few questions:
- We've presented two patterns for using deferreds, specifically
(1) chaining separate methods and (2) a state machine in a
single method. Which is better? Is there an approved or
commonly accepted Twisted pattern for using deferreds? Putting
multiple steps in a single method seems to avoid some code
clutter at the expense of code density.
- I've separated the slow operations into a separate class. Is
that a good idea? In my example, the operations are very
simple, in part because I've kept the logic in the resource
class. Perhaps, instead of putting them in a separate class, I
should just code them in-line in the resource class in the state
machine method. One reason for separate methods is to make them
reusable.
- The state-machine-pattern returns multiple times. It returns
NOT_DONE_YET each time. Is that the correct thing to do?
- Twisted already has an API that creates and returns deferreds
for database operations. What if I need to wrap some other long
lasting operation up in the same way? How do I do
that? Answer: See `The Text Repository Pattern`_.
The Text Repository Pattern
===========================
Assumptions:
- There is a text search engine, e.g. Glimpse. If the repository
is composed of XML documents, then sgrep/pysgrep or XPath might
be appropriate.
- The documents form some kind of accessible collection. For
example, they are all in a single disk directory or set of
sub-directories.
Requirements:
- Searches may be slow, so we consider doing the following:
+ In C code, release the Python global interpreter lock.
+ Wrap the slow operations with a Twisted deferred.
- Define a common API that can be wrapped around and can be
implemented for a variety search engines.
- Provide patterns and code samples that that use the API.
- Provide access to the (underlying) search engine from Python.
For example:
+ Provide a Python extension for a search engine, e.g. Glimpse
or Harvest (which uses Glimpse).
+ Provide a server-client connection to a search engine, e.g.
Glimpse or Harvest.
Definitions:
- In REST terms, the *resource* is the text repository. For
example, a resource might be:
+ The search capability
+ The ability to retrieve the file in which a search string has
been found.
- Again, in REST terms, the *representation* is the content which
the Web application formats and sends to the client. The
representation could be HTML, XML, PDF, etc. For example, a
representation might be:
+ A list of search results along with URI's for accessing each
result
+ A file that contains the string or target of the previous
search
Here is a description of the pattern:
- The search and update operations are collected in a separate
class. Call it the *repository access* class. This class
exposes the following methods:
+ *search()* -- Return a list of repository
locations that satisfy **.
+ *fetch_one()* -- Return the text chunk from the
repository pointed to by **.
+ *replace_one(, )* -- Replace the
chunk at ** with **.
- Application logic is in a separate class. Call it the
*application logic* class. This *application logic* class is a
client of and uses the *repository access* class.
- Filtering of search results can be done in separate wrappers
that are called by the search engine. The logic class can also
do some filtering, of course, but for some purposes it might be
preferable to remove unwanted earlier.
- Formatting of the representation of the resource can be done in
the *application logic* class, or can be separated from
application logic by placing templates and formatting code in a
separate *representation formatting* class or even a separate
module.
The implementation -- Here is a conceptual view of the
implementation pattern:
- The implementation is composed of the following layers:
+ The dispatcher -- ``dispatch.rpy/ResourceDispatcher``
+ The access class -- Class ``GlimpseTextRepositoryAccess``
+ The repository class -- Class ``glimpsetextmod.GlimpseTextRepository``
+ The service class -- Class ``glimpsemod.GlimpseService``
+ The Glimpse library -- Class ``glimpselib.Query``
+ The representation formatting class -- ``glimpsetextmod.Formatter``
- The dispatcher -- Translates the URI path into a call to a
method in the access class.
- The access class -- Contains application logic.
- The repository class -- Calls the service class to create and
return a Twisted deferred.
- The service class -- (1) Creates a Twisted deferred; (2) uses it
schedule the callbacks that request the "slow" operations; (3)
returns the deferred instance.
- The Glimpse library -- Contains functions that make requests
Glimpse.
- The representation formatting class -- Formats the results of
queries for delivery to the client.
You may want to compare the structure of the text repository
pattern with that of the database update pattern. I'm trying to
re-work these patterns so that they are as isomorphic
(structurally similar) as possible. Hopefully, that will make
knowledge and understanding of one pattern transferable to the
other.
Example
-------
Application logic class
.......................
Here is an example of the *application logic* class::
#
# Application logic class
#
class GlimpseTextRepositoryAccess:
#
# Produce a form to be used to search the text repository.
#
def show_search_form(self, request):
self.request = request
self.content = Content_SearchForm
self.request.write(self.content)
self.request.finish()
#
# Produce a list of the search results.
#
def show_search_results(self, request):
self.request = request
self.repository = GlimpseTextRepository()
self.content = Content_ShowSearchResults
self.querystr = request.args.get('querystr')
if self.querystr and len(self.querystr) > 0:
self.querystr = self.querystr[0]
self.casesensitive = request.args.get('casesensitive')
if self.casesensitive and len(self.casesensitive) > 0:
self.casesensitive = self.casesensitive[0]
self.state = STEP_1
self.show_search_results_machine()
return NOT_DONE_YET
def show_search_results_machine(self, *args, **kw):
if self.state == STEP_1:
#dbglogmsg('*** (show_search_results Step_1)')
self.repository.search_repository(
self.show_search_results_machine,
self.querystr,
self.casesensitive)
self.state = STEP_2
elif self.state == STEP_2:
resultlist = args[0]
formatter = Formatter()
content = formatter.format_search_results(self.content, resultlist)
self.request.write(content)
self.request.finish()
#
# Fetch and show one file from the repository.
#
def get_text_file(self, request, path):
self.request = request
infile = file(path, 'r')
filecontent = infile.read()
infile.close()
formatter = Formatter()
content = formatter.format_file(filecontent, path)
self.request.write(content)
self.request.finish()
Explanation:
- Method ``show_search_form`` provides a form that can be used to
request a query.
- Method ``show_search_results_machine`` implements a small FSM
(finite state machine). This FSM contains two steps (states)
which (1) get the deferred that starts the query and (2) format
the results for delivery to the client. The variable
``self.state`` is used to keep track of state and to transfer
from one state to another.
- In step (state) 1, method ``show_search_results_machine`` (1)
calls the ``search_repository`` method in the *repository*
class, (2) adds itself as a callback, and (3) sets the (next)
state to ``STEP_2`` so that when the callback is called, step 2
will be executed.
- In step (state) 2, which is executed when the method is called
as a *callback*, method ``show_search_results_machine`` formats
the results and delivers them to the client.
- Method ``get_text_file`` responds to a request for one of the
files listed in the search results. The right-end of the URL
contains the path to the requested file. Note that the list of
search results contains the URLs needed to make these requests.
In effect, this enables the user to "drill down" into one of the
results (a file) returned by the text search engine.
Repository class
................
Here is an example of the *repository access* class::
#
# Repository class
#
class GlimpseTextRepository:
def search_repository(self, callback, querystr, casesensitive):
flags = ''
if not casesensitive:
flags = '-i'
port = 2001
queryobj = glimpselib.Query()
deferred = queryobj.query_server(querystr, flags, port)
deferred.addCallback(callback)
return deferred
Explanation:
- Method ``search_repository`` calls the method in the Glimpse to
initiate the search and create a deferred object. It then adds
a callback to the deferred.
- This class is intended a "insulation" from the Glimpse library
that actually does the work. It would, hopefully, enable user
to easily switch to another implementation of text searching.
The Glimpse library
...................
This example from the *Glimpse library* directs its requests to
the Glimpse text search engine::
#
# This version uses glimpseserver.
# Doing so saves loading the index into memory for each query.
# It uses twisted.utils.getProcessOutput() to run the query,
# captures the results, then creates a list of tuples to return
# to the callback.
#
class Query:
def query_server(self, querystr, flags='-i', port=2001):
executable = '/usr/local/bin/glimpse'
args = ['-C', '-K', '%d' % port]
if flags:
args.append(flags)
if querystr:
args.append(querystr)
self.deferred = utils.getProcessOutput(executable, args)
self.deferred.addCallbacks(self._query_server_2, self.error_back)
return self.deferred
def _query_server_2(self, result):
lines = result.split('\n')
result = []
for line in lines:
pos = line.find(':')
if pos > -1:
val = (line[:pos], line[pos+2:])
result.append(val)
return result
def error_back(self, result):
return [('***error***', result.getErrorMessage()),]
Explanation:
- The method ``query_server`` uses
``twisted.utils.getProcessOutput()`` to run the Glimpse search
engine (as if through the command line) and capture the results.
(Note to self: We really should wrap Glimpse as a Python
extension module.)
- The ``-C`` flag tells Glimpse to use the Glimpse server to
perform the search. Doing so saves time that would have been
used to load the search engine's index for each search.
- ``getProcessOutput()`` returns a deferred. We add
``_query_server_2`` as a callback to that deferred.
``getProcessOutput()`` returns the output of the executable (in
our case ``glimpse``) as a string to the callback.
- The callback ``_query_server_2`` receives the results of the
query, then creates and returns a list of tuples, one tuple for
each match. Each tuple has (1) the file name of the match and
(2) the line containing the match.
Representation formatting class
...............................
And, finally, when the results have been returned, the *formatting
class* is used to produce a representation of the results.
Putting this formatting code in a separate class has at least two
benefits:
- In a project of any size, there may be specialists working on
the user interface, the formatting of representations, etc.
Separating formatting code would provide a way to provide a
separate set of modules for those specialists to work on.
- It might be useful to do formatting with the Quixote PTL (Python
templating language), which is also available in Twisted. By
putting formatting code in a separate class, we would be able to
put formatting code in ``.ptl`` modules (for example) for
processing with PTL, then instructing Twisted to process them as
resource templates with something like the following::
$ mktap web --path=/var/www \
--processor=.rtl=twisted.web.script.ResourceTemplate
Our example, however, uses simple Python code to perform
formatting. Here is an example of the *representation formatting*
class::
#
# Representation Formatting class
#
class Formatter:
def format_search_results(self, pagecontent, resultlist):
contentlist = []
contentlist.append('')
contentlist.append('| File | Match |
')
idx = 0
prevfilename = ''
for result in resultlist:
filename = result[0]
line = result[1].replace('<', '<')
if filename != prevfilename:
idx = 0
target = '%s' % \
(idx, filename, filename)
linecontent = '| %s | %s |
' % \
(target, line)
contentlist.append(linecontent)
idx += 1
prevfilename = filename
contentlist.append('
')
contentstr = '\n'.join(contentlist)
contentstr = pagecontent % contentstr
return contentstr
def format_file(self, incontent, path):
stem, ext = os.path.splitext(path)
if ext == '.html':
return incontent
else:
content1 = incontent.replace('<', '<')
content2 = TextWrapper % (path, path, content1)
return content2
Explanation:
- Method ``format_search_results`` formats a list of search
results, one for each hit. Note that it generates URLs that can
be used to request the file in which the pattern is found.
- Method ``format_file`` formats one file requested through a URL
generated by ``format_search_results``. If the file contains
HTML, the file's contents are returned unchanged. If the file
does *not* contain HTML, then left-corner-brackets are replaced
and a bit of HTML header and footer is wrapped around the
content.
Questions and discussion
------------------------
And, a few questions:
- How slow does a slow method have to be before it is worth
putting it into a deferred? After all, the purpose of using a
search engine such as Glimpse is to speed up searches. Perhaps
using deferred methods is over-kill. Is there any down-side or
cost or over-head to using deferreds?
The XML-RPC Pattern
===================
This simple pattern can be used to make remote calls to an XML-RPC
server and then to deliver content that contains the results. The
Twisted Web application is acting as an XML-RPC client . The Web
application uses XML-RPC to request information that it will
return in its responses.
Requirements:
- Provide access to some resource through XML-RPC. The Twisted
Web application acts as an XML-RPC client.
- Deliver content that is formatted from the results of one or
more XML-RPC calls.
Definitions:
- In REST terms, the back-end XML-RPC server is the *resource*.
- And, the content that is formatted from the results of one or
more XML-RPC requests is the *representation*.
Example
-------
The application logic class
...........................
This class contains any application logic. A typical example
would be the need to make more than one XML-RPC request, but to
include logic between the requests so that one request is
dependent on the results of a previous request.
Here is an example::
#
# XML-RPC applicationLogic class
#
class TemperatureAccess:
#
# Produce a form to be used to search the text repository.
#
def show_temperature_form(self, request):
self.request = request
self.content = Content_ConvertTemperatureForm
self.request.write(self.content)
self.request.finish()
return NOT_DONE_YET
#
# Convert the temperature and produce the result.
#
def convert_temperature(self, request, direction):
self.request = request
intemp = None
arg = request.args.get('intemp')
if arg and len(arg) > 0:
intemp = arg[0]
self.intemp = intemp
self.converter = Conversion()
self.direction = direction
if direction == 'c2f':
self.content = Content_ConvertC2FResults
deferred = self.converter.c2f(intemp)
deferred.addCallbacks(self.receiveTemperature, self.receiveError)
return NOT_DONE_YET
elif direction == 'f2c':
self.content = Content_ConvertF2CResults
deferred = self.converter.f2c(intemp)
deferred.addCallbacks(self.receiveTemperature, self.receiveError)
return NOT_DONE_YET
def receiveTemperature(self, value):
valuestr = '%.2f' % value
self.outtemp = valuestr
if self.direction == 'c2f':
scale = 'F'
else:
scale = 'C'
deferred = self.converter.category(scale, value)
deferred.addCallbacks(self.receiveCategory, self.receiveError)
return NOT_DONE_YET
def receiveCategory(self, category):
self.category = category
deferred = self.converter.ranges()
deferred.addCallbacks(self.receiveRanges, self.receiveError)
return NOT_DONE_YET
def receiveRanges(self, ranges):
self.ranges = ranges
rangelist = ['']
for range in ranges:
rangelist.append('- Less than %0.1f C is %s
' % \
(range[0], range[1]))
rangelist.append('
')
rangestr = '\n'.join(rangelist)
content = self.content % \
(self.intemp, self.outtemp, self.category, rangestr)
self.request.write(content)
self.request.finish()
return NOT_DONE_YET
def receiveError(self, error):
raise RuntimeError, error
Explanation:
- Method ``show_temperature_form`` delivers a form that enables
the client to provide a temperature and request a temperature
conversion.
- Method ``convert_temperature`` uses the XML-RPC class. It (1)
creates and instance of the XML-RPC class, (2) calls the
conversion method in that class, which returns a deferred, then
(3) adds a callback function (``receiveTemperature``) to that
deferred.
- Method ``receiveTemperature`` is called when the first XML-RPC
request has been completed. It also uses the XML-RPC class. It
(1) saves the converted temperature, (2) calls the ``category``
method in the XML-RPC class, which returns a deferred, then (3)
adds a callback function (``receiveCategory``) to this (new)
deferred.
- Method ``receiveCategory`` (1) receives the next result (the
category) and (2) saves that value in an instance variable
(``self.category``). This method also (3) calls the ``ranges``
method in the XML-RPC class, which returns a deferred, then (4)
adds a callback function (``receiveRanges``) to this (new)
deferred.
- Method ``receiveRanges`` (1) receives the final result (the
category), (2) formats the content, and (3) returns it to the
client.
- The methods described above show how to chain methods together
when each method in a sequence is dependent on the results
returned by the previous method. We could have alternatively
used a single method (in place of ``receiveTemperature``,
``receiveCategory``, and ``receiveRanges``) which implemented a
finite state machine with separate states (for
``receiveTemperature``, ``receiveCategory``, and
``receiveRanges``).
- Method ``receiveError`` reports any errors raised by the XML-RPC
server.
The XML-RPC requester class
...........................
This class makes the XML-RPC requests. It acts as the XML-RPC
client. It uses Twisted's XML-RPC support, which returns a
deferred object.
Here is a sample class::
from twisted.web.xmlrpc import Proxy
#
# XML-RPC back-end resource class.
# This class makes the XML-RPC requests and returns deferreds.
#
class Conversion:
def c2f(self, intempstr):
intemp = -1
try:
intemp = float(intempstr)
except:
pass
proxy = Proxy('http://localhost:8081')
deferred = proxy.callRemote('c2f', intemp)
return deferred
def f2c(self, intempstr):
intemp = -1
try:
intemp = float(intempstr)
except:
pass
proxy = Proxy('http://localhost:8081')
deferred = proxy.callRemote('f2c', intemp)
return deferred
def category(self, scale, intemp):
proxy = Proxy('http://localhost:8081')
deferred = proxy.callRemote('temperature_category', scale, intemp)
return deferred
def ranges(self):
proxy = Proxy('http://localhost:8081')
deferred = proxy.callRemote('temperature_ranges')
return deferred
Explanation:
- Each method in this class makes an XML-RPC request and returns a
deferred.
- These methods use Twisted's XML-RPC support (as opposed to using
module ``xmlrpclib`` in the Python standard library, for
example).
The XML-RPC server
..................
This is the simple XML-RPC server written in Python that I used
while testing my examples::
import SimpleXMLRPCServer
def c2f(ctemp):
#ftemp = ((212.0 - 32.0)/100.0 * ctemp) + 32.0
ftemp = ((9.0 / 5.0) * ctemp) + 32
return ftemp
def f2c(ftemp):
#ctemp = 100.0/(212.0 - 32.0) * (ftemp - 32.0)
ctemp = (5.0 / 9.0) * (ftemp - 32)
return ctemp
Ranges = [
(0.0, 'cold'),
(20.0, 'cool'),
(25.0, 'moderate'),
(30.0, 'warm'),
(35.0, 'hot'),
(40.0, 'very hot'),
(1000.0, 'extremely hot'),
]
def temperature_category(scale, intemp):
if scale.upper() == 'F':
temp = f2c(intemp)
else:
temp = intemp
category = 'Unknown'
for range in Ranges:
if temp <= range[0]:
category = range[1]
break
return category
def temperature_ranges():
return Ranges
def startup():
server = SimpleXMLRPCServer.SimpleXMLRPCServer(("localhost", 8081))
server.register_function(c2f)
server.register_function(f2c)
server.register_function(temperature_category)
server.register_function(temperature_ranges)
server.serve_forever()
Explanation:
- The functions ``c2f``, ``f2c``, ``temperature_category``, and
``temperature_ranges`` are exported and are available to be
called through XML-RPC requests.
- Function ``startup`` creates a server, registers the exported
functions, and starts the server.
An XML-RPC server implemented with Twisted
..........................................
And here is the same XML-RPC server implemented as a Twisted
applications::
from twisted.web import xmlrpc, server
Ranges = [
(0.0, 'cold'),
(20.0, 'cool'),
(25.0, 'moderate'),
(30.0, 'warm'),
(35.0, 'hot'),
(40.0, 'very hot'),
(1000.0, 'extremely hot'),
]
class Converter(xmlrpc.XMLRPC):
def xmlrpc_c2f(self, ctemp):
#ftemp = ((212.0 - 32.0)/100.0 * ctemp) + 32.0
ftemp = ((9.0 / 5.0) * ctemp) + 32
return ftemp
def xmlrpc_f2c(self, ftemp):
#ctemp = 100.0/(212.0 - 32.0) * (ftemp - 32.0)
ctemp = (5.0 / 9.0) * (ftemp - 32)
return ctemp
def xmlrpc_temperature_category(self, scale, intemp):
if scale.upper() == 'F':
temp = self.xmlrpc_f2c(intemp)
else:
temp = intemp
category = 'Unknown'
for range in Ranges:
if temp <= range[0]:
category = range[1]
break
return category
def xmlrpc_temperature_ranges(self):
return Ranges
def main():
from twisted.internet.app import Application
app = Application("xmlrpc")
r = Converter()
app.listenTCP(8081, server.Site(r))
return app
application = main()
if __name__ == '__main__':
application.run(save=0)
Explanation:
- This example is basically a copy of the previous example with
the functions encapsulated in a class.
- To learn how to use Twisted to expose the methods in a class as
XML-RPC services, read `Creating XML-RPC Servers and Clients
with Twisted`_. I followed the instructions and example in that
document. It's simple enough so that I won't add much.
.. _`Creating XML-RPC Servers and Clients with Twisted`:
http://www.twistedmatrix.com/documents/howto/xmlrpc
- By default, methods prefixed with "xmlrpc\_" are exposed to
clients as XML-RPC services. See ``twisted.web.xmlrpc.py``.
Miscellaneous Hints and Howto's
===============================
Here are a few clues and examples that were not covered in the
patterns themselves.
Body data/content
-----------------
In order to extract the data from the body of a request, use
``request.content``. If there is content, then
``request.content`` will contain an instance of ``cStringIO``.
Here is an example that shows how to use it::
data = ''
if request.content:
data = request.content.getvalue()
Debugging
---------
It is often helpful to produce some debugging information. This
example shows how to print messages to a log file::
from twisted.python import log
# Set to 0 (debugging off) or 1 (debugging on).
_DEBUG = 0
def dbglogmsg(mesg):
if _DEBUG:
log.msg(mesg)
l
Sharing values across sessions
------------------------------
Twisted provides a mechanism for sharing values across sessions.
In order to do this, define a class, store the values you wish to
share in an instance of the class, then request the shared values
by asking Twisted for the shared value of that data type.
The `Connection pooling`_ example above shows how to share values
across sessions.
A test harness
--------------
You may want to be able to create a testing harness that acts as a
client. It can be used to send requests to your Twisted Web
application and to display or process the responses. If expanded,
it can serve as a simple client that drives your application. You
can use the following example as a starting point for the
implementation of a Python client that talks to your Twisted Web
application.
Here is the example::
import httplib, urllib
#
# Send a request to my Twisted Web application.
# Add a few arguments to the request and pack some content into
# the body of the request.
# Get the response, display the status of the response, and
# display the content received with the response.
#
def test():
# Create some sample content to send as the body of the request.
datalist = []
datalist.append('')
datalist.append('Here is stuff for a test.')
datalist.append('')
data = '\n'.join(datalist)
# Create a few dummy parameters.
paramdict = {'aaa': 111, 'bbb': 222}
params = urllib.urlencode(paramdict)
# Here are headers for the request. Note the "Content-length".
headers = {"Content-type": "application/x-www-form-urlencoded",
"Accept": "text/html",
"Content-length": len(data),
}
conn = httplib.HTTPConnection("localhost:8080")
conn.set_debuglevel(1)
selector = '/dispatch.rpy/?%s' % params
# Pick one: GET or POST?
conn.putrequest("GET", selector)
## conn.putrequest("POST", selector)
for key, val in headers.iteritems():
conn.putheader(key, val)
conn.endheaders()
conn.send(data)
response = conn.getresponse()
print 'status/reason', response.status, response.reason
data1 = response.read()
print '=' * 50
print data1
Explanation:
- Each call to function ``test`` makes one request to the Web
application.
- Local variables ``datalist/data`` are used to create content to
be sent in the *body* of the request.
- Note the commented lines that would enable you to choose between
making a ``GET`` or a ``POST`` request.
- This example prints out the response from the Web application.
However, your own client implementation could process the
response in other ways.
Summary and Conclusions
=======================
This document has tried to help Twisted developers of Web
applications in two ways:
- Help beginning Twisted users a set of examples (patterns) which
enable them to quickly construct Twisted Web applications.
- Help experienced Twisted users by giving them a set of patterns
(examples) that improve their code through consistency and
clarity.
Thanks in advance for suggestions and guidance.
Acknowledgements
================
Thanks to Andrew Bennetts for many suggestions and criticisms on
an earlier draft.
Thanks to the developers and supporters of Twisted for a powerful
product.
See Also
========
`Twisted`_: The Twisted network framework at TwistedMatrix
.. _`Twisted`: http://www.twistedmatrix.com/
`Python home`_: The Python Web site.
.. _`Python home`: http://www.python.org
RestFaq_: The REST FAQ -- Information and links for
REpresentational State Transfer.
.. _RestFaq: http://internet.conveyor.com/RESTwiki/moin.cgi/RestFaq
Glimpse_: The Glimpse search engine Web site.
.. _Glimpse: http://webglimpse.net/
`Docutils`_: Python Documentation Utilities -- This document was
formatted with Docutils.
.. _`Docutils`:
http://docutils.sourceforge.net/