wsgi.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 #
00003 # Copyright 2009 Facebook
00004 #
00005 # Licensed under the Apache License, Version 2.0 (the "License"); you may
00006 # not use this file except in compliance with the License. You may obtain
00007 # a copy of the License at
00008 #
00009 #     http://www.apache.org/licenses/LICENSE-2.0
00010 #
00011 # Unless required by applicable law or agreed to in writing, software
00012 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
00013 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
00014 # License for the specific language governing permissions and limitations
00015 # under the License.
00016 
00017 """WSGI support for the Tornado web framework.
00018 
00019 WSGI is the Python standard for web servers, and allows for interoperability
00020 between Tornado and other Python web frameworks and servers.  This module
00021 provides WSGI support in two ways:
00022 
00023 * `WSGIApplication` is a version of `tornado.web.Application` that can run
00024   inside a WSGI server.  This is useful for running a Tornado app on another
00025   HTTP server, such as Google App Engine.  See the `WSGIApplication` class
00026   documentation for limitations that apply.
00027 * `WSGIContainer` lets you run other WSGI applications and frameworks on the
00028   Tornado HTTP server.  For example, with this class you can mix Django
00029   and Tornado handlers in a single server.
00030 """
00031 
00032 from __future__ import absolute_import, division, with_statement
00033 
00034 import Cookie
00035 import httplib
00036 import logging
00037 import sys
00038 import time
00039 import tornado
00040 import urllib
00041 
00042 from tornado import escape
00043 from tornado import httputil
00044 from tornado import web
00045 from tornado.escape import native_str, utf8, parse_qs_bytes
00046 from tornado.util import b
00047 
00048 try:
00049     from io import BytesIO  # python 3
00050 except ImportError:
00051     from cStringIO import StringIO as BytesIO  # python 2
00052 
00053 
00054 class WSGIApplication(web.Application):
00055     """A WSGI equivalent of `tornado.web.Application`.
00056 
00057     WSGIApplication is very similar to web.Application, except no
00058     asynchronous methods are supported (since WSGI does not support
00059     non-blocking requests properly). If you call self.flush() or other
00060     asynchronous methods in your request handlers running in a
00061     WSGIApplication, we throw an exception.
00062 
00063     Example usage::
00064 
00065         import tornado.web
00066         import tornado.wsgi
00067         import wsgiref.simple_server
00068 
00069         class MainHandler(tornado.web.RequestHandler):
00070             def get(self):
00071                 self.write("Hello, world")
00072 
00073         if __name__ == "__main__":
00074             application = tornado.wsgi.WSGIApplication([
00075                 (r"/", MainHandler),
00076             ])
00077             server = wsgiref.simple_server.make_server('', 8888, application)
00078             server.serve_forever()
00079 
00080     See the 'appengine' demo for an example of using this module to run
00081     a Tornado app on Google AppEngine.
00082 
00083     Since no asynchronous methods are available for WSGI applications, the
00084     httpclient and auth modules are both not available for WSGI applications.
00085     We support the same interface, but handlers running in a WSGIApplication
00086     do not support flush() or asynchronous methods.
00087     """
00088     def __init__(self, handlers=None, default_host="", **settings):
00089         web.Application.__init__(self, handlers, default_host, transforms=[],
00090                                  wsgi=True, **settings)
00091 
00092     def __call__(self, environ, start_response):
00093         handler = web.Application.__call__(self, HTTPRequest(environ))
00094         assert handler._finished
00095         status = str(handler._status_code) + " " + \
00096             httplib.responses[handler._status_code]
00097         headers = handler._headers.items()
00098         if hasattr(handler, "_new_cookie"):
00099             for cookie in handler._new_cookie.values():
00100                 headers.append(("Set-Cookie", cookie.OutputString(None)))
00101         start_response(status,
00102                        [(native_str(k), native_str(v)) for (k, v) in headers])
00103         return handler._write_buffer
00104 
00105 
00106 class HTTPRequest(object):
00107     """Mimics `tornado.httpserver.HTTPRequest` for WSGI applications."""
00108     def __init__(self, environ):
00109         """Parses the given WSGI environ to construct the request."""
00110         self.method = environ["REQUEST_METHOD"]
00111         self.path = urllib.quote(environ.get("SCRIPT_NAME", ""))
00112         self.path += urllib.quote(environ.get("PATH_INFO", ""))
00113         self.uri = self.path
00114         self.arguments = {}
00115         self.query = environ.get("QUERY_STRING", "")
00116         if self.query:
00117             self.uri += "?" + self.query
00118             arguments = parse_qs_bytes(native_str(self.query))
00119             for name, values in arguments.iteritems():
00120                 values = [v for v in values if v]
00121                 if values:
00122                     self.arguments[name] = values
00123         self.version = "HTTP/1.1"
00124         self.headers = httputil.HTTPHeaders()
00125         if environ.get("CONTENT_TYPE"):
00126             self.headers["Content-Type"] = environ["CONTENT_TYPE"]
00127         if environ.get("CONTENT_LENGTH"):
00128             self.headers["Content-Length"] = environ["CONTENT_LENGTH"]
00129         for key in environ:
00130             if key.startswith("HTTP_"):
00131                 self.headers[key[5:].replace("_", "-")] = environ[key]
00132         if self.headers.get("Content-Length"):
00133             self.body = environ["wsgi.input"].read(
00134                 int(self.headers["Content-Length"]))
00135         else:
00136             self.body = ""
00137         self.protocol = environ["wsgi.url_scheme"]
00138         self.remote_ip = environ.get("REMOTE_ADDR", "")
00139         if environ.get("HTTP_HOST"):
00140             self.host = environ["HTTP_HOST"]
00141         else:
00142             self.host = environ["SERVER_NAME"]
00143 
00144         # Parse request body
00145         self.files = {}
00146         content_type = self.headers.get("Content-Type", "")
00147         if content_type.startswith("application/x-www-form-urlencoded"):
00148             for name, values in parse_qs_bytes(native_str(self.body)).iteritems():
00149                 self.arguments.setdefault(name, []).extend(values)
00150         elif content_type.startswith("multipart/form-data"):
00151             if 'boundary=' in content_type:
00152                 boundary = content_type.split('boundary=', 1)[1]
00153                 if boundary:
00154                     httputil.parse_multipart_form_data(
00155                         utf8(boundary), self.body, self.arguments, self.files)
00156             else:
00157                 logging.warning("Invalid multipart/form-data")
00158 
00159         self._start_time = time.time()
00160         self._finish_time = None
00161 
00162     def supports_http_1_1(self):
00163         """Returns True if this request supports HTTP/1.1 semantics"""
00164         return self.version == "HTTP/1.1"
00165 
00166     @property
00167     def cookies(self):
00168         """A dictionary of Cookie.Morsel objects."""
00169         if not hasattr(self, "_cookies"):
00170             self._cookies = Cookie.SimpleCookie()
00171             if "Cookie" in self.headers:
00172                 try:
00173                     self._cookies.load(
00174                         native_str(self.headers["Cookie"]))
00175                 except Exception:
00176                     self._cookies = None
00177         return self._cookies
00178 
00179     def full_url(self):
00180         """Reconstructs the full URL for this request."""
00181         return self.protocol + "://" + self.host + self.uri
00182 
00183     def request_time(self):
00184         """Returns the amount of time it took for this request to execute."""
00185         if self._finish_time is None:
00186             return time.time() - self._start_time
00187         else:
00188             return self._finish_time - self._start_time
00189 
00190 
00191 class WSGIContainer(object):
00192     r"""Makes a WSGI-compatible function runnable on Tornado's HTTP server.
00193 
00194     Wrap a WSGI function in a WSGIContainer and pass it to HTTPServer to
00195     run it. For example::
00196 
00197         def simple_app(environ, start_response):
00198             status = "200 OK"
00199             response_headers = [("Content-type", "text/plain")]
00200             start_response(status, response_headers)
00201             return ["Hello world!\n"]
00202 
00203         container = tornado.wsgi.WSGIContainer(simple_app)
00204         http_server = tornado.httpserver.HTTPServer(container)
00205         http_server.listen(8888)
00206         tornado.ioloop.IOLoop.instance().start()
00207 
00208     This class is intended to let other frameworks (Django, web.py, etc)
00209     run on the Tornado HTTP server and I/O loop.
00210 
00211     The `tornado.web.FallbackHandler` class is often useful for mixing
00212     Tornado and WSGI apps in the same server.  See
00213     https://github.com/bdarnell/django-tornado-demo for a complete example.
00214     """
00215     def __init__(self, wsgi_application):
00216         self.wsgi_application = wsgi_application
00217 
00218     def __call__(self, request):
00219         data = {}
00220         response = []
00221 
00222         def start_response(status, response_headers, exc_info=None):
00223             data["status"] = status
00224             data["headers"] = response_headers
00225             return response.append
00226         app_response = self.wsgi_application(
00227             WSGIContainer.environ(request), start_response)
00228         response.extend(app_response)
00229         body = b("").join(response)
00230         if hasattr(app_response, "close"):
00231             app_response.close()
00232         if not data:
00233             raise Exception("WSGI app did not call start_response")
00234 
00235         status_code = int(data["status"].split()[0])
00236         headers = data["headers"]
00237         header_set = set(k.lower() for (k, v) in headers)
00238         body = escape.utf8(body)
00239         if "content-length" not in header_set:
00240             headers.append(("Content-Length", str(len(body))))
00241         if "content-type" not in header_set:
00242             headers.append(("Content-Type", "text/html; charset=UTF-8"))
00243         if "server" not in header_set:
00244             headers.append(("Server", "TornadoServer/%s" % tornado.version))
00245 
00246         parts = [escape.utf8("HTTP/1.1 " + data["status"] + "\r\n")]
00247         for key, value in headers:
00248             parts.append(escape.utf8(key) + b(": ") + escape.utf8(value) + b("\r\n"))
00249         parts.append(b("\r\n"))
00250         parts.append(body)
00251         request.write(b("").join(parts))
00252         request.finish()
00253         self._log(status_code, request)
00254 
00255     @staticmethod
00256     def environ(request):
00257         """Converts a `tornado.httpserver.HTTPRequest` to a WSGI environment.
00258         """
00259         hostport = request.host.split(":")
00260         if len(hostport) == 2:
00261             host = hostport[0]
00262             port = int(hostport[1])
00263         else:
00264             host = request.host
00265             port = 443 if request.protocol == "https" else 80
00266         environ = {
00267             "REQUEST_METHOD": request.method,
00268             "SCRIPT_NAME": "",
00269             "PATH_INFO": urllib.unquote(request.path),
00270             "QUERY_STRING": request.query,
00271             "REMOTE_ADDR": request.remote_ip,
00272             "SERVER_NAME": host,
00273             "SERVER_PORT": str(port),
00274             "SERVER_PROTOCOL": request.version,
00275             "wsgi.version": (1, 0),
00276             "wsgi.url_scheme": request.protocol,
00277             "wsgi.input": BytesIO(escape.utf8(request.body)),
00278             "wsgi.errors": sys.stderr,
00279             "wsgi.multithread": False,
00280             "wsgi.multiprocess": True,
00281             "wsgi.run_once": False,
00282         }
00283         if "Content-Type" in request.headers:
00284             environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
00285         if "Content-Length" in request.headers:
00286             environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
00287         for key, value in request.headers.iteritems():
00288             environ["HTTP_" + key.replace("-", "_").upper()] = value
00289         return environ
00290 
00291     def _log(self, status_code, request):
00292         if status_code < 400:
00293             log_method = logging.info
00294         elif status_code < 500:
00295             log_method = logging.warning
00296         else:
00297             log_method = logging.error
00298         request_time = 1000.0 * request.request_time()
00299         summary = request.method + " " + request.uri + " (" + \
00300             request.remote_ip + ")"
00301         log_method("%d %s %.2fms", status_code, summary, request_time)


roswww
Author(s): Jonathan Mace
autogenerated on Thu Jan 2 2014 11:53:30