Add google appengine to repo
[frozenviper.git] / google_appengine / google / appengine / ext / webapp / __init__.py
blobdf7498fc1119f041df64155b5f2cbf324b70307b
1 #!/usr/bin/env python
3 # Copyright 2007 Google Inc.
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
18 """An extremely simple WSGI web application framework.
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:
27 from google.appengine.ext import webapp
28 import wsgiref.simple_server
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>')
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'))
42 application = webapp.WSGIApplication([
43 ('/', MainPage),
44 ('/hello', HelloPage)
45 ], debug=True)
47 server = wsgiref.simple_server.make_server('', 8080, application)
48 print 'Serving on port 8080...'
49 server.serve_forever()
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.
56 The framework does not support streaming output. All output from a response
57 is stored in memory before it is written.
58 """
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.handlers
70 import wsgiref.headers
71 import wsgiref.util
73 wsgiref.handlers.BaseHandler.os_environ = {}
75 RE_FIND_GROUPS = re.compile('\(.*?\)')
76 _CHARSET_RE = re.compile(r';\s*charset=([^;\s]*)', re.I)
78 class Error(Exception):
79 """Base of all exceptions in the webapp module."""
80 pass
83 class NoUrlFoundError(Error):
84 """Thrown when RequestHandler.get_url() fails."""
85 pass
88 class Request(webob.Request):
89 """Abstraction for an HTTP request.
91 Properties:
92 uri: the complete URI requested by the user
93 scheme: 'http' or 'https'
94 host: the host, including the port
95 path: the path up to the ';' or '?' in the URL
96 parameters: the part of the URL between the ';' and the '?', if any
97 query: the part of the URL after the '?'
99 You can access parsed query and POST values with the get() method; do not
100 parse the query string yourself.
103 request_body_tempfile_limit = 0
105 uri = property(lambda self: self.url)
106 query = property(lambda self: self.query_string)
108 def __init__(self, environ):
109 """Constructs a Request object from a WSGI environment.
111 If the charset isn't specified in the Content-Type header, defaults
112 to UTF-8.
114 Args:
115 environ: A WSGI-compliant environment dictionary.
117 match = _CHARSET_RE.search(environ.get('CONTENT_TYPE', ''))
118 if match:
119 charset = match.group(1).lower()
120 else:
121 charset = 'utf-8'
123 webob.Request.__init__(self, environ, charset=charset,
124 unicode_errors= 'ignore', decode_param_names=True)
126 def get(self, argument_name, default_value='', allow_multiple=False):
127 """Returns the query or POST argument with the given name.
129 We parse the query string and POST payload lazily, so this will be a
130 slower operation on the first call.
132 Args:
133 argument_name: the name of the query or POST argument
134 default_value: the value to return if the given argument is not present
135 allow_multiple: return a list of values with the given name (deprecated)
137 Returns:
138 If allow_multiple is False (which it is by default), we return the first
139 value with the given name given in the request. If it is True, we always
140 return an list.
142 param_value = self.get_all(argument_name)
143 if allow_multiple:
144 return param_value
145 else:
146 if len(param_value) > 0:
147 return param_value[0]
148 else:
149 return default_value
151 def get_all(self, argument_name):
152 """Returns a list of query or POST arguments with the given name.
154 We parse the query string and POST payload lazily, so this will be a
155 slower operation on the first call.
157 Args:
158 argument_name: the name of the query or POST argument
160 Returns:
161 A (possibly empty) list of values.
163 if self.charset:
164 argument_name = argument_name.encode(self.charset)
166 param_value = self.params.getall(argument_name)
168 for i in xrange(len(param_value)):
169 if isinstance(param_value[i], cgi.FieldStorage):
170 param_value[i] = param_value[i].value
172 return param_value
174 def arguments(self):
175 """Returns a list of the arguments provided in the query and/or POST.
177 The return value is a list of strings.
179 return list(set(self.params.keys()))
181 def get_range(self, name, min_value=None, max_value=None, default=0):
182 """Parses the given int argument, limiting it to the given range.
184 Args:
185 name: the name of the argument
186 min_value: the minimum int value of the argument (if any)
187 max_value: the maximum int value of the argument (if any)
188 default: the default value of the argument if it is not given
190 Returns:
191 An int within the given range for the argument
193 try:
194 value = int(self.get(name, default))
195 except ValueError:
196 value = default
197 if max_value != None:
198 value = min(value, max_value)
199 if min_value != None:
200 value = max(value, min_value)
201 return value
204 class Response(object):
205 """Abstraction for an HTTP response.
207 Properties:
208 out: file pointer for the output stream
209 headers: wsgiref.headers.Headers instance representing the output headers
211 def __init__(self):
212 """Constructs a response with the default settings."""
213 self.out = StringIO.StringIO()
214 self.__wsgi_headers = []
215 self.headers = wsgiref.headers.Headers(self.__wsgi_headers)
216 self.headers['Content-Type'] = 'text/html; charset=utf-8'
217 self.headers['Cache-Control'] = 'no-cache'
218 self.set_status(200)
220 def set_status(self, code, message=None):
221 """Sets the HTTP status code of this response.
223 Args:
224 message: the HTTP status string to use
226 If no status string is given, we use the default from the HTTP/1.1
227 specification.
229 if not message:
230 message = Response.http_status_message(code)
231 self.__status = (code, message)
233 def clear(self):
234 """Clears all data written to the output stream so that it is empty."""
235 self.out.seek(0)
236 self.out.truncate(0)
238 def wsgi_write(self, start_response):
239 """Writes this response using WSGI semantics with the given WSGI function.
241 Args:
242 start_response: the WSGI-compatible start_response function
244 body = self.out.getvalue()
245 if isinstance(body, unicode):
246 body = body.encode('utf-8')
247 elif self.headers.get('Content-Type', '').endswith('; charset=utf-8'):
248 try:
249 body.decode('utf-8')
250 except UnicodeError, e:
251 logging.warning('Response written is not UTF-8: %s', e)
253 if (self.headers.get('Cache-Control') == 'no-cache' and
254 not self.headers.get('Expires')):
255 self.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
256 self.headers['Content-Length'] = str(len(body))
257 write = start_response('%d %s' % self.__status, self.__wsgi_headers)
258 write(body)
259 self.out.close()
261 def http_status_message(code):
262 """Returns the default HTTP status message for the given code.
264 Args:
265 code: the HTTP code for which we want a message
267 if not Response.__HTTP_STATUS_MESSAGES.has_key(code):
268 raise Error('Invalid HTTP status code: %d' % code)
269 return Response.__HTTP_STATUS_MESSAGES[code]
270 http_status_message = staticmethod(http_status_message)
272 __HTTP_STATUS_MESSAGES = {
273 100: 'Continue',
274 101: 'Switching Protocols',
275 200: 'OK',
276 201: 'Created',
277 202: 'Accepted',
278 203: 'Non-Authoritative Information',
279 204: 'No Content',
280 205: 'Reset Content',
281 206: 'Partial Content',
282 300: 'Multiple Choices',
283 301: 'Moved Permanently',
284 302: 'Moved Temporarily',
285 303: 'See Other',
286 304: 'Not Modified',
287 305: 'Use Proxy',
288 306: 'Unused',
289 307: 'Temporary Redirect',
290 400: 'Bad Request',
291 401: 'Unauthorized',
292 402: 'Payment Required',
293 403: 'Forbidden',
294 404: 'Not Found',
295 405: 'Method Not Allowed',
296 406: 'Not Acceptable',
297 407: 'Proxy Authentication Required',
298 408: 'Request Time-out',
299 409: 'Conflict',
300 410: 'Gone',
301 411: 'Length Required',
302 412: 'Precondition Failed',
303 413: 'Request Entity Too Large',
304 414: 'Request-URI Too Large',
305 415: 'Unsupported Media Type',
306 416: 'Requested Range Not Satisfiable',
307 417: 'Expectation Failed',
308 500: 'Internal Server Error',
309 501: 'Not Implemented',
310 502: 'Bad Gateway',
311 503: 'Service Unavailable',
312 504: 'Gateway Time-out',
313 505: 'HTTP Version not supported'
317 class RequestHandler(object):
318 """Our base HTTP request handler. Clients should subclass this class.
320 Subclasses should override get(), post(), head(), options(), etc to handle
321 different HTTP methods.
323 def initialize(self, request, response):
324 """Initializes this request handler with the given Request and Response."""
325 self.request = request
326 self.response = response
328 def get(self, *args):
329 """Handler method for GET requests."""
330 self.error(405)
332 def post(self, *args):
333 """Handler method for POST requests."""
334 self.error(405)
336 def head(self, *args):
337 """Handler method for HEAD requests."""
338 self.error(405)
340 def options(self, *args):
341 """Handler method for OPTIONS requests."""
342 self.error(405)
344 def put(self, *args):
345 """Handler method for PUT requests."""
346 self.error(405)
348 def delete(self, *args):
349 """Handler method for DELETE requests."""
350 self.error(405)
352 def trace(self, *args):
353 """Handler method for TRACE requests."""
354 self.error(405)
356 def error(self, code):
357 """Clears the response output stream and sets the given HTTP error code.
359 Args:
360 code: the HTTP status error code (e.g., 501)
362 self.response.set_status(code)
363 self.response.clear()
365 def redirect(self, uri, permanent=False):
366 """Issues an HTTP redirect to the given relative URL.
368 Args:
369 uri: a relative or absolute URI (e.g., '../flowers.html')
370 permanent: if true, we use a 301 redirect instead of a 302 redirect
372 if permanent:
373 self.response.set_status(301)
374 else:
375 self.response.set_status(302)
376 absolute_url = urlparse.urljoin(self.request.uri, uri)
377 self.response.headers['Location'] = str(absolute_url)
378 self.response.clear()
380 def handle_exception(self, exception, debug_mode):
381 """Called if this handler throws an exception during execution.
383 The default behavior is to call self.error(500) and print a stack trace
384 if debug_mode is True.
386 Args:
387 exception: the exception that was thrown
388 debug_mode: True if the web application is running in debug mode
390 self.error(500)
391 logging.exception(exception)
392 if debug_mode:
393 lines = ''.join(traceback.format_exception(*sys.exc_info()))
394 self.response.clear()
395 self.response.out.write('<pre>%s</pre>' % (cgi.escape(lines, quote=True)))
397 @classmethod
398 def get_url(cls, *args, **kargs):
399 """Returns the url for the given handler.
401 The default implementation uses the patterns passed to the active
402 WSGIApplication and the django urlresolvers module to create a url.
403 However, it is different from urlresolvers.reverse() in the following ways:
404 - It does not try to resolve handlers via module loading
405 - It does not support named arguments
406 - It performs some post-prosessing on the url to remove some regex
407 operators that urlresolvers.reverse_helper() seems to miss.
408 - It will try to fill in the left-most missing arguments with the args
409 used in the active request.
411 Args:
412 args: Parameters for the url pattern's groups.
413 kwargs: Optionally contains 'implicit_args' that can either be a boolean
414 or a tuple. When it is True, it will use the arguments to the
415 active request as implicit arguments. When it is False (default),
416 it will not use any implicit arguments. When it is a tuple, it
417 will use the tuple as the implicit arguments.
418 the left-most args if some are missing from args.
420 Returns:
421 The url for this handler/args combination.
423 Raises:
424 NoUrlFoundError: No url pattern for this handler has the same
425 number of args that were passed in.
428 app = WSGIApplication.active_instance
429 pattern_map = app._pattern_map
431 implicit_args = kargs.get('implicit_args', ())
432 if implicit_args == True:
433 implicit_args = app.current_request_args
435 min_params = len(args)
437 urlresolvers = None
439 for pattern_tuple in pattern_map.get(cls, ()):
440 num_params_in_pattern = pattern_tuple[1]
441 if num_params_in_pattern < min_params:
442 continue
444 if urlresolvers is None:
445 from django.core import urlresolvers
447 try:
448 num_implicit_args = max(0, num_params_in_pattern - len(args))
449 merged_args = implicit_args[:num_implicit_args] + args
450 url = urlresolvers.reverse_helper(pattern_tuple[0], *merged_args)
451 url = url.replace('\\', '')
452 url = url.replace('?', '')
453 return url
454 except urlresolvers.NoReverseMatch:
455 continue
457 logging.warning('get_url failed for Handler name: %r, Args: %r',
458 cls.__name__, args)
459 raise NoUrlFoundError
462 class WSGIApplication(object):
463 """Wraps a set of webapp RequestHandlers in a WSGI-compatible application.
465 To use this class, pass a list of (URI regular expression, RequestHandler)
466 pairs to the constructor, and pass the class instance to a WSGI handler.
467 See the example in the module comments for details.
469 The URL mapping is first-match based on the list ordering.
472 REQUEST_CLASS = Request
473 RESPONSE_CLASS = Response
475 def __init__(self, url_mapping, debug=False):
476 """Initializes this application with the given URL mapping.
478 Args:
479 url_mapping: list of (URI regular expression, RequestHandler) pairs
480 (e.g., [('/', ReqHan)])
481 debug: if true, we send Python stack traces to the browser on errors
483 self._init_url_mappings(url_mapping)
484 self.__debug = debug
485 WSGIApplication.active_instance = self
486 self.current_request_args = ()
488 def __call__(self, environ, start_response):
489 """Called by WSGI when a request comes in."""
490 request = self.REQUEST_CLASS(environ)
491 response = self.RESPONSE_CLASS()
493 WSGIApplication.active_instance = self
495 handler = None
496 groups = ()
497 for regexp, handler_class in self._url_mapping:
498 match = regexp.match(request.path)
499 if match:
500 handler = handler_class()
501 handler.initialize(request, response)
502 groups = match.groups()
503 break
505 self.current_request_args = groups
507 if handler:
508 try:
509 method = environ['REQUEST_METHOD']
510 if method == 'GET':
511 handler.get(*groups)
512 elif method == 'POST':
513 handler.post(*groups)
514 elif method == 'HEAD':
515 handler.head(*groups)
516 elif method == 'OPTIONS':
517 handler.options(*groups)
518 elif method == 'PUT':
519 handler.put(*groups)
520 elif method == 'DELETE':
521 handler.delete(*groups)
522 elif method == 'TRACE':
523 handler.trace(*groups)
524 else:
525 handler.error(501)
526 except Exception, e:
527 handler.handle_exception(e, self.__debug)
528 else:
529 response.set_status(404)
531 response.wsgi_write(start_response)
532 return ['']
534 def _init_url_mappings(self, handler_tuples):
535 """Initializes the maps needed for mapping urls to handlers and handlers
536 to urls.
538 Args:
539 handler_tuples: list of (URI, RequestHandler) pairs.
542 handler_map = {}
543 pattern_map = {}
544 url_mapping = []
546 for regexp, handler in handler_tuples:
548 try:
549 handler_name = handler.__name__
550 except AttributeError:
551 pass
552 else:
553 handler_map[handler_name] = handler
555 if not regexp.startswith('^'):
556 regexp = '^' + regexp
557 if not regexp.endswith('$'):
558 regexp += '$'
560 if regexp == '^/form$':
561 logging.warning('The URL "/form" is reserved and will not be matched.')
563 compiled = re.compile(regexp)
564 url_mapping.append((compiled, handler))
566 num_groups = len(RE_FIND_GROUPS.findall(regexp))
567 handler_patterns = pattern_map.setdefault(handler, [])
568 handler_patterns.append((compiled, num_groups))
570 self._handler_map = handler_map
571 self._pattern_map = pattern_map
572 self._url_mapping = url_mapping
574 def get_registered_handler_by_name(self, handler_name):
575 """Returns the handler given the handler's name.
577 This uses the application's url mapping.
579 Args:
580 handler_name: The __name__ of a handler to return.
582 Returns:
583 The handler with the given name.
585 Raises:
586 KeyError: If the handler name is not found in the parent application.
588 try:
589 return self._handler_map[handler_name]
590 except:
591 logging.error('Handler does not map to any urls: %s', handler_name)
592 raise