summaryrefslogtreecommitdiff
path: root/wsgitools
diff options
context:
space:
mode:
authorHelmut Grohne <helmut@subdivi.de>2007-04-14 22:37:26 +0200
committerHelmut Grohne <helmut@subdivi.de>2007-04-14 22:37:26 +0200
commit2435f82361f6bc4dcd51e1305905ecbbb5757f50 (patch)
treedf738a4ffdbd212383b6d8df22cb3b74e1fc83f9 /wsgitools
downloadwsgitools-2435f82361f6bc4dcd51e1305905ecbbb5757f50.tar.gz
initial tree
Diffstat (limited to 'wsgitools')
-rw-r--r--wsgitools/__init__.py0
-rw-r--r--wsgitools/applications.py27
-rw-r--r--wsgitools/filters.py218
-rw-r--r--wsgitools/middlewares.py267
-rw-r--r--wsgitools/scgi.py188
5 files changed, 700 insertions, 0 deletions
diff --git a/wsgitools/__init__.py b/wsgitools/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/wsgitools/__init__.py
diff --git a/wsgitools/applications.py b/wsgitools/applications.py
new file mode 100644
index 0000000..1e2b435
--- /dev/null
+++ b/wsgitools/applications.py
@@ -0,0 +1,27 @@
+class StaticContent:
+ """This wsgi application provides static content on whatever request it
+ receives."""
+ def __init__(self, status, headers, content):
+ """status is the HTTP status returned to the browser (ex: "200 OK")
+ headers is a list of (header, value) pairs being delivered as HTTP
+ headers
+ content contains the data to be delivered to the client. It is either a
+ string or some kind of iterable yielding strings.
+ """
+ self.status = status
+ self.headers = headers
+ length = -1
+ if isinstance(content, basestring):
+ self.content = [content]
+ length = len(content)
+ else:
+ self.content = content
+ if isinstance(self.content, list):
+ length = sum(map(len, self.content))
+ if length >= 0:
+ if not [v for h, v in headers if h.lower() == "content-length"]:
+ headers.append(("Content-length", str(length)))
+ def __call__(self, environ, start_response):
+ """wsgi interface"""
+ start_response(self.status, self.headers)
+ return self.content
diff --git a/wsgitools/filters.py b/wsgitools/filters.py
new file mode 100644
index 0000000..1d6154f
--- /dev/null
+++ b/wsgitools/filters.py
@@ -0,0 +1,218 @@
+__all__ = []
+
+import sys
+import time
+
+__all__.append("CloseableIterator")
+class CloseableIterator:
+ """Concatenating iterator with close attribute."""
+ def __init__(self, close_function, *iterators):
+ """If close_function is not None, it will be the close attribute of
+ the created iterator object. Further parameters specify iterators
+ that are to be concatenated."""
+ if close_function is not None:
+ self.close = close_function
+ self.iterators = map(iter, iterators)
+ def __iter__(self):
+ """iterator interface"""
+ return self
+ def next(self):
+ """iterator interface"""
+ if not self.iterators:
+ raise StopIteration
+ try:
+ return self.iterators[0].next()
+ except StopIteration:
+ self.iterators.pop(0)
+ return self.next()
+
+__all__.append("CloseableList")
+class CloseableList(list):
+ """A list with a close attribute."""
+ def __init__(self, close_function, *args):
+ """If close_function is not None, it will be the close attribute of
+ the created list object. Other parameters are passed to the list
+ constructor."""
+ if close_function is not None:
+ self.close = close_function
+ list.__init__(self, *args)
+ def __iter__(self):
+ return CloseableIterator(getattr(self, "close", None),
+ list.__iter__(self))
+
+__all__.append("BaseWSGIFilter")
+class BaseWSGIFilter:
+ """Generic WSGI filter class to be used with WSGIFilterMiddleware.
+
+ For each request a filter object gets created.
+ The environment is then passed through filter_environ.
+ Possible exceptions are filtered by filter_exc_info.
+ After that for each (header, value) tuple filter_header is used.
+ The resulting list is filtered through filter_headers.
+ Any data is filtered through filter_data.
+ In order to possibly append data the append_data method is invoked.
+ When the request has finished handle_close is invoked.
+
+ All methods do not modify the passed data by default. Passing the
+ BaseWSGIFilter class to a WSGIFilterMiddleware will result in not modifying
+ the request at all.
+ """
+ def __init__(self):
+ """This constructor does nothing and can safely be overwritten. It is
+ only listed here to document that it must be callable without additional
+ parameters."""
+ pass
+ def filter_environ(self, environ):
+ """Receives a dict with the environment passed to the wsgi application
+ and a dict must be returned. The default is to return the same dict."""
+ return environ
+ def filter_exc_info(self, exc_info):
+ """Receives either None or a tuple passed as third argument to
+ start_response from the wrapped wsgi application. Either None or such a
+ tuple must be returned."""
+ return exc_info
+ def filter_status(self, status):
+ """Receives a status string passed as first argument to start_response
+ from the wrapped wsgi application. A valid HTTP status string must be
+ returned."""
+ return status
+ def filter_header(self, headername, headervalue):
+ """This function is invoked for each (headername, headervalue) tuple in
+ the second argument to the start_response from the wrapped wsgi
+ application. Such a value or None for discarding the header must be
+ returned."""
+ return (headername, headervalue)
+ def filter_headers(self, headers):
+ """A list of headers passed as the second argument to the start_response
+ from the wrapped wsgi application is passed to this function and such a
+ list must also be returned."""
+ return headers
+ def filter_data(self, data):
+ """For each string that is either written by the write callable or
+ returned from the wrapped wsgi application this method is invoked. It
+ must return a string."""
+ return data
+ def append_data(self):
+ """This function can be used to append data to the response. A list of
+ strings or some kind of iterable yielding strings has to be returned.
+ The default is to return an empty list.
+ """
+ return []
+ def handle_close(self):
+ """This method is invoked after the request has finished."""
+ pass
+
+__all__.append("WSGIFilterMiddleware")
+class WSGIFilterMiddleware:
+ """This wsgi middleware can be used with specialized BaseWSGIFilters to
+ modify wsgi requests and/or reponses."""
+ def __init__(self, app, filterclass):
+ """app is a wsgi application.
+ filterclass is a subclass of BaseWSGIFilter or some class that
+ implements the interface."""
+ self.app = app
+ self.filterclass = filterclass
+ def __call__(self, environ, start_response):
+ """wsgi interface"""
+ filter = self.filterclass()
+ environ = filter.filter_environ(environ)
+
+ def modified_start_response(status, headers, exc_info=None):
+ exc_info = filter.filter_exc_info(exc_info)
+ status = filter.filter_status(status)
+ headers = (filter.filter_header(h, v) for h, v in headers)
+ headers = [h for h in headers if h is not None]
+ headers = filter.filter_headers(headers)
+ write = start_response(status, headers, exc_info)
+ def modified_write(data):
+ write(filter.filter_data(data))
+ return modified_write
+
+ ret = self.app(environ, modified_start_response)
+
+ def modified_close():
+ filter.handle_close()
+ getattr(ret, "close", lambda:0)()
+
+ if isinstance(ret, list):
+ return CloseableList(modified_close,
+ [filter.filter_data(data) for data in ret]
+ + list(filter.append_data()))
+ ret = iter(ret)
+ return CloseableIterator(modified_close,
+ (filter.filter_data(data) for data in ret),
+ filter.append_data())
+
+__all__.append("RequestLogWSGIFilter")
+class RequestLogWSGIFilter(BaseWSGIFilter):
+ """This filter logs all requests in the apache log file format."""
+ @classmethod
+ def creator(cls, log):
+ return lambda:cls(log)
+ def __init__(self, log=sys.stdout):
+ self.log = log
+ self.time = time.strftime("%d/%b/%Y:%T %z")
+ self.length = 0
+ def filter_environ(self, environ):
+ """BaseWSGIFilter interface"""
+ self.remote = environ.get("REMOTE_ADDR", "?")
+ self.reqmethod = environ["REQUEST_METHOD"]
+ self.path = environ["SCRIPT_NAME"] + environ["PATH_INFO"]
+ self.proto = environ.get("SERVER_PROTOCOL", None)
+ self.referrer = environ.get("HTTP_REFERER", "-")
+ self.useragent = environ.get("HTTP_USER_AGENT", "-")
+ return environ
+ def filter_status(self, status):
+ """BaseWSGIFilter interface"""
+ self.status = status.split()[0]
+ return status
+ def filter_data(self, data):
+ """BaseWSGIFilter interface"""
+ self.length += len(data)
+ return data
+ def handle_close(self):
+ """BaseWSGIFilter interface"""
+ line = '%s - - [%s] "%s' % (self.remote, self.time, self.reqmethod)
+ line = '%s %s' % (line, self.path)
+ if self.proto is not None:
+ line = "%s %s" % (line, self.proto)
+ line = '%s" %s %d' % (line, self.status, self.length)
+ if self.referrer is not None:
+ line = '%s "%s"' % (line, self.referrer)
+ else:
+ line += " -"
+ if self.useragent is not None:
+ line = '%s "%s"' % (line, self.useragent)
+ else:
+ line += " -"
+ print >> self.log, line
+
+__all__.append("TimerWSGIFilter")
+class TimerWSGIFilter(BaseWSGIFilter):
+ @classmethod
+ def creator(cls, pattern):
+ return lambda:cls(pattern)
+ def __init__(self, pattern="?GenTime"):
+ self.pattern = pattern
+ self.start = time.time()
+ def filter_data(self, data):
+ """BaseWSGIFilter interface"""
+ if data == self.pattern:
+ return "%8.3g" % (time.time() - self.start)
+ return data
+
+__all__.append("EncodeWSGIFilter")
+class EncodeWSGIFilter(BaseWSGIFilter):
+ """Encodes all body data (no headers) with given charset."""
+ @classmethod
+ def creator(cls, charset):
+ return lambda:cls(charset)
+ def __init__(self, charset="utf-8"):
+ self.charset = charset
+ def filter_data(self, data):
+ """BaseWSGIFilter interface"""
+ return data.encode(self.charset)
+ def filter_header(self, header, value):
+ if header.lower() != "content-type":
+ return (header, value)
+ return (header, "%s; charset=%s" % (value, self.charset))
diff --git a/wsgitools/middlewares.py b/wsgitools/middlewares.py
new file mode 100644
index 0000000..9eab2a1
--- /dev/null
+++ b/wsgitools/middlewares.py
@@ -0,0 +1,267 @@
+__all__ = []
+
+import time
+from filters import CloseableList, CloseableIterator
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+__all__.append("SubdirMiddleware")
+class SubdirMiddleware:
+ def __init__(self, default, mapping={}):
+ self.default = default
+ self.mapping = mapping
+ def __call__(self, environ, start_response):
+ app = None
+ script = environ["PATH_INFO"]
+ path_info = ""
+ while '/' in script:
+ if script in self.mapping:
+ app = self.mapping[script]
+ break
+ script, tail = script.rsplit('/', 1)
+ path_info = "/%s%s" % (tail, path_info)
+ if app is None:
+ app = self.mapping.get(script, None)
+ if app is None:
+ app = self.default
+ environ["SCRIPT_NAME"] += script
+ environ["PATH_INFO"] = path_info
+ return app(environ, start_response)
+
+__all__.append("NoWriteCallableMiddleware")
+class NoWriteCallableMiddleware:
+ """This middleware wraps a wsgi application that needs the return value of
+ start_response function to a wsgi application that doesn't need one by
+ writing the data to a StringIO and then making it be the first result
+ element."""
+ def __init__(self, app):
+ """Wraps wsgi application app."""
+ self.app = app
+ def __call__(self, environ, start_response):
+ """wsgi interface"""
+ todo = []
+ def modified_start_response(status, headers, exc_info=None):
+ if exc_info is not None:
+ todo.append(None)
+ return start_response(status, headers)
+ else:
+ sio = StringIO.StringIO()
+ todo.append((status, headers, sio))
+ return sio.write
+
+ ret = self.app(environ, modified_start_response)
+
+ if todo and todo[0] is None:
+ return ret
+
+ if isinstance(ret, list):
+ status, headers, data = todo[0]
+ data = data.getvalue()
+ if data:
+ ret.insert(0, data)
+ start_response(status, headers)
+ return ret
+
+ ret = iter(ret)
+ stopped = False
+ try:
+ first = ret.next()
+ except StopIteration:
+ stopped = True
+
+ status, headers, data = todo[0]
+ data = data.getvalue()
+ start_response(status, headers)
+
+ if stopped:
+ return CloseableList(getattr(ret, "close", None), (data,))
+
+ return CloseableIterator(getattr(ret, "close", None),
+ (data, first), ret)
+
+__all__.append("ContentLengthMiddleware")
+class ContentLengthMiddleware:
+ """Guesses the content length header if possible.
+ Note: The application used must not use the write callable returned by
+ start_response."""
+ def __init__(self, app, maxstore=0):
+ """Wraps wsgi application app. It can also store the first result bytes
+ to possibly return a list of strings which will make guessing the size
+ of iterators possible. At most maxstore bytes will be accumulated.
+ Please note that a value larger than 0 will violate the wsgi standard.
+ The magical value () will make it always gather all data.
+ """
+ self.app = app
+ self.maxstore = maxstore
+ def __call__(self, environ, start_response):
+ """wsgi interface"""
+ todo = []
+ def modified_start_response(status, headers, exc_info=None):
+ if (exc_info is not None or
+ [v for h, v in headers if h.lower() == "content-length"]):
+ todo[:] = (None,)
+ return start_response(status, headers, exc_info)
+ else:
+ todo[:] = ((status, headers),)
+ def raise_not_imp(*args):
+ raise NotImplementedError
+ return raise_not_imp
+
+ ret = self.app(environ, modified_start_response)
+
+ if todo and todo[0] is None: # nothing to do
+ #print "content-length: nothing"
+ return ret
+
+ if isinstance(ret, list):
+ #print "content-length: simple"
+ status, headers = todo[0]
+ length = sum(map(len, ret))
+ headers.append(("Content-length", str(length)))
+ start_response(status, headers)
+ return ret
+
+ ret = iter(ret)
+ stopped = False
+ data = CloseableList(getattr(ret, "close", None))
+ length = 0
+ try:
+ data.append(ret.next()) # fills todo
+ length += len(data[-1])
+ except StopIteration:
+ stopped = True
+
+ status, headers = todo[0]
+
+ while (not stopped) and length < self.maxstore:
+ try:
+ data.append(ret.next())
+ length += len(data[-1])
+ except StopIteration:
+ stopped = True
+
+ if stopped:
+ #print "content-length: gathered"
+ headers.append(("Content-length", str(length)))
+ start_response(status, headers)
+ return data
+
+ #print "content-length: passthrough"
+ start_response(status, headers)
+
+ return CloseableIterator(getattr(ret, "close", None), data, ret)
+
+def storable(environ):
+ if environ["REQUEST_METHOD"] != "GET":
+ return False
+ return True
+
+def cacheable(environ):
+ if environ.get("HTTP_CACHE_CONTROL", "") == "max-age=0":
+ return False
+ return True
+
+__all__.append("CachingMiddleware")
+class CachingMiddleware:
+ """Caches reponses to requests based on SCRIPT_NAME, PATH_INFO and
+ QUERY_STRING."""
+ def __init__(self, app, maxage=60, storable=storable, cacheable=cacheable):
+ """app is a wsgi application to be cached.
+ maxage is the number of seconds a reponse may be cached.
+ storable is a predicated that determines whether the response may be
+ cached at all based on the environ dict.
+ cacheable is a predicate that determines whether this request
+ invalidates the cache."""
+ self.app = app
+ self.maxage = maxage
+ self.storable = storable
+ self.cacheable = cacheable
+ self.cache = {}
+ def __call__(self, environ, start_response):
+ """wsgi interface"""
+ if not self.storable(environ):
+ return self.app(environ, start_response)
+ path = environ.get("SCRIPT_NAME", "/")
+ path += environ.get("PATH_INFO", '')
+ path += "?" + environ.get("QUERY_STRING", "")
+ if self.cacheable(environ) and path in self.cache:
+ if self.cache[path][0] + self.maxage >= time.time():
+ start_response(self.cache[path][1], self.cache[path][2])
+ return self.cache[path][3]
+ else:
+ del self.cache[path]
+ cache_object = [time.time(), "", [], []]
+ def modified_start_respesponse(status, headers, exc_info):
+ if exc_info is not None:
+ return self.app(status, headers, exc_info)
+ cache_object[1] = status
+ cache_object[2] = headers
+ write = start_response(status, headers)
+ def modified_write(data):
+ cache_object[3].append(data)
+ write(data)
+ return modified_write
+ ret = self.app(environ, modified_start_respesponse)
+ if isinstance(ret, list):
+ cache_object[3].extend(ret)
+ self.cache[path] = cache_object
+ return ret
+ def pass_through():
+ for data in ret:
+ cache_object[3].append(data)
+ yield data
+ self.cache[path] = cache_object
+ return CloseableIterator(getattr(ret, "close", None), pass_through())
+
+__all__.append("DictAuthChecker")
+class DictAuthChecker:
+ def __init__(self, users):
+ self.users = users
+ def __call__(self, username, password):
+ return username in self.users and self.users[username] == password
+
+__all__.append("BasicAuthMiddleware")
+class BasicAuthMiddleware:
+ """Middleware implementing HTTP Basic Auth."""
+ def __init__(self, app, check_function, realm='www'):
+ """app is a WSGI application.
+ check_function is a function taking two arguments username and password
+ returning a bool indicating whether the request may is
+ allowed."""
+ self.app = app
+ self.check_function = check_function
+ self.realm = realm
+
+ def __call__(self, environ, start_response):
+ """wsgi interface"""
+ auth = environ.get("HTTP_AUTHORIZATION")
+ if not auth or ' ' not in auth:
+ return self.authorization_required(environ, start_response)
+ auth_type, enc_auth_info = auth.split(None, 1)
+ try:
+ auth_info = enc_auth_info.decode("base64")
+ except: # It throws some non-standard exception.
+ return self.authorization_required(environ, start_response)
+ if auth_type.lower() != "basic" or ':' not in auth_info:
+ return self.authorization_required(environ, start_response)
+ username, password = auth_info.split(':', 1)
+ if self.check_function(username, password):
+ environ["REMOTE_USER"] = username
+ return self.app(environ, start_response)
+ return self.authorization_required(environ, start_response)
+
+ def authorization_required(self, environ, start_response):
+ """wsgi application for indicating authorization is required."""
+ status = "401 Authorization required"
+ headers = [('Content-type', 'text/html'),
+ ('WWW-Authenticate', 'Basic realm="%s"' % self.realm)]
+ if environ["REQUEST_METHOD"] == "HEAD":
+ start_response(status, headers)
+ return []
+ html = "<html><head><title>Authorization required</title></head>" + \
+ "<body><h1>Authorization required</h1></body></html>\n"
+ headers.append(('Content-length', len(html)))
+ start_response(status, headers)
+ return [html]
diff --git a/wsgitools/scgi.py b/wsgitools/scgi.py
new file mode 100644
index 0000000..e5b7dfc
--- /dev/null
+++ b/wsgitools/scgi.py
@@ -0,0 +1,188 @@
+__all__ = []
+
+import asyncore
+import socket
+import sys
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+
+class SCGIConnection(asyncore.dispatcher):
+ """SCGI connection class used by WSGISCGIServer."""
+ # maximum request size
+ MAX_REQUEST_SIZE = 65536
+ # maximum post size
+ MAX_POST_SIZE = 8 << 20
+ # read and write size
+ BLOCK_SIZE = 4096
+ # connection states
+ NEW = 0*4 | 1 # connection established, waiting for request
+ HEADER = 1*4 | 1 # the request length was received, waiting for the rest
+ BODY = 2*4 | 1 # the request header was received, waiting for the body
+ REQ = 3*4 | 2 # request received, sending response
+ def __init__(self, server, connection, addr):
+ self.server = server # WSGISCGIServer instance
+ self.addr = addr # scgi client address
+ self.state = SCGIConnection.NEW # internal state
+ self.environ = {} # environment passed to wsgi app
+ self.reqlen = -1 # request length used in two different meanings
+ self.inbuff = "" # input buffer
+ self.outbuff = "" # output buffer
+ self.wsgihandler = None # wsgi application iterator
+ self.outheaders = () # headers to be sent
+ # () -> unset, (..,..) -> set, True -> sent
+ self.body = StringIO.StringIO() # request body
+ asyncore.dispatcher.__init__(self, connection)
+
+ def _wsgi_headers(self):
+ return {"wsgi.version": (1, 0),
+ "wsgi.input": self.body,
+ "wsgi.errors": self.server.error,
+ "wsgi.url_scheme": "http", # TODO: this is wrong
+ "wsgi.multithread": False,
+ "wsgi.multiprocess": False,
+ "wsgi.run_once": False}
+
+ def _try_send_headers(self):
+ if self.outheaders != True:
+ assert not self.outbuff
+ status, headers = self.outheaders
+ headdata = "".join(map("%s: %s\r\n".__mod__, headers))
+ self.outbuff = "Status: %s\r\n%s\r\n" % (status, headdata)
+ self.outheaders = True
+
+ def _wsgi_write(self, data):
+ assert self.state >= SCGIConnection.REQ
+ self._try_send_headers()
+ self.outbuff += data
+
+ def readable(self):
+ """asyncore interface"""
+ return self.state & 1 == 1
+
+ def writable(self):
+ """asyncore interface"""
+ return self.state & 2 == 2
+
+ def handle_read(self):
+ """asyncore interface"""
+ data = self.recv(self.BLOCK_SIZE)
+ self.inbuff += data
+ if self.state == SCGIConnection.NEW:
+ if ':' in self.inbuff:
+ l, self.inbuff = self.inbuff.split(':', 1)
+ if not l.isdigit():
+ self.close()
+ return # invalid request format
+ l = long(l)
+ if l > self.MAX_REQUEST_SIZE:
+ self.close()
+ return # request too long
+ self.reqlen = l
+ self.state = SCGIConnection.HEADER
+ elif len(self.inbuff) > self.MAX_REQUEST_SIZE:
+ self.close()
+ return # request too long
+
+ if self.state == SCGIConnection.HEADER:
+ buff = self.inbuff[:self.reqlen]
+ remainder = self.inbuff[self.reqlen:]
+
+ while buff.count('\0') >= 2:
+ k, v, buff = buff.split('\0', 2)
+ self.environ[k] = v
+ self.reqlen -= len(k) + len(v) + 2
+
+ self.inbuff = buff + remainder
+
+ if self.reqlen == 0:
+ if self.inbuff.startswith(','):
+ self.inbuff = self.inbuff[1:]
+ self.reqlen = long(self.environ["CONTENT_LENGTH"])
+ if self.reqlen > self.MAX_POST_SIZE:
+ self.close()
+ return
+ self.state = SCGIConnection.BODY
+ else:
+ self.close()
+ return # protocol violation
+
+ if self.state == SCGIConnection.BODY:
+ if len(self.inbuff) >= self.reqlen:
+ self.body.write(self.inbuff[:self.reqlen])
+ self.body.seek(0)
+ self.inbuff = ""
+ self.reqlen = 0
+ self.environ.update(self._wsgi_headers())
+ if "HTTP_CONTENT_TYPE" in self.environ:
+ self.environ["CONTENT_TYPE"] = \
+ self.environ.pop("HTTP_CONTENT_TYPE")
+ if "HTTP_CONTENT_LENGTH" in self.environ:
+ del self.environ["HTTP_CONTENT_LENGTH"] # TODO: better way?
+ self.wsgihandler = iter(self.server.wsgiapp(
+ self.environ, self.start_response))
+ self.state = SCGIConnection.REQ
+ else:
+ self.body.write(self.inbuff)
+ self.reqlen -= len(self.inbuff)
+ self.inbuff = ""
+
+ def start_response(self, status, headers, exc_info=None):
+ if exc_info:
+ if self.outheaders == True:
+ try:
+ raise exc_info[0], exc_info[1], exc_info[2]
+ finally:
+ exc_info = None
+ assert self.outheaders != True # unsent
+ self.outheaders = (status, headers)
+ return self._wsgi_write
+
+ def handle_write(self):
+ """asyncore interface"""
+ assert self.state >= SCGIConnection.REQ
+ if len(self.outbuff) < self.BLOCK_SIZE:
+ for data in self.wsgihandler:
+ self._try_send_headers()
+ self.outbuff += data
+ if len(self.outbuff) >= self.BLOCK_SIZE:
+ break
+ if len(self.outbuff) == 0:
+ if hasattr(self.wsgihandler, "close"):
+ self.wsgihandler.close()
+ self.close()
+ return
+ try:
+ n = self.send(self.outbuff[:self.BLOCK_SIZE])
+ except socket.error:
+ if hasattr(self.wsgihandler, "close"):
+ self.wsgihandler.close()
+ self.close()
+ return
+ self.outbuff = self.outbuff[n:]
+
+ def handle_close(self):
+ """asyncore interface"""
+ self.close()
+
+__all__.append("SCGIServer")
+class SCGIServer(asyncore.dispatcher):
+ def __init__(self, wsgiapp, port, interface="localhost", error=sys.stderr):
+ asyncore.dispatcher.__init__(self)
+ self.wsgiapp = wsgiapp
+ self.error = error
+ self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.set_reuse_addr()
+ self.bind((interface, port))
+ self.listen(5)
+
+ def handle_accept(self):
+ r = self.accept()
+ if r is not None:
+ conn, addr = r
+ SCGIConnection(self, conn, addr)
+
+ def run(self):
+ asyncore.loop()
+