urlrelay / urlrelay / urlrelay.py

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

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

import re

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


class _Registry(object):

    '''Maintains order of URL preference while updating the central URL path
    registry.'''

    _register = list()

    def __iter__(self):
        '''Iterator for registry.'''
        return iter(self._register)

    def add(self, pattern, mapping):
        '''Add tuple to registry.

        @param pattern URL pattern
        @param mapping WSGI application mapping
        '''
        self._register.append((pattern, mapping))

    def get(self):
        '''Returns current registry.'''
        return tuple(self._register)


# URL registry and cache
_reg, _cache = _Registry(), dict()

def _handler(environ, start_response):
    '''Default HTTP 404 handler.'''
    path = environ['PATH_INFO']
    start_response('404 Not Found', [('content-type', 'text/plain')])
    return ['Requested URL %s was not found on this server.' % path]

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

    @param pattern URL pattern
    @param application WSGI application
    @param method HTTP method (default: None)
    '''
    if method is None:
        _reg.add(pattern, application)
    # Handle URL/method combinations
    else:
        # Update any existing registry entry
        for entry in _reg:
            if entry[0] == pattern:
                entry[1][method] = application
                return
        # Add new registry entry
        _reg.add(pattern, {method:application})

def url(pattern, method=None):
    '''Decorator for registering a path pattern /application pair.

    @param pattern Regex pattern for path
    @param method HTTP method (default: None)
    '''
    def decorator(application):
        register(pattern, application, method)
        return application
    return decorator


class URLRelay(object):

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

    def __init__(self, **k):
        # Add any iterable of pairs consisting of a path pattern and either a
        # callback name or a dictionary of HTTP method/callback names
        self.paths = tuple((re.compile(u), v) for u, v in k.get('paths', _reg.get()))
        # Shortcut for full module search path
        self.modpath = k.get('modpath', '')
        # 404 handler
        self.handler = k.get('handler', _handler)
        # Default function
        self.default = k.get('default')
        # Default positional and keyword arguments
        self.args, self.kwargs = k.get('args', ()), k.get('kwargs', {})
        # routing_args environ keyword
        self.key = k.get('key', 'wsgiorg.routing_args')
        # Key for passing kwargs individually
        self.kkey = k.get('kkey', 'wsgize.kwargs')
        # Key for passing args individually
        self.akey = k.get('akey', 'wsgize.args')
        # URL <-> callable mapping Cache 
        self.cache = k.get('cache', _cache)

    def __call__(self, env, start_response):
        try:
            # Fetch app and any positional or keyword arguments in path
            app, arg, kw = self.resolve(env['PATH_INFO'], env['REQUEST_METHOD'])
            # Place args in environ dictionary
            env[self.key], env[self.akey], env[self.kkey] = (arg, kw), arg, kw
            return app(env, start_response)
        except (ImportError, AttributeError):
            # Return 404 handler for any exceptions
            return self.handler(env, start_response)    

    def getapp(self, app):
        '''Loads a callable based on its name

        @param app An WSGI application's name'''
        # Add shortcut to module if present
        if self.modpath != '': app = '.'.join([self.modpath, app])
        dot = app.rindex('.')
        # Import module
        return getattr(__import__(app[:dot], '', '', ['']), app[dot+1:])        

    def relay(self, environ, start_response):
        '''Legacy method.'''
        return self.__call__(environ, start_response)

    def resolve(self, path, method):
        '''Fetches a WSGI app based on URL path component and method.

        @param path URL path component
        @param method HTTP method
        '''
        key = ':'.join([path, method])
        # Try fetching app and positional and keyword args from cache
        try:
            return self.cache[key]
        except KeyError:
            # Loop through path patterns <-> app mappings
            for pattern, app in self.paths:
                # Test path for match
                search = pattern.search(path)
                # Continue with next iteration if not a match
                if not search: continue                
                # Load app from storage if module name string
                if isinstance(app, basestring):
                    app = self.getapp(app)
                # Get app linked to HTTP method
                elif isinstance(app, dict):
                    app = app.get(method)
                    # Load app from storage if module name string
                    if isinstance(app, basestring): app = self.getapp(app)
                # Ensure we have a callable object
                assert hasattr(app, '__call__')
                # Extract any positional or keywords arguments in the path
                args, kwargs = search.groups(), search.groupdict()
                # Eliminate keyword values in args
                args = tuple(i for i in args if i not in kwargs.itervalues())
                # Cache callable, positional and keyword arguments
                self.cache[key] = (app, args, kwargs)                
                return app, args, kwargs
            # Return defaults if no matching path and default app is set
            if self.default is not None:
                default = self.default
                # Load default app from storage if module name string
                if isinstance(default, basestring):
                    default = self.getapp(default)
                return default, self.args, self.kwargs
            else:
                raise ImportError()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.