PageRenderTime 29ms CodeModel.GetById 2ms app.highlight 22ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/galaxy/web/framework/base.py

https://bitbucket.org/cistrome/cistrome-harvard/
Python | 447 lines | 428 code | 10 blank | 9 comment | 7 complexity | 50d9a7c45dd572499b6ac13871e7ca20 MD5 | raw file
  1"""
  2A simple WSGI application/framework.
  3"""
  4
  5import cgi # For FieldStorage
  6import logging
  7import os.path
  8import socket
  9import tarfile
 10import types
 11
 12import pkg_resources
 13
 14pkg_resources.require("Paste")
 15pkg_resources.require("Routes")
 16pkg_resources.require("WebOb")
 17
 18import routes
 19import webob
 20
 21from Cookie import SimpleCookie
 22
 23# We will use some very basic HTTP/wsgi utilities from the paste library
 24from paste.request import get_cookies
 25from paste import httpexceptions
 26from paste.response import HeaderDict
 27
 28
 29log = logging.getLogger( __name__ )
 30
 31
 32def __resource_with_deleted( self, member_name, collection_name, **kwargs ):
 33    """
 34    Method to monkeypatch on to routes.mapper.Mapper which does the same thing
 35    as resource() with the addition of standardized routes for handling
 36    elements in Galaxy's "deleted but not really deleted" fashion.
 37    """
 38    collection_path = kwargs.get( 'path_prefix', '' ) + '/' + collection_name + '/deleted'
 39    member_path = collection_path + '/:id'
 40    self.connect( 'deleted_' + collection_name, collection_path, controller=collection_name, action='index', deleted=True, conditions=dict( method=['GET'] ) )
 41    self.connect( 'deleted_' + member_name, member_path, controller=collection_name, action='show', deleted=True, conditions=dict( method=['GET'] ) )
 42    self.connect( 'undelete_deleted_' + member_name, member_path + '/undelete', controller=collection_name, action='undelete',
 43                  conditions=dict( method=['POST'] ) )
 44    self.resource( member_name, collection_name, **kwargs )
 45routes.Mapper.resource_with_deleted = __resource_with_deleted
 46
 47
 48class WebApplication( object ):
 49    """
 50    A simple web application which maps requests to objects using routes,
 51    and to methods on those objects in the CherryPy style. Thus simple
 52    argument mapping in the CherryPy style occurs automatically, but more
 53    complicated encoding of arguments in the PATH_INFO can be performed
 54    with routes.
 55    """
 56    def __init__( self ):
 57        """
 58        Create a new web application object. To actually connect some
 59        controllers use `add_controller` and `add_route`. Call
 60        `finalize_config` when all controllers and routes have been added
 61        and `__call__` to handle a request (WSGI style).
 62        """
 63        self.controllers = dict()
 64        self.api_controllers = dict()
 65        self.mapper = routes.Mapper()
 66        # FIXME: The following two options are deprecated and should be
 67        # removed.  Consult the Routes documentation.
 68        self.mapper.minimization = True
 69        #self.mapper.explicit = False
 70        self.transaction_factory = DefaultWebTransaction
 71        # Set if trace logging is enabled
 72        self.trace_logger = None
 73
 74    def add_ui_controller( self, controller_name, controller ):
 75        """
 76        Add a controller class to this application. A controller class has
 77        methods which handle web requests. To connect a URL to a controller's
 78        method use `add_route`.
 79        """
 80        log.debug( "Enabling '%s' controller, class: %s",
 81            controller_name, controller.__class__.__name__ )
 82        self.controllers[ controller_name ] = controller
 83
 84    def add_api_controller( self, controller_name, controller ):
 85        log.debug( "Enabling '%s' API controller, class: %s",
 86            controller_name, controller.__class__.__name__ )
 87        self.api_controllers[ controller_name ] = controller
 88
 89    def add_route( self, route, **kwargs ):
 90        """
 91        Add a route to match a URL with a method. Accepts all keyword
 92        arguments of `routes.Mapper.connect`. Every route should result in
 93        at least a controller value which corresponds to one of the
 94        objects added with `add_controller`. It optionally may yield an
 95        `action` argument which will be used to locate the method to call
 96        on the controller. Additional arguments will be passed to the
 97        method as keyword args.
 98        """
 99        self.mapper.connect( route, **kwargs )
