├── tests ├── __init__.py ├── static │ └── text.txt ├── test_wsgi.py ├── test_inbuf_overflow.py ├── test_max_request_body_size.py ├── test_errors.py ├── test_static.py ├── test_start_response.py ├── base.py └── test_environ.py ├── aiohttp_wsgi ├── py.typed ├── utils.py ├── __init__.py ├── __main__.py └── wsgi.py ├── docs ├── index.rst ├── wsgi.rst ├── main.rst ├── _include │ └── links.rst ├── contributing.rst ├── installation.rst ├── changelog.rst └── conf.py ├── readthedocs.yml ├── .gitignore ├── setup.cfg ├── .github └── workflows │ ├── python-publish.yml │ └── python-package.yml ├── README.rst ├── setup.py └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aiohttp_wsgi/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/static/text.txt: -------------------------------------------------------------------------------- 1 | Test file -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: aiohttp_wsgi 2 | -------------------------------------------------------------------------------- /docs/wsgi.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: aiohttp_wsgi.wsgi 2 | -------------------------------------------------------------------------------- /docs/main.rst: -------------------------------------------------------------------------------- 1 | .. automodule:: aiohttp_wsgi.__main__ 2 | -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | python: 4 | version: 3.6 5 | install: 6 | - path: . 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | *.pyo 4 | __pycache__ 5 | Thumbs.db 6 | /venv 7 | .coverage 8 | *.log 9 | *.egg-info 10 | /dist 11 | /build 12 | /.cache 13 | /docs/_build 14 | -------------------------------------------------------------------------------- /aiohttp_wsgi/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple, Union 2 | 3 | 4 | def parse_sockname(sockname: Union[Tuple, str]) -> Tuple[str, str]: 5 | if isinstance(sockname, tuple): 6 | return sockname[0], str(sockname[1]) 7 | return "unix", sockname 8 | -------------------------------------------------------------------------------- /tests/test_wsgi.py: -------------------------------------------------------------------------------- 1 | from wsgiref.validate import validator 2 | from tests.base import AsyncTestCase, noop_application 3 | 4 | 5 | validator_application = validator(noop_application) 6 | 7 | 8 | class EnvironTest(AsyncTestCase): 9 | 10 | def testValidWsgi(self) -> None: 11 | with self.run_server(validator_application) as client: 12 | client.assert_response() 13 | -------------------------------------------------------------------------------- /docs/_include/links.rst: -------------------------------------------------------------------------------- 1 | .. _contributors: https://github.com/etianen/aiohttp-wsgi/graphs/contributors 2 | .. _documentation: https://aiohttp-wsgi.readthedocs.io/ 3 | .. _Dave Hall: http://etianen.com/ 4 | .. _Django: https://www.djangoproject.com/ 5 | .. _Flask: http://flask.pocoo.org/ 6 | .. _GitHub: http://github.com/etianen/aiohttp-wsgi 7 | .. _issue tracking: https://github.com/etianen/aiohttp-wsgi/issues 8 | .. _PEP3333: https://www.python.org/dev/peps/pep-3333/ 9 | .. _pip: https://pip.pypa.io 10 | .. _source code: https://github.com/etianen/aiohttp-wsgi 11 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Bug reports, bug fixes, and new features are always welcome. Please raise issues on `GitHub`_, and submit pull requests for any new code. 5 | 6 | 7 | Testing 8 | ------- 9 | 10 | It's recommended to test :mod:`aiohttp_wsgi` in a virtual environment using :mod:`venv`. 11 | 12 | Run the test suite using ``unittest``: 13 | 14 | .. code:: bash 15 | 16 | python -m unittest discover tests 17 | 18 | 19 | Contributors 20 | ------------ 21 | 22 | :mod:`aiohttp_wsgi` was developed by `Dave Hall`_ and other `contributors`_. 23 | 24 | 25 | .. include:: /_include/links.rst 26 | -------------------------------------------------------------------------------- /tests/test_inbuf_overflow.py: -------------------------------------------------------------------------------- 1 | from tests.base import AsyncTestCase, streaming_request_body, echo_application 2 | 3 | 4 | class InbufOverflowTest(AsyncTestCase): 5 | 6 | def testInbufOverflow(self) -> None: 7 | with self.run_server(echo_application, inbuf_overflow=3) as client: 8 | response = client.request(data="foobar") 9 | self.assertEqual(response.content, b"foobar") 10 | 11 | def testInbufOverflowStreaming(self) -> None: 12 | with self.run_server(echo_application, inbuf_overflow=20) as client: 13 | response = client.request(data=streaming_request_body()) 14 | self.assertEqual(response.content, b"foobar" * 100) 15 | -------------------------------------------------------------------------------- /tests/test_max_request_body_size.py: -------------------------------------------------------------------------------- 1 | from tests.base import AsyncTestCase, streaming_request_body, noop_application 2 | 3 | 4 | class MaxRequestBodySizeTest(AsyncTestCase): 5 | 6 | def testMaxRequestBodySize(self) -> None: 7 | with self.run_server(noop_application, max_request_body_size=3) as client: 8 | response = client.request(data="foobar") 9 | self.assertEqual(response.status, 413) 10 | 11 | def testMaxRequestBodySizeStreaming(self) -> None: 12 | with self.run_server(noop_application, max_request_body_size=20) as client: 13 | response = client.request(data=streaming_request_body()) 14 | self.assertEqual(response.status, 413) 15 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Requirements 5 | ------------ 6 | 7 | :mod:`aiohttp_wsgi` supports Python 3.5 and above. 8 | 9 | 10 | Installing 11 | ---------- 12 | 13 | It's recommended to install :mod:`aiohttp_wsgi` in a virtual environment using :mod:`venv`. 14 | 15 | Install :mod:`aiohttp_wsgi` using `pip`_. 16 | 17 | .. code:: bash 18 | 19 | pip install aiohttp_wsgi 20 | 21 | 22 | Upgrading 23 | --------- 24 | 25 | Upgrade :mod:`aiohttp_wsgi` using `pip`_: 26 | 27 | .. code:: bash 28 | 29 | pip install --upgrade aiohttp_wsgi 30 | 31 | .. important:: 32 | 33 | Check the :doc:`changelog` before upgrading. 34 | 35 | 36 | .. include:: /_include/links.rst 37 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=120 3 | exclude=venv 4 | ignore=E306 5 | 6 | [coverage:run] 7 | source = 8 | aiohttp_wsgi 9 | tests 10 | omit = 11 | **/__main__.py 12 | 13 | [coverage:report] 14 | exclude_lines = 15 | # Have to re-enable the standard pragma 16 | pragma: no cover 17 | # Don't complain if tests don't hit defensive assertion code: 18 | raise AssertionError 19 | raise NotImplementedError 20 | assert False 21 | show_missing = True 22 | skip_covered = True 23 | 24 | [mypy] 25 | files = aiohttp_wsgi, tests 26 | warn_redundant_casts = True 27 | warn_unused_ignores = True 28 | allow_redefinition = True 29 | disallow_untyped_calls = True 30 | disallow_untyped_defs = True 31 | disallow_incomplete_defs = True 32 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: '3.x' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install setuptools wheel twine 20 | - name: Build and publish 21 | env: 22 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 23 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 24 | run: | 25 | python setup.py sdist bdist_wheel 26 | twine upload dist/* 27 | -------------------------------------------------------------------------------- /tests/test_errors.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Iterable 3 | from tests.base import AsyncTestCase 4 | from aiohttp_wsgi.wsgi import WSGIEnviron, WSGIStartResponse 5 | 6 | 7 | def error_handling_application(environ: WSGIEnviron, start_response: WSGIStartResponse) -> Iterable[bytes]: 8 | try: 9 | start_response("200 OK", []) 10 | raise Exception("Boom!") 11 | except Exception: 12 | start_response("509 Boom", [], sys.exc_info()) # type: ignore 13 | return [b"Boom!"] 14 | 15 | 16 | class ErrorsTest(AsyncTestCase): 17 | 18 | def testErrorHandling(self) -> None: 19 | with self.run_server(error_handling_application) as client: 20 | response = client.request() 21 | self.assertEqual(response.status, 509) 22 | self.assertEqual(response.content, b"Boom!") 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | aiohttp-wsgi 2 | ============ 3 | 4 | WSGI adapter for `aiohttp `_. 5 | 6 | **aiohttp-wsgi** is actively maintained and used in prodution, with any bug reports or feature requests answered promptly. It's a small, stable library, so can go a long time without new releases or updates! 7 | 8 | 9 | Features 10 | -------- 11 | 12 | - Run WSGI applications (e.g. `Django `_, `Flask `_) on `aiohttp `_. 13 | - Handle thousands of client connections, using `asyncio `_. 14 | - Add `websockets `_ to your existing Python web app! 15 | 16 | 17 | Resources 18 | --------- 19 | 20 | - `Documentation `_ is on Read the Docs. 21 | - `Issue tracking `_ and `source code `_ is on GitHub. 22 | -------------------------------------------------------------------------------- /aiohttp_wsgi/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | aiohttp-wsgi 3 | ============ 4 | 5 | WSGI adapter for :ref:`aiohttp `. 6 | 7 | 8 | Features 9 | -------- 10 | 11 | - Run WSGI applications (e.g. `Django`_, `Flask`_) on :ref:`aiohttp `. 12 | - Handle thousands of client connections, using :mod:`asyncio`. 13 | - Add :ref:`websockets ` to your existing Python web app! 14 | 15 | 16 | Resources 17 | --------- 18 | 19 | - `Documentation`_ is on Read the Docs. 20 | - `Issue tracking`_ and `source code`_ is on GitHub. 21 | 22 | 23 | Usage 24 | ----- 25 | 26 | .. toctree:: 27 | :maxdepth: 1 28 | 29 | installation 30 | wsgi 31 | main 32 | 33 | 34 | More information 35 | ---------------- 36 | 37 | .. toctree:: 38 | :maxdepth: 1 39 | 40 | contributing 41 | changelog 42 | 43 | 44 | .. include:: /_include/links.rst 45 | """ 46 | 47 | try: 48 | import aiohttp # noqa 49 | except ImportError: # pragma: no cover 50 | # The top-level API requires aiohttp, which might not be present if setup.py 51 | # is importing aiohttp_wsgi to get __version__. 52 | pass 53 | else: 54 | from aiohttp_wsgi.wsgi import WSGIHandler, serve # noqa 55 | 56 | 57 | __version__ = "0.10.0" 58 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | env: 9 | PYTHONDEVMODE: 1 10 | strategy: 11 | matrix: 12 | python-version: [3.6, 3.7, 3.8, 3.9, "3.10"] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | python -m pip install flake8 mypy coverage sphinx -e . 23 | - name: Lint with flake8 24 | run: | 25 | flake8 26 | - name: Check with mypy 27 | run: | 28 | mypy --no-incremental --warn-unused-configs 29 | - name: Test with unittest 30 | run: | 31 | coverage run -m unittest discover tests 32 | # Since we generate dynamic docstrings, ensure nothing crashes when they're stripped out in optimize mode. 33 | PYTHONOPTIMIZE=2 python -m unittest discover tests 34 | coverage report --fail-under=100 35 | - name: Build docs 36 | run: | 37 | (cd docs && sphinx-build -W . _build) 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import aiohttp_wsgi 3 | 4 | 5 | setup( 6 | name="aiohttp-wsgi", 7 | version=aiohttp_wsgi.__version__, 8 | license="BSD", 9 | description="WSGI adapter for aiohttp.", 10 | author="Dave Hall", 11 | author_email="dave@etianen.com", 12 | url="https://github.com/etianen/aiohttp-wsgi", 13 | packages=find_packages(exclude=("tests",)), 14 | package_data={"aiohttp_wsgi": ["py.typed"]}, 15 | install_requires=[ 16 | "aiohttp>=3.4,<4", 17 | ], 18 | entry_points={ 19 | "console_scripts": ["aiohttp-wsgi-serve=aiohttp_wsgi.__main__:main"], 20 | }, 21 | classifiers=[ 22 | "Development Status :: 4 - Beta", 23 | "Environment :: Web Environment", 24 | "Intended Audience :: Developers", 25 | "License :: OSI Approved :: BSD License", 26 | "Operating System :: OS Independent", 27 | "Programming Language :: Python", 28 | "Programming Language :: Python :: 3", 29 | "Programming Language :: Python :: 3.6", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Programming Language :: Python :: 3.10", 34 | "Framework :: Django", 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /tests/test_static.py: -------------------------------------------------------------------------------- 1 | import os 2 | from tests.base import AsyncTestCase, noop_application 3 | 4 | 5 | STATIC = (("/static", os.path.join(os.path.dirname(__file__), "static")),) 6 | 7 | 8 | class StaticTest(AsyncTestCase): 9 | 10 | def testStaticMiss(self) -> None: 11 | with self.run_server(noop_application, static=STATIC) as client: 12 | response = client.request() 13 | self.assertEqual(response.status, 200) 14 | self.assertEqual(response.content, b"") 15 | 16 | def testStaticHit(self) -> None: 17 | with self.run_server(noop_application, static=STATIC) as client: 18 | response = client.request(path="/static/text.txt") 19 | self.assertEqual(response.status, 200) 20 | self.assertEqual(response.content, b"Test file") 21 | 22 | def testStaticHitMissing(self) -> None: 23 | with self.run_server(noop_application, static=STATIC) as client: 24 | response = client.request(path="/static/missing.txt") 25 | self.assertEqual(response.status, 404) 26 | 27 | def testStaticHitCors(self) -> None: 28 | with self.run_server(noop_application, static=STATIC, static_cors="*") as client: 29 | response = client.request(path="/static/text.txt") 30 | self.assertEqual(response.status, 200) 31 | self.assertEqual(response.content, b"Test file") 32 | self.assertEqual(response.headers["Access-Control-Allow-Origin"], "*") 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, David Hall. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of David Hall nor the names of its contributors may be 15 | used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /tests/test_start_response.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | from tests.base import AsyncTestCase 3 | from aiohttp_wsgi.wsgi import WSGIEnviron, WSGIStartResponse 4 | 5 | 6 | CHUNK = b"foobar" * 1024 7 | CHUNK_COUNT = 64 8 | RESPONSE_CONTENT = CHUNK * CHUNK_COUNT 9 | 10 | 11 | def start_response_application(environ: WSGIEnviron, start_response: WSGIStartResponse) -> Iterable[bytes]: 12 | start_response("201 Created", [ 13 | ("Foo", "Bar"), 14 | ("Foo", "Baz"), 15 | ]) 16 | return [b"foobar"] 17 | 18 | 19 | def streaming_response_application(environ: WSGIEnviron, start_response: WSGIStartResponse) -> Iterable[bytes]: 20 | start_response("200 OK", []) 21 | for _ in range(CHUNK_COUNT): 22 | yield CHUNK 23 | 24 | 25 | def streaming_response_write_application(environ: WSGIEnviron, start_response: WSGIStartResponse) -> Iterable[bytes]: 26 | write = start_response("200 OK", []) 27 | for _ in range(CHUNK_COUNT): 28 | write(CHUNK) 29 | return [] 30 | 31 | 32 | class StartResponseTest(AsyncTestCase): 33 | 34 | def testStartResponse(self) -> None: 35 | with self.run_server(start_response_application) as client: 36 | response = client.request() 37 | self.assertEqual(response.status, 201) 38 | self.assertEqual(response.reason, "Created") 39 | self.assertEqual(response.headers.getall("Foo"), ["Bar", "Baz"]) 40 | self.assertEqual(response.content, b"foobar") 41 | 42 | def testStreamingResponse(self) -> None: 43 | with self.run_server(streaming_response_application) as client: 44 | response = client.request() 45 | self.assertEqual(response.content, RESPONSE_CONTENT) 46 | 47 | def testStreamingResponseWrite(self) -> None: 48 | with self.run_server(streaming_response_write_application) as client: 49 | response = client.request() 50 | self.assertEqual(response.content, RESPONSE_CONTENT) 51 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | from asyncio.base_events import Server 2 | import unittest 3 | from collections import namedtuple 4 | from contextlib import contextmanager 5 | from tempfile import NamedTemporaryFile 6 | from typing import Any, AsyncGenerator, ContextManager, Generator, Iterable 7 | import aiohttp 8 | import asyncio 9 | from aiohttp_wsgi.wsgi import run_server, WSGIEnviron, WSGIStartResponse 10 | from aiohttp_wsgi.utils import parse_sockname 11 | 12 | 13 | Response = namedtuple("Response", ("status", "reason", "headers", "content")) 14 | 15 | 16 | class TestClient: 17 | 18 | def __init__( 19 | self, 20 | test_case: unittest.TestCase, 21 | loop: asyncio.AbstractEventLoop, 22 | host: str, 23 | port: str, 24 | session: aiohttp.ClientSession, 25 | ) -> None: 26 | self._test_case = test_case 27 | self._loop = loop 28 | self._host = host 29 | self._port = port 30 | self._session = session 31 | 32 | def request(self, method: str = "GET", path: str = "/", **kwargs: Any) -> Response: 33 | uri = f"http://{self._host}:{self._port}{path}" 34 | response = self._loop.run_until_complete(self._session.request(method, uri, **kwargs)) 35 | return Response( 36 | response.status, 37 | response.reason, 38 | response.headers, 39 | self._loop.run_until_complete(response.read()), 40 | ) 41 | 42 | def assert_response(self, *args: Any, data: bytes = b"", **kwargs: Any) -> None: 43 | response = self.request(*args, data=data, **kwargs) 44 | self._test_case.assertEqual(response.status, 200) 45 | 46 | 47 | def noop_application(environ: WSGIEnviron, start_response: WSGIStartResponse) -> Iterable[bytes]: 48 | start_response("200 OK", [ 49 | ("Content-Type", "text/plain"), 50 | ]) 51 | return [] 52 | 53 | 54 | def echo_application(environ: WSGIEnviron, start_response: WSGIStartResponse) -> Iterable[bytes]: 55 | start_response("200 OK", [ 56 | ("Content-Type", "text/plain"), 57 | ]) 58 | return [environ["wsgi.input"].read()] 59 | 60 | 61 | async def streaming_request_body() -> AsyncGenerator: 62 | for _ in range(100): 63 | yield b"foobar" 64 | 65 | 66 | class AsyncTestCase(unittest.TestCase): 67 | 68 | @contextmanager 69 | def _run_server(self, *args: Any, **kwargs: Any) -> Generator[TestClient, None, None]: 70 | with run_server(*args, **kwargs) as (loop, site): 71 | assert site._server is not None 72 | assert isinstance(site._server, Server) 73 | assert site._server.sockets is not None 74 | host, port = parse_sockname(site._server.sockets[0].getsockname()) 75 | async def create_session() -> aiohttp.ClientSession: 76 | if host == "unix": 77 | connector: aiohttp.BaseConnector = aiohttp.UnixConnector(path=port) 78 | else: 79 | connector = aiohttp.TCPConnector() 80 | return aiohttp.ClientSession(connector=connector) 81 | session = loop.run_until_complete(create_session()) 82 | try: 83 | yield TestClient(self, loop, host, port, session) 84 | finally: 85 | loop.run_until_complete(session.close()) 86 | 87 | def run_server(self, *args: Any, **kwargs: Any) -> ContextManager[TestClient]: 88 | return self._run_server( 89 | *args, 90 | host="127.0.0.1", 91 | port="0", 92 | **kwargs, 93 | ) 94 | 95 | def run_server_unix(self, *args: Any, **kwargs: Any) -> ContextManager[TestClient]: 96 | socket_file = NamedTemporaryFile() 97 | socket_file.close() 98 | return self._run_server( 99 | *args, 100 | unix_socket=socket_file.name, 101 | **kwargs 102 | ) 103 | -------------------------------------------------------------------------------- /aiohttp_wsgi/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Command line interface (CLI) 3 | ============================ 4 | 5 | If you don't need to add :ref:`websockets ` or 6 | :ref:`async request handlers ` to your app, but still want to run your WSGI app on the 7 | :mod:`asyncio` event loop, :mod:`aiohttp_wsgi` provides a simple command line interface. 8 | 9 | 10 | Example usage 11 | ------------- 12 | 13 | Serve a WSGI application called ``application``, located in the ``your_project.wsgi`` module: 14 | 15 | .. code:: bash 16 | 17 | aiohttp-wsgi-serve your_project.wsgi:application 18 | 19 | Serve a WSGI application and include a static file directory. 20 | 21 | .. code:: bash 22 | 23 | aiohttp-wsgi-serve your_project.wsgi:application --static /static=./static 24 | 25 | 26 | Command reference 27 | ----------------- 28 | 29 | You can view this reference at any time with ``aiohttp-wsgi-serve --help``. 30 | 31 | .. code:: bash 32 | 33 | {help} 34 | 35 | 36 | .. include:: /_include/links.rst 37 | """ 38 | import argparse 39 | import logging 40 | import os 41 | import sys 42 | from importlib import import_module 43 | from typing import Any, Callable, Tuple 44 | import aiohttp_wsgi 45 | from aiohttp_wsgi.wsgi import serve, DEFAULTS, HELP 46 | 47 | 48 | logger = logging.getLogger(__name__) 49 | 50 | 51 | parser = argparse.ArgumentParser( 52 | prog="aiohttp-wsgi-serve", 53 | description="Run a WSGI application.", 54 | ) 55 | 56 | 57 | def add_argument(name: str, *aliases: str, **kwargs: Any) -> None: 58 | varname = name.strip("-").replace("-", "_") 59 | # Format help. 60 | kwargs.setdefault("help", HELP.get(varname, "").replace("``", "")) 61 | assert kwargs["help"] 62 | # Parse action. 63 | kwargs.setdefault("action", "store") 64 | if kwargs["action"] in ("append", "count"): 65 | kwargs["help"] += " Can be specified multiple times." 66 | if kwargs["action"] == "count": 67 | kwargs.setdefault("default", 0) 68 | if kwargs["action"] in ("append", "store"): 69 | kwargs.setdefault("default", DEFAULTS.get(varname)) 70 | kwargs.setdefault("type", type(kwargs["default"])) 71 | assert not isinstance(None, kwargs["type"]) 72 | parser.add_argument(name, *aliases, **kwargs) 73 | 74 | 75 | add_argument( 76 | "application", 77 | metavar="module:application", 78 | type=str, 79 | ) 80 | add_argument( 81 | "--host", 82 | type=str, 83 | action="append", 84 | ) 85 | add_argument( 86 | "--port", 87 | "-p", 88 | ) 89 | add_argument( 90 | "--unix-socket", 91 | type=str, 92 | ) 93 | add_argument( 94 | "--unix-socket-perms", 95 | ) 96 | add_argument( 97 | "--backlog", 98 | ) 99 | add_argument( 100 | "--static", 101 | action="append", 102 | default=[], 103 | type=str, 104 | help=( 105 | "Static route mappings in the form 'path=directory'. " 106 | "`path` must start with a slash, but not end with a slash." 107 | ), 108 | ) 109 | add_argument( 110 | "--static-cors", 111 | type=str, 112 | ) 113 | add_argument( 114 | "--script-name", 115 | ) 116 | add_argument( 117 | "--url-scheme", 118 | type=str, 119 | ) 120 | add_argument( 121 | "--threads", 122 | ) 123 | add_argument( 124 | "--inbuf-overflow", 125 | ) 126 | add_argument( 127 | "--max-request-body-size", 128 | ) 129 | add_argument( 130 | "--shutdown-timeout", 131 | ) 132 | add_argument( 133 | "--verbose", 134 | "-v", 135 | action="count", 136 | help="Increase verbosity.", 137 | ) 138 | add_argument( 139 | "--quiet", 140 | "-q", 141 | action="count", 142 | help="Decrease verbosity.", 143 | ) 144 | add_argument( 145 | "--version", 146 | action="version", 147 | help="Display version information.", 148 | version=f"aiohttp-wsgi v{aiohttp_wsgi.__version__}", 149 | ) 150 | 151 | 152 | def import_func(func: str) -> Callable: 153 | assert ":" in func, f"{func!r} should have format 'module:callable'" 154 | module_name, func_name = func.split(":", 1) 155 | module = import_module(module_name) 156 | func = getattr(module, func_name) 157 | return func 158 | 159 | 160 | def parse_static_item(static_item: str) -> Tuple[str, str]: 161 | assert "=" in static_item, f"{static_item!r} should have format 'path=directory'" 162 | return tuple(static_item.split("=", 1)) # type: ignore 163 | 164 | 165 | def main() -> None: 166 | sys.path.insert(0, os.getcwd()) 167 | # Parse the args. 168 | kwargs = vars(parser.parse_args(sys.argv[1:])) 169 | application = import_func(kwargs.pop("application")) 170 | static = list(map(parse_static_item, kwargs.pop("static"))) 171 | # Set up logging. 172 | verbosity = (kwargs.pop("verbose") - kwargs.pop("quiet")) * 10 173 | logging.basicConfig(level=max(logging.ERROR - verbosity, logging.DEBUG), format="%(message)s") 174 | logging.getLogger("aiohttp").setLevel(max(logging.INFO - verbosity, logging.DEBUG)) 175 | logger.setLevel(max(logging.INFO - verbosity, logging.DEBUG)) 176 | # Serve! 177 | serve(application, static=static, **kwargs) 178 | 179 | 180 | if __debug__: 181 | import textwrap 182 | __doc__ = __doc__.format(help=textwrap.indent(parser.format_help(), " "), **HELP) 183 | -------------------------------------------------------------------------------- /tests/test_environ.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import ThreadPoolExecutor 2 | from functools import wraps 3 | from io import TextIOBase 4 | from typing import Callable, Iterable 5 | from tests.base import AsyncTestCase, noop_application 6 | from aiohttp_wsgi.wsgi import WSGIEnviron, WSGIStartResponse, WSGIApplication 7 | 8 | 9 | def environ_application(func: Callable[[WSGIEnviron], None]) -> WSGIApplication: 10 | @wraps(func) 11 | def do_environ_application(environ: WSGIEnviron, start_response: WSGIStartResponse) -> Iterable[bytes]: 12 | func(environ) 13 | return noop_application(environ, start_response) 14 | return do_environ_application 15 | 16 | 17 | @environ_application 18 | def assert_environ(environ: WSGIEnviron) -> None: 19 | assert environ["REQUEST_METHOD"] == "GET" 20 | assert environ["SCRIPT_NAME"] == "" 21 | assert environ["PATH_INFO"] == "/" 22 | assert environ["CONTENT_TYPE"] == "application/octet-stream" 23 | assert environ["CONTENT_LENGTH"] == "0" 24 | assert environ["SERVER_NAME"] == "127.0.0.1" 25 | assert int(environ["SERVER_PORT"]) > 0 26 | assert environ["REMOTE_ADDR"] == "127.0.0.1" 27 | assert environ["REMOTE_HOST"] == "127.0.0.1" 28 | assert int(environ["REMOTE_PORT"]) > 0 29 | assert environ["SERVER_PROTOCOL"] == "HTTP/1.1" 30 | assert environ["HTTP_FOO"] == "bar" 31 | assert environ["wsgi.version"] == (1, 0) 32 | assert environ["wsgi.url_scheme"] == "http" 33 | assert isinstance(environ["wsgi.errors"], TextIOBase) 34 | assert environ["wsgi.multithread"] 35 | assert not environ["wsgi.multiprocess"] 36 | assert not environ["wsgi.run_once"] 37 | assert isinstance(environ["asyncio.executor"], ThreadPoolExecutor) 38 | assert "aiohttp.request" in environ 39 | 40 | 41 | @environ_application 42 | def assert_environ_post(environ: WSGIEnviron) -> None: 43 | assert environ["REQUEST_METHOD"] == "POST" 44 | assert environ["CONTENT_TYPE"] == "text/plain" 45 | assert environ["CONTENT_LENGTH"] == "6" 46 | assert environ["wsgi.input"].read() == b"foobar" 47 | 48 | 49 | @environ_application 50 | def assert_environ_url_scheme(environ: WSGIEnviron) -> None: 51 | assert environ["wsgi.url_scheme"] == "https" 52 | 53 | 54 | @environ_application 55 | def assert_environ_unix_socket(environ: WSGIEnviron) -> None: 56 | assert environ["SERVER_NAME"] == "unix" 57 | assert environ["SERVER_PORT"].startswith("/") 58 | assert environ["REMOTE_HOST"] == "unix" 59 | assert environ["REMOTE_PORT"] == "" 60 | 61 | 62 | @environ_application 63 | def assert_environ_subdir(environ: WSGIEnviron) -> None: 64 | assert environ["SCRIPT_NAME"] == "" 65 | assert environ["PATH_INFO"] == "/foo" 66 | 67 | 68 | @environ_application 69 | def assert_environ_root_subdir(environ: WSGIEnviron) -> None: 70 | assert environ["SCRIPT_NAME"] == "/foo" 71 | assert environ["PATH_INFO"] == "" 72 | 73 | 74 | @environ_application 75 | def assert_environ_root_subdir_slash(environ: WSGIEnviron) -> None: 76 | assert environ["SCRIPT_NAME"] == "/foo" 77 | assert environ["PATH_INFO"] == "/" 78 | 79 | 80 | @environ_application 81 | def assert_environ_root_subdir_trailing(environ: WSGIEnviron) -> None: 82 | assert environ["SCRIPT_NAME"] == "/foo" 83 | assert environ["PATH_INFO"] == "/bar" 84 | 85 | 86 | @environ_application 87 | def assert_environ_quoted_path_info(environ: WSGIEnviron) -> None: 88 | assert environ['PATH_INFO'] == "/테/스/트" 89 | assert environ['RAW_URI'] == "/%ED%85%8C%2F%EC%8A%A4%2F%ED%8A%B8" 90 | assert environ['REQUEST_URI'] == "/%ED%85%8C%2F%EC%8A%A4%2F%ED%8A%B8" 91 | 92 | 93 | class EnvironTest(AsyncTestCase): 94 | 95 | def testEnviron(self) -> None: 96 | with self.run_server(assert_environ) as client: 97 | client.assert_response(headers={ 98 | "Content-Type": "application/octet-stream", 99 | "Foo": "bar", 100 | }) 101 | 102 | def testEnvironPost(self) -> None: 103 | with self.run_server(assert_environ_post) as client: 104 | client.assert_response( 105 | method="POST", 106 | headers={"Content-Type": "text/plain"}, 107 | data=b"foobar", 108 | ) 109 | 110 | def testEnvironUrlScheme(self) -> None: 111 | with self.run_server(assert_environ_url_scheme, url_scheme="https") as client: 112 | client.assert_response() 113 | 114 | def testEnvironUnixSocket(self) -> None: 115 | with self.run_server_unix(assert_environ_unix_socket) as client: 116 | client.assert_response() 117 | 118 | def testEnvironSubdir(self) -> None: 119 | with self.run_server(assert_environ_subdir) as client: 120 | client.assert_response(path="/foo") 121 | 122 | def testEnvironRootSubdir(self) -> None: 123 | with self.run_server(assert_environ_root_subdir, script_name="/foo") as client: 124 | client.assert_response(path="/foo") 125 | 126 | def testEnvironRootSubdirSlash(self) -> None: 127 | with self.run_server(assert_environ_root_subdir_slash, script_name="/foo") as client: 128 | client.assert_response(path="/foo/") 129 | 130 | def testEnvironRootSubdirTrailing(self) -> None: 131 | with self.run_server(assert_environ_root_subdir_trailing, script_name="/foo") as client: 132 | client.assert_response(path="/foo/bar") 133 | 134 | def testQuotedPathInfo(self) -> None: 135 | with self.run_server(assert_environ_quoted_path_info) as client: 136 | client.assert_response(path="/%ED%85%8C%2F%EC%8A%A4%2F%ED%8A%B8") 137 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | aiohttp-wsgi changelog 2 | ====================== 3 | 4 | .. currentmodule:: aiohttp_wsgi 5 | 6 | 0.10.0 7 | ------ 8 | 9 | - Removed ``loop`` argument from :class:`WSGIHandler` constructor. 10 | - Fixed tests to work with aiohttp 3.8.1+. 11 | 12 | 13 | 0.9.1 14 | ----- 15 | 16 | - **Bugfix:** Added in ``py.typed`` PEP 561 marker file. 17 | 18 | 19 | 0.9.0 20 | ----- 21 | 22 | - Added PEP 484 / PEP 561 type annotations. 23 | - **Bugfix:** Fixed crash when running with ``PYTHONOPTIMIZE=2``. 24 | 25 | 26 | 0.8.2 27 | ----- 28 | 29 | - Added support for ``aiohttp >= 3.4`` (@michael-k). 30 | 31 | 32 | 0.8.1 33 | ----- 34 | 35 | - Added ``static_cors`` argument to :func:`serve()`, allowing CORS to be configured for static files. 36 | - Added ``--static-cors`` argument to :doc:`aiohttp-wsgi-serve
` command line interface. 37 | 38 | 39 | 0.8.0 40 | ----- 41 | 42 | - Added new :func:`serve()` helper for simple WSGI applications that don't need :ref:`websockets ` or :ref:`async request handlers ` (@etianen). 43 | - Updated :mod:`aiohttp` dependency to ``>=3`` (@etianen). 44 | - Improved error message for invalid hop-by-hop headers (@chriskuehl). 45 | - **Breaking:** Dropped support for Python 3.4 (@etianen). 46 | 47 | 48 | 0.7.1 49 | ----- 50 | 51 | - Compatibility with aiohttp>=2.3.1 (@etianen). 52 | 53 | 54 | 0.7.0 55 | ----- 56 | 57 | - Compatibility with aiohttp>=2 (@etianen). 58 | - Added ``"RAW_URI"`` and ``"REQUEST_URI"`` keys to the environ dict, allowing the original quoted path to be accessed (@dahlia). 59 | 60 | 61 | 0.6.6 62 | ----- 63 | 64 | - Python 3.4 support (@fscherf). 65 | 66 | 67 | 0.6.5 68 | ----- 69 | 70 | - Fixed bug with unicode errors in querystring (@Антон Огородников). 71 | 72 | 73 | 0.6.4 74 | ----- 75 | 76 | - Updating aiohttp dependency to >= 1.2. 77 | - Fixing aiohttp deprecation warnings. 78 | 79 | 80 | 0.6.3 81 | ----- 82 | 83 | - Updating aiohttp dependency to >= 1.0. 84 | 85 | 86 | 0.6.2 87 | ----- 88 | 89 | - Fixing incorrect quoting of ``PATH_INFO`` and ``SCRIPT_NAME`` in environ. 90 | 91 | 92 | 0.6.1 93 | ----- 94 | 95 | - Upgrading :mod:`aiohttp` dependency to >= 0.22.2. 96 | 97 | 98 | 0.6.0 99 | ----- 100 | 101 | - Fixing missing multiple headers sent from start_response. 102 | - **Breaking:** Removed outbuf_overflow setting. Responses are always buffered in memory. 103 | - **Breaking:** WSGI streaming responses are buffered fully in memory before being sent. 104 | 105 | 106 | 0.5.2 107 | ----- 108 | 109 | - Identical to 0.5.1, after PyPi release proved mysteriously broken. 110 | 111 | 112 | 0.5.1 113 | ----- 114 | 115 | - ``outbuf_overflow`` no longer creates a temporary buffer file, instead pausing the worker thread until the pending response has been flushed. 116 | 117 | 118 | 0.5.0 119 | ----- 120 | 121 | - Minimum :ref:`aiohttp ` version is now 0.21.2. 122 | - Added :doc:`aiohttp-wsgi-serve
` command line interface. 123 | - Responses over 1MB will be buffered in a temporary file. Can be configured using the ``outbuf_overflow`` argument to :class:`WSGIHandler`. 124 | - **Breaking:** Removed support for Python 3.4. 125 | - **Breaking:** Removed ``aiohttp.concurrent`` helpers, which are no longer required with Python 3.5+. 126 | - **Breaking:** Removed ``configure_server()`` and ``close_server()`` helpers. Use :class:`WSGIHandler` directly. 127 | - **Breaking:** Removed ``serve()`` helpers. Use the :doc:`command line interface
` directly. 128 | 129 | 130 | 0.4.0 131 | ----- 132 | 133 | - Requests over 512KB will be buffered in a temporary file. Can be configured using the ``inbuf_overflow`` argument to :class:`WSGIHandler`. 134 | - Minimum :ref:`aiohttp ` version is now 0.21.2. 135 | - **Breaking**: Maximum request body size is now 1GB. Can be configured using the ``max_request_body_size`` argument to :class:`WSGIHandler`. 136 | 137 | 138 | 0.3.0 139 | ----- 140 | 141 | - ``PATH_INFO`` and ``SCRIPT_NAME`` now contain URL-quoted non-ascii characters, as per `PEP3333`_. 142 | - Minimum :ref:`aiohttp ` version is now 0.19.0. 143 | - **Breaking**: Removed support for Python3.3. 144 | 145 | 146 | 0.2.6 147 | ----- 148 | 149 | - Excluded tests from distribution. 150 | 151 | 152 | 0.2.5 153 | ----- 154 | 155 | - Updated to work with breaking changes in :ref:`aiohttp ` 0.17.0. 156 | 157 | 158 | 0.2.4 159 | ----- 160 | 161 | - Workaround for error in :mod:`asyncio` debug mode on some Python versions when using a callable object, ``WSGIHandler.handle_request``. 162 | 163 | 164 | 0.2.3 165 | ----- 166 | 167 | - Fixed bug with parsing ``SCRIPT_NAME``. 168 | 169 | 170 | 0.2.2 171 | ----- 172 | 173 | - Implemented a standalone concurrent utility module for switching between the event loop and an executor. 174 | See ``aiohttp_wsgi.concurrent`` for more info. 175 | 176 | 177 | 0.2.1 178 | ----- 179 | 180 | - Added ``on_finish`` parameter to ``serve()`` and ``configure_server()``. 181 | - Improved performance and predictability of processing streaming iterators from WSGI applications. 182 | 183 | 184 | 0.2.0 185 | ----- 186 | 187 | - **BREAKING**: Removed ``WSGIMiddleware`` in favor of :class:`WSGIHandler` (required to support :ref:`aiohttp ` 0.15.0 without hacks). 188 | - Added support for :ref:`aiohttp ` 0.15.0. 189 | 190 | 191 | 0.1.2 192 | ----- 193 | 194 | - Added ``socket`` argument to ``serve()`` and ``configure_server()``. 195 | - Added ``backlog`` argument to ``serve()`` and ``configure_server()``. 196 | 197 | 198 | 0.1.1 199 | ----- 200 | 201 | - Fixed ``RuntimeError`` in :ref:`aiohttp ` (@jnurmine). 202 | - Added ``routes`` argument to ``serve()`` and ``configure_server()``. 203 | - Added ``static`` argument to ``serve()`` and ``configure_server()``. 204 | 205 | 206 | 0.1.0 207 | ----- 208 | 209 | - First experimental release. 210 | - Buffering WSGI web server with threaded workers. 211 | - Public ``configure_server()`` and ``serve()`` API. 212 | 213 | 214 | .. include:: /_include/links.rst 215 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # aophttp_wsgi documentation build configuration file, created by 5 | # sphinx-quickstart on Thu Jun 2 08:41:36 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | import aiohttp_wsgi 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = ["sphinx.ext.autodoc", "sphinx.ext.intersphinx"] 36 | 37 | intersphinx_mapping = { 38 | "python": ("https://docs.python.org/3", None), 39 | "aiohttp": ("http://aiohttp.readthedocs.io/en/stable", None), 40 | } 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = [] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # 48 | # source_suffix = ['.rst', '.md'] 49 | source_suffix = '.rst' 50 | 51 | # The encoding of source files. 52 | # 53 | # source_encoding = 'utf-8-sig' 54 | 55 | # The master toctree document. 56 | master_doc = 'index' 57 | 58 | # General information about the project. 59 | project = 'aiohttp_wsgi' 60 | copyright = '2015, Dave Hall' 61 | author = 'Dave Hall' 62 | 63 | # The version info for the project you're documenting, acts as replacement for 64 | # |version| and |release|, also used in various other places throughout the 65 | # built documents. 66 | # 67 | # The short X.Y version. 68 | version = aiohttp_wsgi.__version__ 69 | # The full version, including alpha/beta/rc tags. 70 | release = version 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | # 75 | # This is also used if you do content translation via gettext catalogs. 76 | # Usually you set "language" from the command line for these cases. 77 | language = None 78 | 79 | # There are two options for replacing |today|: either, you set today to some 80 | # non-false value, then it is used: 81 | # 82 | # today = '' 83 | # 84 | # Else, today_fmt is used as the format for a strftime call. 85 | # 86 | # today_fmt = '%B %d, %Y' 87 | 88 | # List of patterns, relative to source directory, that match files and 89 | # directories to ignore when looking for source files. 90 | # This patterns also effect to html_static_path and html_extra_path 91 | exclude_patterns = ['_build', '_include', 'Thumbs.db', '.DS_Store'] 92 | 93 | # The reST default role (used for this markup: `text`) to use for all 94 | # documents. 95 | # 96 | # default_role = None 97 | 98 | # If true, '()' will be appended to :func: etc. cross-reference text. 99 | # 100 | # add_function_parentheses = True 101 | 102 | # If true, the current module name will be prepended to all description 103 | # unit titles (such as .. function::). 104 | # 105 | # add_module_names = True 106 | 107 | # If true, sectionauthor and moduleauthor directives will be shown in the 108 | # output. They are ignored by default. 109 | # 110 | # show_authors = False 111 | 112 | # The name of the Pygments (syntax highlighting) style to use. 113 | pygments_style = 'sphinx' 114 | 115 | # A list of ignored prefixes for module index sorting. 116 | # modindex_common_prefix = [] 117 | 118 | # If true, keep warnings as "system message" paragraphs in the built documents. 119 | # keep_warnings = False 120 | 121 | suppress_warnings = ["image.nonlocal_uri"] 122 | 123 | # If true, `todo` and `todoList` produce output, else they produce nothing. 124 | todo_include_todos = False 125 | 126 | 127 | # -- Options for HTML output ---------------------------------------------- 128 | 129 | # The name for this set of Sphinx documents. 130 | # " v documentation" by default. 131 | # 132 | # html_title = 'aophttp_wsgi v0.4.0' 133 | 134 | # A shorter title for the navigation bar. Default is the same as html_title. 135 | # 136 | # html_short_title = None 137 | 138 | # The name of an image file (relative to this directory) to place at the top 139 | # of the sidebar. 140 | # 141 | # html_logo = None 142 | 143 | # The name of an image file (relative to this directory) to use as a favicon of 144 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 145 | # pixels large. 146 | # 147 | # html_favicon = None 148 | 149 | # Add any paths that contain custom static files (such as style sheets) here, 150 | # relative to this directory. They are copied after the builtin static files, 151 | # so a file named "default.css" will overwrite the builtin "default.css". 152 | html_static_path = [] 153 | 154 | # Add any extra paths that contain custom files (such as robots.txt or 155 | # .htaccess) here, relative to this directory. These files are copied 156 | # directly to the root of the documentation. 157 | # 158 | # html_extra_path = [] 159 | 160 | # If not None, a 'Last updated on:' timestamp is inserted at every page 161 | # bottom, using the given strftime format. 162 | # The empty string is equivalent to '%b %d, %Y'. 163 | # 164 | # html_last_updated_fmt = None 165 | 166 | # If true, SmartyPants will be used to convert quotes and dashes to 167 | # typographically correct entities. 168 | # 169 | # html_use_smartypants = True 170 | 171 | # Custom sidebar templates, maps document names to template names. 172 | # 173 | # html_sidebars = {} 174 | 175 | # Additional templates that should be rendered to pages, maps page names to 176 | # template names. 177 | # 178 | # html_additional_pages = {} 179 | 180 | # If false, no module index is generated. 181 | # 182 | # html_domain_indices = True 183 | 184 | # If false, no index is generated. 185 | # 186 | # html_use_index = True 187 | 188 | # If true, the index is split into individual pages for each letter. 189 | # 190 | # html_split_index = False 191 | 192 | # If true, links to the reST sources are added to the pages. 193 | # 194 | # html_show_sourcelink = True 195 | 196 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 197 | # 198 | # html_show_sphinx = True 199 | 200 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 201 | # 202 | # html_show_copyright = True 203 | 204 | # If true, an OpenSearch description file will be output, and all pages will 205 | # contain a tag referring to it. The value of this option must be the 206 | # base URL from which the finished HTML is served. 207 | # 208 | # html_use_opensearch = '' 209 | 210 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 211 | # html_file_suffix = None 212 | 213 | # Language to be used for generating the HTML full-text search index. 214 | # Sphinx supports the following languages: 215 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 216 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 217 | # 218 | # html_search_language = 'en' 219 | 220 | # A dictionary with options for the search language support, empty by default. 221 | # 'ja' uses this config value. 222 | # 'zh' user can custom change `jieba` dictionary path. 223 | # 224 | # html_search_options = {'type': 'default'} 225 | 226 | # The name of a javascript file (relative to the configuration directory) that 227 | # implements a search results scorer. If empty, the default will be used. 228 | # 229 | # html_search_scorer = 'scorer.js' 230 | 231 | # Output file base name for HTML help builder. 232 | htmlhelp_basename = 'aophttp_wsgidoc' 233 | 234 | # -- Options for LaTeX output --------------------------------------------- 235 | 236 | latex_elements = { 237 | # The paper size ('letterpaper' or 'a4paper'). 238 | # 239 | # 'papersize': 'letterpaper', 240 | 241 | # The font size ('10pt', '11pt' or '12pt'). 242 | # 243 | # 'pointsize': '10pt', 244 | 245 | # Additional stuff for the LaTeX preamble. 246 | # 247 | # 'preamble': '', 248 | 249 | # Latex figure (float) alignment 250 | # 251 | # 'figure_align': 'htbp', 252 | } 253 | 254 | # Grouping the document tree into LaTeX files. List of tuples 255 | # (source start file, target name, title, 256 | # author, documentclass [howto, manual, or own class]). 257 | latex_documents = [ 258 | (master_doc, 'aophttp_wsgi.tex', 'aophttp_wsgi Documentation', 259 | 'Dave Hall', 'manual'), 260 | ] 261 | 262 | # The name of an image file (relative to this directory) to place at the top of 263 | # the title page. 264 | # 265 | # latex_logo = None 266 | 267 | # For "manual" documents, if this is true, then toplevel headings are parts, 268 | # not chapters. 269 | # 270 | # latex_use_parts = False 271 | 272 | # If true, show page references after internal links. 273 | # 274 | # latex_show_pagerefs = False 275 | 276 | # If true, show URL addresses after external links. 277 | # 278 | # latex_show_urls = False 279 | 280 | # Documents to append as an appendix to all manuals. 281 | # 282 | # latex_appendices = [] 283 | 284 | # If false, no module index is generated. 285 | # 286 | # latex_domain_indices = True 287 | 288 | 289 | # -- Options for manual page output --------------------------------------- 290 | 291 | # One entry per manual page. List of tuples 292 | # (source start file, name, description, authors, manual section). 293 | man_pages = [ 294 | (master_doc, 'aophttp_wsgi', 'aophttp_wsgi Documentation', 295 | [author], 1) 296 | ] 297 | 298 | # If true, show URL addresses after external links. 299 | # 300 | # man_show_urls = False 301 | 302 | 303 | # -- Options for Texinfo output ------------------------------------------- 304 | 305 | # Grouping the document tree into Texinfo files. List of tuples 306 | # (source start file, target name, title, author, 307 | # dir menu entry, description, category) 308 | texinfo_documents = [ 309 | (master_doc, 'aophttp_wsgi', 'aophttp_wsgi Documentation', 310 | author, 'aophttp_wsgi', 'One line description of project.', 311 | 'Miscellaneous'), 312 | ] 313 | 314 | # Documents to append as an appendix to all manuals. 315 | # 316 | # texinfo_appendices = [] 317 | 318 | # If false, no module index is generated. 319 | # 320 | # texinfo_domain_indices = True 321 | 322 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 323 | # 324 | # texinfo_show_urls = 'footnote' 325 | 326 | # If true, do not generate a @detailmenu in the "Top" node's menu. 327 | # 328 | # texinfo_no_detailmenu = False 329 | -------------------------------------------------------------------------------- /aiohttp_wsgi/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Running a WSGI app 3 | ================== 4 | 5 | .. currentmodule:: aiohttp_wsgi 6 | 7 | :mod:`aiohttp_wsgi` allows you to run WSGI applications (e.g. `Django`_, `Flask`_) on :ref:`aiohttp `. 8 | This allows you to add async features like websockets and long-polling to an existing Python web app. 9 | 10 | .. hint:: 11 | 12 | If you don't need to add :ref:`websockets ` or 13 | :ref:`async request handlers ` to your app, but still want to run your WSGI app on the 14 | :mod:`asyncio` event loop, :mod:`aiohttp_wsgi` provides a simpler :doc:`command line interface
`. 15 | 16 | 17 | Run a web server 18 | ---------------- 19 | 20 | In order to implement a WSGI server, first import your WSGI application and wrap it in a :class:`WSGIHandler`. 21 | 22 | .. code:: python 23 | 24 | from aiohttp import web 25 | from aiohttp_wsgi import WSGIHandler 26 | from your_project.wsgi import application 27 | 28 | wsgi_handler = WSGIHandler(application) 29 | 30 | 31 | Next, create an :class:`Application ` instance and register the request handler with the 32 | application's :class:`router ` on a particular HTTP *method* and *path*: 33 | 34 | .. code:: python 35 | 36 | app = web.Application() 37 | app.router.add_route("*", "/{path_info:.*}", wsgi_handler) 38 | 39 | After that, run the application by :func:`run_app() ` call: 40 | 41 | .. code:: python 42 | 43 | web.run_app(app) 44 | 45 | See the :ref:`aiohttp.web ` documentation for information on adding 46 | :ref:`websockets ` and :ref:`async request handlers ` to your app. 47 | 48 | 49 | Serving simple WSGI apps 50 | ------------------------ 51 | 52 | If you don't need to add :ref:`websockets ` or 53 | :ref:`async request handlers ` to your app, but still want to run your WSGI app on the 54 | :mod:`asyncio` event loop, :mod:`aiohttp_wsgi` provides a simple :func:`serve()` helper. 55 | 56 | .. code:: python 57 | 58 | from aiohttp_wsgi import serve 59 | 60 | serve(application) 61 | 62 | 63 | Extra environ keys 64 | ------------------ 65 | 66 | :mod:`aiohttp_wsgi` adds the following additional keys to the WSGI environ: 67 | 68 | ``asyncio.executor`` 69 | The :class:`Executor ` running the WSGI request. 70 | 71 | ``aiohttp.request`` 72 | The raw :class:`aiohttp.web.Request` that initiated the WSGI request. Use this to access additional 73 | request :ref:`metadata `. 74 | 75 | 76 | API reference 77 | ------------- 78 | 79 | .. autoclass:: WSGIHandler 80 | :members: 81 | 82 | .. autofunction:: serve 83 | 84 | 85 | .. include:: /_include/links.rst 86 | """ 87 | import asyncio 88 | from asyncio.base_events import Server 89 | from functools import partial 90 | from io import BytesIO 91 | import logging 92 | import os 93 | import sys 94 | from concurrent.futures import Executor, ThreadPoolExecutor 95 | from contextlib import contextmanager 96 | from tempfile import SpooledTemporaryFile 97 | from typing import Any, Awaitable, IO, Callable, Dict, Generator, Iterable, List, Optional, Tuple 98 | from wsgiref.util import is_hop_by_hop 99 | from aiohttp.web import ( 100 | Application, 101 | AppRunner, 102 | BaseSite, 103 | TCPSite, 104 | UnixSite, 105 | Request, 106 | Response, 107 | StreamResponse, 108 | HTTPRequestEntityTooLarge, 109 | middleware, 110 | ) 111 | from aiohttp.web_response import CIMultiDict 112 | from aiohttp_wsgi.utils import parse_sockname 113 | 114 | WSGIEnviron = Dict[str, Any] 115 | WSGIHeaders = List[Tuple[str, str]] 116 | WSGIAppendResponse = Callable[[bytes], None] 117 | WSGIStartResponse = Callable[[str, WSGIHeaders], Callable[[bytes], None]] 118 | WSGIApplication = Callable[[WSGIEnviron, WSGIStartResponse], Iterable[bytes]] 119 | 120 | logger = logging.getLogger(__name__) 121 | 122 | 123 | def _run_application(application: WSGIApplication, environ: WSGIEnviron) -> Response: 124 | # Response data. 125 | response_status: Optional[int] = None 126 | response_reason: Optional[str] = None 127 | response_headers: Optional[WSGIHeaders] = None 128 | response_body: List[bytes] = [] 129 | # Simple start_response callable. 130 | def start_response(status: str, headers: WSGIHeaders, exc_info: Optional[Exception] = None) -> WSGIAppendResponse: 131 | nonlocal response_status, response_reason, response_headers, response_body 132 | status_code, reason = status.split(None, 1) 133 | status_code = int(status_code) 134 | # Check the headers. 135 | if __debug__: 136 | for header_name, header_value in headers: 137 | assert not is_hop_by_hop(header_name), f"hop-by-hop headers are forbidden: {header_name}" 138 | # Start the response. 139 | response_status = status_code 140 | response_reason = reason 141 | response_headers = headers 142 | del response_body[:] 143 | return response_body.append 144 | # Run the application. 145 | body_iterable = application(environ, start_response) 146 | try: 147 | response_body.extend(body_iterable) 148 | assert ( 149 | response_status is not None and response_reason is not None and response_headers is not None 150 | ), "application did not call start_response()" 151 | return Response( 152 | status=response_status, 153 | reason=response_reason, 154 | headers=CIMultiDict(response_headers), 155 | body=b"".join(response_body), 156 | ) 157 | finally: 158 | # Close the body. 159 | if hasattr(body_iterable, "close"): 160 | body_iterable.close() # type: ignore 161 | 162 | 163 | class WSGIHandler: 164 | 165 | """ 166 | An adapter for WSGI applications, allowing them to run on :ref:`aiohttp `. 167 | 168 | :param application: {application} 169 | :param str url_scheme: {url_scheme} 170 | :param io.BytesIO stderr: {stderr} 171 | :param int inbuf_overflow: {inbuf_overflow} 172 | :param int max_request_body_size: {max_request_body_size} 173 | :param concurrent.futures.Executor executor: {executor} 174 | """ 175 | 176 | def __init__( 177 | self, 178 | application: WSGIApplication, 179 | *, 180 | # Handler config. 181 | url_scheme: Optional[str] = None, 182 | stderr: Optional[IO[bytes]] = None, 183 | inbuf_overflow: int = 524288, 184 | max_request_body_size: int = 1073741824, 185 | # asyncio config. 186 | executor: Optional[Executor] = None, 187 | ): 188 | assert callable(application), "application should be callable" 189 | self._application = application 190 | # Handler config. 191 | self._url_scheme = url_scheme 192 | self._stderr = stderr or sys.stderr 193 | assert isinstance(inbuf_overflow, int), "inbuf_overflow should be int" 194 | assert inbuf_overflow >= 0, "inbuf_overflow should be >= 0" 195 | assert isinstance(max_request_body_size, int), "max_request_body_size should be int" 196 | assert max_request_body_size >= 0, "max_request_body_size should be >= 0" 197 | if inbuf_overflow < max_request_body_size: 198 | self._body_io: Callable[[], IO[bytes]] = partial(SpooledTemporaryFile, max_size=inbuf_overflow) 199 | else: 200 | # Use BytesIO as an optimization if we'll never overflow to disk. 201 | self._body_io = BytesIO 202 | self._max_request_body_size = max_request_body_size 203 | # asyncio config. 204 | self._executor = executor 205 | 206 | def _get_environ(self, request: Request, body: IO[bytes], content_length: int) -> WSGIEnviron: 207 | # Resolve the path info. 208 | path_info = request.match_info["path_info"] 209 | script_name = request.rel_url.path[:len(request.rel_url.path) - len(path_info)] 210 | # Special case: If the app was mounted on the root, then the script name will 211 | # currently be set to "/", which is illegal in the WSGI spec. The script name 212 | # could also end with a slash if the WSGIHandler was mounted as a route 213 | # manually with a trailing slash before the path_info. In either case, we 214 | # correct this according to the WSGI spec by transferring the trailing slash 215 | # from script_name to the start of path_info. 216 | if script_name.endswith("/"): 217 | script_name = script_name[:-1] 218 | path_info = "/" + path_info 219 | # Parse the connection info. 220 | assert request.transport is not None 221 | server_name, server_port = parse_sockname(request.transport.get_extra_info("sockname")) 222 | remote_addr, remote_port = parse_sockname(request.transport.get_extra_info("peername")) 223 | # Detect the URL scheme. 224 | url_scheme = self._url_scheme 225 | if url_scheme is None: 226 | url_scheme = "http" if request.transport.get_extra_info("sslcontext") is None else "https" 227 | # Create the environ. 228 | environ = { 229 | "REQUEST_METHOD": request.method, 230 | "SCRIPT_NAME": script_name, 231 | "PATH_INFO": path_info, 232 | "RAW_URI": request.raw_path, 233 | # RAW_URI: Gunicorn's non-standard field 234 | "REQUEST_URI": request.raw_path, 235 | # REQUEST_URI: uWSGI/Apache mod_wsgi's non-standard field 236 | "QUERY_STRING": request.rel_url.raw_query_string, 237 | "CONTENT_TYPE": request.headers.get("Content-Type", ""), 238 | "CONTENT_LENGTH": str(content_length), 239 | "SERVER_NAME": server_name, 240 | "SERVER_PORT": server_port, 241 | "REMOTE_ADDR": remote_addr, 242 | "REMOTE_HOST": remote_addr, 243 | "REMOTE_PORT": remote_port, 244 | "SERVER_PROTOCOL": "HTTP/{}.{}".format(*request.version), 245 | "wsgi.version": (1, 0), 246 | "wsgi.url_scheme": url_scheme, 247 | "wsgi.input": body, 248 | "wsgi.errors": self._stderr, 249 | "wsgi.multithread": True, 250 | "wsgi.multiprocess": False, 251 | "wsgi.run_once": False, 252 | "asyncio.executor": self._executor, 253 | "aiohttp.request": request, 254 | } 255 | # Add in additional HTTP headers. 256 | for header_name in request.headers: 257 | header_name = header_name.upper() 258 | if not(is_hop_by_hop(header_name)) and header_name not in ("CONTENT-LENGTH", "CONTENT-TYPE"): 259 | header_value = ",".join(request.headers.getall(header_name)) 260 | environ["HTTP_" + header_name.replace("-", "_")] = header_value 261 | # All done! 262 | return environ 263 | 264 | async def handle_request(self, request: Request) -> Response: 265 | # Check for body size overflow. 266 | if request.content_length is not None and request.content_length > self._max_request_body_size: 267 | raise HTTPRequestEntityTooLarge( 268 | max_size=self._max_request_body_size, 269 | actual_size=request.content_length, 270 | ) 271 | # Buffer the body. 272 | content_length = 0 273 | with self._body_io() as body: 274 | while True: 275 | block = await request.content.readany() 276 | if not block: 277 | break 278 | content_length += len(block) 279 | if content_length > self._max_request_body_size: 280 | raise HTTPRequestEntityTooLarge( 281 | max_size=self._max_request_body_size, 282 | actual_size=content_length, 283 | ) 284 | body.write(block) 285 | body.seek(0) 286 | # Get the environ. 287 | environ = self._get_environ(request, body, content_length) 288 | loop = asyncio.get_event_loop() 289 | return await loop.run_in_executor( 290 | self._executor, 291 | _run_application, 292 | self._application, 293 | environ, 294 | ) 295 | 296 | __call__ = handle_request 297 | 298 | 299 | def format_path(path: str) -> str: 300 | assert not path.endswith("/"), f"{path!r} name should not end with /" 301 | if path == "": 302 | path = "/" 303 | assert path.startswith("/"), f"{path!r} name should start with /" 304 | return path 305 | 306 | 307 | Handler = Callable[[Request], Awaitable[StreamResponse]] 308 | Middleware = Callable[[Request, Handler], Awaitable[StreamResponse]] 309 | 310 | 311 | def static_cors_middleware(*, static: Iterable[Tuple[str, str]], static_cors: str) -> Middleware: 312 | @middleware 313 | async def do_static_cors_middleware(request: Request, handler: Handler) -> StreamResponse: 314 | response = await handler(request) 315 | for path, _ in static: 316 | if request.path.startswith(path): 317 | response.headers["Access-Control-Allow-Origin"] = static_cors 318 | break 319 | return response 320 | return do_static_cors_middleware 321 | 322 | 323 | @contextmanager 324 | def run_server( 325 | application: WSGIApplication, 326 | *, 327 | # asyncio config. 328 | threads: int = 4, 329 | # Server config. 330 | host: Optional[str] = None, 331 | port: int = 8080, 332 | # Unix server config. 333 | unix_socket: Optional[str] = None, 334 | unix_socket_perms: int = 0o600, 335 | # Shared server config. 336 | backlog: int = 1024, 337 | # aiohttp config. 338 | static: Iterable[Tuple[str, str]] = (), 339 | static_cors: Optional[str] = None, 340 | script_name: str = "", 341 | shutdown_timeout: float = 60.0, 342 | **kwargs: Any, 343 | ) -> Generator[Tuple[asyncio.AbstractEventLoop, BaseSite], None, None]: 344 | # Set up async context. 345 | loop = asyncio.new_event_loop() 346 | asyncio.set_event_loop(loop) 347 | assert threads >= 1, "threads should be >= 1" 348 | executor = ThreadPoolExecutor(threads) 349 | # Create aiohttp app. 350 | app = Application() 351 | # Add static routes. 352 | static = [(format_path(path), dirname) for path, dirname in static] 353 | for path, dirname in static: 354 | app.router.add_static(path, dirname) 355 | # Add the wsgi application. This has to be last. 356 | app.router.add_route( 357 | "*", 358 | f"{format_path(script_name)}{{path_info:.*}}", 359 | WSGIHandler( 360 | application, 361 | executor=executor, 362 | **kwargs 363 | ).handle_request, 364 | ) 365 | # Configure middleware. 366 | if static_cors: 367 | app.middlewares.append(static_cors_middleware( 368 | static=static, 369 | static_cors=static_cors, 370 | )) 371 | # Start the app runner. 372 | runner = AppRunner(app) 373 | loop.run_until_complete(runner.setup()) 374 | # Set up the server. 375 | if unix_socket is not None: 376 | site: BaseSite = UnixSite(runner, path=unix_socket, backlog=backlog, shutdown_timeout=shutdown_timeout) 377 | else: 378 | site = TCPSite(runner, host=host, port=port, backlog=backlog, shutdown_timeout=shutdown_timeout) 379 | loop.run_until_complete(site.start()) 380 | # Set socket permissions. 381 | if unix_socket is not None: 382 | os.chmod(unix_socket, unix_socket_perms) 383 | # Report. 384 | assert site._server is not None 385 | assert isinstance(site._server, Server) 386 | assert site._server.sockets is not None 387 | server_uri = " ".join( 388 | "http://{}:{}".format(*parse_sockname(socket.getsockname())) 389 | for socket 390 | in site._server.sockets 391 | ) 392 | logger.info("Serving on %s", server_uri) 393 | try: 394 | yield loop, site 395 | finally: 396 | # Clean up unix sockets. 397 | for socket in site._server.sockets: 398 | sock_host, sock_port = parse_sockname(socket.getsockname()) 399 | if sock_host == "unix": 400 | os.unlink(sock_port) 401 | # Close the server. 402 | logger.debug("Shutting down server on %s", server_uri) 403 | loop.run_until_complete(site.stop()) 404 | # Shut down app. 405 | logger.debug("Shutting down app on %s", server_uri) 406 | loop.run_until_complete(runner.cleanup()) 407 | # Shut down executor. 408 | logger.debug("Shutting down executor on %s", server_uri) 409 | executor.shutdown() 410 | # Shut down loop. 411 | logger.debug("Shutting down loop on %s", server_uri) 412 | loop.close() 413 | asyncio.set_event_loop(None) 414 | # All done! 415 | logger.info("Stopped serving on %s", server_uri) 416 | 417 | 418 | def serve(application: WSGIApplication, **kwargs: Any) -> None: # pragma: no cover 419 | """ 420 | Runs the WSGI application on :ref:`aiohttp `, serving it until keyboard interrupt. 421 | 422 | :param application: {application} 423 | :param str url_scheme: {url_scheme} 424 | :param io.BytesIO stderr: {stderr} 425 | :param int inbuf_overflow: {inbuf_overflow} 426 | :param int max_request_body_size: {max_request_body_size} 427 | :param int threads: {threads} 428 | :param str host: {host} 429 | :param int port: {port} 430 | :param str unix_socket: {unix_socket} 431 | :param int unix_socket_perms: {unix_socket_perms} 432 | :param int backlog: {backlog} 433 | :param list static: {static} 434 | :param list static_cors: {static_cors} 435 | :param str script_name: {script_name} 436 | :param int shutdown_timeout: {shutdown_timeout} 437 | """ 438 | with run_server(application, **kwargs) as (loop, site): 439 | try: 440 | loop.run_forever() 441 | except KeyboardInterrupt: 442 | pass 443 | 444 | 445 | DEFAULTS = {} 446 | DEFAULTS.update(WSGIHandler.__init__.__kwdefaults__) # type: ignore 447 | DEFAULTS.update(run_server.__wrapped__.__kwdefaults__) # type: ignore 448 | 449 | HELP = { 450 | "application": "A WSGI application callable.", 451 | "url_scheme": ( 452 | "A hint about the URL scheme used to access the application. Corresponds to ``environ['wsgi.url_scheme']``. " 453 | "Default is auto-detected to ``'http'`` or ``'https'``." 454 | ), 455 | "stderr": ( 456 | "A file-like value for WSGI error logging. Corresponds to ``environ['wsgi.errors']``. " 457 | "Defaults to ``sys.stderr``." 458 | ), 459 | "inbuf_overflow": ( 460 | "A tempfile will be created if the request body is larger than this value, which is measured in bytes. " 461 | "Defaults to ``{inbuf_overflow!r}``." 462 | ).format_map(DEFAULTS), 463 | "max_request_body_size": ( 464 | "Maximum number of bytes in request body. Defaults to ``{max_request_body_size!r}``. " 465 | "Larger requests will receive a HTTP 413 (Request Entity Too Large) response." 466 | ).format_map(DEFAULTS), 467 | "executor": "An Executor instance used to run WSGI requests. Defaults to the :mod:`asyncio` base executor.", 468 | "host": "Host interfaces to bind. Defaults to ``'0.0.0.0'`` and ``'::'``.", 469 | "port": "Port to bind. Defaults to ``{port!r}``.".format_map(DEFAULTS), 470 | "unix_socket": "Path to a unix socket to bind, cannot be used with ``host``.", 471 | "unix_socket_perms": ( 472 | "Filesystem permissions to apply to the unix socket. Defaults to ``{unix_socket_perms!r}``." 473 | ).format_map(DEFAULTS), 474 | "backlog": "Socket connection backlog. Defaults to {backlog!r}.".format_map(DEFAULTS), 475 | "static": "Static root mappings in the form (path, directory). Defaults to {static!r}".format_map(DEFAULTS), 476 | "static_cors": ( 477 | "Set to '*' to enable CORS on static files for all origins, or a string to enable CORS for a specific origin. " 478 | "Defaults to {static_cors!r}" 479 | ).format_map(DEFAULTS), 480 | "script_name": ( 481 | "URL prefix for the WSGI application, should start with a slash, but not end with a slash. " 482 | "Defaults to ``{script_name!r}``." 483 | ).format_map(DEFAULTS), 484 | "threads": "Number of threads used to process application logic. Defaults to ``{threads!r}``.".format_map(DEFAULTS), 485 | "shutdown_timeout": ( 486 | "Timeout when closing client connections on server shutdown. Defaults to ``{shutdown_timeout!r}``." 487 | ).format_map(DEFAULTS), 488 | } 489 | 490 | 491 | if __debug__: 492 | assert WSGIHandler.__doc__ is not None 493 | WSGIHandler.__doc__ = WSGIHandler.__doc__.format_map(HELP) 494 | assert serve.__doc__ is not None 495 | serve.__doc__ = serve.__doc__.format_map(HELP) 496 | --------------------------------------------------------------------------------