|
1 #!/usr/bin/env python |
|
2 # |
|
3 # Copyright 2007 Google Inc. |
|
4 # |
|
5 # Licensed under the Apache License, Version 2.0 (the "License"); |
|
6 # you may not use this file except in compliance with the License. |
|
7 # You may obtain a copy of the License at |
|
8 # |
|
9 # http://www.apache.org/licenses/LICENSE-2.0 |
|
10 # |
|
11 # Unless required by applicable law or agreed to in writing, software |
|
12 # distributed under the License is distributed on an "AS IS" BASIS, |
|
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
14 # See the License for the specific language governing permissions and |
|
15 # limitations under the License. |
|
16 # |
|
17 |
|
18 """An extremely simple WSGI web application framework. |
|
19 |
|
20 This module exports three primary classes: Request, Response, and |
|
21 RequestHandler. You implement a web application by subclassing RequestHandler. |
|
22 As WSGI requests come in, they are passed to instances of your RequestHandlers. |
|
23 The RequestHandler class provides access to the easy-to-use Request and |
|
24 Response objects so you can interpret the request and write the response with |
|
25 no knowledge of the esoteric WSGI semantics. Here is a simple example: |
|
26 |
|
27 from google.appengine.ext import webapp |
|
28 import wsgiref.simple_server |
|
29 |
|
30 class MainPage(webapp.RequestHandler): |
|
31 def get(self): |
|
32 self.response.out.write( |
|
33 '<html><body><form action="/hello" method="post">' |
|
34 'Name: <input name="name" type="text" size="20"> ' |
|
35 '<input type="submit" value="Say Hello"></form></body></html>') |
|
36 |
|
37 class HelloPage(webapp.RequestHandler): |
|
38 def post(self): |
|
39 self.response.headers['Content-Type'] = 'text/plain' |
|
40 self.response.out.write('Hello, %s' % self.request.get('name')) |
|
41 |
|
42 application = webapp.WSGIApplication([ |
|
43 ('/', MainPage), |
|
44 ('/hello', HelloPage) |
|
45 ], debug=True) |
|
46 |
|
47 server = wsgiref.simple_server.make_server('', 8080, application) |
|
48 print 'Serving on port 8080...' |
|
49 server.serve_forever() |
|
50 |
|
51 The WSGIApplication class maps URI regular expressions to your RequestHandler |
|
52 classes. It is a WSGI-compatible application object, so you can use it in |
|
53 conjunction with wsgiref to make your web application into, e.g., a CGI |
|
54 script or a simple HTTP server, as in the example above. |
|
55 |
|
56 The framework does not support streaming output. All output from a response |
|
57 is stored in memory before it is written. |
|
58 """ |
|
59 |
|
60 |
|
61 import cgi |
|
62 import StringIO |
|
63 import logging |
|
64 import re |
|
65 import sys |
|
66 import traceback |
|
67 import urlparse |
|
68 import webob |
|
69 import wsgiref.headers |
|
70 import wsgiref.util |
|
71 |
|
72 RE_FIND_GROUPS = re.compile('\(.*?\)') |
|
73 |
|
74 class Error(Exception): |
|
75 """Base of all exceptions in the webapp module.""" |
|
76 pass |
|
77 |
|
78 |
|
79 class NoUrlFoundError(Error): |
|
80 """Thrown when RequestHandler.get_url() fails.""" |
|
81 pass |
|
82 |
|
83 |
|
84 class Request(webob.Request): |
|
85 """Abstraction for an HTTP request. |
|
86 |
|
87 Properties: |
|
88 uri: the complete URI requested by the user |
|
89 scheme: 'http' or 'https' |
|
90 host: the host, including the port |
|
91 path: the path up to the ';' or '?' in the URL |
|
92 parameters: the part of the URL between the ';' and the '?', if any |
|
93 query: the part of the URL after the '?' |
|
94 |
|
95 You can access parsed query and POST values with the get() method; do not |
|
96 parse the query string yourself. |
|
97 """ |
|
98 uri = property(lambda self: self.url) |
|
99 query = property(lambda self: self.query_string) |
|
100 |
|
101 def __init__(self, environ): |
|
102 """Constructs a Request object from a WSGI environment. |
|
103 |
|
104 If the charset isn't specified in the Content-Type header, defaults |
|
105 to UTF-8. |
|
106 |
|
107 Args: |
|
108 environ: A WSGI-compliant environment dictionary. |
|
109 """ |
|
110 charset = webob.NoDefault |
|
111 if environ.get('CONTENT_TYPE', '').find('charset') == -1: |
|
112 charset = 'utf-8' |
|
113 |
|
114 webob.Request.__init__(self, environ, charset=charset, |
|
115 unicode_errors= 'ignore', decode_param_names=True) |
|
116 |
|
117 def get(self, argument_name, default_value='', allow_multiple=False): |
|
118 """Returns the query or POST argument with the given name. |
|
119 |
|
120 We parse the query string and POST payload lazily, so this will be a |
|
121 slower operation on the first call. |
|
122 |
|
123 Args: |
|
124 argument_name: the name of the query or POST argument |
|
125 default_value: the value to return if the given argument is not present |
|
126 allow_multiple: return a list of values with the given name (deprecated) |
|
127 |
|
128 Returns: |
|
129 If allow_multiple is False (which it is by default), we return the first |
|
130 value with the given name given in the request. If it is True, we always |
|
131 return an list. |
|
132 """ |
|
133 param_value = self.get_all(argument_name) |
|
134 if allow_multiple: |
|
135 return param_value |
|
136 else: |
|
137 if len(param_value) > 0: |
|
138 return param_value[0] |
|
139 else: |
|
140 return default_value |
|
141 |
|
142 def get_all(self, argument_name): |
|
143 """Returns a list of query or POST arguments with the given name. |
|
144 |
|
145 We parse the query string and POST payload lazily, so this will be a |
|
146 slower operation on the first call. |
|
147 |
|
148 Args: |
|
149 argument_name: the name of the query or POST argument |
|
150 |
|
151 Returns: |
|
152 A (possibly empty) list of values. |
|
153 """ |
|
154 if self.charset: |
|
155 argument_name = argument_name.encode(self.charset) |
|
156 |
|
157 try: |
|
158 param_value = self.params.getall(argument_name) |
|
159 except KeyError: |
|
160 return default_value |
|
161 |
|
162 for i in range(len(param_value)): |
|
163 if isinstance(param_value[i], cgi.FieldStorage): |
|
164 param_value[i] = param_value[i].value |
|
165 |
|
166 return param_value |
|
167 |
|
168 def arguments(self): |
|
169 """Returns a list of the arguments provided in the query and/or POST. |
|
170 |
|
171 The return value is a list of strings. |
|
172 """ |
|
173 return list(set(self.params.keys())) |
|
174 |
|
175 def get_range(self, name, min_value=None, max_value=None, default=0): |
|
176 """Parses the given int argument, limiting it to the given range. |
|
177 |
|
178 Args: |
|
179 name: the name of the argument |
|
180 min_value: the minimum int value of the argument (if any) |
|
181 max_value: the maximum int value of the argument (if any) |
|
182 default: the default value of the argument if it is not given |
|
183 |
|
184 Returns: |
|
185 An int within the given range for the argument |
|
186 """ |
|
187 try: |
|
188 value = int(self.get(name, default)) |
|
189 except ValueError: |
|
190 value = default |
|
191 if max_value != None: |
|
192 value = min(value, max_value) |
|
193 if min_value != None: |
|
194 value = max(value, min_value) |
|
195 return value |
|
196 |
|
197 |
|
198 class Response(object): |
|
199 """Abstraction for an HTTP response. |
|
200 |
|
201 Properties: |
|
202 out: file pointer for the output stream |
|
203 headers: wsgiref.headers.Headers instance representing the output headers |
|
204 """ |
|
205 def __init__(self): |
|
206 """Constructs a response with the default settings.""" |
|
207 self.out = StringIO.StringIO() |
|
208 self.__wsgi_headers = [] |
|
209 self.headers = wsgiref.headers.Headers(self.__wsgi_headers) |
|
210 self.headers['Content-Type'] = 'text/html; charset=utf-8' |
|
211 self.headers['Cache-Control'] = 'no-cache' |
|
212 self.set_status(200) |
|
213 |
|
214 def set_status(self, code, message=None): |
|
215 """Sets the HTTP status code of this response. |
|
216 |
|
217 Args: |
|
218 message: the HTTP status string to use |
|
219 |
|
220 If no status string is given, we use the default from the HTTP/1.1 |
|
221 specification. |
|
222 """ |
|
223 if not message: |
|
224 message = Response.http_status_message(code) |
|
225 self.__status = (code, message) |
|
226 |
|
227 def clear(self): |
|
228 """Clears all data written to the output stream so that it is empty.""" |
|
229 self.out.seek(0) |
|
230 self.out.truncate(0) |
|
231 |
|
232 def wsgi_write(self, start_response): |
|
233 """Writes this response using WSGI semantics with the given WSGI function. |
|
234 |
|
235 Args: |
|
236 start_response: the WSGI-compatible start_response function |
|
237 """ |
|
238 body = self.out.getvalue() |
|
239 if isinstance(body, unicode): |
|
240 body = body.encode('utf-8') |
|
241 elif self.headers.get('Content-Type', '').endswith('; charset=utf-8'): |
|
242 try: |
|
243 body.decode('utf-8') |
|
244 except UnicodeError, e: |
|
245 logging.warning('Response written is not UTF-8: %s', e) |
|
246 |
|
247 self.headers['Content-Length'] = str(len(body)) |
|
248 write = start_response('%d %s' % self.__status, self.__wsgi_headers) |
|
249 write(body) |
|
250 self.out.close() |
|
251 |
|
252 def http_status_message(code): |
|
253 """Returns the default HTTP status message for the given code. |
|
254 |
|
255 Args: |
|
256 code: the HTTP code for which we want a message |
|
257 """ |
|
258 if not Response.__HTTP_STATUS_MESSAGES.has_key(code): |
|
259 raise Error('Invalid HTTP status code: %d' % code) |
|
260 return Response.__HTTP_STATUS_MESSAGES[code] |
|
261 http_status_message = staticmethod(http_status_message) |
|
262 |
|
263 __HTTP_STATUS_MESSAGES = { |
|
264 100: 'Continue', |
|
265 101: 'Switching Protocols', |
|
266 200: 'OK', |
|
267 201: 'Created', |
|
268 202: 'Accepted', |
|
269 203: 'Non-Authoritative Information', |
|
270 204: 'No Content', |
|
271 205: 'Reset Content', |
|
272 206: 'Partial Content', |
|
273 300: 'Multiple Choices', |
|
274 301: 'Moved Permanently', |
|
275 302: 'Moved Temporarily', |
|
276 303: 'See Other', |
|
277 304: 'Not Modified', |
|
278 305: 'Use Proxy', |
|
279 306: 'Unused', |
|
280 307: 'Temporary Redirect', |
|
281 400: 'Bad Request', |
|
282 401: 'Unauthorized', |
|
283 402: 'Payment Required', |
|
284 403: 'Forbidden', |
|
285 404: 'Not Found', |
|
286 405: 'Method Not Allowed', |
|
287 406: 'Not Acceptable', |
|
288 407: 'Proxy Authentication Required', |
|
289 408: 'Request Time-out', |
|
290 409: 'Conflict', |
|
291 410: 'Gone', |
|
292 411: 'Length Required', |
|
293 412: 'Precondition Failed', |
|
294 413: 'Request Entity Too Large', |
|
295 414: 'Request-URI Too Large', |
|
296 415: 'Unsupported Media Type', |
|
297 416: 'Requested Range Not Satisfiable', |
|
298 417: 'Expectation Failed', |
|
299 500: 'Internal Server Error', |
|
300 501: 'Not Implemented', |
|
301 502: 'Bad Gateway', |
|
302 503: 'Service Unavailable', |
|
303 504: 'Gateway Time-out', |
|
304 505: 'HTTP Version not supported' |
|
305 } |
|
306 |
|
307 |
|
308 class RequestHandler(object): |
|
309 """Our base HTTP request handler. Clients should subclass this class. |
|
310 |
|
311 Subclasses should override get(), post(), head(), options(), etc to handle |
|
312 different HTTP methods. |
|
313 """ |
|
314 def initialize(self, request, response): |
|
315 """Initializes this request handler with the given Request and Response.""" |
|
316 self.request = request |
|
317 self.response = response |
|
318 |
|
319 def get(self, *args): |
|
320 """Handler method for GET requests.""" |
|
321 self.error(405) |
|
322 |
|
323 def post(self, *args): |
|
324 """Handler method for POST requests.""" |
|
325 self.error(405) |
|
326 |
|
327 def head(self, *args): |
|
328 """Handler method for HEAD requests.""" |
|
329 self.error(405) |
|
330 |
|
331 def options(self, *args): |
|
332 """Handler method for OPTIONS requests.""" |
|
333 self.error(405) |
|
334 |
|
335 def put(self, *args): |
|
336 """Handler method for PUT requests.""" |
|
337 self.error(405) |
|
338 |
|
339 def delete(self, *args): |
|
340 """Handler method for DELETE requests.""" |
|
341 self.error(405) |
|
342 |
|
343 def trace(self, *args): |
|
344 """Handler method for TRACE requests.""" |
|
345 self.error(405) |
|
346 |
|
347 def error(self, code): |
|
348 """Clears the response output stream and sets the given HTTP error code. |
|
349 |
|
350 Args: |
|
351 code: the HTTP status error code (e.g., 501) |
|
352 """ |
|
353 self.response.set_status(code) |
|
354 self.response.clear() |
|
355 |
|
356 def redirect(self, uri, permanent=False): |
|
357 """Issues an HTTP redirect to the given relative URL. |
|
358 |
|
359 Args: |
|
360 uri: a relative or absolute URI (e.g., '../flowers.html') |
|
361 permanent: if true, we use a 301 redirect instead of a 302 redirect |
|
362 """ |
|
363 if permanent: |
|
364 self.response.set_status(301) |
|
365 else: |
|
366 self.response.set_status(302) |
|
367 absolute_url = urlparse.urljoin(self.request.uri, uri) |
|
368 self.response.headers['Location'] = str(absolute_url) |
|
369 self.response.clear() |
|
370 |
|
371 def handle_exception(self, exception, debug_mode): |
|
372 """Called if this handler throws an exception during execution. |
|
373 |
|
374 The default behavior is to call self.error(500) and print a stack trace |
|
375 if debug_mode is True. |
|
376 |
|
377 Args: |
|
378 exception: the exception that was thrown |
|
379 debug_mode: True if the web application is running in debug mode |
|
380 """ |
|
381 self.error(500) |
|
382 lines = ''.join(traceback.format_exception(*sys.exc_info())) |
|
383 logging.error(lines) |
|
384 if debug_mode: |
|
385 self.response.clear() |
|
386 self.response.headers['Content-Type'] = 'text/plain' |
|
387 self.response.out.write(lines) |
|
388 |
|
389 @classmethod |
|
390 def get_url(cls, *args, **kargs): |
|
391 """Returns the url for the given handler. |
|
392 |
|
393 The default implementation uses the patterns passed to the active |
|
394 WSGIApplication and the django urlresolvers module to create a url. |
|
395 However, it is different from urlresolvers.reverse() in the following ways: |
|
396 - It does not try to resolve handlers via module loading |
|
397 - It does not support named arguments |
|
398 - It performs some post-prosessing on the url to remove some regex |
|
399 operators that urlresolvers.reverse_helper() seems to miss. |
|
400 - It will try to fill in the left-most missing arguments with the args |
|
401 used in the active request. |
|
402 |
|
403 Args: |
|
404 args: Parameters for the url pattern's groups. |
|
405 kwargs: Optionally contains 'implicit_args' that can either be a boolean |
|
406 or a tuple. When it is True, it will use the arguments to the |
|
407 active request as implicit arguments. When it is False (default), |
|
408 it will not use any implicit arguments. When it is a tuple, it |
|
409 will use the tuple as the implicit arguments. |
|
410 the left-most args if some are missing from args. |
|
411 |
|
412 Returns: |
|
413 The url for this handler/args combination. |
|
414 |
|
415 Raises: |
|
416 NoUrlFoundError: No url pattern for this handler has the same |
|
417 number of args that were passed in. |
|
418 """ |
|
419 |
|
420 app = WSGIApplication.active_instance |
|
421 pattern_map = app._pattern_map |
|
422 |
|
423 implicit_args = kargs.get('implicit_args', ()) |
|
424 if implicit_args == True: |
|
425 implicit_args = app.current_request_args |
|
426 |
|
427 min_params = len(args) |
|
428 |
|
429 urlresolvers = None |
|
430 |
|
431 for pattern_tuple in pattern_map.get(cls, ()): |
|
432 num_params_in_pattern = pattern_tuple[1] |
|
433 if num_params_in_pattern < min_params: |
|
434 continue |
|
435 |
|
436 if urlresolvers is None: |
|
437 from django.core import urlresolvers |
|
438 |
|
439 try: |
|
440 num_implicit_args = max(0, num_params_in_pattern - len(args)) |
|
441 merged_args = implicit_args[:num_implicit_args] + args |
|
442 url = urlresolvers.reverse_helper(pattern_tuple[0], *merged_args) |
|
443 url = url.replace('\\', '') |
|
444 url = url.replace('?', '') |
|
445 return url |
|
446 except urlresolvers.NoReverseMatch: |
|
447 continue |
|
448 |
|
449 logging.warning('get_url failed for Handler name: %r, Args: %r', |
|
450 cls.__name__, args) |
|
451 raise NoUrlFoundError |
|
452 |
|
453 |
|
454 class WSGIApplication(object): |
|
455 """Wraps a set of webapp RequestHandlers in a WSGI-compatible application. |
|
456 |
|
457 To use this class, pass a list of (URI regular expression, RequestHandler) |
|
458 pairs to the constructor, and pass the class instance to a WSGI handler. |
|
459 See the example in the module comments for details. |
|
460 |
|
461 The URL mapping is first-match based on the list ordering. |
|
462 """ |
|
463 |
|
464 def __init__(self, url_mapping, debug=False): |
|
465 """Initializes this application with the given URL mapping. |
|
466 |
|
467 Args: |
|
468 url_mapping: list of (URI, RequestHandler) pairs (e.g., [('/', ReqHan)]) |
|
469 debug: if true, we send Python stack traces to the browser on errors |
|
470 """ |
|
471 self._init_url_mappings(url_mapping) |
|
472 self.__debug = debug |
|
473 WSGIApplication.active_instance = self |
|
474 self.current_request_args = () |
|
475 |
|
476 def __call__(self, environ, start_response): |
|
477 """Called by WSGI when a request comes in.""" |
|
478 request = Request(environ) |
|
479 response = Response() |
|
480 |
|
481 WSGIApplication.active_instance = self |
|
482 |
|
483 handler = None |
|
484 groups = () |
|
485 for regexp, handler_class in self._url_mapping: |
|
486 match = regexp.match(request.path) |
|
487 if match: |
|
488 handler = handler_class() |
|
489 handler.initialize(request, response) |
|
490 groups = match.groups() |
|
491 break |
|
492 |
|
493 self.current_request_args = groups |
|
494 |
|
495 if handler: |
|
496 try: |
|
497 method = environ['REQUEST_METHOD'] |
|
498 if method == 'GET': |
|
499 handler.get(*groups) |
|
500 elif method == 'POST': |
|
501 handler.post(*groups) |
|
502 elif method == 'HEAD': |
|
503 handler.head(*groups) |
|
504 elif method == 'OPTIONS': |
|
505 handler.options(*groups) |
|
506 elif method == 'PUT': |
|
507 handler.put(*groups) |
|
508 elif method == 'DELETE': |
|
509 handler.delete(*groups) |
|
510 elif method == 'TRACE': |
|
511 handler.trace(*groups) |
|
512 else: |
|
513 handler.error(501) |
|
514 except Exception, e: |
|
515 handler.handle_exception(e, self.__debug) |
|
516 else: |
|
517 response.set_status(404) |
|
518 |
|
519 response.wsgi_write(start_response) |
|
520 return [''] |
|
521 |
|
522 def _init_url_mappings(self, handler_tuples): |
|
523 """Initializes the maps needed for mapping urls to handlers and handlers |
|
524 to urls. |
|
525 |
|
526 Args: |
|
527 handler_tuples: list of (URI, RequestHandler) pairs. |
|
528 """ |
|
529 |
|
530 handler_map = {} |
|
531 pattern_map = {} |
|
532 url_mapping = [] |
|
533 |
|
534 for regexp, handler in handler_tuples: |
|
535 |
|
536 handler_map[handler.__name__] = handler |
|
537 |
|
538 if not regexp.startswith('^'): |
|
539 regexp = '^' + regexp |
|
540 if not regexp.endswith('$'): |
|
541 regexp += '$' |
|
542 |
|
543 compiled = re.compile(regexp) |
|
544 url_mapping.append((compiled, handler)) |
|
545 |
|
546 num_groups = len(RE_FIND_GROUPS.findall(regexp)) |
|
547 handler_patterns = pattern_map.setdefault(handler, []) |
|
548 handler_patterns.append((compiled, num_groups)) |
|
549 |
|
550 self._handler_map = handler_map |
|
551 self._pattern_map = pattern_map |
|
552 self._url_mapping = url_mapping |
|
553 |
|
554 def get_registered_handler_by_name(self, handler_name): |
|
555 """Returns the handler given the handler's name. |
|
556 |
|
557 This uses the application's url mapping. |
|
558 |
|
559 Args: |
|
560 handler_name: The __name__ of a handler to return. |
|
561 |
|
562 Returns: |
|
563 The handler with the given name. |
|
564 |
|
565 Raises: |
|
566 KeyError: If the handler name is not found in the parent application. |
|
567 """ |
|
568 try: |
|
569 return self._handler_map[handler_name] |
|
570 except: |
|
571 logging.error('Handler does not map to any urls: %s', handler_name) |
|
572 raise |