100
101    def set_transaction_factory( self, transaction_factory ):
102        """
103        Use the callable `transaction_factory` to create the transaction
104        which will be passed to requests.
105        """
106        self.transaction_factory = transaction_factory
107
108    def finalize_config( self ):
109        """
110        Call when application is completely configured and ready to serve
111        requests
112        """
113        # Create/compile the regular expressions for route mapping
114        self.mapper.create_regs( self.controllers.keys() )
115
116    def trace( self, **fields ):
117        if self.trace_logger:
118            self.trace_logger.log( "WebApplication", **fields )
119
120    def __call__( self, environ, start_response ):
121        """
122        Call interface as specified by WSGI. Wraps the environment in user
123        friendly objects, finds the appropriate method to handle the request
124        and calls it.
125        """
126        # Immediately create request_id which we will use for logging
127        request_id = environ.get( 'request_id', 'unknown' )
128        if self.trace_logger:
129            self.trace_logger.context_set( "request_id", request_id )
130        self.trace( message="Starting request" )
131        try:
132            return self.handle_request( environ, start_response )
133        finally:
134            self.trace( message="Handle request finished" )
135            if self.trace_logger:
136                self.trace_logger.context_remove( "request_id" )
137
138    def handle_request( self, environ, start_response ):
139        # Grab the request_id (should have been set by middleware)
140        request_id = environ.get( 'request_id', 'unknown' )
141        # Map url using routes
142        path_info = environ.get( 'PATH_INFO', '' )
143        map = self.mapper.match( path_info, environ )
144        if path_info.startswith('/api'):
145            environ[ 'is_api_request' ] = True
146            controllers = self.api_controllers
147        else:
148            environ[ 'is_api_request' ] = False
149            controllers = self.controllers
150        if map == None:
151            raise httpexceptions.HTTPNotFound( "No route for " + path_info )
152        self.trace( path_info=path_info, map=map )
153        # Setup routes
154        rc = routes.request_config()
155        rc.mapper = self.mapper
156        rc.mapper_dict = map
157        rc.environ = environ
158        # Setup the transaction
159        trans = self.transaction_factory( environ )
160        trans.request_id = request_id
161        rc.redirect = trans.response.send_redirect
162        # Get the controller class
163        controller_name = map.pop( 'controller', None )
164        controller = controllers.get( controller_name, None )
165        if controller_name is None:
166            raise httpexceptions.HTTPNotFound( "No controller for " + path_info )
167        # Resolve action method on controller
168        action = map.pop( 'action', 'index' )
169        # This is the easiest way to make the controller/action accessible for
170        # url_for invocations.  Specifically, grids.
171        trans.controller = controller_name
172        trans.action = action
173        method = getattr( controller, action, None )
174        if method is None:
175            method = getattr( controller, 'default', None )
176        if method is None:
177            raise httpexceptions.HTTPNotFound( "No action for " + path_info )
178        # Is the method exposed
179        if not getattr( method, 'exposed', False ):
180            raise httpexceptions.HTTPNotFound( "Action not exposed for " + path_info )
181        # Is the method callable
182        if not callable( method ):
183            raise httpexceptions.HTTPNotFound( "Action not callable for " + path_info )
184        # Combine mapper args and query string / form args and call
185        kwargs = trans.request.params.mixed()
186        kwargs.update( map )
187        # Special key for AJAX debugging, remove to avoid confusing methods
188        kwargs.pop( '_', None )
189        try:
190            body = method( trans, **kwargs )
191        except Exception, e:
192            body = self.handle_controller_exception( e, trans, **kwargs )
193            if not body:
194                raise
195        # Now figure out what we got back and try to get it to the browser in
196        # a smart way
197        if callable( body ):
198            # Assume the callable is another WSGI application to run
199            return body( environ, start_response )
200        elif isinstance( body, types.FileType ):
201            # Stream the file back to the browser
202            return send_file( start_response, trans, body )
203        elif isinstance( body, tarfile.ExFileObject ):
204            # Stream the tarfile member back to the browser
205            body = iterate_file( body )
206            start_response( trans.response.wsgi_status(),
207                            trans.response.wsgi_headeritems() )
208            return body
209        else:
210            start_response( trans.response.wsgi_status(),
211                            trans.response.wsgi_headeritems() )
212            return self.make_body_iterable( trans, body )
213
214    def make_body_iterable( self, trans, body ):
215        if isinstance( body, ( types.GeneratorType, list, tuple ) ):
216            # Recursively stream the iterable
217            return flatten( body )
218        elif isinstance( body, basestring ):
219            # Wrap the string so it can be iterated
220            return [ body ]
221        elif body is None:
222            # Returns an empty body
223            return []
224        else:
225            # Worst case scenario
226            return [ str( body ) ]
227
228    def handle_controller_exception( self, e, trans, **kwargs ):
229        """
230        Allow handling of exceptions raised in controller methods.
231        """
232        return False
233
234
235class WSGIEnvironmentProperty( object ):
236    """
237    Descriptor that delegates a property to a key in the environ member of the
238    associated object (provides property style access to keys in the WSGI
239    environment)
240    """
241    def __init__( self, key, default = '' ):
242        self.key = key
243        self.default = default
244    def __get__( self, obj, type = None ):
245        if obj is None: return self
246        return obj.environ.get( self.key, self.default )
247
248
249class LazyProperty( object ):
250    """
251    Property that replaces itself with a calculated value the first time
252    it is used.
253    """
254    def __init__( self, func ):
255        self.func = func
256    def __get__(self, obj, type = None ):
257        if obj is None: return self
258        value = self.func( obj )
259        setattr( obj, self.func.func_name, value )
260        return value
261lazy_property = LazyProperty
262
263
264class DefaultWebTransaction( object ):
265    """
266    Wraps the state of a single web transaction (request/response cycle).
267
268    TODO: Provide hooks to allow application specific state to be included
269          in here.
270    """
271    def __init__( self, environ ):
272        self.environ = environ
273        self.request = Request( environ )
274        self.response = Response()
275    @lazy_property
276    def session( self ):
277        """
278        Get the user's session state. This is laze since we rarely use it
279        and the creation/serialization cost is high.
280        """
281        if 'com.saddi.service.session' in self.environ:
282            return self.environ['com.saddi.service.session'].session
283        elif 'beaker.session' in self.environ:
284            return self.environ['beaker.session']
285        else:
286            return None
287
288# For request.params, override cgi.FieldStorage.make_file to create persistent
289# tempfiles.  Necessary for externalizing the upload tool.  It's a little hacky
290# but for performance reasons it's way better to use Paste's tempfile than to
291# create a new one and copy.
292import tempfile
293class FieldStorage( cgi.FieldStorage ):
294    def make_file(self, binary=None):
295        return tempfile.NamedTemporaryFile()
296    def read_lines(self):
297    # Always make a new file
298        self.file = self.make_file()
299        self.__file = None
300        if self.outerboundary:
301            self.read_lines_to_outerboundary()
302        else:
303            self.read_lines_to_eof()
304cgi.FieldStorage = FieldStorage
305
306
307class Request( webob.Request ):
308    """
309    Encapsulates an HTTP request.
310    """
311    def __init__( self, environ ):
312        """
313        Create a new request wrapping the WSGI environment `environ`
314        """
315        ## self.environ = environ
316        webob.Request.__init__( self, environ, charset='utf-8', decode_param_names=False )
317    # Properties that are computed and cached on first use
318    @lazy_property
319    def remote_host( self ):
320        try:
321            return socket.gethostbyname( self.remote_addr )
322        except socket.error:
323            return self.remote_addr
324    @lazy_property
325    def remote_hostname( self ):
326        try:
327            return socket.gethostbyaddr( self.remote_addr )[0]
328        except socket.error:
329            return self.remote_addr
330    @lazy_property
331    def cookies( self ):
332        return get_cookies( self.environ )
333    @lazy_property
334    def base( self ):
335        return ( self.scheme + "://" + self.host )
336    ## @lazy_property
337    ## def params( self ):
338    ##     return parse_formvars( self.environ )
339    @lazy_property
340    def path( self ):
341        return self.environ['SCRIPT_NAME'] + self.environ['PATH_INFO']
342    @lazy_property
343    def browser_url( self ):
344        return self.base + self.path
345    # Descriptors that map properties to the associated environment
346    ## scheme = WSGIEnvironmentProperty( 'wsgi.url_scheme' )
347    ## remote_addr = WSGIEnvironmentProperty( 'REMOTE_ADDR' )
348    remote_port = WSGIEnvironmentProperty( 'REMOTE_PORT' )
349    ## method = WSGIEnvironmentProperty( 'REQUEST_METHOD' )
350    ## script_name = WSGIEnvironmentProperty( 'SCRIPT_NAME' )
351    protocol = WSGIEnvironmentProperty( 'SERVER_PROTOCOL' )
352    ## query_string = WSGIEnvironmentProperty( 'QUERY_STRING' )
353    ## path_info = WSGIEnvironmentProperty( 'PATH_INFO' )
354
355
356class Response( object ):
357    """
358    Describes an HTTP response. Currently very simple since the actual body
359    of the request is handled separately.
360    """
361    def __init__( self ):
362        """
363        Create a new Response defaulting to HTML content and "200 OK" status
364        """
365        self.status = "200 OK"
366        self.headers = HeaderDict( { "content-type": "text/html" } )
367        self.cookies = SimpleCookie()
368
369    def set_content_type( self, type ):
370        """
371        Sets the Content-Type header
372        """
373        self.headers[ "content-type" ] = type
374
375    def get_content_type( self ):
376        return self.headers[ "content-type" ]
377
378    def send_redirect( self, url ):
379        """
380        Send an HTTP redirect response to (target `url`)
381        """
382        raise httpexceptions.HTTPFound( url.encode('utf-8'), headers=self.wsgi_headeritems() )
383
384    def wsgi_headeritems( self ):
385        """
386        Return headers in format appropriate for WSGI `start_response`
387        """
388        result = self.headers.headeritems()
389        # Add cookie to header
390        for name in self.cookies.keys():
391            crumb = self.cookies[name]
392            header, value = str( crumb ).split( ': ', 1 )
393            result.append( ( header, value ) )
394        return result
395
396    def wsgi_status( self ):
397        """
398        Return status line in format appropriate for WSGI `start_response`
399        """
400        if isinstance( self.status, int ):
401            exception = httpexceptions.get_exception( self.status )
402            return "%d %s" % ( exception.code, exception.title )
403        else:
404            return self.status
405
406# ---- Utilities ------------------------------------------------------------
407
408CHUNK_SIZE = 2**16
409
410def send_file( start_response, trans, body ):
411    # If configured use X-Accel-Redirect header for nginx
412    base = trans.app.config.nginx_x_accel_redirect_base
413    apache_xsendfile = trans.app.config.apache_xsendfile
414    if base:
415        trans.response.headers['X-Accel-Redirect'] = \
416            base + os.path.abspath( body.name )
417        body = [ "" ]
418    elif apache_xsendfile:
419        trans.response.headers['X-Sendfile'] = os.path.abspath( body.name )
420        body = [ "" ]
421    # Fall back on sending the file in chunks
422    else:
423        body = iterate_file( body )
424    start_response( trans.response.wsgi_status(),
425                    trans.response.wsgi_headeritems() )
426    return body
427
428def iterate_file( file ):
429    """
430    Progressively return chunks from `file`.
431    """
432    while 1:
433        chunk = file.read( CHUNK_SIZE )
434        if not chunk:
435            break
436        yield chunk
437
438def flatten( seq ):
439    """
440    Flatten a possible nested set of iterables
441    """
442    for x in seq:
443        if isinstance( x, ( types.GeneratorType, list, tuple ) ):
444            for y in flatten( x, encoding ):
445                yield y
446        else:
447            yield x