Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
wsgi.py269 linesDownload Raw Back to tornado
1#
2# Copyright 2009 Facebook
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8#     http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16"""WSGI support for the Tornado web framework.
17
18WSGI is the Python standard for web servers, and allows for interoperability
19between Tornado and other Python web frameworks and servers.
20
21This module provides WSGI support via the `WSGIContainer` class, which
22makes it possible to run applications using other WSGI frameworks on
23the Tornado HTTP server. The reverse is not supported; the Tornado
24`.Application` and `.RequestHandler` classes are designed for use with
25the Tornado `.HTTPServer` and cannot be used in a generic WSGI
26container.
27
28"""
29
30import concurrent.futures
31from io import BytesIO
32import tornado
33import sys
34
35from tornado.concurrent import dummy_executor
36from tornado import escape
37from tornado import httputil
38from tornado.ioloop import IOLoop
39from tornado.log import access_log
40
41from typing import List, Tuple, Optional, Callable, Any, Dict
42from types import TracebackType
43import typing
44
45if typing.TYPE_CHECKING:
46    from typing import Type  # noqa: F401
47    from _typeshed.wsgi import WSGIApplication as WSGIAppType  # noqa: F401
48
49
50# PEP 3333 specifies that WSGI on python 3 generally deals with byte strings
51# that are smuggled inside objects of type unicode (via the latin1 encoding).
52# This function is like those in the tornado.escape module, but defined
53# here to minimize the temptation to use it in non-wsgi contexts.
54def to_wsgi_str(s: bytes) -> str:
55    assert isinstance(s, bytes)
56    return s.decode("latin1")
57
58
59class WSGIContainer:
60    r"""Makes a WSGI-compatible application runnable on Tornado's HTTP server.
61
62    .. warning::
63
64       WSGI is a *synchronous* interface, while Tornado's concurrency model
65       is based on single-threaded *asynchronous* execution.  Many of Tornado's
66       distinguishing features are not available in WSGI mode, including efficient
67       long-polling and websockets. The primary purpose of `WSGIContainer` is
68       to support both WSGI applications and native Tornado ``RequestHandlers`` in
69       a single process. WSGI-only applications are likely to be better off
70       with a dedicated WSGI server such as ``gunicorn`` or ``uwsgi``.
71
72    Wrap a WSGI application in a `WSGIContainer` to make it implement the Tornado
73    `.HTTPServer` ``request_callback`` interface.  The `WSGIContainer` object can
74    then be passed to classes from the `tornado.routing` module,
75    `tornado.web.FallbackHandler`, or to `.HTTPServer` directly.
76
77    This class is intended to let other frameworks (Django, Flask, etc)
78    run on the Tornado HTTP server and I/O loop.
79
80    Realistic usage will be more complicated, but the simplest possible example uses a
81    hand-written WSGI application with `.HTTPServer`::
82
83        def simple_app(environ, start_response):
84            status = "200 OK"
85            response_headers = [("Content-type", "text/plain")]
86            start_response(status, response_headers)
87            return [b"Hello world!\n"]
88
89        async def main():
90            container = tornado.wsgi.WSGIContainer(simple_app)
91            http_server = tornado.httpserver.HTTPServer(container)
92            http_server.listen(8888)
93            await asyncio.Event().wait()
94
95        asyncio.run(main())
96
97    The recommended pattern is to use the `tornado.routing` module to set up routing
98    rules between your WSGI application and, typically, a `tornado.web.Application`.
99    Alternatively, `tornado.web.Application` can be used as the top-level router
100    and `tornado.web.FallbackHandler` can embed a `WSGIContainer` within it.
101
102    If the ``executor`` argument is provided, the WSGI application will be executed
103    on that executor. This must be an instance of `concurrent.futures.Executor`,
104    typically a ``ThreadPoolExecutor`` (``ProcessPoolExecutor`` is not supported).
105    If no ``executor`` is given, the application will run on the event loop thread in
106    Tornado 6.3; this will change to use an internal thread pool by default in
107    Tornado 7.0.
108
109    .. warning::
110       By default, the WSGI application is executed on the event loop's thread. This
111       limits the server to one request at a time (per process), making it less scalable
112       than most other WSGI servers. It is therefore highly recommended that you pass
113       a ``ThreadPoolExecutor`` when constructing the `WSGIContainer`, after verifying
114       that your application is thread-safe. The default will change to use a
115       ``ThreadPoolExecutor`` in Tornado 7.0.
116
117    .. versionadded:: 6.3
118       The ``executor`` parameter.
119
120    .. deprecated:: 6.3
121       The default behavior of running the WSGI application on the event loop thread
122       is deprecated and will change in Tornado 7.0 to use a thread pool by default.
123    """
124
125    def __init__(
126        self,
127        wsgi_application: "WSGIAppType",
128        executor: Optional[concurrent.futures.Executor] = None,
129    ) -> None:
130        self.wsgi_application = wsgi_application
131        self.executor = dummy_executor if executor is None else executor
132
133    def __call__(self, request: httputil.HTTPServerRequest) -> None:
134        IOLoop.current().spawn_callback(self.handle_request, request)
135
136    async def handle_request(self, request: httputil.HTTPServerRequest) -> None:
137        data = {}  # type: Dict[str, Any]
138        response = []  # type: List[bytes]
139
140        def start_response(
141            status: str,
142            headers: List[Tuple[str, str]],
143            exc_info: Optional[
144                Tuple[
145                    "Optional[Type[BaseException]]",
146                    Optional[BaseException],
147                    Optional[TracebackType],
148                ]
149            ] = None,
150        ) -> Callable[[bytes], Any]:
151            data["status"] = status
152            data["headers"] = headers
153            return response.append
154
155        loop = IOLoop.current()
156        app_response = await loop.run_in_executor(
157            self.executor,
158            self.wsgi_application,
159            self.environ(request),
160            start_response,
161        )
162        try:
163            app_response_iter = iter(app_response)
164
165            def next_chunk() -> Optional[bytes]:
166                try:
167                    return next(app_response_iter)
168                except StopIteration:
169                    # StopIteration is special and is not allowed to pass through
170                    # coroutines normally.
171                    return None
172
173            while True:
174                chunk = await loop.run_in_executor(self.executor, next_chunk)
175                if chunk is None:
176                    break
177                response.append(chunk)
178        finally:
179            if hasattr(app_response, "close"):
180                app_response.close()  # type: ignore
181        body = b"".join(response)
182        if not data:
183            raise Exception("WSGI app did not call start_response")
184
185        status_code_str, reason = data["status"].split(" ", 1)
186        status_code = int(status_code_str)
187        headers = data["headers"]  # type: List[Tuple[str, str]]
188        header_set = {k.lower() for (k, v) in headers}
189        body = escape.utf8(body)
190        if status_code != 304:
191            if "content-length" not in header_set:
192                headers.append(("Content-Length", str(len(body))))
193            if "content-type" not in header_set:
194                headers.append(("Content-Type", "text/html; charset=UTF-8"))
195        if "server" not in header_set:
196            headers.append(("Server", "TornadoServer/%s" % tornado.version))
197
198        start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason)
199        header_obj = httputil.HTTPHeaders()
200        for key, value in headers:
201            header_obj.add(key, value)
202        assert request.connection is not None
203        request.connection.write_headers(start_line, header_obj, chunk=body)
204        request.connection.finish()
205        self._log(status_code, request)
206
207    def environ(self, request: httputil.HTTPServerRequest) -> Dict[str, Any]:
208        """Converts a `tornado.httputil.HTTPServerRequest` to a WSGI environment.
209
210        .. versionchanged:: 6.3
211           No longer a static method.
212        """
213        hostport = request.host.split(":")
214        if len(hostport) == 2:
215            host = hostport[0]
216            port = int(hostport[1])
217        else:
218            host = request.host
219            port = 443 if request.protocol == "https" else 80
220        environ = {
221            "REQUEST_METHOD": request.method,
222            "SCRIPT_NAME": "",
223            "PATH_INFO": to_wsgi_str(
224                escape.url_unescape(request.path, encoding=None, plus=False)
225            ),
226            "QUERY_STRING": request.query,
227            "REMOTE_ADDR": request.remote_ip,
228            "SERVER_NAME": host,
229            "SERVER_PORT": str(port),
230            "SERVER_PROTOCOL": request.version,
231            "wsgi.version": (1, 0),
232            "wsgi.url_scheme": request.protocol,
233            "wsgi.input": BytesIO(escape.utf8(request.body)),
234            "wsgi.errors": sys.stderr,
235            "wsgi.multithread": self.executor is not dummy_executor,
236            "wsgi.multiprocess": True,
237            "wsgi.run_once": False,
238        }
239        if "Content-Type" in request.headers:
240            environ["CONTENT_TYPE"] = request.headers.pop("Content-Type")
241        if "Content-Length" in request.headers:
242            environ["CONTENT_LENGTH"] = request.headers.pop("Content-Length")
243        for key, value in request.headers.items():
244            environ["HTTP_" + key.replace("-", "_").upper()] = value
245        return environ
246
247    def _log(self, status_code: int, request: httputil.HTTPServerRequest) -> None:
248        if status_code < 400:
249            log_method = access_log.info
250        elif status_code < 500:
251            log_method = access_log.warning
252        else:
253            log_method = access_log.error
254        request_time = 1000.0 * request.request_time()
255        assert request.method is not None
256        assert request.uri is not None
257        summary = (
258            request.method  # type: ignore[operator]
259            + " "
260            + request.uri
261            + " ("
262            + request.remote_ip
263            + ")"
264        )
265        log_method("%d %s %.2fms", status_code, summary, request_time)
266
267
268HTTPRequest = httputil.HTTPServerRequest
269 
codekingpro/portable-devtools · Team Ai