Package wsgiwapi :: Module application
[frames] | no frames]

Source Code for Module wsgiwapi.application

  1  # Copyright (c) 2009 Richard Boulton 
  2  # 
  3  # Permission is hereby granted, free of charge, to any person obtaining a copy 
  4  # of this software and associated documentation files (the "Software"), to deal 
  5  # in the Software without restriction, including without limitation the rights 
  6  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
  7  # copies of the Software, and to permit persons to whom the Software is 
  8  # furnished to do so, subject to the following conditions: 
  9  # 
 10  # The above copyright notice and this permission notice shall be included in 
 11  # all copies or substantial portions of the Software. 
 12  # 
 13  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 14  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 15  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 16  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 17  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 18  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
 19  # SOFTWARE. 
 20  r"""Create a WSGI application providing a web API. 
 21   
 22  """ 
 23  __docformat__ = "restructuredtext en" 
 24   
 25  import sys 
 26   
 27  from decorators import jsonreturning, _get_props 
 28  import decorators 
 29  import postdata 
 30  from logging import StdoutLogger 
 31  from wsgisupport import Request, \ 
 32           HTTPError, \ 
 33           HTTPNotFound, \ 
 34           HTTPServerError, \ 
 35           HTTPMethodNotAllowed, \ 
 36           WSGIResponse, \ 
 37           Response 
