thirdparty/google_appengine/google/appengine/ext/webapp/__init__.py
changeset 109 620f9b141567
child 149 f2e327a7c5de
equal deleted inserted replaced
108:261778de26ff 109:620f9b141567
       
     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