thirdparty/google_appengine/google/appengine/ext/webapp/__init__.py
changeset 109 620f9b141567
child 149 f2e327a7c5de
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thirdparty/google_appengine/google/appengine/ext/webapp/__init__.py	Tue Aug 26 21:49:54 2008 +0000
@@ -0,0 +1,572 @@
+#!/usr/bin/env python
+#
+# Copyright 2007 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""An extremely simple WSGI web application framework.
+
+This module exports three primary classes: Request, Response, and
+RequestHandler. You implement a web application by subclassing RequestHandler.
+As WSGI requests come in, they are passed to instances of your RequestHandlers.
+The RequestHandler class provides access to the easy-to-use Request and
+Response objects so you can interpret the request and write the response with
+no knowledge of the esoteric WSGI semantics.  Here is a simple example:
+
+  from google.appengine.ext import webapp
+  import wsgiref.simple_server
+
+  class MainPage(webapp.RequestHandler):
+    def get(self):
+      self.response.out.write(
+        '<html><body><form action="/hello" method="post">'
+        'Name: <input name="name" type="text" size="20"> '
+        '<input type="submit" value="Say Hello"></form></body></html>')
+
+  class HelloPage(webapp.RequestHandler):
+    def post(self):
+      self.response.headers['Content-Type'] = 'text/plain'
+      self.response.out.write('Hello, %s' % self.request.get('name'))
+
+  application = webapp.WSGIApplication([
+    ('/', MainPage),
+    ('/hello', HelloPage)
+  ], debug=True)
+
+  server = wsgiref.simple_server.make_server('', 8080, application)
+  print 'Serving on port 8080...'
+  server.serve_forever()
+
+The WSGIApplication class maps URI regular expressions to your RequestHandler
+classes.  It is a WSGI-compatible application object, so you can use it in
+conjunction with wsgiref to make your web application into, e.g., a CGI
+script or a simple HTTP server, as in the example above.
+
+The framework does not support streaming output. All output from a response
+is stored in memory before it is written.
+"""
+
+
+import cgi
+import StringIO
+import logging
+import re
+import sys
+import traceback
+import urlparse
+import webob
+import wsgiref.headers
+import wsgiref.util
+
+RE_FIND_GROUPS = re.compile('\(.*?\)')
+
+class Error(Exception):
+  """Base of all exceptions in the webapp module."""
+  pass
+
+
+class NoUrlFoundError(Error):
+  """Thrown when RequestHandler.get_url() fails."""
+  pass
+
+
+class Request(webob.Request):
+  """Abstraction for an HTTP request.
+
+  Properties:
+    uri: the complete URI requested by the user
+    scheme: 'http' or 'https'
+    host: the host, including the port
+    path: the path up to the ';' or '?' in the URL
+    parameters: the part of the URL between the ';' and the '?', if any
+    query: the part of the URL after the '?'
+
+  You can access parsed query and POST values with the get() method; do not
+  parse the query string yourself.
+  """
+  uri = property(lambda self: self.url)
+  query = property(lambda self: self.query_string)
+
+  def __init__(self, environ):
+    """Constructs a Request object from a WSGI environment.
+
+    If the charset isn't specified in the Content-Type header, defaults
+    to UTF-8.
+
+    Args:
+      environ: A WSGI-compliant environment dictionary.
+    """
+    charset = webob.NoDefault
+    if environ.get('CONTENT_TYPE', '').find('charset') == -1:
+      charset = 'utf-8'
+
+    webob.Request.__init__(self, environ, charset=charset,
+                           unicode_errors= 'ignore', decode_param_names=True)
+
+  def get(self, argument_name, default_value='', allow_multiple=False):
+    """Returns the query or POST argument with the given name.
+
+    We parse the query string and POST payload lazily, so this will be a
+    slower operation on the first call.
+
+    Args:
+      argument_name: the name of the query or POST argument
+      default_value: the value to return if the given argument is not present
+      allow_multiple: return a list of values with the given name (deprecated)
+
+    Returns:
+      If allow_multiple is False (which it is by default), we return the first
+      value with the given name given in the request. If it is True, we always
+      return an list.
+    """
+    param_value = self.get_all(argument_name)
+    if allow_multiple:
+      return param_value
+    else:
+      if len(param_value) > 0:
+        return param_value[0]
+      else:
+        return default_value
+
+  def get_all(self, argument_name):
+    """Returns a list of query or POST arguments with the given name.
+
+    We parse the query string and POST payload lazily, so this will be a
+    slower operation on the first call.
+
+    Args:
+      argument_name: the name of the query or POST argument
+
+    Returns:
+      A (possibly empty) list of values.
+    """
+    if self.charset:
+      argument_name = argument_name.encode(self.charset)
+
+    try:
+      param_value = self.params.getall(argument_name)
+    except KeyError:
+      return default_value
+
+    for i in range(len(param_value)):
+      if isinstance(param_value[i], cgi.FieldStorage):
+        param_value[i] = param_value[i].value
+
+    return param_value
+
+  def arguments(self):
+    """Returns a list of the arguments provided in the query and/or POST.
+
+    The return value is a list of strings.
+    """
+    return list(set(self.params.keys()))
+
+  def get_range(self, name, min_value=None, max_value=None, default=0):
+    """Parses the given int argument, limiting it to the given range.
+
+    Args:
+      name: the name of the argument
+      min_value: the minimum int value of the argument (if any)
+      max_value: the maximum int value of the argument (if any)
+      default: the default value of the argument if it is not given
+
+    Returns:
+      An int within the given range for the argument
+    """
+    try:
+      value = int(self.get(name, default))
+    except ValueError:
+      value = default
+    if max_value != None:
+      value = min(value, max_value)
+    if min_value != None:
+      value = max(value, min_value)
+    return value
+
+
+class Response(object):
+  """Abstraction for an HTTP response.
+
+  Properties:
+    out: file pointer for the output stream
+    headers: wsgiref.headers.Headers instance representing the output headers
+  """
+  def __init__(self):
+    """Constructs a response with the default settings."""
+    self.out = StringIO.StringIO()
+    self.__wsgi_headers = []
+    self.headers = wsgiref.headers.Headers(self.__wsgi_headers)
+    self.headers['Content-Type'] = 'text/html; charset=utf-8'
+    self.headers['Cache-Control'] = 'no-cache'
+    self.set_status(200)
+
+  def set_status(self, code, message=None):
+    """Sets the HTTP status code of this response.
+
+    Args:
+      message: the HTTP status string to use
+
+    If no status string is given, we use the default from the HTTP/1.1
+    specification.
+    """
+    if not message:
+      message = Response.http_status_message(code)
+    self.__status = (code, message)
+
+  def clear(self):
+    """Clears all data written to the output stream so that it is empty."""
+    self.out.seek(0)
+    self.out.truncate(0)
+
+  def wsgi_write(self, start_response):
+    """Writes this response using WSGI semantics with the given WSGI function.
+
+    Args:
+      start_response: the WSGI-compatible start_response function
+    """
+    body = self.out.getvalue()
+    if isinstance(body, unicode):
+      body = body.encode('utf-8')
+    elif self.headers.get('Content-Type', '').endswith('; charset=utf-8'):
+      try:
+        body.decode('utf-8')
+      except UnicodeError, e:
+        logging.warning('Response written is not UTF-8: %s', e)
+
+    self.headers['Content-Length'] = str(len(body))
+    write = start_response('%d %s' % self.__status, self.__wsgi_headers)
+    write(body)
+    self.out.close()
+
+  def http_status_message(code):
+    """Returns the default HTTP status message for the given code.
+
+    Args:
+      code: the HTTP code for which we want a message
+    """
+    if not Response.__HTTP_STATUS_MESSAGES.has_key(code):
+      raise Error('Invalid HTTP status code: %d' % code)
+    return Response.__HTTP_STATUS_MESSAGES[code]
+  http_status_message = staticmethod(http_status_message)
+
+  __HTTP_STATUS_MESSAGES = {
+    100: 'Continue',
+    101: 'Switching Protocols',
+    200: 'OK',
+    201: 'Created',
+    202: 'Accepted',
+    203: 'Non-Authoritative Information',
+    204: 'No Content',
+    205: 'Reset Content',
+    206: 'Partial Content',
+    300: 'Multiple Choices',
+    301: 'Moved Permanently',
+    302: 'Moved Temporarily',
+    303: 'See Other',
+    304: 'Not Modified',
+    305: 'Use Proxy',
+    306: 'Unused',
+    307: 'Temporary Redirect',
+    400: 'Bad Request',
+    401: 'Unauthorized',
+    402: 'Payment Required',
+    403: 'Forbidden',
+    404: 'Not Found',
+    405: 'Method Not Allowed',
+    406: 'Not Acceptable',
+    407: 'Proxy Authentication Required',
+    408: 'Request Time-out',
+    409: 'Conflict',
+    410: 'Gone',
+    411: 'Length Required',
+    412: 'Precondition Failed',
+    413: 'Request Entity Too Large',
+    414: 'Request-URI Too Large',
+    415: 'Unsupported Media Type',
+    416: 'Requested Range Not Satisfiable',
+    417: 'Expectation Failed',
+    500: 'Internal Server Error',
+    501: 'Not Implemented',
+    502: 'Bad Gateway',
+    503: 'Service Unavailable',
+    504: 'Gateway Time-out',
+    505: 'HTTP Version not supported'
+  }
+
+
+class RequestHandler(object):
+  """Our base HTTP request handler. Clients should subclass this class.
+
+  Subclasses should override get(), post(), head(), options(), etc to handle
+  different HTTP methods.
+  """
+  def initialize(self, request, response):
+    """Initializes this request handler with the given Request and Response."""
+    self.request = request
+    self.response = response
+
+  def get(self, *args):
+    """Handler method for GET requests."""
+    self.error(405)
+
+  def post(self, *args):
+    """Handler method for POST requests."""
+    self.error(405)
+
+  def head(self, *args):
+    """Handler method for HEAD requests."""
+    self.error(405)
+
+  def options(self, *args):
+    """Handler method for OPTIONS requests."""
+    self.error(405)
+
+  def put(self, *args):
+    """Handler method for PUT requests."""
+    self.error(405)
+
+  def delete(self, *args):
+    """Handler method for DELETE requests."""
+    self.error(405)
+
+  def trace(self, *args):
+    """Handler method for TRACE requests."""
+    self.error(405)
+
+  def error(self, code):
+    """Clears the response output stream and sets the given HTTP error code.
+
+    Args:
+      code: the HTTP status error code (e.g., 501)
+    """
+    self.response.set_status(code)
+    self.response.clear()
+
+  def redirect(self, uri, permanent=False):
+    """Issues an HTTP redirect to the given relative URL.
+
+    Args:
+      uri: a relative or absolute URI (e.g., '../flowers.html')
+      permanent: if true, we use a 301 redirect instead of a 302 redirect
+    """
+    if permanent:
+      self.response.set_status(301)
+    else:
+      self.response.set_status(302)
+    absolute_url = urlparse.urljoin(self.request.uri, uri)
+    self.response.headers['Location'] = str(absolute_url)
+    self.response.clear()
+
+  def handle_exception(self, exception, debug_mode):
+    """Called if this handler throws an exception during execution.
+
+    The default behavior is to call self.error(500) and print a stack trace
+    if debug_mode is True.
+
+    Args:
+      exception: the exception that was thrown
+      debug_mode: True if the web application is running in debug mode
+    """
+    self.error(500)
+    lines = ''.join(traceback.format_exception(*sys.exc_info()))
+    logging.error(lines)
+    if debug_mode:
+      self.response.clear()
+      self.response.headers['Content-Type'] = 'text/plain'
+      self.response.out.write(lines)
+
+  @classmethod
+  def get_url(cls, *args, **kargs):
+    """Returns the url for the given handler.
+
+    The default implementation uses the patterns passed to the active
+    WSGIApplication and the django urlresolvers module to create a url.
+    However, it is different from urlresolvers.reverse() in the following ways:
+      - It does not try to resolve handlers via module loading
+      - It does not support named arguments
+      - It performs some post-prosessing on the url to remove some regex
+        operators that urlresolvers.reverse_helper() seems to miss.
+      - It will try to fill in the left-most missing arguments with the args
+        used in the active request.
+
+    Args:
+      args: Parameters for the url pattern's groups.
+      kwargs: Optionally contains 'implicit_args' that can either be a boolean
+              or a tuple. When it is True, it will use the arguments to the
+              active request as implicit arguments. When it is False (default),
+              it will not use any implicit arguments. When it is a tuple, it
+              will use the tuple as the implicit arguments.
+              the left-most args if some are missing from args.
+
+    Returns:
+      The url for this handler/args combination.
+
+    Raises:
+      NoUrlFoundError: No url pattern for this handler has the same
+        number of args that were passed in.
+    """
+
+    app = WSGIApplication.active_instance
+    pattern_map = app._pattern_map
+
+    implicit_args = kargs.get('implicit_args', ())
+    if implicit_args == True:
+      implicit_args = app.current_request_args
+
+    min_params = len(args)
+
+    urlresolvers = None
+
+    for pattern_tuple in pattern_map.get(cls, ()):
+      num_params_in_pattern = pattern_tuple[1]
+      if num_params_in_pattern < min_params:
+        continue
+
+      if urlresolvers is None:
+        from django.core import urlresolvers
+
+      try:
+        num_implicit_args = max(0, num_params_in_pattern - len(args))
+        merged_args = implicit_args[:num_implicit_args] + args
+        url = urlresolvers.reverse_helper(pattern_tuple[0], *merged_args)
+        url = url.replace('\\', '')
+        url = url.replace('?', '')
+        return url
+      except urlresolvers.NoReverseMatch:
+        continue
+
+    logging.warning('get_url failed for Handler name: %r, Args: %r',
+                    cls.__name__, args)
+    raise NoUrlFoundError
+
+
+class WSGIApplication(object):
+  """Wraps a set of webapp RequestHandlers in a WSGI-compatible application.
+
+  To use this class, pass a list of (URI regular expression, RequestHandler)
+  pairs to the constructor, and pass the class instance to a WSGI handler.
+  See the example in the module comments for details.
+
+  The URL mapping is first-match based on the list ordering.
+  """
+
+  def __init__(self, url_mapping, debug=False):
+    """Initializes this application with the given URL mapping.
+
+    Args:
+      url_mapping: list of (URI, RequestHandler) pairs (e.g., [('/', ReqHan)])
+      debug: if true, we send Python stack traces to the browser on errors
+    """
+    self._init_url_mappings(url_mapping)
+    self.__debug = debug
+    WSGIApplication.active_instance = self
+    self.current_request_args = ()
+
+  def __call__(self, environ, start_response):
+    """Called by WSGI when a request comes in."""
+    request = Request(environ)
+    response = Response()
+
+    WSGIApplication.active_instance = self
+
+    handler = None
+    groups = ()
+    for regexp, handler_class in self._url_mapping:
+      match = regexp.match(request.path)
+      if match:
+        handler = handler_class()
+        handler.initialize(request, response)
+        groups = match.groups()
+        break
+
+    self.current_request_args = groups
+
+    if handler:
+      try:
+        method = environ['REQUEST_METHOD']
+        if method == 'GET':
+          handler.get(*groups)
+        elif method == 'POST':
+          handler.post(*groups)
+        elif method == 'HEAD':
+          handler.head(*groups)
+        elif method == 'OPTIONS':
+          handler.options(*groups)
+        elif method == 'PUT':
+          handler.put(*groups)
+        elif method == 'DELETE':
+          handler.delete(*groups)
+        elif method == 'TRACE':
+          handler.trace(*groups)
+        else:
+          handler.error(501)
+      except Exception, e:
+        handler.handle_exception(e, self.__debug)
+    else:
+      response.set_status(404)
+
+    response.wsgi_write(start_response)
+    return ['']
+
+  def _init_url_mappings(self, handler_tuples):
+    """Initializes the maps needed for mapping urls to handlers and handlers
+    to urls.
+
+    Args:
+      handler_tuples: list of (URI, RequestHandler) pairs.
+    """
+
+    handler_map = {}
+    pattern_map = {}
+    url_mapping = []
+
+    for regexp, handler in handler_tuples:
+
+      handler_map[handler.__name__] = handler
+
+      if not regexp.startswith('^'):
+        regexp = '^' + regexp
+      if not regexp.endswith('$'):
+        regexp += '$'
+
+      compiled = re.compile(regexp)
+      url_mapping.append((compiled, handler))
+
+      num_groups = len(RE_FIND_GROUPS.findall(regexp))
+      handler_patterns = pattern_map.setdefault(handler, [])
+      handler_patterns.append((compiled, num_groups))
+
+    self._handler_map = handler_map
+    self._pattern_map = pattern_map
+    self._url_mapping = url_mapping
+
+  def get_registered_handler_by_name(self, handler_name):
+    """Returns the handler given the handler's name.
+
+    This uses the application's url mapping.
+
+    Args:
+      handler_name: The __name__ of a handler to return.
+
+    Returns:
+      The handler with the given name.
+
+    Raises:
+      KeyError: If the handler name is not found in the parent application.
+    """
+    try:
+      return self._handler_map[handler_name]
+    except:
+      logging.error('Handler does not map to any urls: %s', handler_name)
+      raise