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