###############################################################################
#
# Copyright 2011-2012 Pants Developers (see AUTHORS.txt)
#
# 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.
#
###############################################################################
"""
``pants.web.wsgi`` implements a WSGI compatibility class that lets you run
WSGI applications using the Pants :class:`~pants.http.server.HTTPServer`.
Currently, this module uses the :pep:`333` standard. Future releases will add
support for :pep:`3333`, as well as the ability to host a Pants
:class:`~pants.web.application.Application` from a standard WSGI server.
"""
###############################################################################
# Imports
###############################################################################
import cStringIO
import sys
import traceback
from pants.web.application import error
from pants.web.utils import log, SERVER
###############################################################################
# WSGIConnector Class
###############################################################################
[docs]class WSGIConnector(object):
"""
This class functions as a request handler for the Pants
:class:`~pants.http.server.HTTPServer` that wraps WSGI applications to
allow them to work correctly.
Class instances are callable, and when called with a
:class:`~pants.http.server.HTTPRequest` instance, they construct a WSGI
environment and invoke the application.
.. code-block:: python
from pants import Engine
from pants.http import HTTPServer
from pants.web import WSGIConnector
def hello_app(environ, start_response):
start_response("200 OK", {"Content-Type": "text/plain"})
return ["Hello, World!"]
connector = WSGIConnector(hello_app)
HTTPServer(connector).listen()
Engine.instance().start()
``WSGIConnector`` supports sending responses with
``Transfer-Encoding: chunked`` and will do so automatically when the WSGI
application's response does not contain information about the response's
length.
============ ============
Argument Description
============ ============
application The WSGI application that will handle incoming requests.
debug *Optional.* Whether or not to display tracebacks and additional debugging information for a request within ``500 Internal Server Error`` pages.
============ ============
"""
def __init__(self, application, debug=False):
self.app = application
self.debug = debug
[docs] def attach(self, application, rule, methods=('HEAD','GET','POST','PUT')):
"""
Attach the WSGIConnector to an instance of
:class:`~pants.web.application.Application` at the given
:ref:`route <app-routing>`.
You may use route variables to strip information out of a URL. In the
event that variables exist, they will be made available within the WSGI
environment under the key `wsgiorg.routing_args <http://wsgi.readthedocs.org/en/latest/specifications/routing_args.html>`_
.. warning::
When using WSGIConnector within an Application, WSGIConnector
expects the final variable in the rule to capture the remainder of
the URL, and it treats the last variable as containing the value
for the ``PATH_INFO`` variable in the WSGI environment. This method
adds such a variable automatically. However, if you add the
WSGIConnector manually you will have to be prepared.
============ ============
Argument Description
============ ============
application The :class:`~pants.web.Application` to attach to.
rule The path to serve requests from.
methods *Optional.* The HTTP methods to accept.
============ ============
"""
if not rule.endswith('/'):
rule += '/'
application.route(rule + '<regex("(.*)"):path>', methods=methods, func=self)
def __call__(self, request, *args):
"""
Handle the given request.
"""
# Make sure this plays nice with Web.
request.auto_finish = False
request._headers = None
request._head_status = None
request._chunk_it = False
def write(data):
if not request._started:
# Before the first output, send the headers.
# But before that, figure out if we've got a set length.
for k,v in request._headers:
if k.lower() == 'content-length' or k.lower() == 'transfer-encoding':
break
else:
request._headers.append(('Transfer-Encoding', 'chunked'))
request._chunk_it = True
request.send_status(request._head_status)
request.send_headers(request._headers)
if request._chunk_it:
request.write("%x\r\n%s\r\n" % (len(data), data))
else:
request.write(data)
def start_response(status, head, exc_info=None):
if exc_info:
try:
if request._started:
raise exc_info[0], exc_info[1], exc_info[2]
finally:
exc_info = None
elif request._head_status is not None:
raise RuntimeError("Headers already set.")
if not isinstance(status, (int, str)):
raise ValueError("status must be a string or int")
if not isinstance(head, list):
if isinstance(head, dict):
head = [(k,v) for k,v in head.iteritems()]
else:
try:
head = list(head)
except ValueError:
raise ValueError("headers must be a list")
request._head_status = status
request._headers = head
return write
# Check for extra arguments that would mean we're being used
# within Application.
if hasattr(request, '_converted_match'):
path = request._converted_match[-1]
routing_args = request._converted_match[:-1]
else:
path = request.path
if hasattr(request, 'match'):
routing_args = request.match.groups()
else:
routing_args = None
# Build an environment for the WSGI application.
environ = {
'REQUEST_METHOD' : request.method,
'SCRIPT_NAME' : '',
'PATH_INFO' : path,
'QUERY_STRING' : request.query,
'SERVER_NAME' : request.headers.get('Host','127.0.0.1'),
'SERVER_PROTOCOL' : request.protocol,
'SERVER_SOFTWARE' : SERVER,
'REMOTE_ADDR' : request.remote_ip,
'GATEWAY_INTERFACE' : 'WSGI/1.0',
'wsgi.version' : (1,0),
'wsgi.url_scheme' : request.scheme,
'wsgi.input' : cStringIO.StringIO(request.body),
'wsgi.errors' : sys.stderr,
'wsgi.multithread' : False,
'wsgi.multiprocess' : False,
'wsgi.run_once' : False
}
if isinstance(request.connection.server.local_address, tuple):
environ['SERVER_PORT'] = request.connection.server.local_address[1]
if routing_args:
environ['wsgiorg.routing_args'] = (routing_args, {})
if 'Content-Type' in request.headers:
environ['CONTENT_TYPE'] = request.headers['Content-Type']
if 'Content-Length' in request.headers:
environ['CONTENT_LENGTH'] = request.headers['Content-Length']
for k,v in request.headers._data.iteritems():
environ['HTTP_%s' % k.replace('-','_').upper()] = v
# Run the WSGI Application.
try:
result = self.app(environ, start_response)
if result:
try:
if isinstance(result, str):
write(result)
else:
for data in result:
if data:
write(data)
finally:
try:
if hasattr(result, 'close'):
result.close()
except Exception:
log.warning("Exception running result.close() for: "
"%s %s", request.method, request.path,
exc_info=True)
result = None
except Exception:
log.exception('Exception running WSGI application for: %s %s',
request.method, request.path)
# If we've started, bad stuff.
if request._started:
# We can't recover, so close the connection.
if request._chunk_it:
request.write("0\r\n\r\n\r\n")
request.connection.close(True)
return
# Use the default behavior if we're not debugging.
if not self.debug:
raise
resp = u''.join([
u"<h2>Traceback</h2>\n",
u"<pre>%s</pre>\n" % traceback.format_exc(),
u"<h2>HTTP Request</h2>\n",
request.__html__(),
])
body, status, headers = error(resp, 500, request=request,
debug=True)
request.send_status(500)
if not 'Content-Length' in headers:
headers['Content-Length'] = len(body)
request.send_headers(headers)
request.write(body)
request.finish()
return
# Finish up here.
if not request._started:
write('')
if request._chunk_it:
request.write("0\r\n\r\n\r\n")
request.finish()