1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
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
49 """Exception used to indicate that parameters failed validation.
50
51 """
54
55 @apply
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
67 return "ValidationError(\"%s\")" % self._message.\
68 replace('\\', '\\\\').\
69 replace('"', '\"')
70
89
103
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
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
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
185
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)
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
215
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
237
238
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
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
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
284 import cpwsgiserver
285 server = cpwsgiserver.CherryPyWSGIServer(bind_addr, app, *args, **kwargs)
286 return server
287
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
297 handler_props = _get_props(handler)
298 request._set_handler_props(handler_props)
299
300
301
302 if len(pathinfo) != 0:
303
304
305 if handler_props is None or \
306 handler_props.get('pathinfo_allow', None) is None:
307 raise HTTPNotFound(request.path)
308
309
310 request._set_pathinfo(pathinfo)
311
312
313 request = apply_request_checks_and_transforms(request, handler_props)
314
315
316 response = handler(request)
317
318
319 response = apply_response_checks_and_transforms(request, response, handler_props)
320 assert response is not None
321
322
323
324 if isinstance(response, basestring):
325 response = Response(body=response)
326 assert isinstance(response, Response)
327
328 return response
329
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
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
362