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

https://bitbucket.org/cistrome/cistrome-harvard/ · Python · 447 lines · 347 code · 24 blank · 76 comment · 24 complexity · 50d9a7c45dd572499b6ac13871e7ca20 MD5 · raw file

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