1. Lynn Rees
  2. urlrelay

Source

urlrelay / branches / 0.5 / trunk / urlrelay.py

The branch 'urlrelay' does not exist.
# 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.

'''RESTful URL dispatcher.'''

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

import re, sys

# URL registry and cache
urlregistry, cache = dict(), dict()

def register(pattern, application, method=None):
    '''Registers a pattern, application, and optional method.

    @param pattern URL pattern
    @param application WSGI application
    @param method HTTP method (default: None)
    '''
    if method is None:
        urlregistry[pattern] = application
    # Handle URL/method combinations
    else:
        # Try loading in existing dictionary
        try:
            urlregistry[pattern][method] = application
        # Make new method dictionary
        except KeyError:
            urlregistry[pattern] = {method:application}


class URLRelay(object):

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

    def __init__(self, urls=None, **kw):
        '''
        @param urls Iterable of pairs consisting of a URL pattern and either a
            callback name or a dictionary of HTTP method/callback names (default: None)
        @param kwargs Keyword arguments
        '''
        # Use internal url registry if no URL iterable passed
        if urls is None:            
            urls = iter((k, w) for (k, w) in urlregistry.iteritems())
        self.urls = tuple((re.compile(uri[0]), uri[1]) for uri in urls)
        # Shortcut for full module search path
        self.modpath = kw.get('modpath', '')
        # Custom error handler
        self.handler = kw.get('handler', URLRelay._handler)
        # Default function
        self.default = kw.get('default')
        # Djangoesque default arguments
        self.defaultargs = kw.get('defaultargs', {})
        # Argument/Keyword argument environ keywords
        self.kkey = kw.get('kwargs', 'wsgize.kwargs')
        self.akey = kw.get('args', 'wsgize.args')
        # Cache 
        self.cache = kw.get('cache', cache)
        # Custom additions to $PYTHONPATH
        paths = kw.get('paths')
        if paths is not None:
            for path in paths: sys.path.append(path)

    def __call__(self, environ, start_response):
        '''Passes WSGI params to a callable based on URL path and method.'''
        try:
            path = environ['SCRIPT_NAME'] + environ['PATH_INFO']
            callback, args, kws = self.resolve(path, environ['REQUEST_METHOD'])
            environ[self.akey], environ[self.kkey] = args, kws
            return callback(environ, start_response)
        except ImportError:
            return self.handler(environ, start_response)        

    def getmodfunc(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):
        return self.__call__(environ, start_response)

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

        uri URL path component
        method HTTP method (default: '')
        '''
        key = ':'.join([uri, method])
        try:
            callback, pattern = self.cache[key]
            kw, args = self._getargs(pattern.search(uri))
            return callback, args, kw
        except KeyError:
            for pattern, callbacks in self.urls:
                # Match URL
                search = pattern.search(uri)
                if search:
                    # Extract any keywords or arguments in the URL
                    kw, args = self._getargs(search)
                    # If callable object, return callable
                    if hasattr(callbacks, '__call__'):
                        callback = callbacks
                    # If module name string, resolve name and return callable
                    elif isinstance(callbacks, basestring):
                        callback = self.getcallback(callbacks)
                    # Get callable based on URL and HTTP method
                    elif isinstance(callbacks, dict):
                        tcallback = callbacks[method]
                        # If callable object, return callable
                        if hasattr(tcallback, '__call__'):
                            callback = tcallback
                        # If module name string, resolve name and return callable
                        elif isinstance(tcallback, basestring):
                            callback = self.getcallback(tcallback)
                    # Store in cache
                    self.cache[key] = (callback, pattern)
                    return callback, args, kw          
            if self.default is not None:
                return self.default, (), {}
            else:
                raise ImportError()        

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

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

    @classmethod
    def _handler(cls, environ, start_response):
        '''Handles 404 errors.'''
        path = environ['SCRIPT_NAME'] + environ['PATH_INFO']
        start_response('404 Not Found', [('content-type', 'text/plain')])
        return ['Requested URL %s was not found on this server.' % path]

    def _getargs(self, search):
        '''Extracts arguments from URLs.'''
        # If there are any named groups, use those as kwargs, ignoring
        # non-named groups. Otherwise, pass all non-named arguments as
        # positional arguments.
        kw = search.groupdict()
        if kw: args = ()
        else: args = search.groups()
        # In both cases, pass any extra_kwargs as **kwargs.
        kw.update(self.defaultargs)
        return args, kw


def url(pattern, method=None):
    '''Callable decorator for registering a URL.'''
    def decorator(application):
        register(pattern, application, method)
        return application
    return decorator        


__all__ = ['URLRelay', 'url', 'register']