| #!/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. |
| # |
| |
| |
| |
| |
| """WSGI server interface to Python runtime. |
| |
| WSGI-compliant interface between the Python runtime and user-provided Python |
| code. |
| """ |
| |
| |
| |
| |
| |
| |
| |
| |
| import logging |
| import sys |
| import types |
| |
| from google.appengine import runtime |
| from google.appengine.api import lib_config |
| |
| |
| |
| |
| _DEADLINE_DURING_LOADING = 22 |
| |
| |
| class Error(Exception): |
| pass |
| |
| |
| class InvalidResponseError(Error): |
| """An error indicating that the response is invalid.""" |
| pass |
| |
| |
| def _GetTypeName(x): |
| """Returns a user-friendly name descriping the given object's type.""" |
| if type(x) is types.InstanceType: |
| return x.__class__.__name__ |
| else: |
| return type(x).__name__ |
| |
| |
| |
| |
| |
| |
| def LoadObject(object_name): |
| """Find and return a Python object specified by object_name. |
| |
| Packages and modules are imported as necessary. |
| |
| Args: |
| object_name: (string) An object specification. |
| |
| Returns: |
| A tuple of the form (object, string, error). If object_name can be |
| fully traversed, object is the specified object, string is the filename |
| containing the object, and error is None. Otherwise, object_name is |
| maximal partial match specified by object_name, string is the filename |
| containing object, and error is an ImportError. |
| """ |
| containing_file = None |
| path = object_name.split('.') |
| obj = __import__(path[0]) |
| is_parent_package = True |
| cumulative_path = path[0] |
| for name in path[1:]: |
| if hasattr(obj, '__file__'): |
| containing_file = obj.__file__ |
| is_parent_package = is_parent_package and hasattr(obj, '__path__') |
| cumulative_path += '.' + name |
| if hasattr(obj, name): |
| obj = getattr(obj, name) |
| elif is_parent_package: |
| __import__(cumulative_path) |
| obj = getattr(obj, name) |
| else: |
| return obj, containing_file, ImportError( |
| '%s has no attribute %s' % (obj, name)) |
| |
| |
| |
| return obj, containing_file, None |
| |
| |
| class WsgiRequest(object): |
| """A single WSGI request.""" |
| |
| def __init__(self, environ, handler_name, url, post_data, error): |
| """Creates a single WSGI request. |
| |
| Creates a request for handler_name in the form 'path.to.handler' for url |
| with the environment contained in environ. |
| |
| Args: |
| environ: A dict containing the environ for this request (e.g. like from |
| os.environ). |
| handler_name: A str containing the user-specified handler to use for this |
| request as specified in the script field of a handler in app.yaml |
| using the Python dot notation; e.g. 'package.module.application'. |
| url: An urlparse.SplitResult instance containing the request url. |
| post_data: A stream containing the post data for this request. |
| error: A stream into which errors are to be written. |
| """ |
| self._handler = handler_name |
| self._status = 500 |
| self._response_headers = [] |
| self._started_handling = False |
| self._body = [] |
| self._written_body = [] |
| environ['wsgi.multiprocess'] = True |
| environ['wsgi.run_once'] = False |
| environ['wsgi.version'] = (1, 0) |
| |
| |
| environ.setdefault('wsgi.multithread', False) |
| self._error = error |
| environ['wsgi.url_scheme'] = url.scheme |
| environ['wsgi.input'] = post_data |
| environ['wsgi.errors'] = self._error |
| self._environ = environ |
| |
| def _Write(self, body_data): |
| """Writes some body_data to the response. |
| |
| Args: |
| body_data: data to be written. |
| |
| Raises: |
| InvalidResponseError: body_data is not a str. |
| """ |
| if not isinstance(body_data, str): |
| raise InvalidResponseError('body_data must be a str, got %r' % |
| _GetTypeName(body_data)) |
| self._written_body.append(body_data) |
| |
| def _StartResponse(self, status, response_headers, exc_info=None): |
| """A PEP 333 start_response callable. |
| |
| Implements the start_response behaviour of PEP 333. Sets the status code and |
| response headers as provided. If exc_info is not None, then the previously |
| provided status and response headers are replaced; this implementation |
| buffers the complete response so valid use of exc_info never raises an |
| exception. Otherwise, _StartResponse may only be called once. |
| |
| Args: |
| status: A string containing the status code and status string. |
| response_headers: a list of pairs representing header keys and values. |
| exc_info: exception info as obtained from sys.exc_info(). |
| |
| Returns: |
| A Write method as per PEP 333. |
| |
| Raises: |
| InvalidResponseError: The arguments passed are invalid. |
| """ |
| if not isinstance(status, str): |
| raise InvalidResponseError('status must be a str, got %r (%r)' % |
| (_GetTypeName(status), status)) |
| if not status: |
| raise InvalidResponseError('status must not be empty') |
| if not isinstance(response_headers, list): |
| raise InvalidResponseError('response_headers must be a list, got %r' % |
| _GetTypeName(response_headers)) |
| for header in response_headers: |
| if not isinstance(header, tuple): |
| raise InvalidResponseError('response_headers items must be tuple, ' |
| 'got %r' % _GetTypeName(header)) |
| if len(header) != 2: |
| raise InvalidResponseError('header tuples must have length 2, ' |
| 'actual length %d' % len(header)) |
| name, value = header |
| if not isinstance(name, str): |
| raise InvalidResponseError('header names must be str, got %r (%r)' % |
| (_GetTypeName(name), name)) |
| if not isinstance(value, str): |
| raise InvalidResponseError('header values must be str, ' |
| 'got %r (%r) for %r' % |
| (_GetTypeName(value), value, name)) |
| try: |
| status_number = int(status.split(' ')[0]) |
| except ValueError: |
| raise InvalidResponseError('status code %r is not a number' % status) |
| if status_number < 200 or status_number >= 600: |
| raise InvalidResponseError('status code must be in the range [200,600), ' |
| 'got %d' % status_number) |
| |
| if exc_info is not None: |
| |
| self._status = status_number |
| self._response_headers = response_headers |
| exc_info = None |
| elif self._started_handling: |
| raise InvalidResponseError('_StartResponse may only be called once' |
| ' without exc_info') |
| else: |
| self._status = status_number |
| self._response_headers = response_headers |
| self._started_handling = True |
| self._body = [] |
| self._written_body = [] |
| return self._Write |
| |
| def Handle(self): |
| """Handles the request represented by the WsgiRequest object. |
| |
| Loads the handler from the handler name provided. Calls the handler with the |
| environ. Any exceptions in loading the user handler and executing it are |
| caught and logged. |
| |
| Returns: |
| A dict containing: |
| error: App Engine error code. 0 for OK, 1 for error. |
| response_code: HTTP response code. |
| headers: A list of tuples (key, value) of HTTP headers. |
| body: A str of the body of the response |
| """ |
| try: |
| handler = _config_handle.add_wsgi_middleware(self._LoadHandler()) |
| except runtime.DeadlineExceededError: |
| |
| |
| |
| |
| |
| |
| |
| |
| exc_info = sys.exc_info() |
| try: |
| logging.error('', exc_info=exc_info) |
| except runtime.DeadlineExceededError: |
| |
| logging.exception('Deadline exception occurred while logging a ' |
| 'deadline exception.') |
| |
| |
| |
| logging.error('Original exception:', exc_info=exc_info) |
| return {'error': _DEADLINE_DURING_LOADING} |
| except: |
| logging.exception('') |
| return {'error': 1} |
| result = None |
| try: |
| result = handler(dict(self._environ), self._StartResponse) |
| for chunk in result: |
| if not isinstance(chunk, str): |
| raise InvalidResponseError('handler must return an iterable of str') |
| self._body.append(chunk) |
| |
| |
| |
| body = ''.join(self._written_body + self._body) |
| return {'response_code': self._status, 'headers': |
| self._response_headers, 'body': body} |
| except: |
| logging.exception('') |
| return {'error': 1} |
| finally: |
| if hasattr(result, 'close'): |
| result.close() |
| |
| def _LoadHandler(self): |
| """Find and return a Python object with name self._handler. |
| |
| Sets _environ so that PATH_TRANSLATED is equal to the file containing the |
| handler. |
| |
| Packages and modules are imported as necessary. |
| |
| Returns: |
| The python object specified by self._handler. |
| |
| Raises: |
| ImportError: An element of the path cannot be resolved. |
| """ |
| handler, path, err = LoadObject(self._handler) |
| self._environ['PATH_TRANSLATED'] = path |
| if err: |
| raise err |
| return handler |
| |
| |
| def HandleRequest(environ, handler_name, url, post_data, error): |
| """Handle a single WSGI request. |
| |
| Creates a request for handler_name in the form 'path.to.handler' for url with |
| the environment contained in environ. |
| |
| Args: |
| environ: A dict containing the environ for this request (e.g. like from |
| os.environ). |
| handler_name: A str containing the user-specified handler to use for this |
| request as specified in the script field of a handler in app.yaml using |
| the Python dot notation; e.g. 'package.module.application'. |
| url: An urlparse.SplitResult instance containing the request url. |
| post_data: A stream containing the post data for this request. |
| error: A stream into which errors are to be written. |
| |
| Returns: |
| A dict containing: |
| error: App Engine error code. 0 for OK, 1 for error. |
| response_code: HTTP response code. |
| headers: A list of tuples (key, value) of HTTP headers. |
| body: A str of the body of the response |
| """ |
| return WsgiRequest(environ, handler_name, url, post_data, error).Handle() |
| |
| _config_handle = lib_config.register( |
| 'webapp', |
| {'add_wsgi_middleware': lambda app: app}) |