Source code for galaxy.web.framework.base

"""
A simple WSGI application/framework.
"""

import cgi  # For FieldStorage
import logging
import os.path
import socket
import tarfile
import types

import pkg_resources

pkg_resources.require("Paste")
pkg_resources.require("repoze.lru")  # used by Routes
pkg_resources.require("Routes")
pkg_resources.require("WebOb")

import routes
import webob

from Cookie import SimpleCookie

# We will use some very basic HTTP/wsgi utilities from the paste library
from paste.request import get_cookies
from paste import httpexceptions
from paste.response import HeaderDict


log = logging.getLogger( __name__ )


def __resource_with_deleted( self, member_name, collection_name, **kwargs ):
    """
    Method to monkeypatch on to routes.mapper.Mapper which does the same thing
    as resource() with the addition of standardized routes for handling
    elements in Galaxy's "deleted but not really deleted" fashion.
    """
    collection_path = kwargs.get( 'path_prefix', '' ) + '/' + collection_name + '/deleted'
    member_path = collection_path + '/:id'
    self.connect( 'deleted_' + collection_name, collection_path, controller=collection_name, action='index', deleted=True, conditions=dict( method=['GET'] ) )
    self.connect( 'deleted_' + member_name, member_path, controller=collection_name, action='show', deleted=True, conditions=dict( method=['GET'] ) )
    self.connect( 'undelete_deleted_' + member_name, member_path + '/undelete', controller=collection_name, action='undelete',
                  conditions=dict( method=['POST'] ) )
    self.resource( member_name, collection_name, **kwargs )
routes.Mapper.resource_with_deleted = __resource_with_deleted


