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