From cbe792618a1e6b1aeddd8d751dbeefe6a981ba23 Mon Sep 17 00:00:00 2001 From: Allan Saddi Date: Tue, 5 Jun 2007 09:42:39 -0700 Subject: Remove publisher and middleware packages. Add cgi server for completeness. --- ChangeLog | 9 +- flup/middleware/__init__.py | 1 - flup/middleware/error.py | 352 ------------------ flup/middleware/gzip.py | 279 -------------- flup/middleware/session.py | 770 --------------------------------------- flup/publisher.py | 767 -------------------------------------- flup/resolver/__init__.py | 2 - flup/resolver/complex.py | 112 ------ flup/resolver/function.py | 46 --- flup/resolver/importingmodule.py | 129 ------- flup/resolver/module.py | 86 ----- flup/resolver/nopathinfo.py | 57 --- flup/resolver/objectpath.py | 156 -------- flup/resolver/resolver.py | 81 ---- flup/server/cgi.py | 71 ++++ setup.py | 6 +- 16 files changed, 79 insertions(+), 2845 deletions(-) delete mode 100644 flup/middleware/__init__.py delete mode 100644 flup/middleware/error.py delete mode 100644 flup/middleware/gzip.py delete mode 100644 flup/middleware/session.py delete mode 100644 flup/publisher.py delete mode 100644 flup/resolver/__init__.py delete mode 100644 flup/resolver/complex.py delete mode 100644 flup/resolver/function.py delete mode 100644 flup/resolver/importingmodule.py delete mode 100644 flup/resolver/module.py delete mode 100644 flup/resolver/nopathinfo.py delete mode 100644 flup/resolver/objectpath.py delete mode 100644 flup/resolver/resolver.py create mode 100644 flup/server/cgi.py 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 + + * Remove publisher and middleware packages. + * Add cgi server for completeness. + 2007-05-17 Allan Saddi * 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 * 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 -# 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 ' -__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 -# 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 ' -__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(' 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(' -# 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 ' -__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 -# 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 ' -__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 [""" - -404 Not Found - -

Not Found

-The requested URL %s was not found on this server.

-


-%s -""" % (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 [""" - -%s - -

Found

-

The document has moved here.

-
-%s -""" % (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 -# 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 ' -__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 -# 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 ' -__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 -# 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 ' -__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 -# 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 ' -__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 -# 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 ' -__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 -# 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 ' -__version__ = '$Revision$' - -import re - -from flup.resolver.resolver import * - -__all__ = ['ObjectPathResolver', 'expose'] - -class NoDefault(object): - pass - -class ObjectPathResolver(Resolver): - """ - Inspired by CherryPy . :) For an explanation - of how this works, see the excellent tutorial at - . 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 -# 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 ' -__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 +# 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 = [ -- cgit v1.2.1