[docs]class WebApplication( object ): """ A simple web application which maps requests to objects using routes, and to methods on those objects in the CherryPy style. Thus simple argument mapping in the CherryPy style occurs automatically, but more complicated encoding of arguments in the PATH_INFO can be performed with routes. """ def __init__( self ): """ Create a new web application object. To actually connect some controllers use `add_controller` and `add_route`. Call `finalize_config` when all controllers and routes have been added and `__call__` to handle a request (WSGI style). """ self.controllers = dict() self.api_controllers = dict() self.mapper = routes.Mapper() # FIXME: The following two options are deprecated and should be # removed. Consult the Routes documentation. self.mapper.minimization = True # self.mapper.explicit = False self.transaction_factory = DefaultWebTransaction # Set if trace logging is enabled self.trace_logger = None
[docs] def add_ui_controller( self, controller_name, controller ): """ Add a controller class to this application. A controller class has methods which handle web requests. To connect a URL to a controller's method use `add_route`. """ log.debug( "Enabling '%s' controller, class: %s", controller_name, controller.__class__.__name__ ) self.controllers[ controller_name ] = controller
[docs] def add_api_controller( self, controller_name, controller ): log.debug( "Enabling '%s' API controller, class: %s", controller_name, controller.__class__.__name__ ) self.api_controllers[ controller_name ] = controller
[docs] def add_route( self, route, **kwargs ): """ Add a route to match a URL with a method. Accepts all keyword arguments of `routes.Mapper.connect`. Every route should result in at least a controller value which corresponds to one of the objects added with `add_controller`. It optionally may yield an `action` argument which will be used to locate the method to call on the controller. Additional arguments will be passed to the method as keyword args. """ self.mapper.connect( route, **kwargs )
[docs] def set_transaction_factory( self, transaction_factory ): """ Use the callable `transaction_factory` to create the transaction which will be passed to requests. """ self.transaction_factory = transaction_factory
[docs] def finalize_config( self ): """ Call when application is completely configured and ready to serve requests """ # Create/compile the regular expressions for route mapping self.mapper.create_regs( self.controllers.keys() )
[docs] def trace( self, **fields ): if self.trace_logger: self.trace_logger.log( "WebApplication", **fields )
def __call__( self, environ, start_response ): """ Call interface as specified by WSGI. Wraps the environment in user friendly objects, finds the appropriate method to handle the request and calls it. """ # Immediately create request_id which we will use for logging request_id = environ.get( 'request_id', 'unknown' ) if self.trace_logger: self.trace_logger.context_set( "request_id", request_id ) self.trace( message="Starting request" ) try: return self.handle_request( environ, start_response ) finally: self.trace( message="Handle request finished" ) if self.trace_logger: self.trace_logger.context_remove( "request_id" )
[docs] def handle_request( self, environ, start_response ): # Grab the request_id (should have been set by middleware) request_id = environ.get( 'request_id', 'unknown' ) # Map url using routes path_info = environ.get( 'PATH_INFO', '' ) map = self.mapper.match( path_info, environ ) if path_info.startswith('/api'): environ[ 'is_api_request' ] = True controllers = self.api_controllers else: environ[ 'is_api_request' ] = False controllers = self.controllers if map is None: raise httpexceptions.HTTPNotFound( "No route for " + path_info ) self.trace( path_info=path_info, map=map ) # Setup routes rc = routes.request_config() rc.mapper = self.mapper rc.mapper_dict = map rc.environ = environ # Setup the transaction trans = self.transaction_factory( environ ) trans.request_id = request_id rc.redirect = trans.response.send_redirect # Get the controller class controller_name = map.pop( 'controller', None ) controller = controllers.get( controller_name, None ) if controller_name is None: raise httpexceptions.HTTPNotFound( "No controller for " + path_info ) # Resolve action method on controller action = map.pop( 'action', 'index' ) # This is the easiest way to make the controller/action accessible for # url_for invocations. Specifically, grids. trans.controller = controller_name trans.action = action method = getattr( controller, action, None ) if method is None: method = getattr( controller, 'default', None ) if method is None: raise httpexceptions.HTTPNotFound( "No action for " + path_info ) # Is the method exposed if not getattr( method, 'exposed', False ): raise httpexceptions.HTTPNotFound( "Action not exposed for " + path_info ) # Is the method callable if not callable( method ): raise httpexceptions.HTTPNotFound( "Action not callable for " + path_info ) # Combine mapper args and query string / form args and call kwargs = trans.request.params.mixed() kwargs.update( map ) # Special key for AJAX debugging, remove to avoid confusing methods kwargs.pop( '_', None ) try: body = method( trans, **kwargs ) except Exception, e: body = self.handle_controller_exception( e, trans, **kwargs ) if not body: raise # Now figure out what we got back and try to get it to the browser in # a smart way if callable( body ): # Assume the callable is another WSGI application to run return body( environ, start_response ) elif isinstance( body, types.FileType ): # Stream the file back to the browser return send_file( start_response, trans, body ) elif isinstance( body, tarfile.ExFileObject ): # Stream the tarfile member back to the browser body = iterate_file( body ) start_response( trans.response.wsgi_status(), trans.response.wsgi_headeritems() ) return body else: start_response( trans.response.wsgi_status(), trans.response.wsgi_headeritems() ) return self.make_body_iterable( trans, body )
[docs] def make_body_iterable( self, trans, body ): if isinstance( body, ( types.GeneratorType, list, tuple ) ): # Recursively stream the iterable return flatten( body ) elif isinstance( body, basestring ): # Wrap the string so it can be iterated return [ body ] elif body is None: # Returns an empty body return [] else: # Worst case scenario return [ str( body ) ]
[docs] def handle_controller_exception( self, e, trans, **kwargs ): """ Allow handling of exceptions raised in controller methods. """ return False
[docs]class WSGIEnvironmentProperty( object ): """ Descriptor that delegates a property to a key in the environ member of the associated object (provides property style access to keys in the WSGI environment) """ def __init__( self, key, default='' ): self.key = key self.default = default def __get__( self, obj, type=None ): if obj is None: return self return obj.environ.get( self.key, self.default )
[docs]class LazyProperty( object ): """ Property that replaces itself with a calculated value the first time it is used. """ def __init__( self, func ): self.func = func def __get__(self, obj, type=None ): if obj is None: return self value = self.func( obj ) setattr( obj, self.func.func_name, value ) return value
lazy_property = LazyProperty
[docs]class DefaultWebTransaction( object ): """ Wraps the state of a single web transaction (request/response cycle). TODO: Provide hooks to allow application specific state to be included in here. """ def __init__( self, environ ): self.environ = environ self.request = Request( environ ) self.response = Response() @lazy_property
[docs] def session( self ): """ Get the user's session state. This is laze since we rarely use it and the creation/serialization cost is high. """ if 'com.saddi.service.session' in self.environ: return self.environ['com.saddi.service.session'].session elif 'beaker.session' in self.environ: return self.environ['beaker.session'] else: return None # For request.params, override cgi.FieldStorage.make_file to create persistent # tempfiles. Necessary for externalizing the upload tool. It's a little hacky # but for performance reasons it's way better to use Paste's tempfile than to # create a new one and copy.
import tempfile
[docs]class FieldStorage( cgi.FieldStorage ):
[docs] def make_file(self, binary=None): return tempfile.NamedTemporaryFile()
[docs] def read_lines(self): # Always make a new file self.file = self.make_file() self.__file = None if self.outerboundary: self.read_lines_to_outerboundary() else: self.read_lines_to_eof()
cgi.FieldStorage = FieldStorage
[docs]class Request( webob.Request ): """ Encapsulates an HTTP request. """ def __init__( self, environ ): """ Create a new request wrapping the WSGI environment `environ` """ # self.environ = environ webob.Request.__init__( self, environ, charset='utf-8', decode_param_names=False ) # Properties that are computed and cached on first use @lazy_property
[docs] def remote_host( self ): try: return socket.gethostbyname( self.remote_addr ) except socket.error: return self.remote_addr
@lazy_property
[docs] def remote_hostname( self ): try: return socket.gethostbyaddr( self.remote_addr )[0] except socket.error: return self.remote_addr
@lazy_property
[docs] def cookies( self ): return get_cookies( self.environ )
@lazy_property
[docs] def base( self ): return ( self.scheme + "://" + self.host ) # @lazy_property # def params( self ): # return parse_formvars( self.environ )
@lazy_property
[docs] def path( self ): return self.environ['SCRIPT_NAME'] + self.environ['PATH_INFO']
@lazy_property
[docs] def browser_url( self ): return self.base + self.path # Descriptors that map properties to the associated environment # scheme = WSGIEnvironmentProperty( 'wsgi.url_scheme' ) # remote_addr = WSGIEnvironmentProperty( 'REMOTE_ADDR' )
remote_port = WSGIEnvironmentProperty( 'REMOTE_PORT' ) # method = WSGIEnvironmentProperty( 'REQUEST_METHOD' ) # script_name = WSGIEnvironmentProperty( 'SCRIPT_NAME' ) protocol = WSGIEnvironmentProperty( 'SERVER_PROTOCOL' ) # query_string = WSGIEnvironmentProperty( 'QUERY_STRING' ) # path_info = WSGIEnvironmentProperty( 'PATH_INFO' )
[docs]class Response( object ): """ Describes an HTTP response. Currently very simple since the actual body of the request is handled separately. """ def __init__( self ): """ Create a new Response defaulting to HTML content and "200 OK" status """ self.status = "200 OK" self.headers = HeaderDict( { "content-type": "text/html" } ) self.cookies = SimpleCookie()
[docs] def set_content_type( self, type ): """ Sets the Content-Type header """ self.headers[ "content-type" ] = type
[docs] def get_content_type( self ): return self.headers[ "content-type" ]
[docs] def send_redirect( self, url ): """ Send an HTTP redirect response to (target `url`) """ raise httpexceptions.HTTPFound( url.encode('utf-8'), headers=self.wsgi_headeritems() )
[docs] def wsgi_headeritems( self ): """ Return headers in format appropriate for WSGI `start_response` """ result = self.headers.headeritems() # Add cookie to header for name in self.cookies.keys(): crumb = self.cookies[name] header, value = str( crumb ).split( ': ', 1 ) result.append( ( header, value ) ) return result
[docs] def wsgi_status( self ): """ Return status line in format appropriate for WSGI `start_response` """ if isinstance( self.status, int ): exception = httpexceptions.get_exception( self.status ) return "%d %s" % ( exception.code, exception.title ) else: return self.status # ---- Utilities ------------------------------------------------------------
CHUNK_SIZE = 2 ** 16
[docs]def send_file( start_response, trans, body ): # If configured use X-Accel-Redirect header for nginx base = trans.app.config.nginx_x_accel_redirect_base apache_xsendfile = trans.app.config.apache_xsendfile if base: trans.response.headers['X-Accel-Redirect'] = \ base + os.path.abspath( body.name ) body = [ "" ] elif apache_xsendfile: trans.response.headers['X-Sendfile'] = os.path.abspath( body.name ) body = [ "" ] # Fall back on sending the file in chunks else: body = iterate_file( body ) start_response( trans.response.wsgi_status(), trans.response.wsgi_headeritems() ) return body
[docs]def iterate_file( file ): """ Progressively return chunks from `file`. """ while 1: chunk = file.read( CHUNK_SIZE ) if not chunk: break yield chunk
[docs]def flatten( seq ): """ Flatten a possible nested set of iterables """ for x in seq: if isinstance( x, ( types.GeneratorType, list, tuple ) ): for y in flatten( x ): yield y else: yield x