summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAllan Saddi <allan@saddi.com>2007-06-05 09:42:39 -0700
committerAllan Saddi <allan@saddi.com>2007-06-05 09:42:39 -0700
commitcbe792618a1e6b1aeddd8d751dbeefe6a981ba23 (patch)
tree803d38bc8a5f7c10b86efd36742a925ad5a6960b
parentac40930383daac77cb44bd75b2a899fa2934a81e (diff)
downloadflup-cbe792618a1e6b1aeddd8d751dbeefe6a981ba23.tar.gz
Remove publisher and middleware packages. Add cgi server for completeness.1.0
-rw-r--r--ChangeLog9
-rw-r--r--flup/middleware/__init__.py1
-rw-r--r--flup/middleware/error.py352
-rw-r--r--flup/middleware/gzip.py279
-rw-r--r--flup/middleware/session.py770
-rw-r--r--flup/publisher.py767
-rw-r--r--flup/resolver/__init__.py2
-rw-r--r--flup/resolver/complex.py112
-rw-r--r--flup/resolver/function.py46
-rw-r--r--flup/resolver/importingmodule.py129
-rw-r--r--flup/resolver/module.py86
-rw-r--r--flup/resolver/nopathinfo.py57
-rw-r--r--flup/resolver/objectpath.py156
-rw-r--r--flup/resolver/resolver.py81
-rw-r--r--flup/server/cgi.py71
-rw-r--r--setup.py6
16 files changed, 79 insertions, 2845 deletions
diff --git a/ChangeLog b/ChangeLog
index e37b3e3..022312f 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -8,6 +8,11 @@
* Prevent ThreadPool inconsistences if an exception is
actually raised. Thanks to Tim Chen for the patch.
+2007-06-05 Allan Saddi <allan@saddi.com>
+
+ * Remove publisher and middleware packages.
+ * Add cgi server for completeness.
+
2007-05-17 Allan Saddi <allan@saddi.com>
* Fix fcgi_fork so it can run on Solaris. Thanks to
@@ -36,9 +41,7 @@
* Update servers to default to an empty QUERY_STRING if
not present in the environ.
-
* Update gzip.py: compresslevel -> compress_level
-
* Update gzip.py by updating docstrings and renaming
classes/methods/functions to better follow Python naming
conventions. NB: mimeTypes keyword parameter is now
@@ -65,10 +68,8 @@
2006-11-24 Allan Saddi <allan@saddi.com>
* Add *_thread egg entry-point aliases.
-
* Add UNIX domain socket support to scgi, scgi_fork,
scgi_app.
-
* Add flup.client package which contains various
WSGI -> connector client implentations. (So far: FastCGI,
and SCGI.)
diff --git a/flup/middleware/__init__.py b/flup/middleware/__init__.py
deleted file mode 100644
index 792d600..0000000
--- a/flup/middleware/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-#
diff --git a/flup/middleware/error.py b/flup/middleware/error.py
deleted file mode 100644
index 637e842..0000000
--- a/flup/middleware/error.py
+++ /dev/null
@@ -1,352 +0,0 @@
-# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-import sys
-import os
-import traceback
-import time
-from email.Message import Message
-from email.MIMEMultipart import MIMEMultipart
-from email.MIMEText import MIMEText
-import smtplib
-
-try:
- import thread
-except ImportError:
- import dummy_thread as thread
-
-__all__ = ['ErrorMiddleware']
-
-def _wrapIterator(appIter, errorMiddleware, environ, start_response):
- """
- Wrapper around the application's iterator which catches any unhandled
- exceptions. Forwards close() and __len__ to the application iterator,
- if necessary.
- """
- class metaIterWrapper(type):
- def __init__(cls, name, bases, clsdict):
- super(metaIterWrapper, cls).__init__(name, bases, clsdict)
- if hasattr(appIter, '__len__'):
- cls.__len__ = appIter.__len__
-
- class iterWrapper(object):
- __metaclass__ = metaIterWrapper
- def __init__(self):
- self._next = iter(appIter).next
- if hasattr(appIter, 'close'):
- self.close = appIter.close
-
- def __iter__(self):
- return self
-
- def next(self):
- try:
- return self._next()
- except StopIteration:
- raise
- except:
- errorMiddleware.exceptionHandler(environ)
-
- # I'm not sure I like this next part.
- try:
- errorIter = errorMiddleware.displayErrorPage(environ,
- start_response)
- except:
- # Headers already sent, what can be done?
- raise
- else:
- # The exception occurred early enough for start_response()
- # to succeed. Swap iterators!
- self._next = iter(errorIter).next
- return self._next()
-
- return iterWrapper()
-
-class ErrorMiddleware(object):
- """
- Middleware that catches any unhandled exceptions from the application.
- Displays a (static) error page to the user while emailing details about
- the exception to an administrator.
- """
- def __init__(self, application, adminAddress,
- fromAddress='wsgiapp',
- smtpHost='localhost',
-
- applicationName=None,
-
- errorPageMimeType='text/html',
- errorPage=None,
- errorPageFile='error.html',
-
- emailInterval=15,
- intervalCheckFile='errorEmailCheck',
-
- debug=False):
- """
- Explanation of parameters:
-
- application - WSGI application.
-
- adminAddress - Email address of administrator.
- fromAddress - Email address that the error email should appear to
- originate from. By default 'wsgiapp@hostname.of.server'.
- smtpHost - SMTP email server, through which to send the email.
-
- applicationName - Name of your WSGI application, to help differentiate
- it from other applications in email. By default, this is the Python
- name of the application object. (You should explicitly set this
- if you use other middleware components, otherwise the name
- deduced will probably be that of a middleware component.)
-
- errorPageMimeType - MIME type of the static error page. 'text/html'
- by default.
- errorPage - String representing the body of the static error page.
- If None (the default), errorPageFile must point to an existing file.
- errorPageFile - File from which to take the static error page (may
- be relative to current directory or an absolute filename).
-
- emailInterval - Minimum number of minutes between error mailings,
- to prevent the administrator's mailbox from filling up.
- intervalCheckFile - When running in one-shot mode (as determined by
- the 'wsgi.run_once' environment variable), this file is used to
- keep track of the last time an email was sent. May be relative
- (to the current directory) or an absolute filename.
-
- debug - If True, will attempt to display the traceback as a webpage.
- No email is sent. If False (the default), the static error page is
- displayed and the error email is sent, if necessary.
- """
- self._application = application
-
- self._adminAddress = adminAddress
- self._fromAddress = fromAddress
- self._smtpHost = smtpHost
-
- # Set up a generic application name if not specified.
- if applicationName is None:
- applicationName = []
- if application.__module__ != '__main__':
- applicationName.append('%s.' % application.__module__)
- applicationName.append(application.__name__)
- applicationName = ''.join(applicationName)
- self._applicationName = applicationName
-
- self._errorPageMimeType = errorPageMimeType
- # If errorPage was unspecified, set it from the static file
- # specified by errorPageFile.
- if errorPage is None:
- f = open(errorPageFile)
- errorPage = f.read()
- f.close
- self._errorPage = errorPage
-
- self._emailInterval = emailInterval * 60
- self._lastEmailTime = 0
- self._intervalCheckFile = intervalCheckFile
-
- # Set up displayErrorPage appropriately.
- self._debug = debug
- if debug:
- self.displayErrorPage = self._displayDebugPage
- else:
- self.displayErrorPage = self._displayErrorPage
-
- # Lock for _lastEmailTime
- self._lock = thread.allocate_lock()
-
- def _displayErrorPage(self, environ, start_response):
- """
- Displays the static error page. May be overridden. (Maybe you'd
- rather redirect or something?) This is basically a mini-WSGI
- application, except that start_response() is called with the third
- argument.
-
- Really, there's nothing keeping you from overriding this method
- and displaying a dynamic error page. But I thought it might be safer
- to display a static page. :)
- """
- start_response('200 OK', [('Content-Type', self._errorPageMimeType),
- ('Content-Length',
- str(len(self._errorPage)))],
- sys.exc_info())
- return [self._errorPage]
-
- def _displayDebugPage(self, environ, start_response):
- """
- When debugging, display an informative traceback of the exception.
- """
- import cgitb
- result = [cgitb.html(sys.exc_info())]
- start_response('200 OK', [('Content-Type', 'text/html'),
- ('Content-Length', str(len(result[0])))],
- sys.exc_info())
- return result
-
- def _generateHTMLErrorEmail(self):
- """
- Generates the HTML version of the error email. Must return a string.
- """
- import cgitb
- return cgitb.html(sys.exc_info())
-
- def _generatePlainErrorEmail(self):
- """
- Generates the plain-text version of the error email. Must return a
- string.
- """
- import cgitb
- return cgitb.text(sys.exc_info())
-
- def _generateErrorEmail(self):
- """
- Generates the error email. Must return an instance of email.Message
- or subclass.
-
- This implementation generates a MIME multipart/alternative email with
- an HTML description of the error and a simpler plain-text alternative
- of the traceback.
- """
- msg = MIMEMultipart('alternative')
- msg.attach(MIMEText(self._generatePlainErrorEmail()))
- msg.attach(MIMEText(self._generateHTMLErrorEmail(), 'html'))
- return msg
-
- def _sendErrorEmail(self, environ):
- """
- Sends the error email as generated by _generateErrorEmail(). If
- anything goes wrong sending the email, the exception is caught
- and reported to wsgi.errors. I don't think there's really much else
- that can be done in that case.
- """
- msg = self._generateErrorEmail()
-
- msg['From'] = self._fromAddress
- msg['To'] = self._adminAddress
- msg['Subject'] = '%s: unhandled exception' % self._applicationName
-
- try:
- server = smtplib.SMTP(self._smtpHost)
- server.sendmail(self._fromAddress, self._adminAddress,
- msg.as_string())
- server.quit()
- except Exception, e:
- stderr = environ['wsgi.errors']
- stderr.write('%s: Failed to send error email: %r %s\n' %
- (self.__class__.__name__, e, e))
- stderr.flush()
-
- def _shouldSendEmail(self, environ):
- """
- Returns True if an email should be sent. The last time an email was
- sent is tracked by either an instance variable (if oneShot is False),
- or the mtime of a file on the filesystem (if oneShot is True).
- """
- if self._debug or self._adminAddress is None:
- # Never send email when debugging or when there's no admin
- # address.
- return False
-
- now = time.time()
- if not environ['wsgi.run_once']:
- self._lock.acquire()
- ret = (self._lastEmailTime + self._emailInterval) < now
- if ret:
- self._lastEmailTime = now
- self._lock.release()
- else:
- # The following should be protected, but do I *really* want
- # to get into the mess of using filesystem and file-based locks?
- # At worse, multiple emails get sent.
- ret = True
-
- try:
- mtime = os.path.getmtime(self._intervalCheckFile)
- except:
- # Assume file doesn't exist, which is OK. Send email
- # unconditionally.
- pass
- else:
- if (mtime + self._emailInterval) >= now:
- ret = False
-
- if ret:
- # NB: If _intervalCheckFile cannot be created or written to
- # for whatever reason, you will *always* get an error email.
- try:
- open(self._intervalCheckFile, 'w').close()
- except:
- # Probably a good idea to report failure.
- stderr = environ['wsgi.errors']
- stderr.write('%s: Error writing intervalCheckFile %r\n'
- % (self.__class__.__name__,
- self._intervalCheckFile))
- stderr.flush()
- return ret
-
- def exceptionHandler(self, environ):
- """
- Common handling of exceptions.
- """
- # Unconditionally report to wsgi.errors.
- stderr = environ['wsgi.errors']
- traceback.print_exc(file=stderr)
- stderr.flush()
-
- # Send error email, if needed.
- if self._shouldSendEmail(environ):
- self._sendErrorEmail(environ)
-
- def __call__(self, environ, start_response):
- """
- WSGI application interface. Simply wraps the call to the application
- with a try ... except. All the fancy stuff happens in the except
- clause.
- """
- try:
- return _wrapIterator(self._application(environ, start_response),
- self, environ, start_response)
- except:
- # Report the exception.
- self.exceptionHandler(environ)
-
- # Display static error page.
- return self.displayErrorPage(environ, start_response)
-
-if __name__ == '__main__':
- def myapp(environ, start_response):
- start_response('200 OK', [('Content-Type', 'text/plain')])
- raise RuntimeError, "I'm broken!"
- return ['Hello World!\n']
-
- # Note - email address is taken from sys.argv[1]. I'm not leaving
- # my email address here. ;)
- app = ErrorMiddleware(myapp, sys.argv[1])
-
- from ajp import WSGIServer
- WSGIServer(app).run()
diff --git a/flup/middleware/gzip.py b/flup/middleware/gzip.py
deleted file mode 100644
index 59a800d..0000000
--- a/flup/middleware/gzip.py
+++ /dev/null
@@ -1,279 +0,0 @@
-# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-"""WSGI response gzipper middleware.
-
-This gzip middleware component differentiates itself from others in that
-it (hopefully) follows the spec more closely. Namely with regard to the
-application iterator and buffering. (It doesn't buffer.) See
-`Middleware Handling of Block Boundaries`_.
-
-Of course this all comes with a price... just LOOK at this mess! :)
-
-The inner workings of gzip and the gzip file format were gleaned from gzip.py.
-
-.. _Middleware Handling of Block Boundaries: http://www.python.org/dev/peps/pep-0333/#middleware-handling-of-block-boundaries
-"""
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-
-import struct
-import time
-import zlib
-import re
-
-
-__all__ = ['GzipMiddleware']
-
-
-def _gzip_header():
- """Returns a gzip header (with no filename)."""
- # See GzipFile._write_gzip_header in gzip.py
- return '\037\213' \
- '\010' \
- '\0' + \
- struct.pack('<L', long(time.time())) + \
- '\002' \
- '\377'
-
-
-class _GzipIterWrapper(object):
- """gzip application iterator wrapper.
-
- It ensures that: the application iterator's ``close`` method (if any) is
- called by the parent server; and at least one value is yielded each time
- the application's iterator yields a value.
-
- If the application's iterator yields N values, this iterator will yield
- N+1 values. This is to account for the gzip trailer.
- """
-
- def __init__(self, app_iter, gzip_middleware):
- self._g = gzip_middleware
- self._next = iter(app_iter).next
-
- self._last = False # True if app_iter has yielded last value.
- self._trailer_sent = False
-
- if hasattr(app_iter, 'close'):
- self.close = app_iter.close
-
- def __iter__(self):
- return self
-
- # This would've been a lot easier had I used a generator. But then I'd have
- # to wrap the generator anyway to ensure that any existing close() method
- # was called. (Calling it within the generator is not the same thing,
- # namely it does not ensure that it will be called no matter what!)
- def next(self):
- if not self._last:
- # Need to catch StopIteration here so we can append trailer.
- try:
- data = self._next()
- except StopIteration:
- self._last = True
-
- if not self._last:
- if self._g.gzip_ok:
- return self._g.gzip_data(data)
- else:
- return data
- else:
- # See if trailer needs to be sent.
- if self._g.header_sent and not self._trailer_sent:
- self._trailer_sent = True
- return self._g.gzip_trailer()
- # Otherwise, that's the end of this iterator.
- raise StopIteration
-
-
-class _GzipMiddleware(object):
- """The actual gzip middleware component.
-
- Holds compression state as well implementations of ``start_response`` and
- ``write``. Instantiated before each call to the underlying application.
-
- This class is private. See ``GzipMiddleware`` for the public interface.
- """
-
- def __init__(self, start_response, mime_types, compress_level):
- self._start_response = start_response
- self._mime_types = mime_types
-
- self.gzip_ok = False
- self.header_sent = False
-
- # See GzipFile.__init__ and GzipFile._init_write in gzip.py
- self._crc = zlib.crc32('')
- self._size = 0
- self._compress = zlib.compressobj(compress_level,
- zlib.DEFLATED,
- -zlib.MAX_WBITS,
- zlib.DEF_MEM_LEVEL,
- 0)
-
- def gzip_data(self, data):
- """Compresses the given data, prepending the gzip header if necessary.
-
- Returns the result as a string.
- """
- if not self.header_sent:
- self.header_sent = True
- out = _gzip_header()
- else:
- out = ''
-
- # See GzipFile.write in gzip.py
- length = len(data)
- if length > 0:
- self._size += length
- self._crc = zlib.crc32(data, self._crc)
- out += self._compress.compress(data)
- return out
-
- def gzip_trailer(self):
- """Returns the gzip trailer."""
- # See GzipFile.close in gzip.py
- return self._compress.flush() + \
- struct.pack('<l', self._crc) + \
- struct.pack('<L', self._size & 0xffffffffL)
-
- def start_response(self, status, headers, exc_info=None):
- """WSGI ``start_response`` implementation."""
- self.gzip_ok = False
-
- # Scan the headers. Only allow gzip compression if the Content-Type
- # is one that we're flagged to compress AND the headers do not
- # already contain Content-Encoding.
- for name,value in headers:
- name = name.lower()
- if name == 'content-type':
- value = value.split(';')[0].strip()
- for p in self._mime_types:
- if p.match(value) is not None:
- self.gzip_ok = True
- break # NB: Breaks inner loop only
- elif name == 'content-encoding':
- self.gzip_ok = False
- break
-
- if self.gzip_ok:
- # Remove Content-Length, if present, because compression will
- # most surely change it. (And unfortunately, we can't predict
- # the final size...)
- headers = [(name,value) for name,value in headers
- if name.lower() != 'content-length']
- headers.append(('Content-Encoding', 'gzip'))
-
- _write = self._start_response(status, headers, exc_info)
-
- if self.gzip_ok:
- def write_gzip(data):
- _write(self.gzip_data(data))
- return write_gzip
- else:
- return _write
-
-
-class GzipMiddleware(object):
- """WSGI middleware component that gzip compresses the application's
- response (if the client supports gzip compression - gleaned from the
- ``Accept-Encoding`` request header).
- """
-
- def __init__(self, application, mime_types=None, compress_level=9):
- """Initializes this GzipMiddleware.
-
- ``mime_types``
- A list of Content-Types that are OK to compress. Regular
- expressions are accepted. Defaults to ``[text/.*]`` if not
- specified.
-
- ``compress_level``
- The gzip compression level, an integer from 1 to 9; 1 is the
- fastest and produces the least compression, and 9 is the slowest,
- producing the most compression. The default is 9.
- """
- if mime_types is None:
- mime_types = ['text/', r'''application/(?:.+\+)?xml$''']
-
- self._application = application
- self._mime_types = [re.compile(m) for m in mime_types]
- self._compress_level = compress_level
-
- def __call__(self, environ, start_response):
- """WSGI application interface."""
- # If the client doesn't support gzip encoding, just pass through
- # directly to the application.
- if 'gzip' not in environ.get('HTTP_ACCEPT_ENCODING', ''):
- return self._application(environ, start_response)
-
- # All of the work is done in _GzipMiddleware and _GzipIterWrapper.
- g = _GzipMiddleware(start_response, self._mime_types,
- self._compress_level)
-
- result = self._application(environ, g.start_response)
-
- # See if it's a length 1 iterable...
- try:
- shortcut = len(result) == 1
- except:
- shortcut = False
-
- if shortcut:
- # Special handling if application returns a length 1 iterable:
- # also return a length 1 iterable!
- try:
- i = iter(result)
- # Hmmm, if we get a StopIteration here, the application's
- # broken (__len__ lied!)
- data = i.next()
- if g.gzip_ok:
- return [g.gzip_data(data) + g.gzip_trailer()]
- else:
- return [data]
- finally:
- if hasattr(result, 'close'):
- result.close()
-
- return _GzipIterWrapper(result, g)
-
-
-if __name__ == '__main__':
- def app(environ, start_response):
- start_response('200 OK', [('Content-Type', 'text/html')])
- yield 'Hello World!\n'
-
- from wsgiref import validate
- app = validate.validator(app)
- app = GzipMiddleware(app)
- app = validate.validator(app)
-
- from flup.server.ajp import WSGIServer
- import logging
- WSGIServer(app, loggingLevel=logging.DEBUG).run()
diff --git a/flup/middleware/session.py b/flup/middleware/session.py
deleted file mode 100644
index 3c67422..0000000
--- a/flup/middleware/session.py
+++ /dev/null
@@ -1,770 +0,0 @@
-# Copyright (c) 2005, 2006 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-import os
-import errno
-import string
-import time
-import weakref
-import atexit
-import shelve
-import cPickle as pickle
-
-try:
- import threading
-except ImportError:
- import dummy_threading as threading
-
-__all__ = ['Session',
- 'SessionStore',
- 'MemorySessionStore',
- 'ShelveSessionStore',
- 'DiskSessionStore',
- 'SessionMiddleware']
-
-class Session(dict):
- """
- Session objects, basically dictionaries.
- """
- identifierLength = 32
- # Would be nice if len(identifierChars) were some power of 2.
- identifierChars = string.digits + string.ascii_letters + '-_'
-
- def __init__(self, identifier):
- super(Session, self).__init__()
-
- assert self.isIdentifierValid(identifier)
- self._identifier = identifier
-
- self._creationTime = self._lastAccessTime = time.time()
- self._isValid = True
-
- def _get_identifier(self):
- return self._identifier
- identifier = property(_get_identifier, None, None,
- 'Unique identifier for Session within its Store')
-
- def _get_creationTime(self):
- return self._creationTime
- creationTime = property(_get_creationTime, None, None,
- 'Time when Session was created')
-
- def _get_lastAccessTime(self):
- return self._lastAccessTime
- lastAccessTime = property(_get_lastAccessTime, None, None,
- 'Time when Session was last accessed')
-
- def _get_isValid(self):
- return self._isValid
- isValid = property(_get_isValid, None, None,
- 'Whether or not this Session is valid')
-
- def touch(self):
- """Update Session's access time."""
- self._lastAccessTime = time.time()
-
- def invalidate(self):
- """Invalidate this Session."""
- self.clear()
- self._creationTime = self._lastAccessTime = 0
- self._isValid = False
-
- def isIdentifierValid(cls, ident):
- """
- Returns whether or not the given string *could be* a valid session
- identifier.
- """
- if type(ident) is str and len(ident) == cls.identifierLength:
- for c in ident:
- if c not in cls.identifierChars:
- return False
- return True
- return False
- isIdentifierValid = classmethod(isIdentifierValid)
-
- def generateIdentifier(cls):
- """
- Generate a random session identifier.
- """
- raw = os.urandom(cls.identifierLength)
-
- sessId = ''
- for c in raw:
- # So we lose 2 bits per random byte...
- sessId += cls.identifierChars[ord(c) % len(cls.identifierChars)]
- return sessId
- generateIdentifier = classmethod(generateIdentifier)
-
-def _shutdown(ref):
- store = ref()
- if store is not None:
- store.shutdown()
-
-class SessionStore(object):
- """
- Abstract base class for session stores. You first acquire a session by
- calling createSession() or checkOutSession(). After using the session,
- you must call checkInSession(). You must not keep references to sessions
- outside of a check in/check out block. Always obtain a fresh reference.
-
- Some external mechanism must be set up to call periodic() periodically
- (perhaps every 5 minutes).
-
- After timeout minutes of inactivity, sessions are deleted.
- """
- _sessionClass = Session
-
- def __init__(self, timeout=60, sessionClass=None):
- self._lock = threading.Condition()
-
- # Timeout in minutes
- self._sessionTimeout = timeout
-
- if sessionClass is not None:
- self._sessionClass = sessionClass
-
- self._checkOutList = {}
- self._shutdownRan = False
-
- # Ensure shutdown is called.
- atexit.register(_shutdown, weakref.ref(self))
-
- # Public interface.
-
- def createSession(self):
- """
- Create a new session with a unique identifier. Should never fail.
- (Will raise a RuntimeError in the rare event that it does.)
-
- The newly-created session should eventually be released by
- a call to checkInSession().
- """
- assert not self._shutdownRan
- self._lock.acquire()
- try:
- attempts = 0
- while attempts < 10000:
- sessId = self._sessionClass.generateIdentifier()
- sess = self._createSession(sessId)
- if sess is not None: break
- attempts += 1
-
- if attempts >= 10000:
- raise RuntimeError, self.__class__.__name__ + \
- '.createSession() failed'
-
- assert sess.identifier not in self._checkOutList
- self._checkOutList[sess.identifier] = sess
- return sess
- finally:
- self._lock.release()
-
- def checkOutSession(self, identifier):
- """
- Checks out a session for use. Returns the session if it exists,
- otherwise returns None. If this call succeeds, the session
- will be touch()'ed and locked from use by other processes.
- Therefore, it should eventually be released by a call to
- checkInSession().
- """
- assert not self._shutdownRan
-
- if not self._sessionClass.isIdentifierValid(identifier):
- return None
-
- self._lock.acquire()
- try:
- # If we know it's already checked out, block.
- while identifier in self._checkOutList:
- self._lock.wait()
- sess = self._loadSession(identifier)
- if sess is not None:
- if sess.isValid:
- assert sess.identifier not in self._checkOutList
- self._checkOutList[sess.identifier] = sess
- sess.touch()
- else:
- # No longer valid (same as not existing). Delete/unlock
- # the session.
- self._deleteSession(sess.identifier)
- sess = None
- return sess
- finally:
- self._lock.release()
-
- def checkInSession(self, session):
- """
- Returns the session for use by other threads/processes. Safe to
- pass None.
- """
- assert not self._shutdownRan
-
- if session is None:
- return
-
- self._lock.acquire()
- try:
- assert session.identifier in self._checkOutList
- if session.isValid:
- self._saveSession(session)
- else:
- self._deleteSession(session.identifier)
- del self._checkOutList[session.identifier]
- self._lock.notify()
- finally:
- self._lock.release()
-
- def shutdown(self):
- """Clean up outstanding sessions."""
- self._lock.acquire()
- try:
- if not self._shutdownRan:
- # Save or delete any sessions that are still out there.
- for key,sess in self._checkOutList.items():
- if sess.isValid:
- self._saveSession(sess)
- else:
- self._deleteSession(sess.identifier)
- self._checkOutList.clear()
- self._shutdown()
- self._shutdownRan = True
- finally:
- self._lock.release()
-
- def __del__(self):
- self.shutdown()
-
- def periodic(self):
- """Timeout old sessions. Should be called periodically."""
- self._lock.acquire()
- try:
- if not self._shutdownRan:
- self._periodic()
- finally:
- self._lock.release()
-
- # To be implemented by subclasses. self._lock will be held whenever
- # these are called and for methods that take an identifier,
- # the identifier will be guaranteed to be valid (but it will not
- # necessarily exist).
-
- def _createSession(self, identifier):
- """
- Attempt to create the session with the given identifier. If
- successful, return the newly-created session, which must
- also be implicitly locked from use by other processes. (The
- session returned should be an instance of self._sessionClass.)
- If unsuccessful, return None.
- """
- raise NotImplementedError, self.__class__.__name__ + '._createSession'
-
- def _loadSession(self, identifier):
- """
- Load the session with the identifier from secondary storage returning
- None if it does not exist. If the load is successful, the session
- must be locked from use by other processes.
- """
- raise NotImplementedError, self.__class__.__name__ + '._loadSession'
-
- def _saveSession(self, session):
- """
- Store the session into secondary storage. Also implicitly releases
- the session for use by other processes.
- """
- raise NotImplementedError, self.__class__.__name__ + '._saveSession'
-
- def _deleteSession(self, identifier):
- """
- Deletes the session from secondary storage. Must be OK to pass
- in an invalid (non-existant) identifier. If the session did exist,
- it must be released for use by other processes.
- """
- raise NotImplementedError, self.__class__.__name__ + '._deleteSession'
-
- def _periodic(self):
- """Remove timedout sessions from secondary storage."""
- raise NotImplementedError, self.__class__.__name__ + '._periodic'
-
- def _shutdown(self):
- """Performs necessary shutdown actions for secondary store."""
- raise NotImplementedError, self.__class__.__name__ + '._shutdown'
-
- # Utilities
-
- def _isSessionTimedout(self, session, now=time.time()):
- return (session.lastAccessTime + self._sessionTimeout * 60) < now
-
-class MemorySessionStore(SessionStore):
- """
- Memory-based session store. Great for persistent applications, terrible
- for one-shot ones. :)
- """
- def __init__(self, *a, **kw):
- super(MemorySessionStore, self).__init__(*a, **kw)
-
- # Our "secondary store".
- self._secondaryStore = {}
-
- def _createSession(self, identifier):
- if self._secondaryStore.has_key(identifier):
- return None
- sess = self._sessionClass(identifier)
- self._secondaryStore[sess.identifier] = sess
- return sess
-
- def _loadSession(self, identifier):
- return self._secondaryStore.get(identifier, None)
-
- def _saveSession(self, session):
- self._secondaryStore[session.identifier] = session
-
- def _deleteSession(self, identifier):
- if self._secondaryStore.has_key(identifier):
- del self._secondaryStore[identifier]
-
- def _periodic(self):
- now = time.time()
- for key,sess in self._secondaryStore.items():
- if self._isSessionTimedout(sess, now):
- del self._secondaryStore[key]
-
- def _shutdown(self):
- pass
-
-class ShelveSessionStore(SessionStore):
- """
- Session store based on Python "shelves." Only use if you can guarantee
- that storeFile will NOT be accessed concurrently by other instances.
- (In other processes, threads, anywhere!)
- """
- def __init__(self, storeFile='sessions', *a, **kw):
- super(ShelveSessionStore, self).__init__(*a, **kw)
-
- self._secondaryStore = shelve.open(storeFile,
- protocol=pickle.HIGHEST_PROTOCOL)
-
- def _createSession(self, identifier):
- if self._secondaryStore.has_key(identifier):
- return None
- sess = self._sessionClass(identifier)
- self._secondaryStore[sess.identifier] = sess
- return sess
-
- def _loadSession(self, identifier):
- return self._secondaryStore.get(identifier, None)
-
- def _saveSession(self, session):
- self._secondaryStore[session.identifier] = session
-
- def _deleteSession(self, identifier):
- if self._secondaryStore.has_key(identifier):
- del self._secondaryStore[identifier]
-
- def _periodic(self):
- now = time.time()
- for key,sess in self._secondaryStore.items():
- if self._isSessionTimedout(sess, now):
- del self._secondaryStore[key]
-
- def _shutdown(self):
- self._secondaryStore.close()
-
-class DiskSessionStore(SessionStore):
- """
- Disk-based session store that stores each session as its own file
- within a specified directory. Should be safe for concurrent use.
- (As long as the underlying OS/filesystem respects create()'s O_EXCL.)
- """
- def __init__(self, storeDir='sessions', *a, **kw):
- super(DiskSessionStore, self).__init__(*a, **kw)
-
- self._sessionDir = storeDir
- if not os.access(self._sessionDir, os.F_OK):
- # Doesn't exist, try to create it.
- os.mkdir(self._sessionDir)
-
- def _filenameForSession(self, identifier):
- return os.path.join(self._sessionDir, identifier + '.sess')
-
- def _lockSession(self, identifier, block=True):
- # Release SessionStore lock so we don't deadlock.
- self._lock.release()
- try:
- fn = self._filenameForSession(identifier) + '.lock'
- while True:
- try:
- fd = os.open(fn, os.O_WRONLY|os.O_CREAT|os.O_EXCL)
- except OSError, e:
- if e.errno != errno.EEXIST:
- raise
- else:
- os.close(fd)
- break
-
- if not block:
- return False
-
- # See if the lock is stale. If so, remove it.
- try:
- now = time.time()
- mtime = os.path.getmtime(fn)
- if (mtime + 60) < now:
- os.unlink(fn)
- except OSError, e:
- if e.errno != errno.ENOENT:
- raise
-
- time.sleep(0.1)
-
- return True
- finally:
- self._lock.acquire()
-
- def _unlockSession(self, identifier):
- fn = self._filenameForSession(identifier) + '.lock'
- os.unlink(fn) # Need to catch errors?
-
- def _createSession(self, identifier):
- fn = self._filenameForSession(identifier)
- lfn = fn + '.lock'
- # Attempt to create the file's *lock* first.
- lfd = fd = -1
- try:
- lfd = os.open(lfn, os.O_WRONLY|os.O_CREAT|os.O_EXCL)
- fd = os.open(fn, os.O_WRONLY|os.O_CREAT|os.O_EXCL)
- except OSError, e:
- if e.errno == errno.EEXIST:
- if lfd >= 0:
- # Remove lockfile.
- os.close(lfd)
- os.unlink(lfn)
- return None
- raise
- else:
- # Success.
- os.close(fd)
- os.close(lfd)
- return self._sessionClass(identifier)
-
- def _loadSession(self, identifier, block=True):
- if not self._lockSession(identifier, block):
- return None
- try:
- return pickle.load(open(self._filenameForSession(identifier)))
- except:
- self._unlockSession(identifier)
- return None
-
- def _saveSession(self, session):
- f = open(self._filenameForSession(session.identifier), 'w+')
- pickle.dump(session, f, protocol=pickle.HIGHEST_PROTOCOL)
- f.close()
- self._unlockSession(session.identifier)
-
- def _deleteSession(self, identifier):
- try:
- os.unlink(self._filenameForSession(identifier))
- except:
- pass
- self._unlockSession(identifier)
-
- def _periodic(self):
- now = time.time()
- sessions = os.listdir(self._sessionDir)
- for name in sessions:
- if not name.endswith('.sess'):
- continue
- identifier = name[:-5]
- if not self._sessionClass.isIdentifierValid(identifier):
- continue
- # Not very efficient.
- sess = self._loadSession(identifier, block=False)
- if sess is None:
- continue
- if self._isSessionTimedout(sess, now):
- self._deleteSession(identifier)
- else:
- self._unlockSession(identifier)
-
- def _shutdown(self):
- pass
-
-# SessionMiddleware stuff.
-
-from Cookie import SimpleCookie
-import cgi
-import urlparse
-
-class SessionService(object):
- """
- WSGI extension API passed to applications as
- environ['com.saddi.service.session'].
-
- Public API: (assume service = environ['com.saddi.service.session'])
- service.session - Returns the Session associated with the client.
- service.hasSession - True if the client is currently associated with
- a Session.
- service.isSessionNew - True if the Session was created in this
- transaction.
- service.hasSessionExpired - True if the client is associated with a
- non-existent Session.
- service.encodesSessionInURL - True if the Session ID should be encoded in
- the URL. (read/write)
- service.encodeURL(url) - Returns url encoded with Session ID (if
- necessary).
- service.cookieAttributes - Dictionary of additional RFC2109 attributes
- to be added to the generated cookie.
- service.forceCookieOutput - Normally False. Set to True to force
- output of the Set-Cookie header during this request.
- """
- _expiredSessionIdentifier = 'expired session'
-
- def __init__(self, store, environ,
- cookieName='_SID_',
- cookieExpiration=None, # Deprecated
- cookieAttributes={},
- fieldName='_SID_'):
- self._store = store
- self._cookieName = cookieName
- self._cookieExpiration = cookieExpiration
- self.cookieAttributes = dict(cookieAttributes)
- self.forceCookieOutput = False
- self._fieldName = fieldName
-
- self._session = None
- self._newSession = False
- self._expired = False
- self.encodesSessionInURL = False
-
- if __debug__: self._closed = False
-
- self._loadExistingSession(environ)
-
- def _loadSessionFromCookie(self, environ):
- """
- Attempt to load the associated session using the identifier from
- the cookie.
- """
- C = SimpleCookie(environ.get('HTTP_COOKIE'))
- morsel = C.get(self._cookieName, None)
- if morsel is not None:
- self._session = self._store.checkOutSession(morsel.value)
- self._expired = self._session is None
-
- def _loadSessionFromQueryString(self, environ):
- """
- Attempt to load the associated session using the identifier from
- the query string.
- """
- qs = cgi.parse_qsl(environ.get('QUERY_STRING', ''))
- for name,value in qs:
- if name == self._fieldName:
- self._session = self._store.checkOutSession(value)
- self._expired = self._session is None
- self.encodesSessionInURL = True
- break
-
- def _loadExistingSession(self, environ):
- """Attempt to associate with an existing Session."""
- # Try cookie first.
- self._loadSessionFromCookie(environ)
-
- # Next, try query string.
- if self._session is None:
- self._loadSessionFromQueryString(environ)
-
- def _sessionIdentifier(self):
- """Returns the identifier of the current session."""
- assert self._session is not None
- return self._session.identifier
-
- def _shouldAddCookie(self):
- """
- Returns True if the session cookie should be added to the header
- (if not encoding the session ID in the URL). The cookie is added if
- one of these three conditions are true: a) the session was just
- created, b) the session is no longer valid, or c) the client is
- associated with a non-existent session.
- """
- return self._newSession or \
- (self._session is not None and not self._session.isValid) or \
- (self._session is None and self._expired)
-
- def addCookie(self, headers):
- """Adds Set-Cookie header if needed."""
- if not self.encodesSessionInURL and \
- (self._shouldAddCookie() or self.forceCookieOutput):
- if self._session is not None:
- sessId = self._sessionIdentifier()
- expireCookie = not self._session.isValid
- else:
- sessId = self._expiredSessionIdentifier
- expireCookie = True
-
- C = SimpleCookie()
- name = self._cookieName
- C[name] = sessId
- C[name]['path'] = '/'
- if self._cookieExpiration is not None:
- C[name]['expires'] = self._cookieExpiration
- C[name].update(self.cookieAttributes)
- if expireCookie:
- # Expire cookie
- C[name]['expires'] = -365*24*60*60
- C[name]['max-age'] = 0
- headers.append(('Set-Cookie', C[name].OutputString()))
-
- def close(self):
- """Checks session back into session store."""
- if self._session is None:
- return
- # Check the session back in and get rid of our reference.
- self._store.checkInSession(self._session)
- self._session = None
- if __debug__: self._closed = True
-
- # Public API
-
- def _get_session(self):
- if __debug__: assert not self._closed
- if self._session is None:
- self._session = self._store.createSession()
- self._newSession = True
-
- assert self._session is not None
- return self._session
- session = property(_get_session, None, None,
- 'Returns the Session object associated with this '
- 'client')
-
- def _get_hasSession(self):
- if __debug__: assert not self._closed
- return self._session is not None
- hasSession = property(_get_hasSession, None, None,
- 'True if a Session currently exists for this client')
-
- def _get_isSessionNew(self):
- if __debug__: assert not self._closed
- return self._newSession
- isSessionNew = property(_get_isSessionNew, None, None,
- 'True if the Session was created in this '
- 'transaction')
-
- def _get_hasSessionExpired(self):
- if __debug__: assert not self._closed
- return self._expired
- hasSessionExpired = property(_get_hasSessionExpired, None, None,
- 'True if the client was associated with a '
- 'non-existent Session')
-
- # Utilities
-
- def encodeURL(self, url):
- """Encodes session ID in URL, if necessary."""
- if __debug__: assert not self._closed
- if not self.encodesSessionInURL or self._session is None:
- return url
- u = list(urlparse.urlsplit(url))
- q = '%s=%s' % (self._fieldName, self._sessionIdentifier())
- if u[3]:
- u[3] = q + '&' + u[3]
- else:
- u[3] = q
- return urlparse.urlunsplit(u)
-
-def _addClose(appIter, closeFunc):
- """
- Wraps an iterator so that its close() method calls closeFunc. Respects
- the existence of __len__ and the iterator's own close() method.
-
- Need to use metaclass magic because __len__ and next are not
- recognized unless they're part of the class. (Can't assign at
- __init__ time.)
- """
- class metaIterWrapper(type):
- def __init__(cls, name, bases, clsdict):
- super(metaIterWrapper, cls).__init__(name, bases, clsdict)
- if hasattr(appIter, '__len__'):
- cls.__len__ = appIter.__len__
- cls.next = iter(appIter).next
- if hasattr(appIter, 'close'):
- def _close(self):
- appIter.close()
- closeFunc()
- cls.close = _close
- else:
- cls.close = closeFunc
-
- class iterWrapper(object):
- __metaclass__ = metaIterWrapper
- def __iter__(self):
- return self
-
- return iterWrapper()
-
-class SessionMiddleware(object):
- """
- WSGI middleware that adds a session service. A SessionService instance
- is passed to the application in environ['com.saddi.service.session'].
- A references to this instance should not be saved. (A new instance is
- instantiated with every call to the application.)
- """
- _serviceClass = SessionService
-
- def __init__(self, store, application, serviceClass=None, **kw):
- self._store = store
- self._application = application
- if serviceClass is not None:
- self._serviceClass = serviceClass
- self._serviceKW = kw
-
- def __call__(self, environ, start_response):
- service = self._serviceClass(self._store, environ, **self._serviceKW)
- environ['com.saddi.service.session'] = service
-
- def my_start_response(status, headers, exc_info=None):
- service.addCookie(headers)
- return start_response(status, headers, exc_info)
-
- try:
- result = self._application(environ, my_start_response)
- except:
- # If anything goes wrong, ensure the session is checked back in.
- service.close()
- raise
-
- # The iterator must be unconditionally wrapped, just in case it
- # is a generator. (In which case, we may not know that a Session
- # has been checked out until completion of the first iteration.)
- return _addClose(result, service.close)
-
-if __name__ == '__main__':
- mss = MemorySessionStore(timeout=5)
-# sss = ShelveSessionStore(timeout=5)
- dss = DiskSessionStore(timeout=5)
diff --git a/flup/publisher.py b/flup/publisher.py
deleted file mode 100644
index a0c6676..0000000
--- a/flup/publisher.py
+++ /dev/null
@@ -1,767 +0,0 @@
-# Copyright (c) 2002, 2005, 2006 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-import os
-import inspect
-import cgi
-import types
-from Cookie import SimpleCookie
-
-__all__ = ['Request',
- 'Response',
- 'Transaction',
- 'Publisher',
- 'File',
- 'Action',
- 'Redirect',
- 'InternalRedirect',
- 'getexpected',
- 'trimkw']
-
-class NoDefault(object):
- """Sentinel object so we can distinguish None in keyword arguments."""
- pass
-
-class Request(object):
- """
- Encapsulates data about the HTTP request.
-
- Supported attributes that originate from here: (The attributes are
- read-only, however you may modify the contents of environ.)
- transaction - Enclosing Transaction object.
- environ - Environment variables, as passed from WSGI adapter.
- method - Request method.
- publisherScriptName - SCRIPT_NAME of Publisher.
- scriptName - SCRIPT_NAME for request.
- pathInfo - PATH_INFO for request.
- form - Dictionary containing data from query string and/or POST request.
- cookie - Cookie from request.
- """
- def __init__(self, transaction, environ):
- self._transaction = transaction
-
- self._environ = environ
- self._publisherScriptName = environ.get('SCRIPT_NAME', '')
-
- self._form = {}
- self._parseFormData()
-
- # Is this safe? Will it ever raise exceptions?
- self._cookie = SimpleCookie(environ.get('HTTP_COOKIE', None))
-
- def _parseFormData(self):
- """
- Fills self._form with data from a FieldStorage. May be overidden to
- provide custom form processing. (Like no processing at all!)
- """
- # Parse query string and/or POST data.
- form = FieldStorage(fp=self._environ['wsgi.input'],
- environ=self._environ, keep_blank_values=1)
-
- # Collapse FieldStorage into a simple dict.
- if form.list is not None:
- for field in form.list:
- # Wrap uploaded files
- if field.filename:
- val = File(field)
- else:
- val = field.value
-
- # Add File/value to args, constructing a list if there are
- # multiple values.
- if self._form.has_key(field.name):
- self._form[field.name].append(val)
- else:
- self._form[field.name] = [val]
-
- # Unwrap lists with a single item.
- for name,val in self._form.items():
- if len(val) == 1:
- self._form[name] = val[0]
-
- def _get_transaction(self):
- return self._transaction
- transaction = property(_get_transaction, None, None,
- "Transaction associated with this Request")
-
- def _get_environ(self):
- return self._environ
- environ = property(_get_environ, None, None,
- "Environment variables passed from adapter")
-
- def _get_method(self):
- return self._environ['REQUEST_METHOD']
- method = property(_get_method, None, None,
- "Request method")
-
- def _get_publisherScriptName(self):
- return self._publisherScriptName
- publisherScriptName = property(_get_publisherScriptName, None, None,
- 'SCRIPT_NAME of Publisher')
-
- def _get_scriptName(self):
- return self._environ.get('SCRIPT_NAME', '')
- scriptName = property(_get_scriptName, None, None,
- "SCRIPT_NAME of request")
-
- def _get_pathInfo(self):
- return self._environ.get('PATH_INFO', '')
- pathInfo = property(_get_pathInfo, None, None,
- "PATH_INFO of request")
-
- def _get_form(self):
- return self._form
- form = property(_get_form, None, None,
- "Parsed GET/POST data")
-
- def _get_cookie(self):
- return self._cookie
- cookie = property(_get_cookie, None, None,
- "Cookie received from client")
-
-class Response(object):
- """
- Encapsulates response-related data.
-
- Supported attributes:
- transaction - Enclosing Transaction object.
- status - Response status code (and message).
- headers - Response headers.
- cookie - Response cookie.
- contentType - Content type of body.
- body - Response body. Must be an iterable that yields strings.
-
- Since there are multiple ways of passing response info back to
- Publisher, here is their defined precedence:
- status - headers['Status'] first, then status
- cookie - Any values set in this cookie are added to the headers in
- addition to existing Set-Cookie headers.
- contentType - headers['Content-Type'] first, then contentType. If
- neither are specified, defaults to 'text/html'.
- body - Return of function takes precedence. If function returns None,
- body is used instead.
- """
- _initialHeaders = {
- 'Pragma': 'no-cache',
- 'Cache-Control': 'no-cache'
- }
-
- def __init__(self, transaction):
- self._transaction = transaction
-
- self._status = '200 OK'
-
- # Initial response headers.
- self._headers = header_dict(self._initialHeaders)
-
- self._cookie = SimpleCookie()
-
- self.body = []
-
- def _get_transaction(self):
- return self._transaction
- transaction = property(_get_transaction, None, None,
- "Transaction associated with this Response")
-
- def _get_status(self):
- status = self._headers.get('Status')
- if status is not None:
- return status
- return self._status
- def _set_status(self, value):
- if self._headers.has_key('Status'):
- self._headers['Status'] = value
- else:
- self._status = value
- status = property(_get_status, _set_status, None,
- 'Response status')
-
- def _get_headers(self):
- return self._headers
- headers = property(_get_headers, None, None,
- "Headers to send in response")
-
- def _get_cookie(self):
- return self._cookie
- cookie = property(_get_cookie, None, None,
- "Cookie to send in response")
-
- def _get_contentType(self):
- return self._headers.get('Content-Type', 'text/html')
- def _set_contentType(self, value):
- self._headers['Content-Type'] = value
- contentType = property(_get_contentType, _set_contentType, None,
- 'Content-Type of the response body')
-
-class Transaction(object):
- """
- Encapsulates objects associated with a single transaction (Request,
- Response, and possibly a Session).
-
- Public attributes: (all read-only)
- request - Request object.
- response - Response object.
-
- If Publisher sits on top of SessionMiddleware, the public API of
- SessionService is also available through the Transaction object.
- """
- _requestClass = Request
- _responseClass = Response
-
- def __init__(self, publisher, environ):
- self._publisher = publisher
- self._request = self._requestClass(self, environ)
- self._response = self._responseClass(self)
-
- # Session support.
- self._sessionService = environ.get('com.saddi.service.session')
- if self._sessionService is not None:
- self.encodeURL = self._sessionService.encodeURL
-
- def _get_request(self):
- return self._request
- request = property(_get_request, None, None,
- "Request associated with this Transaction")
-
- def _get_response(self):
- return self._response
- response = property(_get_response, None, None,
- "Response associated with this Transaction")
-
- # Export SessionService API
-
- def _get_session(self):
- assert self._sessionService is not None, 'No session service found'
- return self._sessionService.session
- session = property(_get_session, None, None,
- 'Returns the Session object associated with this '
- 'client')
-
- def _get_hasSession(self):
- assert self._sessionService is not None, 'No session service found'
- return self._sessionService.hasSession
- hasSession = property(_get_hasSession, None, None,
- 'True if a Session currently exists for this client')
-
- def _get_isSessionNew(self):
- assert self._sessionService is not None, 'No session service found'
- return self._sessionService.isSessionNew
- isSessionNew = property(_get_isSessionNew, None, None,
- 'True if the Session was created in this '
- 'transaction')
-
- def _get_hasSessionExpired(self):
- assert self._sessionService is not None, 'No session service found'
- return self._sessionService.hasSessionExpired
- hasSessionExpired = property(_get_hasSessionExpired, None, None,
- 'True if the client was associated with a '
- 'non-existent Session')
-
- def _get_encodesSessionInURL(self):
- assert self._sessionService is not None, 'No session service found'
- return self._sessionService.encodesSessionInURL
- def _set_encodesSessionInURL(self, value):
- assert self._sessionService is not None, 'No session service found'
- self._sessionService.encodesSessionInURL = value
- encodesSessionInURL = property(_get_encodesSessionInURL,
- _set_encodesSessionInURL, None,
- 'True if the Session ID should be encoded '
- 'in the URL')
-
- def prepare(self):
- """
- Called before resolved function is invoked. If overridden,
- super's prepare() MUST be called and it must be called first.
- """
- # Pass form values as keyword arguments.
- args = dict(self._request.form)
-
- # Pass Transaction to function if it wants it.
- args['transaction'] = args['trans'] = self
-
- self._args = args
-
- def call(self, func, args):
- """
- May be overridden to provide custom exception handling and/or
- per-request additions, e.g. opening a database connection,
- starting a transaction, etc.
- """
- # Trim down keywords to only what the callable expects.
- expected, varkw = getexpected(func)
- trimkw(args, expected, varkw)
-
- return func(**args)
-
- def run(self, func):
- """
- Execute the function, doing any Action processing and also
- post-processing the function/Action's return value.
- """
- try:
- # Call the function.
- result = self.call(func, self._args)
- except Action, a:
- # Caught an Action, have it do whatever it does
- result = a.run(self)
-
- response = self._response
- headers = response.headers
-
- if result is not None:
- if type(result) in types.StringTypes:
- assert type(result) is str, 'result cannot be unicode!'
-
- # Set Content-Length, if needed
- if not headers.has_key('Content-Length'):
- headers['Content-Length'] = str(len(result))
-
- # Wrap in list for WSGI
- response.body = [result]
- else:
- if __debug__:
- try:
- iter(result)
- except TypeError:
- raise AssertionError, 'result not iterable!'
- response.body = result
-
- # If result was None, assume response.body was appropriately set.
-
- def finish(self):
- """
- Called after resolved function returns, but before response is
- sent. If overridden, super's finish() should be called last.
- """
- response = self._response
- headers = response.headers
-
- # Copy cookie to headers.
- items = response.cookie.items()
- items.sort()
- for name,morsel in items:
- headers.add('Set-Cookie', morsel.OutputString())
-
- # If there is a Status header, transfer its value to response.status.
- # It must not remain in the headers!
- status = headers.get('Status', NoDefault)
- if status is not NoDefault:
- del headers['Status']
- response.status = status
-
- code = int(response.status[:3])
- # Check if it's a response that must not include a body.
- # (See 4.3 in RFC2068.)
- if code / 100 == 1 or code in (204, 304):
- # Remove any trace of the response body, if that's the case.
- for header,value in headers.items():
- if header.lower().startswith('content-'):
- del headers[header]
- response.body = []
- else:
- if self._request.method == 'HEAD':
- # HEAD reponse must not return a body (but the headers must be
- # kept intact).
- response.body = []
-
- # Add Content-Type, if missing.
- if not headers.has_key('Content-Type'):
- headers['Content-Type'] = response.contentType
-
- # If we have a close() method, ensure that it is called.
- if hasattr(self, 'close'):
- response.body = _addClose(response.body, self.close)
-
-class Publisher(object):
- """
- WSGI application that publishes Python functions as web pages. Constructor
- takes an instance of a concrete subclass of Resolver, which is responsible
- for mapping requests to functions.
-
- Query string/POST data values are passed to the function as keyword
- arguments. If the function does not support variable keywords (e.g.
- does not have a ** parameter), the function will only be passed
- keywords which it expects. It is recommended that all keyword parameters
- have defaults so that missing form data does not raise an exception.
-
- A Transaction instance is always passed to the function via the
- "transaction" or "trans" keywords. See the Transaction, Request, and
- Response classes.
-
- Valid return types for the function are: a string, None, or an iterable
- that yields strings. If returning None, it is expected that Response.body
- has been appropriately set. (It must be an iterable that yields strings.)
-
- An instance of Publisher itself is the WSGI application.
- """
- _transactionClass = Transaction
-
- def __init__(self, resolver, transactionClass=None, error404=None):
- self._resolver = resolver
-
- if transactionClass is not None:
- self._transactionClass = transactionClass
-
- if error404 is not None:
- self._error404 = error404
-
- def _get_resolver(self):
- return self._resolver
- resolver = property(_get_resolver, None, None,
- 'Associated Resolver for this Publisher')
-
- def __call__(self, environ, start_response):
- """
- WSGI application interface. Creates a Transaction (which does most
- of the work) and sends the response.
- """
- # Set up a Transaction.
- transaction = self._transactionClass(self, environ)
-
- # Make any needed preparations.
- transaction.prepare()
-
- redirect = False
-
- while True:
- # Attempt to resolve the function.
- func = self._resolver.resolve(transaction.request,
- redirect=redirect)
- if func is None:
- func = self._error404
-
- try:
- # Call the function.
- transaction.run(func)
- except InternalRedirect, r:
- # Internal redirect. Set up environment and resolve again.
- environ['SCRIPT_NAME'] = transaction.request.publisherScriptName
- environ['PATH_INFO'] = r.pathInfo
- redirect = True
- else:
- break
-
- # Give Transaction a chance to do modify/add to the response.
- transaction.finish()
-
- # Transform headers into a list. (Need to pay attention to
- # multiple values.)
- responseHeaders = []
- for key,value in transaction.response.headers.items():
- if type(value) is list:
- for v in value:
- responseHeaders.append((key, v))
- else:
- responseHeaders.append((key, value))
-
- start_response(transaction.response.status, responseHeaders)
- return transaction.response.body
-
- def _error404(self, transaction):
- """Error page to display when resolver fails."""
- transaction.response.status = '404 Not Found'
- request_uri = transaction.request.environ.get('REQUEST_URI')
- if request_uri is None:
- request_uri = transaction.request.environ.get('SCRIPT_NAME', '') + \
- transaction.request.environ.get('PATH_INFO', '')
- return ["""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
-<html><head>
-<title>404 Not Found</title>
-</head><body>
-<h1>Not Found</h1>
-The requested URL %s was not found on this server.<p>
-<hr>
-%s</body></html>
-""" % (request_uri, transaction.request.environ.get('SERVER_SIGNATURE', ''))]
-
-class File(object):
- """
- Wrapper so we can identify uploaded files.
- """
- def __init__(self, field):
- self.filename = field.filename
- self.file = field.file
- self.type = field.type
- self.type_options = field.type_options
- self.headers = field.headers
-
-class Action(Exception):
- """
- Abstract base class for 'Actions', which are just exceptions.
- Within Publisher, Actions have no inherent meaning and are used
- as a shortcut to perform specific actions where it's ok for a
- function to abruptly halt. (Like redirects or displaying an
- error page.)
-
- I don't know if using exceptions this way is good form (something
- tells me no ;) Their usage isn't really required, but they're
- convenient in some situations.
- """
- def run(self, transaction):
- """Override to perform your action."""
- raise NotImplementedError, self.__class__.__name__ + '.run'
-
-class Redirect(Action):
- """
- Redirect to the given URL.
- """
- def __init__(self, url, permanent=False):
- self._url = url
- self._permanent = permanent
-
- def run(self, transaction):
- response = transaction.response
- response.status = self._permanent and '301 Moved Permanently' or \
- '302 Moved Temporarily'
- response.headers.reset()
- response.headers['Location'] = self._url
- response.contentType = 'text/html'
- return ["""<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
-<html><head>
-<title>%s</title>
-</head><body>
-<h1>Found</h1>
-<p>The document has moved <a href="%s">here</a>.</p>
-<hr>
-%s</body></html>
-""" % (response.status, self._url,
- transaction.request.environ.get('SERVER_SIGNATURE', ''))]
-
-class InternalRedirect(Exception):
- """
- An internal redirect using a new PATH_INFO (relative to Publisher's
- SCRIPT_NAME).
-
- When handling an InternalRedirect, the behavior of all included
- Resolvers is to expose a larger set of callables (that would normally
- be hidden). Therefore, it is important that you set pathInfo securely -
- preferably, it should not depend on any data from the request. Ideally,
- pathInfo should be a constant string.
- """
- def __init__(self, pathInfo):
- self.pathInfo = pathInfo
-
-class FieldStorage(cgi.FieldStorage):
- def __init__(self, *args, **kw):
- """
- cgi.FieldStorage only parses the query string during a GET or HEAD
- request. Fix this.
- """
- cgi.FieldStorage.__init__(self, *args, **kw)
-
- environ = kw.get('environ') or os.environ
- method = environ.get('REQUEST_METHOD', 'GET').upper()
- if method not in ('GET', 'HEAD'): # cgi.FieldStorage already parsed?
- qs = environ.get('QUERY_STRING')
- if qs:
- if self.list is None:
- self.list = []
- for key,value in cgi.parse_qsl(qs, self.keep_blank_values,
- self.strict_parsing):
- self.list.append(cgi.MiniFieldStorage(key, value))
-
-class header_dict(dict):
- """
- This is essentially a case-insensitive dictionary, with some additions
- geared towards supporting HTTP headers (like __str__(), add(), and
- reset()).
- """
- def __init__(self, val=None):
- """
- If initialized with an existing dictionary, calling reset() will
- reset our contents back to that initial dictionary.
- """
- dict.__init__(self)
- self._keymap = {}
- if val is None:
- val = {}
- self.update(val)
- self._reset_state = dict(val)
-
- def __contains__(self, key):
- return key.lower() in self._keymap
-
- def __delitem__(self, key):
- lower_key = key.lower()
- real_key = self._keymap.get(lower_key)
- if real_key is None:
- raise KeyError, key
- del self._keymap[lower_key]
- dict.__delitem__(self, real_key)
-
- def __getitem__(self, key):
- lower_key = key.lower()
- real_key = self._keymap.get(lower_key)
- if real_key is None:
- raise KeyError, key
- return dict.__getitem__(self, real_key)
-
- def __str__(self):
- """Output as HTTP headers."""
- s = ''
- for k,v in self.items():
- if type(v) is list:
- for i in v:
- s += '%s: %s\n' % (k, i)
- else:
- s += '%s: %s\n' % (k, v)
- return s
-
- def __setitem__(self, key, value):
- lower_key = key.lower()
- real_key = self._keymap.get(lower_key)
- if real_key is None:
- self._keymap[lower_key] = key
- dict.__setitem__(self, key, value)
- else:
- dict.__setitem__(self, real_key, value)
-
- def clear(self):
- self._keymap.clear()
- dict.clear(self)
-
- def copy(self):
- c = self.__class__(self)
- c._reset_state = self._reset_state
- return c
-
- def get(self, key, failobj=None):
- lower_key = key.lower()
- real_key = self._keymap.get(lower_key)
- if real_key is None:
- return failobj
- return dict.__getitem__(self, real_key)
-
- def has_key(self, key):
- return key.lower() in self._keymap
-
- def setdefault(self, key, failobj=None):
- lower_key = key.lower()
- real_key = self._keymap.get(lower_key)
- if real_key is None:
- self._keymap[lower_key] = key
- dict.__setitem__(self, key, failobj)
- return failobj
- else:
- return dict.__getitem__(self, real_key)
-
- def update(self, d):
- for k,v in d.items():
- self[k] = v
-
- def add(self, key, value):
- """
- Add a new header value. Does not overwrite previous value of header
- (in contrast to __setitem__()).
- """
- if self.has_key(key):
- if type(self[key]) is list:
- self[key].append(value)
- else:
- self[key] = [self[key], value]
- else:
- self[key] = value
-
- def reset(self):
- """Reset contents to that at the time this instance was created."""
- self.clear()
- self.update(self._reset_state)
-
-def _addClose(appIter, closeFunc):
- """
- Wraps an iterator so that its close() method calls closeFunc. Respects
- the existence of __len__ and the iterator's own close() method.
-
- Need to use metaclass magic because __len__ and next are not
- recognized unless they're part of the class. (Can't assign at
- __init__ time.)
- """
- class metaIterWrapper(type):
- def __init__(cls, name, bases, clsdict):
- super(metaIterWrapper, cls).__init__(name, bases, clsdict)
- if hasattr(appIter, '__len__'):
- cls.__len__ = appIter.__len__
- cls.next = iter(appIter).next
- if hasattr(appIter, 'close'):
- def _close(self):
- appIter.close()
- closeFunc()
- cls.close = _close
- else:
- cls.close = closeFunc
-
- class iterWrapper(object):
- __metaclass__ = metaIterWrapper
- def __iter__(self):
- return self
-
- return iterWrapper()
-
-# Utilities which may be useful outside of Publisher? Perhaps for decorators...
-
-def getexpected(func):
- """
- Returns as a 2-tuple the passed in object's expected arguments and
- whether or not it accepts variable keywords.
- """
- assert callable(func), 'object not callable'
-
- if not inspect.isclass(func):
- # At this point, we assume func is either a function, method, or
- # callable instance.
- if not inspect.isfunction(func) and not inspect.ismethod(func):
- func = getattr(func, '__call__') # When would this fail?
-
- argspec = inspect.getargspec(func)
- expected, varkw = argspec[0], argspec[2] is not None
- if inspect.ismethod(func):
- expected = expected[1:]
- else:
- # A class. Try to figure out the calling conventions of the
- # constructor.
- init = getattr(func, '__init__', None)
- # Sigh, this is getting into the realm of black magic...
- if init is not None and inspect.ismethod(init):
- argspec = inspect.getargspec(init)
- expected, varkw = argspec[0], argspec[2] is not None
- expected = expected[1:]
- else:
- expected, varkw = [], False
-
- return expected, varkw
-
-def trimkw(kw, expected, varkw):
- """
- If necessary, trims down a dictionary of keyword arguments to only
- what's expected.
- """
- if not varkw: # Trimming only necessary if it doesn't accept variable kw
- for name in kw.keys():
- if name not in expected:
- del kw[name]
diff --git a/flup/resolver/__init__.py b/flup/resolver/__init__.py
deleted file mode 100644
index bc9c7bf..0000000
--- a/flup/resolver/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from resolver import *
-del resolver
diff --git a/flup/resolver/complex.py b/flup/resolver/complex.py
deleted file mode 100644
index f5b3aa0..0000000
--- a/flup/resolver/complex.py
+++ /dev/null
@@ -1,112 +0,0 @@
-# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-import re
-
-from flup.resolver.resolver import *
-
-__all__ = ['ComplexResolver']
-
-class ComplexResolver(Resolver):
- """
- A meta-Resolver that allows you to "graft" different resolvers at various
- points in your URL space.
-
- It works as follows: given a PATH_INFO, it will try all matching
- resolvers, starting with the most specific. The first function returned
- by a resolver is returned as the result.
-
- If no matching resolvers return a function, then the search is
- considered a failure.
-
- Assumes that none of the registered resolvers modify environ when
- they fail to resolve.
-
- Upon successful resolution, SCRIPT_NAME will contain the path up to
- and including the resolved function (as determined by the resolver) and
- PATH_INFO will contain all remaining components.
- """
- _slashRE = re.compile(r'''/{2,}''')
-
- def __init__(self):
- self.resolverMap = {}
-
- def _canonicalUrl(self, url):
- if not url: # Represents default
- return url
-
- # Get rid of adjacent slashes
- url = self._slashRE.sub('/', url)
-
- # No trailing slash
- if url.endswith('/'):
- url = url[:-1]
-
- # Make sure it starts with a slash
- if not url.startswith('/'):
- url = '/' + url
-
- return url
-
- def addResolver(self, url, resolver):
- """
- Registers a resolver at a particular URL. The empty URL ''
- represents the default resolver. It will be matched when no
- other matching resolvers are found.
- """
- url = self._canonicalUrl(url)
- self.resolverMap[url] = resolver
-
- def removeResolver(self, url):
- """Removes the resolver at a particular URL."""
- url = self._canonicalUrl(url)
- del self.resolverMap[url]
-
- def resolve(self, request, redirect=False):
- orig_script_name = request.scriptName
- orig_path_info = path_info = request.pathInfo
- path_info = path_info.split(';')[0]
- path_info = path_info.split('/')
-
- assert len(path_info) > 0
- assert not path_info[0]
-
- while path_info:
- try_path_info = '/'.join(path_info)
- resolver = self.resolverMap.get(try_path_info)
- if resolver is not None:
- self._updatePath(request, len(path_info) - 1)
- func = resolver.resolve(request, redirect)
- if func is not None:
- return func
- request.environ['SCRIPT_NAME'] = orig_script_name
- request.environ['PATH_INFO'] = orig_path_info
- path_info.pop()
-
- return None
diff --git a/flup/resolver/function.py b/flup/resolver/function.py
deleted file mode 100644
index 15458be..0000000
--- a/flup/resolver/function.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-from flup.resolver.resolver import *
-
-__all__ = ['FunctionResolver']
-
-class FunctionResolver(Resolver):
- """
- Very basic, almost brain-dead Resolver. Simply resolves to the passed
- in function, no matter what. :)
-
- Can be used as a decorator and might actually have uses when used with
- the ComplexResolver.
- """
- def __init__(self, func):
- self._func = func
-
- def resolve(self, request, redirect=False):
- return self._func
diff --git a/flup/resolver/importingmodule.py b/flup/resolver/importingmodule.py
deleted file mode 100644
index 37fd83b..0000000
--- a/flup/resolver/importingmodule.py
+++ /dev/null
@@ -1,129 +0,0 @@
-# Copyright (c) 2002, 2005 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-import sys
-import os
-import imp
-
-from flup.resolver.resolver import *
-
-__all__ = ['ImportingModuleResolver']
-
-class NoDefault(object):
- pass
-
-class ImportingModuleResolver(Resolver):
- """
- Constructor takes a directory name or a list of directories. Interprets
- the first two components of PATH_INFO as 'module/function'. Modules
- are imported as needed and must reside in the directories specified.
- Module and function names beginning with underscore are ignored.
-
- If the 'module' part of PATH_INFO is missing, it is assumed to be
- self.default_module.
-
- If the 'function' part of PATH_INFO is missing, it is assumed to be
- self.index_page.
-
- Upon successful resolution, appends the module and function names to
- SCRIPT_NAME and updates PATH_INFO as the remaining components of the path.
-
- NB: I would recommend explicitly setting all modules' __all__ list.
- Otherwise, be sure all the names of module-level callables that you
- don't want exported begin with underscore.
- """
- # No default module by default.
- default_module = None
- index_page = 'index'
-
- def __init__(self, path, defaultModule=NoDefault, index=NoDefault):
- self.path = path
- if defaultModule is not NoDefault:
- self.default_module = defaultModule
- if index is not NoDefault:
- self.index_page = index
-
- def resolve(self, request, redirect=False):
- path_info = request.pathInfo.split(';')[0]
- path_info = path_info.split('/')
-
- assert len(path_info) > 0
- assert not path_info[0]
-
- while len(path_info) < 3:
- path_info.append('')
-
- module_name, func_name = path_info[1:3]
-
- if not module_name:
- module_name = self.default_module
-
- if not func_name:
- func_name = self.index_page
-
- module = None
- if module_name and (module_name[0] != '_' or redirect) and \
- not module_name.count('.'):
- module = _import_module(module_name, path=self.path)
-
- if module is not None:
- if func_name and (func_name[0] != '_' or redirect):
- module_all = getattr(module, '__all__', None)
- if module_all is None or func_name in module_all or redirect:
- func = getattr(module, func_name, None)
- if callable(func):
- self._updatePath(request, 2)
- return func
-
- return None
-
-def _import_module(name, path=None):
- """
- Imports a module. If path is None, module will be searched for in
- sys.path. If path is given (which may be a single string or a list),
- the module will only be searched for in those directories.
- """
- if path is not None and type(path) is not list:
- path = [path]
-
- module = sys.modules.get(name)
- if module is not None:
- module_file = getattr(module, '__file__')
- if module_file is None or \
- (path is not None and os.path.dirname(module_file) not in path):
- return None
-
- return module
-
- fp, pathname, description = imp.find_module(name, path)
- try:
- return imp.load_module(name, fp, pathname, description)
- finally:
- if fp:
- fp.close()
diff --git a/flup/resolver/module.py b/flup/resolver/module.py
deleted file mode 100644
index 79171d6..0000000
--- a/flup/resolver/module.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# Copyright (c) 2002, 2005 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-from flup.resolver.resolver import *
-
-__all__ = ['ModuleResolver']
-
-class NoDefault(object):
- pass
-
-class ModuleResolver(Resolver):
- """
- Exposes all top-level callables within a module. The module's __all__
- attribute is respected, if it exists. Names beginning with underscore
- are ignored.
-
- Uses the first component of PATH_INFO as the callable's name and if
- empty, will instead use self.index_page.
-
- Upon successful resolution, appends the callable's name to SCRIPT_NAME
- and updates PATH_INFO as the remaining components of the path.
-
- NB: I would recommend explicitly setting the module's __all__ list.
- Otherwise, be sure all the names of module-level callables that you
- don't want exported begin with underscore.
- """
- index_page = 'index'
-
- def __init__(self, module, index=NoDefault):
- self.module = module
- if index is not NoDefault:
- self.index_page = index
-
- def resolve(self, request, redirect=False):
- path_info = request.pathInfo.split(';')[0]
- path_info = path_info.split('/')
-
- assert len(path_info) > 0
- assert not path_info[0]
-
- if len(path_info) < 2:
- path_info.append('')
-
- func_name = path_info[1]
-
- if func_name:
- if func_name[0] == '_' and not redirect:
- func_name = None
- else:
- func_name = self.index_page
-
- if func_name:
- module_all = getattr(self.module, '__all__', None)
- if module_all is None or func_name in module_all or redirect:
- func = getattr(self.module, func_name, None)
- if callable(func):
- self._updatePath(request, 1)
- return func
-
- return None
diff --git a/flup/resolver/nopathinfo.py b/flup/resolver/nopathinfo.py
deleted file mode 100644
index db9bac0..0000000
--- a/flup/resolver/nopathinfo.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-from flup.resolver.resolver import *
-
-__all__ = ['NoPathInfoResolver']
-
-class NoPathInfoResolver(Resolver):
- """
- Another meta-resolver. Disallows the existence of PATH_INFO (beyond
- what's needed to resolve the function). Optionally allows a trailing
- slash.
- """
- def __init__(self, resolver, allowTrailingSlash=False):
- self._resolver = resolver
- self._allowTrailingSlash = allowTrailingSlash
-
- def resolve(self, request, redirect=False):
- orig_script_name, orig_path_info = request.scriptName, request.pathInfo
- func = self._resolver.resolve(request, redirect)
- try:
- if func is not None:
- path_info = request.pathInfo.split(';')[0]
- if path_info and \
- (not self._allowTrailingSlash or path_info != '/'):
- func = None
- return func
- finally:
- if func is None:
- request.environ['SCRIPT_NAME'] = orig_script_name
- request.environ['PATH_INFO'] = orig_path_info
diff --git a/flup/resolver/objectpath.py b/flup/resolver/objectpath.py
deleted file mode 100644
index 94e5558..0000000
--- a/flup/resolver/objectpath.py
+++ /dev/null
@@ -1,156 +0,0 @@
-# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-import re
-
-from flup.resolver.resolver import *
-
-__all__ = ['ObjectPathResolver', 'expose']
-
-class NoDefault(object):
- pass
-
-class ObjectPathResolver(Resolver):
- """
- Inspired by CherryPy <http://www.cherrypy.org/>. :) For an explanation
- of how this works, see the excellent tutorial at
- <http://www.cherrypy.org/wiki/CherryPyTutorial>. We support the index and
- default methods, though the calling convention for the default method
- is different - we do not pass PATH_INFO as positional arguments. (It
- is passed through the request/environ as normal.)
-
- Also, we explicitly block certain function names. See below. I don't
- know if theres really any harm in letting those attributes be followed,
- but I'd rather not take the chance. And unfortunately, this solution
- is pretty half-baked as well (I'd rather only allow certain object
- types to be traversed, rather than disallow based on names.) Better
- than nothing though...
- """
- index_page = 'index'
- default_page = 'default'
-
- def __init__(self, root, index=NoDefault, default=NoDefault,
- favorIndex=True):
- """
- root is the root object of your URL hierarchy. In CherryPy, this
- would be cpg.root.
-
- When the last component of a path has an index method and some
- object along the path has a default method, favorIndex determines
- which method is called when the URL has a trailing slash. If
- True, the index method will be called. Otherwise, the default method.
- """
- self.root = root
- if index is not NoDefault:
- self.index_page = index
- if default is not NoDefault:
- self.default_page = default
- self._favorIndex = favorIndex
-
- # Certain names should be disallowed for safety. If one of your pages
- # is showing up unexpectedly as a 404, make sure the function name doesn't
- # begin with one of these prefixes.
- _disallowed = re.compile(r'''(?:_|im_|func_|tb_|f_|co_).*''')
-
- def _exposed(self, obj, redirect):
- # If redirecting, allow non-exposed objects as well.
- return callable(obj) and (getattr(obj, 'exposed', False) or redirect)
-
- def resolve(self, request, redirect=False):
- path_info = request.pathInfo.split(';')[0]
- path_info = path_info.split('/')
-
- assert len(path_info) > 0
- assert not path_info[0]
-
- current = self.root
- current_default = None
- i = 0
- for i in range(1, len(path_info)):
- component = path_info[i]
-
- # See if we have an index page (needed for index/default
- # disambiguation, unfortunately).
- current_index = None
- if self.index_page:
- current_index = getattr(current, self.index_page, None)
- if not self._exposed(current_index, redirect):
- current_index = None
-
- if self.default_page:
- # Remember the last default page we've seen.
- new_default = getattr(current, self.default_page, None)
- if self._exposed(new_default, redirect):
- current_default = (i - 1, new_default)
-
- # Test for trailing slash.
- if not component and current_index is not None and \
- (self._favorIndex or current_default is None):
- # Breaking out of the loop here favors index over default.
- break
-
- # Respect __all__ attribute. (Ok to generalize to all objects?)
- all = getattr(current, '__all__', None)
-
- current = getattr(current, component, None)
- # Path doesn't exist
- if current is None or self._disallowed.match(component) or \
- (all is not None and component not in all and not redirect):
- # Use path up to latest default page.
- if current_default is not None:
- i, current = current_default
- break
- # No default at all, so we fail.
- return None
-
- func = None
- if self._exposed(current, redirect): # Exposed?
- func = current
- else:
- # If not, see if it as an exposed index page
- if self.index_page:
- index = getattr(current, self.index_page, None)
- if self._exposed(index, redirect): func = index
- # How about a default page?
- if func is None and self.default_page:
- default = getattr(current, self.default_page, None)
- if self._exposed(default, redirect): func = default
- # Lastly, see if we have an ancestor's default page to fall back on.
- if func is None and current_default is not None:
- i, func = current_default
-
- if func is not None:
- self._updatePath(request, i)
-
- return func
-
-def expose(func):
- """Decorator to expose functions."""
- func.exposed = True
- return func
diff --git a/flup/resolver/resolver.py b/flup/resolver/resolver.py
deleted file mode 100644
index ce782c8..0000000
--- a/flup/resolver/resolver.py
+++ /dev/null
@@ -1,81 +0,0 @@
-# Copyright (c) 2002, 2005 Allan Saddi <allan@saddi.com>
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-#
-# $Id$
-
-__author__ = 'Allan Saddi <allan@saddi.com>'
-__version__ = '$Revision$'
-
-__all__ = ['Resolver']
-
-class Resolver(object):
- """
- Abstract base class for 'Resolver' objects. (An instance of which is
- passed to Publisher's constructor.)
-
- Given a Request, either return a callable (Publisher expects it to
- be a function, method, class, or callable instance), or return None.
- Typically Request.pathInfo is used to resolve the function.
- Request.environ may be modified by the Resolver, for example, to re-adjust
- SCRIPT_NAME/PATH_INFO after successful resolution. It is NOT recommended
- that it be modified if resolution fails.
-
- When resolving an InternalRedirect, redirect will be True.
- """
- def resolve(self, request, redirect=False):
- raise NotImplementedError, self.__class__.__name__ + '.resolve'
-
- def _updatePath(self, request, num):
- """
- Utility function to update SCRIPT_NAME and PATH_INFO in a sane
- manner. Transfers num components from PATH_INFO to SCRIPT_NAME.
- Keeps URL path parameters intact.
- """
- assert num >= 0
- if not num:
- return # Nothing to do
- numScriptName = len(request.scriptName.split('/'))
- totalPath = request.scriptName + request.pathInfo
- if __debug__:
- origTotalPath = totalPath
- # Extract and save params
- i = totalPath.find(';')
- if i >= 0:
- params = totalPath[i:]
- totalPath = totalPath[:i]
- else:
- params = ''
- totalPath = totalPath.split('/')
- scriptName = '/'.join(totalPath[:numScriptName + num])
- pathInfo = '/'.join([''] + totalPath[numScriptName + num:])
- # SCRIPT_NAME shouldn't have trailing slash
- if scriptName.endswith('/'):
- scriptName = scriptName[:-1]
- # Transfer to PATH_INFO (most likely empty, but just to be safe...)
- pathInfo = '/' + pathInfo
- request.environ['SCRIPT_NAME'] = scriptName
- request.environ['PATH_INFO'] = pathInfo + params
- if __debug__:
- assert request.scriptName + request.pathInfo == origTotalPath
-
diff --git a/flup/server/cgi.py b/flup/server/cgi.py
new file mode 100644
index 0000000..17cc3ca
--- /dev/null
+++ b/flup/server/cgi.py
@@ -0,0 +1,71 @@
+# Taken from <http://www.python.org/dev/peps/pep-0333/>
+# which was placed in the public domain.
+
+import os, sys
+
+
+__all__ = ['WSGIServer']
+
+
+class WSGIServer(object):
+
+ def __init__(self, application):
+ self.application = application
+
+ def run(self):
+
+ environ = dict(os.environ.items())
+ environ['wsgi.input'] = sys.stdin
+ environ['wsgi.errors'] = sys.stderr
+ environ['wsgi.version'] = (1,0)
+ environ['wsgi.multithread'] = False
+ environ['wsgi.multiprocess'] = True
+ environ['wsgi.run_once'] = True
+
+ if environ.get('HTTPS','off') in ('on','1'):
+ environ['wsgi.url_scheme'] = 'https'
+ else:
+ environ['wsgi.url_scheme'] = 'http'
+
+ headers_set = []
+ headers_sent = []
+
+ def write(data):
+ if not headers_set:
+ raise AssertionError("write() before start_response()")
+
+ elif not headers_sent:
+ # Before the first output, send the stored headers
+ status, response_headers = headers_sent[:] = headers_set
+ sys.stdout.write('Status: %s\r\n' % status)
+ for header in response_headers:
+ sys.stdout.write('%s: %s\r\n' % header)
+ sys.stdout.write('\r\n')
+
+ sys.stdout.write(data)
+ sys.stdout.flush()
+
+ def start_response(status,response_headers,exc_info=None):
+ if exc_info:
+ try:
+ if headers_sent:
+ # Re-raise original exception if headers sent
+ raise exc_info[0], exc_info[1], exc_info[2]
+ finally:
+ exc_info = None # avoid dangling circular ref
+ elif headers_set:
+ raise AssertionError("Headers already set!")
+
+ headers_set[:] = [status,response_headers]
+ return write
+
+ result = self.application(environ, start_response)
+ try:
+ for data in result:
+ if data: # don't send headers until body appears
+ write(data)
+ if not headers_sent:
+ write('') # send headers now if body was empty
+ finally:
+ if hasattr(result,'close'):
+ result.close()
diff --git a/setup.py b/setup.py
index 0f2b3e1..ca3ec4b 100644
--- a/setup.py
+++ b/setup.py
@@ -5,9 +5,9 @@ use_setuptools()
from setuptools import setup, find_packages
setup(
name = 'flup',
- version = '0.5',
+ version = '1.0',
packages = find_packages(),
- zip_safe = True, # Despite flup.resolver.importingmodule
+ zip_safe = True,
entry_points = """
[paste.server_factory]
@@ -24,7 +24,7 @@ setup(
author = 'Allan Saddi',
author_email = 'allan@saddi.com',
- description = 'Random assortment of WSGI servers, middleware',
+ description = 'Random assortment of WSGI servers',
license = 'BSD',
url='http://www.saddi.com/software/flup/',
classifiers = [