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 """Exception used to indicate that parameters failed validation.
41
42 """
45
46 @apply
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
58 return "ValidationError(\"%s\")" % self._message.\
59 replace('\\', '\\\\').\
60 replace('"', '\"')
61
80
94
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
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
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
176
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)
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
206
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
228
229
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
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
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
270 import cpwsgiserver
271 server = cpwsgiserver.CherryPyWSGIServer(bind_addr, app, *args, **kwargs)
272 return server
273
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
283 handler_props = _get_props(handler)
284 request._set_handler_props(handler_props)
285
286
287
288 if len(pathinfo) != 0:
289
290
291 if handler_props is None or \
292 handler_props.get('pathinfo_allow', None) is None:
293 raise HTTPNotFound(request.path)
294
295
296 request._set_pathinfo(pathinfo)
297
298
299 request = apply_request_checks_and_transforms(request, handler_props)
300
301
302 response = handler(request)
303
304
305 response = apply_response_checks_and_transforms(request, response, handler_props)
306 assert response is not None
307
308
309
310 if isinstance(response, basestring):
311 response = Response(body=response)
312 assert isinstance(response, Response)
313
314 return response
315
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
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
348