root/galaxy-central/lib/galaxy/web/framework/base.py @ 2

リビジョン 2, 14.3 KB (コミッタ: hatakeyama, 14 年 前)

import galaxy-central

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