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, \
38 method_known
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
50 """Exception used to indicate that parameters failed validation.
51
52 """
55
56 @apply
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
68 return "ValidationError(\"%s\")" % self._message.\
69 replace('\\', '\\\\').\
70 replace('"', '\"')
71
90
104
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
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
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
186
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)
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
216
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
238
239
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
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
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
285 import cpwsgiserver
286 server = cpwsgiserver.CherryPyWSGIServer(bind_addr, app, *args, **kwargs)
287 return server
288
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
298 handler_props = _get_props(handler)
299 request._set_handler_props(handler_props)
300
301
302
303 if len(pathinfo) != 0:
304
305
306 if handler_props is None or \
307 handler_props.get('pathinfo_allow', None) is None:
308 raise HTTPNotFound(request.path)
309
310
311 request._set_pathinfo(pathinfo)
312
313
314 request = apply_request_checks_and_transforms(request, handler_props)
315
316
317 response = handler(request)
318
319
320 response = apply_response_checks_and_transforms(request, response, handler_props)
321 assert response is not None
322
323
324
325 if isinstance(response, basestring):
326 response = Response(body=response)
327 assert isinstance(response, Response)
328
329 return response
330
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
350
351 valid_methods = set(('get', 'post', 'put', 'delete', 'head',))
352
353 - def __init__(self, get=None, post=None, put=None, delete=None, head=None):
364
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
375