Source

urlrelay / branches / 0.3 / trunk / urlrelay.py

The branch 'urlrelay' does not exist.
Full commit
# Copyright (c) 2005, the Lawrence Journal-World
# Copyright (c) 2006 L. C. Rees
# 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.
#
#    3. Neither the name of Django nor the names of its contributors may be used
#       to endorse or promote products derived from this software without
#       specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT OWNER 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.

'''Simple relay for passing HTTP messages to a WSGI callable based on URL
pattern and HTTP request method.'''

__author__ = 'L.C. Rees (lcrees@gmail.com)'
__revision__ = '0.3'

import re, sys


def addsyspath(path):
    '''Adds a filesystem path to the PYTHONPATH.

    @param path A filesystem path.'''
    sys.path.append(path)


class URLRelay(object):

    '''Passes HTTP messages to a WSGI callable based on a URL and HTTP method.'''

    def __init__(self, urls, **kwargs):
        '''Init

        @param urls Iterable of pairs consisting of a URL pattern and either a
            callback name or a dictionary of HTTP method/callback names
        @param kwargs Keyword arguments
        '''
        self.urls = tuple((re.compile(url[0]), url[1]) for url in urls)
        # Shortcut for full module search path
        self.modpath = kwargs.get('modpath', '')
        # Use built-in 404 
        self.use404 = kwargs.get('use404', True)
        # Custom error handler
        self.errorhandler = kwargs.get('errorhandler', None)
        # Default function
        self.default = kwargs.get('default', None)
        # Custom additions to PYTHONPATH
        syspaths = kwargs.get('paths', None)
        if syspaths is not None:
            for path in syspaths: addsyspath(path)

    def get_mod_func(self, callback):
        '''Breaks a callable name out from a module name.

        @param callback Name of a callback        
        '''
        # Add shortcut to module if present
        if self.modpath != '': callback = '.'.join([self.modpath, callback])
        dot = callback.rindex('.')
        return callback[:dot], callback[dot+1:]        

    def relay(self, environ, start_response):
        '''Passes WSGI params to a callable based on URL path and method.
        
        environ Dictionary of WSGI environment variables
        start_response Stores headers until any HTTP content is returned'''
        try:
            callback = self.resolve(environ['PATH_INFO'], environ['REQUEST_METHOD'])        
            return callback(environ, start_response)
        except ImportError:
            if self.use404:
                start_response('404 Not Found', [("content-type","text/plain")])
                return ['Requested URL %s was not found on this server.'
                    % environ['PATH_INFO']]
            else:
                return self.errorhandler(environ, start_response)

    def resolve(self, url, method):
        '''Fetches a callable based on a URL and method.

        url URL path component
        method HTTP method (default: '')
        '''
        for pattern, callbacks in self.urls:
            if pattern.search(url):
                # If callable object, return callable
                if hasattr(callbacks, '__call__'):
                    return callbacks
                # If module name string, resolve name and return callable
                elif isinstance(callbacks, basestring):
                    return self.get_callback(callbacks)
                # Get callable based on URL and HTTP method
                elif isinstance(callbacks, dict):
                    callback = callbacks.get(method)
                    # If callable object, return callable
                    if hasattr(callback, '__call__'):
                        return callback
                    # If module name string, resolve name and return callable
                    elif isinstance(callback, basestring):
                        return self.get_callback(callback)
        if self.default is not None:
            return self.default
        else:
            raise ImportError()    

    def get_callback(self, callback):
        '''Loads a callable based on its name

        callback A callback's name'''        
        mod_name, func_name = self.get_mod_func(callback)
        try:
            return getattr(__import__(mod_name, '', '', ['']), func_name)
        except ImportError, error:
            raise ImportError(
                'Could not import %s. Error was: %s' % (mod_name, str(error)))
        except AttributeError, error:
            raise AttributeError(
                'Tried %s in module %s. Error was: %s' % (func_name,
                 mod_name, str(error)))


__all__ = ['URLRelay']