38 39 -class ValidationError(Exception):
40 """Exception used to indicate that parameters failed validation. 41 42 """
43 - def __init__(self, message):
44 self._message = message
45 46 @apply
47 - def message():
48 def get(self): 49 return self._message
50 51 def set(self, value): 52 self._message = value
53 54 return property(get, set, 55 doc="Get a message explaining why validation failed.") 56
57 - def __str__(self):
58 return "ValidationError(\"%s\")" % self._message.\ 59 replace('\\', '\\\\').\ 60 replace('"', '\"')
61
62 -def apply_request_checks_and_transforms(request, props):
63 """Apply all the checks and transforms listed in props to a request. 64 65 This is typically called from a decorator, and the props are read from the 66 decorated function. 67 68 """ 69 # if POST/PUT data is not handled by a filter, use the default handler 70 if request.method in ('POST', 'PUT') \ 71 and (props is None or not props.get('postdata_is_processed')): 72 postdata.process_default(request) 73 74 if props is None: 75 return request 76 request_filters = props.get('request_filters', []) 77 for request_filter in request_filters: 78 request = request_filter(request, props) 79 return request
80
81 -def apply_response_checks_and_transforms(request, response, props):
82 """Apply all the checks and transforms listed in props to a response. 83 84 This is typically called from a decorator, and the props are read from the 85 decorated function. 86 87 """ 88 if props is None: 89 return response 90 response_filters = props.get('response_filters', []) 91 for response_filter in response_filters: 92 response = response_filter(request, response, props) 93 return response
94
95 -def handle_validation_error(err):
96 """Default handler for validation errors. 97 98 Returns a Response with status code 400. 99 100 """ 101 response = Response(u"Validation Error: " + err.message) 102 response.status = 400 103 return response
104
105 -def unflatten_urls(flat_urls):
106 """Unflatten a sequence or dict of url components. 107 108 """ 109 urls = {} 110 for path, handler in flat_urls.iteritems(): 111 suburls = urls 112 components = path.split('/') 113 for component in components[:-1]: 114 try: 115 new_suburls = suburls[component] 116 except KeyError: 117 new_suburls = {} 118 suburls[component] = new_suburls 119 if not isinstance(new_suburls, dict): 120 new_suburls = {None: new_suburls} 121 suburls[component] = new_suburls 122 suburls = new_suburls 123 124 component = components[-1] 125 old_handler = suburls.get(component) 126 if old_handler is None: 127 suburls[component] = handler 128 else: 129 if not isinstance(old_handler, dict): 130 raise TypeError("duplicated component at end of path '%s'" 131 % path) 132 if None in old_handler: 133 raise TypeError("duplicated component at end of path '%s'" 134 % path) 135 old_handler[None] = handler 136 return urls
137
138 -def make_application(urls, 139 autodoc=None, 140 validation_error_handler=handle_validation_error, 141 logger=None, 142 ):
143 """Make a web application for a given set of URLs. 144 145 - `urls` is a dict of urls to support: keys are url components, values are 146 either sub dictionaries, or callables. 147 148 - `logger` is a callable which returns a Logger. When the application 149 object returned is instantiated, it will call this callable, and use the 150 returned object for logging. 151 152 FIXME - document the other parameters to this function. 153 154 """ 155 if logger is None: 156 logger = StdoutLogger 157 158 urls = unflatten_urls(urls) 159 160 class NotFound(object): pass 161 162 class Application(object): 163 """WSGI application wrapping the search server. 164 165 """ 166 def __init__(self): 167 self.logger = logger()
168 169 def __call__(self, environ, start_response): 170 logstart = self.logger.request_start(environ) 171 try: 172 logged, request, response = \ 173 self._do_call(environ, start_response, logstart) 174 except Exception, e: 175 # We get here only if there's an error building the Request 176 # object from the environ. 177 self.logger.request_failed(environ, logstart, sys.exc_info()) 178 return HTTPServerError(str(e)) 179 else: 180 if not logged: 181 self.logger.request_end(environ, logstart, 182 request, response) 183 return response 184 185 def _do_call(self, environ, start_response, logstart): 186 request = Request(environ) 187 try: 188 handlers = urls 189 handler = NotFound 190 pathinfo = [] 191 defaulthandler, defaulti = (NotFound, None) # handler for '' components 192 for i in xrange(0, len(request.path_components)): 193 handler = handlers.get(request.path_components[i], NotFound) 194 if handler is NotFound: 195 handler = handlers.get('*', NotFound) 196 if handler is not NotFound: 197 pathinfo.append(request.path_components[i]) 198 if handler is NotFound: 199 defaulthandler, defaulti = handlers.get('', defaulthandler), i - 1 200 break 201 if hasattr(handler, '__call__'): 202 break 203 handlers = handler 204 205 # If we're at the end of the path, do special-case lookup for 206 # None. 207 if i + 1 == len(request.path_components): 208 if isinstance(handler, dict): 209 try: 210 handler = handler[None] 211 except KeyError: 212 pass 213 214 if handler is NotFound: 215 handler, i = defaulthandler, defaulti 216 217 if handler is NotFound: 218 raise HTTPNotFound(request.path) 219 220 pathinfo.extend(request.path_components[i + 1:]) 221 if isinstance(handler, MethodSwitch): 222 response = handler(request, pathinfo) 223 elif hasattr(handler, '__call__'): 224 response = _process_request(handler, request, pathinfo) 225 226 else: 227 # FIXME - perhaps we should raise an server error 228 # exception here - this shouldn't happen if the application 229 # has been set up correctly. 230 raise HTTPNotFound(request.path) 231 232 return False, request, WSGIResponse(start_response, response) 233 234 except ValidationError, e: 235 response = validation_error_handler(e) 236 return False, request, WSGIResponse(start_response, response) 237 except HTTPError, e: 238 return False, request, WSGIResponse(start_response, e) 239 except Exception, e: 240 # Handle uncaught exceptions by returning a 500 error. 241 self.logger.request_failed(environ, logstart, sys.exc_info()) 242 return True, request, WSGIResponse(start_response, HTTPServerError(str(e))) 243 244 if autodoc: 245 components = autodoc.split('/') 246 suburls = urls 247 for component in components[:-1]: 248 suburls = suburls.setdefault(component, {}) 249 from autodoc import make_doc 250 suburls[components[-1]] = make_doc(urls, autodoc) 251 252 return Application() 253
254 -def make_server(app, bind_addr, *args, **kwargs):
255 """Make a server for an application. 256 257 This uses CherryPy's standalone WSGI server. The first argument is the 258 WSGI application to run; all subsequent arguments are passed directly to 259 the server. The CherryPyWSGIServer is accessible as 260 wsgiwapi.cpwsgiserver: see the documentation in that module for calling 261 details. 262 263 Note that you will always need to set the bind_addr parameter; this is a 264 (host, port) tuple for TCP sockets, or a filename for UNIX sockets. The 265 host part may be set to '0.0.0.0' to listen on all active IPv4 interfaces 266 (or similarly, '::' to listen on all active IPv6 interfaces). 267 268 """ 269 # Lazy import, so we don't pull cherrypy in unless we're using it. 270 import cpwsgiserver 271 server = cpwsgiserver.CherryPyWSGIServer(bind_addr, app, *args, **kwargs) 272 return server
273
274 -def _process_request(handler, request, pathinfo):
275 """Process a request, with a given handler. 276 277 `unhandled_path_index` is the index in request.path_components of the first 278 component which wasn't used in looking up handler - ie, the start of the 279 pathinfo components. 280 281 """ 282 # Read the properties from the handler 283 handler_props = _get_props(handler) 284 request._set_handler_props(handler_props) 285 286 # Check that there are no tail components if the handler is not 287 # marked as accepting pathinfo. 288 if len(pathinfo) != 0: 289 # Raise a NotFound error unless the handler is marked as 290 # accepting pathinfo. 291 if handler_props is None or \ 292 handler_props.get('pathinfo_allow', None) is None: 293 raise HTTPNotFound(request.path) 294 295 # Set the path info 296 request._set_pathinfo(pathinfo) 297 298 # Apply the pre-checks to the request. 299 request = apply_request_checks_and_transforms(request, handler_props) 300 301 # Call the handler. 302 response = handler(request) 303 304 # Apply the post-checks to the response 305 response = apply_response_checks_and_transforms(request, response, handler_props) 306 assert response is not None 307 308 # Allow handlers to return strings - just wrap them in a 309 # default response object. 310 if isinstance(response, basestring): 311 response = Response(body=response) 312 assert isinstance(response, Response) 313 314 return response
315
316 -class MethodSwitch(object):
317 """Class for switching callables by request method. 318 319 In wsgiwapi.make_application, instances of this class can be substituted for 320 callables, e.g.: 321 322 make_application({'foo': MethodSwitch(foo_get, foo_post, default=foo_other)}) 323 324 If the default handler is supplied, it will be called for any request method not 325 explicitly specified. 326 327 """
328 - def __init__(self, get=None, post=None, put=None, delete=None, 329 head=None, options=None, trace=None, connect=None, 330 default=None):
331 self.methods = { 332 'GET': get, 'POST': post, 'PUT': put, 'DELETE': delete, 333 'HEAD': head, 'OPTIONS': options, 'TRACE': trace, 'CONNECT': connect 334 } 335 self.default = default
336
337 - def __call__(self, request, pathinfo):
338 handler = self.methods.get(request.method) 339 if handler is not None: 340 return _process_request(handler, request, pathinfo) 341 elif self.default is not None: 342 return _process_request(self.default, request, pathinfo) 343 else: 344 allowed_methods = [x[0] for x in self.methods.iteritems() if x[1]] 345 raise HTTPMethodNotAllowed(request.method, allowed_methods)
346 347 # vim: set fileencoding=utf-8 : 348