| 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.
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.
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.
Here are some goals for applications developed with the use of these 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:
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.
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:
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 pattern uses Twisted deferred objects to request database operations. Here is a description:
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('<table border="5" width="80%">')
plantlist.append('<caption>Plant DB</caption>')
plantlist.append('<tr><th width="25%">Name</th><th width="65%">Description</th><th width="10%">Rating</th></tr>')
for plant in resultlist:
plantlist.append('<tr>')
s1 = '<td>%s</td><td>%s</td><td align="right">%s</td>' % \
(plant[0], plant[1], plant[2])
plantlist.append(s1)
plantlist.append('</tr>')
plantlist.append('</table>')
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:
Comparison and analysis:
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:
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:
This section provides an example that shows both:
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:
Here are a few questions:
Assumptions:
Requirements:
Definitions:
Here is a description of the pattern:
The implementation -- Here is a conceptual view of the implementation pattern:
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.
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:
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:
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:
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('<table border="1">')
contentlist.append('<tr><th>File</th><th>Match</th></tr>')
idx = 0
prevfilename = ''
for result in resultlist:
filename = result[0]
line = result[1].replace('<', '<')
if filename != prevfilename:
idx = 0
target = '<a href="/dispatch.rpy/get_text_file/%d%s">%s</a>' % \
(idx, filename, filename)
linecontent = '<tr><td>%s</td><td>%s</td></tr>' % \
(target, line)
contentlist.append(linecontent)
idx += 1
prevfilename = filename
contentlist.append('</table>')
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:
And, a few questions:
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:
Definitions:
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 = ['<ul>']
for range in ranges:
rangelist.append('<li>Less than %0.1f C is %s</li>' % \
(range[0], range[1]))
rangelist.append('</ul>')
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:
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:
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:
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:
Here are a few clues and examples that were not covered in the patterns themselves.
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()
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.
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('<testcontent>')
datalist.append('Here is stuff for a test.')
datalist.append('</testcontent>')
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:
This document has tried to help Twisted developers of Web applications in two ways:
Thanks in advance for suggestions and guidance.
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.
Twisted: The Twisted network framework at TwistedMatrix
Python home: The Python Web site.
RestFaq: The REST FAQ -- Information and links for REpresentational State Transfer.
Glimpse: The Glimpse search engine Web site.
Docutils: Python Documentation Utilities -- This document was formatted with Docutils.