blob: 344a6bd809fe2a6f2a9f9142d70a96afb48f8a3e [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2011 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 utilities
Small collection of helpful utilities for working with WSGI.
"""
import six
__author__ = 'rafek@google.com (Rafe Kaplan)'
import six.moves.http_client
import re
from .. import util
__all__ = ['static_page',
'error',
'first_found',
]
_STATUS_PATTERN = re.compile('^(\d{3})\s')
@util.positional(1)
def static_page(content='',
status='200 OK',
content_type='text/html; charset=utf-8',
headers=None):
"""Create a WSGI application that serves static content.
A static page is one that will be the same every time it receives a request.
It will always serve the same status, content and headers.
Args:
content: Content to serve in response to HTTP request.
status: Status to serve in response to HTTP request. If string, status
is served as is without any error checking. If integer, will look up
status message. Otherwise, parameter is tuple (status, description):
status: Integer status of response.
description: Brief text description of response.
content_type: Convenient parameter for content-type header. Will appear
before any content-type header that appears in 'headers' parameter.
headers: Dictionary of headers or iterable of tuples (name, value):
name: String name of header.
value: String value of header.
Returns:
WSGI application that serves static content.
"""
if isinstance(status, six.integer_types):
status = '%d %s' % (status, six.moves.http_client.responses.get(status, 'Unknown Error'))
elif not isinstance(status, six.string_types):
status = '%d %s' % tuple(status)
if isinstance(headers, dict):
headers = six.iteritems(headers)
headers = [('content-length', str(len(content))),
('content-type', content_type),
] + list(headers or [])
# Ensure all headers are str.
for index, (key, value) in enumerate(headers):
if isinstance(value, six.text_type):
value = value.encode('utf-8')
headers[index] = key, value
if not isinstance(key, str):
raise TypeError('Header key must be str, found: %r' % (key,))
if not isinstance(value, str):
raise TypeError(
'Header %r must be type str or unicode, found: %r' % (key, value))
def static_page_application(environ, start_response):
start_response(status, headers)
return [content]
return static_page_application
@util.positional(2)
def error(status_code, status_message=None,
content_type='text/plain; charset=utf-8',
headers=None, content=None):
"""Create WSGI application that statically serves an error page.
Creates a static error page specifically for non-200 HTTP responses.
Browsers such as Internet Explorer will display their own error pages for
error content responses smaller than 512 bytes. For this reason all responses
are right-padded up to 512 bytes.
Error pages that are not provided will content will contain the standard HTTP
status message as their content.
Args:
status_code: Integer status code of error.
status_message: Status message.
Returns:
Static WSGI application that sends static error response.
"""
if status_message is None:
status_message = six.moves.http_client.responses.get(status_code, 'Unknown Error')
if content is None:
content = status_message
content = util.pad_string(content)
return static_page(content,
status=(status_code, status_message),
content_type=content_type,
headers=headers)
def first_found(apps):
"""Serve the first application that does not response with 404 Not Found.
If no application serves content, will respond with generic 404 Not Found.
Args:
apps: List of WSGI applications to search through. Will serve the content
of the first of these that does not return a 404 Not Found. Applications
in this list must not modify the environment or any objects in it if they
do not match. Applications that do not obey this restriction can create
unpredictable results.
Returns:
Compound application that serves the contents of the first application that
does not response with 404 Not Found.
"""
apps = tuple(apps)
not_found = error(six.moves.http_client.NOT_FOUND)
def first_found_app(environ, start_response):
"""Compound application returned from the first_found function."""
final_result = {} # Used in absence of Python local scoping.
def first_found_start_response(status, response_headers):
"""Replacement for start_response as passed in to first_found_app.
Called by each application in apps instead of the real start response.
Checks the response status, and if anything other than 404, sets 'status'
and 'response_headers' in final_result.
"""
status_match = _STATUS_PATTERN.match(status)
assert status_match, ('Status must be a string beginning '
'with 3 digit number. Found: %s' % status)
status_code = status_match.group(0)
if int(status_code) == six.moves.http_client.NOT_FOUND:
return
final_result['status'] = status
final_result['response_headers'] = response_headers
for app in apps:
response = app(environ, first_found_start_response)
if final_result:
start_response(final_result['status'], final_result['response_headers'])
return response
return not_found(environ, start_response)
return first_found_app