SVN: r22348 - in trunk/quixote: . server
Neil Schemenauer <nascheme-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Wed, 27 Aug 2003 16:52:23 -0400
| Newsgroups | gmane.comp.web.quixote.cvs |
|---|---|
| Message-ID | <[email protected]> |
Author: nascheme
Date: 2003-08-27 16:52:23 -0400 (Wed, 27 Aug 2003)
New Revision: 22348
Modified:
trunk/quixote/http_response.py
trunk/quixote/publish.py
trunk/quixote/server/medusa_http.py
trunk/quixote/server/twisted_http.py
trunk/quixote/util.py
Log:
* Allow HTTPResponse.body to be a Stream object as well as a string. Stream
objects are useful if the body of the response is very large.
* Refactor HTTPResponse and add generate_headers() method. It makes the
medusa_http and twisted_http modules simpler.
* Change StaticFile to return Stream objects. Also, fix a minor bug in
StaticDirectory (_q_lookup should return a namespace).
Modified: trunk/quixote/http_response.py
===================================================================
--- trunk/quixote/http_response.py 2003-08-27 20:25:14 UTC (rev 22347)
+++ trunk/quixote/http_response.py 2003-08-27 20:52:23 UTC (rev 22348)
@@ -190,18 +190,20 @@
self.headers["content-type"] = ctype
def set_body (self, body):
- """set_body(body : string)
+ """set_body(body : any)
- Sets the return body equal to the (string) argument "body". Also
- updates the "Content-length" return header. If the
- "Content-type" header has not yet been set, it is set to
+ Sets the return body equal to the argument "body". Also updates the
+ "Content-length" header if the length is of the body is known. If
+ the "Content-type" header has not yet been set, it is set to
"text/html".
"""
- if type(body) is not StringType:
- raise TypeError("'body' must be a string")
-
- self.body = body
- self.set_header('content-length', len(body))
+ if isinstance(body, Stream):
+ self.body = body
+ if body.length is not None:
+ self.set_header('content-length', body.length)
+ else:
+ self.body = str(body)
+ self.set_header('content-length', len(self.body))
if not self.headers.has_key('content-type'):
self.set_header('content-type', 'text/html; charset=iso-8859-1')
@@ -317,13 +319,49 @@
elif name == 'secure' and val:
chunks.append("secure")
- cookie = "Set-Cookie: " + ("; ".join(chunks))
- cookie_list.append(cookie)
+ cookie_list.append(("Set-Cookie", ("; ".join(chunks))))
# Should really check size of cookies here!
return cookie_list
+ def generate_headers (self):
+ """generate_headers() -> [(name:string, value:string)]
+
+ Generate a list of headers to be returned as part of the response.
+ """
+ headers = []
+
+ # "Status" header must come first.
+ headers.append(("Status", "%03d %s" % (self.status_code,
+ self.reason_phrase)))
+
+ for name, value in self.headers.items():
+ headers.append((name.title(), value))
+
+ # All the "Set-Cookie" headers.
+ if self.cookies:
+ headers.extend(self._gen_cookie_headers())
+
+ # Date header
+ now = time.time()
+ if not self.headers.has_key("date"):
+ headers.append(("Date", formatdate(now)))
+
+ # Cache directives
+ if self.cache is None:
+ pass # don't mess with the expires header
+ elif not self.headers.has_key("expires"):
+ if self.cache > 0:
+ expire_date = formatdate(now + self.cache)
+ else:
+ expire_date = "-1" # allowed by HTTP spec and may work better
+ # with some clients
+ headers.append(("Expires", expire_date))
+
+ return headers
+
+
def write (self, file):
"""write(file : file)
@@ -334,7 +372,6 @@
is expected that this response is parsed by the web server
and turned into a complete HTTP response.
"""
-
# XXX currently we write a response like this:
# Status: 200 OK
# Content-type: text/html; charset=iso-8859-1
@@ -352,41 +389,35 @@
# "non-parsed header" mode, where the CGI script is responsible
# for generating a complete HTTP response with no help from the
# server.
+ for name, value in self.generate_headers():
+ file.write("%s: %s\r\n" % (name, value))
+ file.write("\r\n")
+ if self.body is not None:
+ if isinstance(self.body, Stream):
+ for chunk in self.body:
+ file.write(chunk)
+ else:
+ file.write(self.body)
- headers = self.headers
- # "Status" header must come first.
- file.write("Status: %03d %s\r\n" % (self.status_code,
- self.reason_phrase))
+class Stream:
+ """
+ A wrapper around response data that can be streamed. The 'iterable'
+ argument must support the iteration protocol. Items returned by 'next()'
+ must be strings. Beware that exceptions raised while writing the stream
+ will not be handled gracefully.
- # Now the bulk of the headers (anything set with 'set_header()',
- # most likely "Content-type" and "Content-length").
- for (key, val) in headers.items():
- file.write("%s: %s\r\n" % (key.title(), val))
+ Instance attributes:
+ iterable : any
+ an object that supports the iteration protocol. The items produced
+ by the stream must be strings.
+ length: int | None
+ the number of bytes that will be produced by the stream, None
+ if it is not known. Used to set the Content-Length header.
+ """
+ def __init__(self, iterable, length=None):
+ self.iterable = iterable
+ self.length = length
- # All the "Set-Cookie" headers.
- if self.cookies:
- for hdr in self._gen_cookie_headers():
- file.write(hdr + "\r\n")
-
- # Date header
- now = time.time()
- if not headers.has_key("date"):
- file.write("Date: %s\r\n" % formatdate(now))
-
- # Cache directives
- if self.cache is None:
- pass # don't mess with the expires header
- elif not headers.has_key("expires"):
- if self.cache > 0:
- expire_date = formatdate(now + self.cache)
- else:
- expire_date = "-1" # allowed by HTTP spec and may work better
- # with some clients
- file.write("Expires: %s\r\n" % expire_date)
-
-
- # And the response body.
- file.write("\r\n")
- if self.body is not None:
- file.write(self.body)
+ def __iter__(self):
+ return iter(self.iterable)
Modified: trunk/quixote/publish.py
===================================================================
--- trunk/quixote/publish.py 2003-08-27 20:25:14 UTC (rev 22347)
+++ trunk/quixote/publish.py 2003-08-27 20:52:23 UTC (rev 22348)
@@ -540,7 +540,7 @@
# Output results from Response object
if output:
- request.response.set_body(str(output))
+ request.response.set_body(output)
try:
request.response.write(stdout)
except IOError, exc:
Modified: trunk/quixote/server/medusa_http.py
===================================================================
--- trunk/quixote/server/medusa_http.py 2003-08-27 20:25:14 UTC (rev 22347)
+++ trunk/quixote/server/medusa_http.py 2003-08-27 20:52:23 UTC (rev 22348)
@@ -13,9 +13,22 @@
from StringIO import StringIO
from medusa import http_server, xmlrpc_handler
from quixote.http_request import HTTPRequest
+from quixote.http_response import Stream
from quixote.publish import Publisher
from quixote.errors import PublishError
+
+class StreamProducer:
+ def __init__(self, stream):
+ self.iterator = iter(stream)
+
+ def more(self):
+ try:
+ return self.iterator.next()
+ except StopIteration:
+ return ''
+
+
class QuixoteHandler:
def __init__ (self, publisher, server_name, server):
"""QuixoteHandler(publisher:Publisher, server_name:string,
@@ -87,31 +100,24 @@
except:
output = self.publisher.finish_failed_request(qreq)
+ qresponse = qreq.response
if output:
- qreq.response.set_body(str(output))
+ qresponse.set_body(output)
- output_file = StringIO()
- qreq.response.write(output_file)
- output_file.seek(0)
- msg = rfc822.Message(output_file)
- msg.rewindbody()
- output = output_file.read()
-
# Copy headers from Quixote's HTTP response
- for hdr in msg.keys():
- values = msg.getheaders(hdr)
+ for name, value in qresponse.generate_headers():
# XXX Medusa's HTTP request is buggy, and only allows unique
# headers.
- for v in values:
- request[hdr.title()] = v
+ request[name] = value
+
+ request.response(qresponse.status_code)
- request.response(qreq.response.status_code)
-
# XXX should we set a default Last-Modified time?
- request['Content-Length'] = '0'
- if output:
- request.push(output)
- request['Content-Length'] = str(len(output))
+ if qresponse.body is not None:
+ if isinstance(qresponse.body, Stream):
+ request.push(StreamProducer(qresponse.body))
+ else:
+ request.push(qresponse.body)
request.done()
Modified: trunk/quixote/server/twisted_http.py
===================================================================
--- trunk/quixote/server/twisted_http.py 2003-08-27 20:25:14 UTC (rev 22347)
+++ trunk/quixote/server/twisted_http.py 2003-08-27 20:52:23 UTC (rev 22348)
@@ -19,14 +19,11 @@
from twisted.protocols import http
from twisted.web import server
-from rfc822 import formatdate
-import time
-
import quixote
quixote.enable_ptl()
-import quixote.demo
from quixote import errors
from quixote.publish import Publisher
+from quixote.http_response import Stream
class QuixoteTWRequest(server.Request):
@@ -37,43 +34,15 @@
self.content.seek(0,0)
qxrequest = self.publisher.create_request(self.content, environ)
self.quixote_publish(qxrequest, environ)
- self.massage_response(qxrequest)
- self.write(qxrequest.response.body)
- self.finish()
-
-
- def massage_response(self, qxrequest):
- """
- Does the work of reponse.write(), without actually writing,
- just massages the response into a Twisted fashion.
- """
resp = qxrequest.response
self.setResponseCode(resp.status_code)
-
- for hdr, value in resp.headers.items():
+ for hdr, value in resp.generate_headers():
self.setHeader(hdr, value)
+ if resp.body is not None:
+ self.write(resp.body)
+ self.finish()
- now = time.time()
- if not resp.headers.has_key('date'):
- resp.headers['date'] = '%s\r\n' % formatdate(now)
- if not resp.headers.has_key('expires'):
- if resp.cache > 0:
- expire_date = formatdate(now + resp.cache)
- else:
- expire_date = "-1" # allowed by HTTP spec, may work
- # better with some clients
- resp.headers['expires'] = '%s\r\n' % expire_date
-
- for k, d in resp.cookies.items():
- value = d['value']
- del d['value']
- # this might be a bit risky, the addCookie signature is
- # addCookie(self, k, v, expires=None, domain=None,
- # path=None, max_age=None, comment=None, secure=None)
- self.addCookie(k, value, **d)
-
-
def quixote_publish(self, qxrequest, env):
"""
Warning, this sidesteps the Publisher.publish method,
@@ -95,7 +64,10 @@
# don't write out the output, just set the response body
# the calling method will do the rest.
if output:
- qxrequest.response.set_body(str(output))
+ if isinstance(output, Stream):
+ # XXX a Twisted person needs to add support for streams
+ output = ''.join(map(str, output))
+ qxrequest.response.set_body(output)
pub._clear_request()
@@ -157,6 +129,7 @@
def run ():
+ import quixote.demo
# Ports this server will listen on
http_port = 8080
namespace = quixote.demo
Modified: trunk/quixote/util.py
===================================================================
--- trunk/quixote/util.py 2003-08-27 20:25:14 UTC (rev 22347)
+++ trunk/quixote/util.py 2003-08-27 20:52:23 UTC (rev 22348)
@@ -20,6 +20,7 @@
import sys, xmlrpclib
import os, mimetypes, urllib
from quixote import errors, html
+from quixote.http_response import Stream
from cStringIO import StringIO
from rfc822 import formatdate
@@ -59,6 +60,24 @@
return result
+class FileStream(Stream):
+
+ CHUNK_SIZE = 20000
+
+ def __init__(self, fp, size=None):
+ self.fp = fp
+ self.length = size
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ chunk = self.fp.read(self.CHUNK_SIZE)
+ if not chunk:
+ raise StopIteration
+ return chunk
+
+
class StaticFile:
"""
@@ -99,7 +118,8 @@
self.cache_time = cache_time
def __call__(self, request):
- last_modified = formatdate(os.stat(self.path).st_mtime)
+ stat = os.stat(self.path)
+ last_modified = formatdate(stat.st_mtime)
if last_modified == request.get_header('If-Modified-Since'):
# handle exact match of If-Modified-Since header
request.response.set_status(304)
@@ -109,9 +129,6 @@
request.response.set_content_type(self.mime_type)
if self.encoding:
request.response.set_header("Content-Encoding", self.encoding)
- fsfile = open(self.path, 'rb')
- contents = fsfile.read()
- fsfile.close()
request.response.set_header('Last-Modified', last_modified)
@@ -124,7 +141,7 @@
# to contact the server
request.response.cache = self.cache_time
- return contents
+ return FileStream(open(self.path, 'rb'), stat.st_size)
class StaticDirectory:
@@ -218,9 +235,4 @@
raise errors.TraversalError
if self.use_cache:
self.cache[name] = item
- if isinstance(item, StaticDirectory):
- return item
- else:
- return item(request)
-
-
+ return item