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 JsonResponse(object):
40 """A class used to return a JSON response with a specific status code. 41 42 """
43 - def __init__(self, jsonobj, status_code=None, content_type=None):
44 self.jsonobj = jsonobj 45 self.status_code = status_code 46 self.content_type = content_type
47
48 -class ValidationError(Exception):
49 """Exception used to indicate that parameters failed validation. 50 51 """
52 - def __init__(self, message):
53 self._message = message
54 55 @apply
56 - def message():
57 def get(self): 58 return self._message
59 60 def set(self, value): 61 self._message = value
62 63 return property(get, set, 64 doc="Get a message explaining why validation failed.") 65
66 - def __str__(self):
67 return "ValidationError(\"%s\")" % self._message.\ 68 replace('\\', '\\\\').\ 69 replace('"', '\"')
70
71 -def apply_request_checks_and_transforms(request, props):
72 """Apply all the checks and transforms listed in props to a request. 73 74 This is typically called from a decorator, and the props are read from the 75 decorated function. 76 77 """ 78 # if POST/PUT data is not handled by a filter, use the default handler 79 if request.method in ('POST', 'PUT') \ 80 and (props is None or not props.get('postdata_is_processed')): 81 postdata.process_default(request) 82 83 if props is None: 84 return request 85 request_filters = props.get('request_filters', []) 86 for request_filter in request_filters: 87 request = request_filter(request, props) 88 return request
89
90 -def apply_response_checks_and_transforms(request, response, props):
91 """Apply all the checks and transforms listed in props to a response. 92 93 This is typically called from a decorator, and the props are read from the 94 decorated function. 95 96 """ 97 if props is None: 98 return response 99 response_filters = props.get('response_filters', []) 100 for response_filter in response_filters: 101 response = response_filter(request, response, props) 102 return response
103
104 -def handle_validation_error(err):
105 """Default handler for validation errors. 106 107 Returns a Response with status code 400. 108 109 """ 110 response = Response(u"Validation Error: " + err.message) 111 response.status = 400 112 return response
113
114 -def unflatten_urls(flat_urls):
115 """Unflatten a sequence or dict of url components. 116 117 """ 118 urls = {} 119 for path, handler in flat_urls.iteritems(): 120 suburls = urls 121 components = path.split('/') 122 for component in components[:-1]: 123 try: 124 new_suburls = suburls[component] 125 except KeyError: 126 new_suburls = {} 127 suburls[component] = new_suburls 128 if not isinstance(new_suburls, dict): 129 new_suburls = {None: new_suburls} 130 suburls[component] = new_suburls 131 suburls = new_suburls 132 133 component = components[-1] 134 old_handler = suburls.get(component) 135 if old_handler is None: 136 suburls[component] = handler 137 else: 138 if not isinstance(old_handler, dict): 139 raise TypeError("duplicated component at end of path '%s'" 140 % path) 141 if None in old_handler: 142 raise TypeError("duplicated component at end of path '%s'" 143 % path) 144 old_handler[None] = handler 145 return urls
146
147 -def make_application(urls, 148 autodoc=None, 149 validation_error_handler=handle_validation_error, 150 logger=None, 151 ):
152 """Make a web application for a given set of URLs. 153 154 - `urls` is a dict of urls to support: keys are url components, values are 155 either sub dictionaries, or callables. 156 157 - `logger` is a callable which returns a Logger. When the application 158 object returned is instantiated, it will call this callable, and use the 159 returned object for logging. 160 161 FIXME - document the other parameters to this function. 162 163 """ 164 if logger is None: 165 logger = StdoutLogger 166 167 urls = unflatten_urls(urls) 168 169 class NotFound(object): pass 170 171 class Application(object): 172 """WSGI application wrapping the search server. 173 174 """ 175 def __init__(self): 176 self.logger = logger()
177 178 def __call__(self, environ, start_response): 179 logstart = self.logger.request_start(environ) 180 try: 181 logged, request, response = \ 182 self._do_call(environ, start_response, logstart) 183 except Exception, e: 184 # We get here only if there's an error building the Request 185 # object from the environ. 186 self.logger.request_failed(environ, logstart, sys.exc_info()) 187 return HTTPServerError(str(e)) 188 else: 189 if not logged: 190 self.logger.request_end(environ, logstart, 191 request, response) 192 return response 193 194 def _do_call(self, environ, start_response, logstart): 195 request = Request(environ) 196 try: 197 handlers = urls 198 handler = NotFound 199 pathinfo = [] 200 defaulthandler, defaulti = (NotFound, None) # handler for '' components 201 for i in xrange(0, len(request.path_components)): 202 handler = handlers.get(request.path_components[i], NotFound) 203 if handler is NotFound: 204 handler = handlers.get('*', NotFound) 205 if handler is not NotFound: 206 pathinfo.append(request.path_components[i]) 207 if handler is NotFound: 208 defaulthandler, defaulti = handlers.get('', defaulthandler), i - 1 209 break 210 if hasattr(handler, '__call__'): 211 break 212 handlers = handler 213 214 # If we're at the end of the path, do special-case lookup for 215 # None. 216 if i + 1 == len(request.path_components): 217 if isinstance(handler, dict): 218 try: 219 handler = handler[None] 220 except KeyError: 221 pass 222 223 if handler is NotFound: 224 handler, i = defaulthandler, defaulti 225 226 if handler is NotFound: 227 raise HTTPNotFound(request.path) 228 229 pathinfo.extend(request.path_components[i + 1:]) 230 if isinstance(handler, MethodSwitch): 231 response = handler(request, pathinfo) 232 elif hasattr(handler, '__call__'): 233 response = _process_request(handler, request, pathinfo) 234 235 else: 236 # FIXME - perhaps we should raise an server error 237 # exception here - this shouldn't happen if the application 238 # has been set up correctly. 239 raise HTTPNotFound(request.path) 240 241 return False, request, WSGIResponse(start_response, response) 242 243 except ValidationError, e: 244 response = validation_error_handler(e) 245 return False, request, WSGIResponse(start_response, response) 246 except HTTPNotFound, e: 247 if e.path is None: 248 e.path = request.path 249 e.body += '\nPath \'%s\' not found' % e.path 250 return False, request, WSGIResponse(start_response, e) 251 except HTTPError, e: 252 return False, request, WSGIResponse(start_response, e) 253 except Exception, e: 254 # Handle uncaught exceptions by returning a 500 error. 255 self.logger.request_failed(environ, logstart, sys.exc_info()) 256 return True, request, WSGIResponse(start_response, HTTPServerError(str(e))) 257 258 if autodoc: 259 components = autodoc.split('/') 260 suburls = urls 261 for component in components[:-1]: 262 suburls = suburls.setdefault(component, {}) 263 from autodoc import make_doc 264 suburls[components[-1]] = make_doc(urls, autodoc) 265 266 return Application() 267
268 -def make_server(app, bind_addr, *args, **kwargs):
269 """Make a server for an application. 270 271 This uses CherryPy's standalone WSGI server. The first argument is the 272 WSGI application to run; all subsequent arguments are passed directly to 273 the server. The CherryPyWSGIServer is accessible as 274 wsgiwapi.cpwsgiserver: see the documentation in that module for calling 275 details. 276 277 Note that you will always need to set the bind_addr parameter; this is a 278 (host, port) tuple for TCP sockets, or a filename for UNIX sockets. The 279 host part may be set to '0.0.0.0' to listen on all active IPv4 interfaces 280 (or similarly, '::' to listen on all active IPv6 interfaces). 281 282 """ 283 # Lazy import, so we don't pull cherrypy in unless we're using it. 284 import cpwsgiserver 285 server = cpwsgiserver.CherryPyWSGIServer(bind_addr, app, *args, **kwargs) 286 return server
287
288 -def _process_request(handler, request, pathinfo):
289 """Process a request, with a given handler. 290 291 `unhandled_path_index` is the index in request.path_components of the first 292 component which wasn't used in looking up handler - ie, the start of the 293 pathinfo components. 294 295 """ 296 # Read the properties from the handler 297 handler_props = _get_props(handler) 298 request._set_handler_props(handler_props) 299 300 # Check that there are no tail components if the handler is not 301 # marked as accepting pathinfo. 302 if len(pathinfo) != 0: 303 # Raise a NotFound error unless the handler is marked as 304 # accepting pathinfo. 305 if handler_props is None or \ 306 handler_props.get('pathinfo_allow', None) is None: 307 raise HTTPNotFound(request.path) 308 309 # Set the path info 310 request._set_pathinfo(pathinfo) 311 312 # Apply the pre-checks to the request. 313 request = apply_request_checks_and_transforms(request, handler_props) 314 315 # Call the handler. 316 response = handler(request) 317 318 # Apply the post-checks to the response 319 response = apply_response_checks_and_transforms(request, response, handler_props) 320 assert response is not None 321 322 # Allow handlers to return strings - just wrap them in a 323 # default response object. 324 if isinstance(response, basestring): 325 response = Response(body=response) 326 assert isinstance(response, Response) 327 328 return response
329
330 -class MethodSwitch(object):
331 """Class for switching callables by request method. 332 333 In wsgiwapi.make_application, instances of this class can be substituted for 334 callables, e.g.: 335 336 make_application({'foo': MethodSwitch(foo_get, foo_post, default=foo_other)}) 337 338 If the default handler is supplied, it will be called for any request method not 339 explicitly specified. 340 341 """
342 - def __init__(self, get=None, post=None, put=None, delete=None, 343 head=None, options=None, trace=None, connect=None, 344 default=None):
345 self.methods = { 346 'GET': get, 'POST': post, 'PUT': put, 'DELETE': delete, 347 'HEAD': head, 'OPTIONS': options, 'TRACE': trace, 'CONNECT': connect 348 } 349 self.default = default
350
351 - def __call__(self, request, pathinfo):
352 handler = self.methods.get(request.method) 353 if handler is not None: 354 return _process_request(handler, request, pathinfo) 355 elif self.default is not None: 356 return _process_request(self.default, request, pathinfo) 357 else: 358 allowed_methods = [x[0] for x in self.methods.iteritems() if x[1]] 359 raise HTTPMethodNotAllowed(request.method, allowed_methods)
360 361 # vim: set fileencoding=utf-8 : 362