Upload folder using huggingface_hub (part 16)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +23 -0
- .venv/Lib/site-packages/urllib3-2.7.0.dist-info/INSTALLER +1 -0
- .venv/Lib/site-packages/urllib3-2.7.0.dist-info/METADATA +163 -0
- .venv/Lib/site-packages/urllib3-2.7.0.dist-info/RECORD +79 -0
- .venv/Lib/site-packages/urllib3-2.7.0.dist-info/WHEEL +4 -0
- .venv/Lib/site-packages/urllib3-2.7.0.dist-info/licenses/LICENSE.txt +21 -0
- .venv/Lib/site-packages/urllib3/util/wait.py +124 -0
- .venv/Lib/site-packages/uvicorn-0.51.0.dist-info/INSTALLER +1 -0
- .venv/Lib/site-packages/uvicorn-0.51.0.dist-info/METADATA +189 -0
- .venv/Lib/site-packages/uvicorn-0.51.0.dist-info/RECORD +90 -0
- .venv/Lib/site-packages/uvicorn-0.51.0.dist-info/WHEEL +4 -0
- .venv/Lib/site-packages/uvicorn-0.51.0.dist-info/entry_points.txt +2 -0
- .venv/Lib/site-packages/uvicorn-0.51.0.dist-info/licenses/LICENSE.md +27 -0
- .venv/Lib/site-packages/uvicorn/__init__.py +5 -0
- .venv/Lib/site-packages/uvicorn/__main__.py +4 -0
- .venv/Lib/site-packages/uvicorn/_ansi.py +29 -0
- .venv/Lib/site-packages/uvicorn/_compat.py +91 -0
- .venv/Lib/site-packages/uvicorn/_subprocess.py +84 -0
- .venv/Lib/site-packages/uvicorn/_types.py +274 -0
- .venv/Lib/site-packages/uvicorn/config.py +600 -0
- .venv/Lib/site-packages/uvicorn/importer.py +34 -0
- .venv/Lib/site-packages/uvicorn/lifespan/__init__.py +0 -0
- .venv/Lib/site-packages/uvicorn/lifespan/off.py +17 -0
- .venv/Lib/site-packages/uvicorn/lifespan/on.py +137 -0
- .venv/Lib/site-packages/uvicorn/logging.py +117 -0
- .venv/Lib/site-packages/uvicorn/loops/__init__.py +0 -0
- .venv/Lib/site-packages/uvicorn/loops/asyncio.py +11 -0
- .venv/Lib/site-packages/uvicorn/loops/auto.py +17 -0
- .venv/Lib/site-packages/uvicorn/loops/uvloop.py +10 -0
- .venv/Lib/site-packages/uvicorn/main.py +645 -0
- .venv/Lib/site-packages/uvicorn/middleware/__init__.py +0 -0
- .venv/Lib/site-packages/uvicorn/middleware/asgi2.py +15 -0
- .venv/Lib/site-packages/uvicorn/middleware/message_logger.py +87 -0
- .venv/Lib/site-packages/uvicorn/middleware/proxy_headers.py +187 -0
- .venv/Lib/site-packages/uvicorn/middleware/wsgi.py +199 -0
- .venv/Lib/site-packages/uvicorn/protocols/__init__.py +0 -0
- .venv/Lib/site-packages/uvicorn/protocols/http/__init__.py +0 -0
- .venv/Lib/site-packages/uvicorn/protocols/http/auto.py +15 -0
- .venv/Lib/site-packages/uvicorn/protocols/http/flow_control.py +54 -0
- .venv/Lib/site-packages/uvicorn/protocols/http/h11_impl.py +545 -0
- .venv/Lib/site-packages/uvicorn/protocols/http/httptools_impl.py +577 -0
- .venv/Lib/site-packages/uvicorn/protocols/utils.py +62 -0
- .venv/Lib/site-packages/uvicorn/protocols/websockets/__init__.py +0 -0
- .venv/Lib/site-packages/uvicorn/protocols/websockets/auto.py +21 -0
- .venv/Lib/site-packages/uvicorn/protocols/websockets/websockets_impl.py +380 -0
- .venv/Lib/site-packages/uvicorn/protocols/websockets/websockets_sansio_impl.py +484 -0
- .venv/Lib/site-packages/uvicorn/protocols/websockets/wsproto_impl.py +469 -0
- .venv/Lib/site-packages/uvicorn/py.typed +1 -0
- .venv/Lib/site-packages/uvicorn/server.py +344 -0
- .venv/Lib/site-packages/uvicorn/supervisors/__init__.py +16 -0
.gitattributes
CHANGED
|
@@ -72,3 +72,26 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 72 |
.venv/Lib/site-packages/streamlit/static/static/media/SourceSansVF-Upright.ttf.BsWL4Kly.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 73 |
.venv/Lib/site-packages/streamlit/static/static/media/SourceSerifVariable-Italic.ttf.CVdzAtxO.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 74 |
.venv/Lib/site-packages/streamlit/static/static/media/SourceSerifVariable-Roman.ttf.mdpVL9bi.woff2 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
.venv/Lib/site-packages/streamlit/static/static/media/SourceSansVF-Upright.ttf.BsWL4Kly.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 73 |
.venv/Lib/site-packages/streamlit/static/static/media/SourceSerifVariable-Italic.ttf.CVdzAtxO.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 74 |
.venv/Lib/site-packages/streamlit/static/static/media/SourceSerifVariable-Roman.ttf.mdpVL9bi.woff2 filter=lfs diff=lfs merge=lfs -text
|
| 75 |
+
.venv/Scripts/f2py.exe filter=lfs diff=lfs merge=lfs -text
|
| 76 |
+
.venv/Scripts/hf.exe filter=lfs diff=lfs merge=lfs -text
|
| 77 |
+
.venv/Scripts/httpx.exe filter=lfs diff=lfs merge=lfs -text
|
| 78 |
+
.venv/Scripts/huggingface-cli.exe filter=lfs diff=lfs merge=lfs -text
|
| 79 |
+
.venv/Scripts/idna.exe filter=lfs diff=lfs merge=lfs -text
|
| 80 |
+
.venv/Scripts/jsonschema.exe filter=lfs diff=lfs merge=lfs -text
|
| 81 |
+
.venv/Scripts/normalizer.exe filter=lfs diff=lfs merge=lfs -text
|
| 82 |
+
.venv/Scripts/numpy-config.exe filter=lfs diff=lfs merge=lfs -text
|
| 83 |
+
.venv/Scripts/pip.exe filter=lfs diff=lfs merge=lfs -text
|
| 84 |
+
.venv/Scripts/pip3.12.exe filter=lfs diff=lfs merge=lfs -text
|
| 85 |
+
.venv/Scripts/pip3.exe filter=lfs diff=lfs merge=lfs -text
|
| 86 |
+
.venv/Scripts/py.test.exe filter=lfs diff=lfs merge=lfs -text
|
| 87 |
+
.venv/Scripts/pygmentize.exe filter=lfs diff=lfs merge=lfs -text
|
| 88 |
+
.venv/Scripts/pytest.exe filter=lfs diff=lfs merge=lfs -text
|
| 89 |
+
.venv/Scripts/python.exe filter=lfs diff=lfs merge=lfs -text
|
| 90 |
+
.venv/Scripts/pythonw.exe filter=lfs diff=lfs merge=lfs -text
|
| 91 |
+
.venv/Scripts/streamlit.exe filter=lfs diff=lfs merge=lfs -text
|
| 92 |
+
.venv/Scripts/tiny-agents.exe filter=lfs diff=lfs merge=lfs -text
|
| 93 |
+
.venv/Scripts/tqdm.exe filter=lfs diff=lfs merge=lfs -text
|
| 94 |
+
.venv/Scripts/uvicorn.exe filter=lfs diff=lfs merge=lfs -text
|
| 95 |
+
.venv/Scripts/watchmedo.exe filter=lfs diff=lfs merge=lfs -text
|
| 96 |
+
.venv/Scripts/websockets.exe filter=lfs diff=lfs merge=lfs -text
|
| 97 |
+
.venv/share/jupyter/nbextensions/pydeck/index.js.map filter=lfs diff=lfs merge=lfs -text
|
.venv/Lib/site-packages/urllib3-2.7.0.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
.venv/Lib/site-packages/urllib3-2.7.0.dist-info/METADATA
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: urllib3
|
| 3 |
+
Version: 2.7.0
|
| 4 |
+
Summary: HTTP library with thread-safe connection pooling, file post, and more.
|
| 5 |
+
Project-URL: Changelog, https://github.com/urllib3/urllib3/blob/main/CHANGES.rst
|
| 6 |
+
Project-URL: Documentation, https://urllib3.readthedocs.io
|
| 7 |
+
Project-URL: Code, https://github.com/urllib3/urllib3
|
| 8 |
+
Project-URL: Issue tracker, https://github.com/urllib3/urllib3/issues
|
| 9 |
+
Author-email: Andrey Petrov <andrey.petrov@shazow.net>
|
| 10 |
+
Maintainer-email: Seth Michael Larson <sethmichaellarson@gmail.com>, Quentin Pradet <quentin@pradet.me>, Illia Volochii <illia.volochii@gmail.com>
|
| 11 |
+
License-Expression: MIT
|
| 12 |
+
License-File: LICENSE.txt
|
| 13 |
+
Keywords: filepost,http,httplib,https,pooling,ssl,threadsafe,urllib
|
| 14 |
+
Classifier: Environment :: Web Environment
|
| 15 |
+
Classifier: Intended Audience :: Developers
|
| 16 |
+
Classifier: Operating System :: OS Independent
|
| 17 |
+
Classifier: Programming Language :: Python
|
| 18 |
+
Classifier: Programming Language :: Python :: 3
|
| 19 |
+
Classifier: Programming Language :: Python :: 3 :: Only
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 21 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 22 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 23 |
+
Classifier: Programming Language :: Python :: 3.13
|
| 24 |
+
Classifier: Programming Language :: Python :: 3.14
|
| 25 |
+
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
|
| 26 |
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
| 27 |
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
| 28 |
+
Classifier: Topic :: Internet :: WWW/HTTP
|
| 29 |
+
Classifier: Topic :: Software Development :: Libraries
|
| 30 |
+
Requires-Python: >=3.10
|
| 31 |
+
Provides-Extra: brotli
|
| 32 |
+
Requires-Dist: brotli>=1.2.0; (platform_python_implementation == 'CPython') and extra == 'brotli'
|
| 33 |
+
Requires-Dist: brotlicffi>=1.2.0.0; (platform_python_implementation != 'CPython') and extra == 'brotli'
|
| 34 |
+
Provides-Extra: h2
|
| 35 |
+
Requires-Dist: h2<5,>=4; extra == 'h2'
|
| 36 |
+
Provides-Extra: socks
|
| 37 |
+
Requires-Dist: pysocks!=1.5.7,<2.0,>=1.5.6; extra == 'socks'
|
| 38 |
+
Provides-Extra: zstd
|
| 39 |
+
Requires-Dist: backports-zstd>=1.0.0; (python_version < '3.14') and extra == 'zstd'
|
| 40 |
+
Description-Content-Type: text/markdown
|
| 41 |
+
|
| 42 |
+
<h1 align="center">
|
| 43 |
+
|
| 44 |
+

|
| 45 |
+
|
| 46 |
+
</h1>
|
| 47 |
+
|
| 48 |
+
<p align="center">
|
| 49 |
+
<a href="https://pypi.org/project/urllib3"><img alt="PyPI Version" src="https://img.shields.io/pypi/v/urllib3.svg?maxAge=86400" /></a>
|
| 50 |
+
<a href="https://pypi.org/project/urllib3"><img alt="Python Versions" src="https://img.shields.io/pypi/pyversions/urllib3.svg?maxAge=86400" /></a>
|
| 51 |
+
<a href="https://discord.gg/urllib3"><img alt="Join our Discord" src="https://img.shields.io/discord/756342717725933608?color=%237289da&label=discord" /></a>
|
| 52 |
+
<a href="https://github.com/urllib3/urllib3/actions?query=workflow%3ACI"><img alt="Coverage Status" src="https://img.shields.io/badge/coverage-100%25-success" /></a>
|
| 53 |
+
<a href="https://github.com/urllib3/urllib3/actions/workflows/ci.yml?query=branch%3Amain"><img alt="Build Status on GitHub" src="https://github.com/urllib3/urllib3/actions/workflows/ci.yml/badge.svg?branch:main&workflow:CI" /></a>
|
| 54 |
+
<a href="https://urllib3.readthedocs.io"><img alt="Documentation Status" src="https://readthedocs.org/projects/urllib3/badge/?version=latest" /></a><br>
|
| 55 |
+
<a href="https://deps.dev/pypi/urllib3"><img alt="OpenSSF Scorecard" src="https://api.securityscorecards.dev/projects/github.com/urllib3/urllib3/badge" /></a>
|
| 56 |
+
<a href="https://slsa.dev"><img alt="SLSA 3" src="https://slsa.dev/images/gh-badge-level3.svg" /></a>
|
| 57 |
+
<a href="https://bestpractices.coreinfrastructure.org/projects/6227"><img alt="CII Best Practices" src="https://bestpractices.coreinfrastructure.org/projects/6227/badge" /></a>
|
| 58 |
+
</p>
|
| 59 |
+
|
| 60 |
+
urllib3 is a powerful, *user-friendly* HTTP client for Python.
|
| 61 |
+
urllib3 brings many critical features that are missing from the Python
|
| 62 |
+
standard libraries:
|
| 63 |
+
|
| 64 |
+
- Thread safety.
|
| 65 |
+
- Connection pooling.
|
| 66 |
+
- Client-side SSL/TLS verification.
|
| 67 |
+
- File uploads with multipart encoding.
|
| 68 |
+
- Helpers for retrying requests and dealing with HTTP redirects.
|
| 69 |
+
- Support for gzip, deflate, brotli, and zstd encoding.
|
| 70 |
+
- Proxy support for HTTP and SOCKS.
|
| 71 |
+
- 100% test coverage.
|
| 72 |
+
|
| 73 |
+
... and many more features, but most importantly: Our maintainers have a 15+
|
| 74 |
+
year track record of maintaining urllib3 with the highest code standards and
|
| 75 |
+
attention to security and safety.
|
| 76 |
+
|
| 77 |
+
[Much of the Python ecosystem already uses urllib3](https://urllib3.readthedocs.io/en/stable/#who-uses)
|
| 78 |
+
and you should too.
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
## Installing
|
| 82 |
+
|
| 83 |
+
urllib3 can be installed with [pip](https://pip.pypa.io):
|
| 84 |
+
|
| 85 |
+
```bash
|
| 86 |
+
$ python -m pip install urllib3
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
Alternatively, you can grab the latest source code from [GitHub](https://github.com/urllib3/urllib3):
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
$ git clone https://github.com/urllib3/urllib3.git
|
| 93 |
+
$ cd urllib3
|
| 94 |
+
$ pip install .
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
## Getting Started
|
| 98 |
+
|
| 99 |
+
urllib3 is easy to use:
|
| 100 |
+
|
| 101 |
+
```python3
|
| 102 |
+
>>> import urllib3
|
| 103 |
+
>>> resp = urllib3.request("GET", "http://httpbin.org/robots.txt")
|
| 104 |
+
>>> resp.status
|
| 105 |
+
200
|
| 106 |
+
>>> resp.data
|
| 107 |
+
b"User-agent: *\nDisallow: /deny\n"
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
urllib3 has usage and reference documentation at [urllib3.readthedocs.io](https://urllib3.readthedocs.io).
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
## Community
|
| 114 |
+
|
| 115 |
+
urllib3 has a [community Discord channel](https://discord.gg/urllib3) for asking questions and
|
| 116 |
+
collaborating with other contributors. Drop by and say hello 👋
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
## Contributing
|
| 120 |
+
|
| 121 |
+
urllib3 happily accepts contributions. Please see our
|
| 122 |
+
[contributing documentation](https://urllib3.readthedocs.io/en/latest/contributing.html)
|
| 123 |
+
for some tips on getting started.
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
## Security Disclosures
|
| 127 |
+
|
| 128 |
+
To report a security vulnerability, please use the
|
| 129 |
+
[Tidelift security contact](https://tidelift.com/security).
|
| 130 |
+
Tidelift will coordinate the fix and disclosure with maintainers.
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
## Maintainers
|
| 134 |
+
|
| 135 |
+
Meet our maintainers since 2008:
|
| 136 |
+
|
| 137 |
+
- Current Lead: [@illia-v](https://github.com/illia-v) (Illia Volochii)
|
| 138 |
+
- [@sethmlarson](https://github.com/sethmlarson) (Seth M. Larson)
|
| 139 |
+
- [@pquentin](https://github.com/pquentin) (Quentin Pradet)
|
| 140 |
+
- [@theacodes](https://github.com/theacodes) (Thea Flowers)
|
| 141 |
+
- [@haikuginger](https://github.com/haikuginger) (Jess Shapiro)
|
| 142 |
+
- [@lukasa](https://github.com/lukasa) (Cory Benfield)
|
| 143 |
+
- [@sigmavirus24](https://github.com/sigmavirus24) (Ian Stapleton Cordasco)
|
| 144 |
+
- [@shazow](https://github.com/shazow) (Andrey Petrov)
|
| 145 |
+
|
| 146 |
+
👋
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
## Sponsorship
|
| 150 |
+
|
| 151 |
+
If your company benefits from this library, please consider [sponsoring its
|
| 152 |
+
development](https://urllib3.readthedocs.io/en/latest/sponsors.html).
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
## For Enterprise
|
| 156 |
+
|
| 157 |
+
Professional support for urllib3 is available as part of the [Tidelift
|
| 158 |
+
Subscription][1]. Tidelift gives software development teams a single source for
|
| 159 |
+
purchasing and maintaining their software, with professional grade assurances
|
| 160 |
+
from the experts who know it best, while seamlessly integrating with existing
|
| 161 |
+
tools.
|
| 162 |
+
|
| 163 |
+
[1]: https://tidelift.com/subscription/pkg/pypi-urllib3?utm_source=pypi-urllib3&utm_medium=referral&utm_campaign=readme
|
.venv/Lib/site-packages/urllib3-2.7.0.dist-info/RECORD
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
urllib3-2.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
urllib3-2.7.0.dist-info/METADATA,sha256=ZhR4VtMLu2vtAcbOMybQ9I2E7V8oasF3YzoUhrgurOU,6852
|
| 3 |
+
urllib3-2.7.0.dist-info/RECORD,,
|
| 4 |
+
urllib3-2.7.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
| 5 |
+
urllib3-2.7.0.dist-info/licenses/LICENSE.txt,sha256=Ew46ZNX91dCWp1JpRjSn2d8oRGnehuVzIQAmgEHj1oY,1093
|
| 6 |
+
urllib3/__init__.py,sha256=JMo1tg1nIV1AeJ2vENC_Txfl0e5h6Gzl9DGVk1rWRbo,6979
|
| 7 |
+
urllib3/__pycache__/__init__.cpython-312.pyc,,
|
| 8 |
+
urllib3/__pycache__/_base_connection.cpython-312.pyc,,
|
| 9 |
+
urllib3/__pycache__/_collections.cpython-312.pyc,,
|
| 10 |
+
urllib3/__pycache__/_request_methods.cpython-312.pyc,,
|
| 11 |
+
urllib3/__pycache__/_version.cpython-312.pyc,,
|
| 12 |
+
urllib3/__pycache__/connection.cpython-312.pyc,,
|
| 13 |
+
urllib3/__pycache__/connectionpool.cpython-312.pyc,,
|
| 14 |
+
urllib3/__pycache__/exceptions.cpython-312.pyc,,
|
| 15 |
+
urllib3/__pycache__/fields.cpython-312.pyc,,
|
| 16 |
+
urllib3/__pycache__/filepost.cpython-312.pyc,,
|
| 17 |
+
urllib3/__pycache__/poolmanager.cpython-312.pyc,,
|
| 18 |
+
urllib3/__pycache__/response.cpython-312.pyc,,
|
| 19 |
+
urllib3/_base_connection.py,sha256=HzcSEHexgDrRUr60jNniB2KQjdw97SedD5luRfHtCXg,5580
|
| 20 |
+
urllib3/_collections.py,sha256=aOVm2mKilvuvT1efAGtkA7pi65C1NC3HJfpFxvYBS8A,17522
|
| 21 |
+
urllib3/_request_methods.py,sha256=gCeF85SO_UU4WoPwYHIoz_tw-eM_EVOkLFp8OFsC7DA,9931
|
| 22 |
+
urllib3/_version.py,sha256=-gMrKhsPiOj0XzUezVFa59d1S6QgQiKgQT5gGgyM8-w,520
|
| 23 |
+
urllib3/connection.py,sha256=Zos3qxKDW9-GQ6aVqfBQVRM5soB0IjHxY_xXWpJaFTI,42786
|
| 24 |
+
urllib3/connectionpool.py,sha256=sGFnddXYwlx7KC4JCP1gKvdNGLNK-YTCJDdGACGj3Y8,44164
|
| 25 |
+
urllib3/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 26 |
+
urllib3/contrib/__pycache__/__init__.cpython-312.pyc,,
|
| 27 |
+
urllib3/contrib/__pycache__/pyopenssl.cpython-312.pyc,,
|
| 28 |
+
urllib3/contrib/__pycache__/socks.cpython-312.pyc,,
|
| 29 |
+
urllib3/contrib/emscripten/__init__.py,sha256=wyXve8rmqX7s2KqRQBxD5Wl48jzWPn5-1u_XoQBELVc,836
|
| 30 |
+
urllib3/contrib/emscripten/__pycache__/__init__.cpython-312.pyc,,
|
| 31 |
+
urllib3/contrib/emscripten/__pycache__/connection.cpython-312.pyc,,
|
| 32 |
+
urllib3/contrib/emscripten/__pycache__/fetch.cpython-312.pyc,,
|
| 33 |
+
urllib3/contrib/emscripten/__pycache__/request.cpython-312.pyc,,
|
| 34 |
+
urllib3/contrib/emscripten/__pycache__/response.cpython-312.pyc,,
|
| 35 |
+
urllib3/contrib/emscripten/connection.py,sha256=giElsBoUsKVURbZzb8GCrJmqW23Xnvj2aNyQVF42slg,8960
|
| 36 |
+
urllib3/contrib/emscripten/emscripten_fetch_worker.js,sha256=z1k3zZ4_hDKd3-tN7wzz8LHjHC2pxN_uu8B3k9D9A3c,3677
|
| 37 |
+
urllib3/contrib/emscripten/fetch.py,sha256=5xcd--viFxZd2nBy0aK73dtJ9Tsh1yYZU_SUXwnwibk,23520
|
| 38 |
+
urllib3/contrib/emscripten/request.py,sha256=mL28szy1KvE3NJhWor5jNmarp8gwplDU-7gwGZY5g0Q,566
|
| 39 |
+
urllib3/contrib/emscripten/response.py,sha256=CDpY0GFoluR3IxECkM2gH5uDfYDM0EL1FWr7B76wtgM,9719
|
| 40 |
+
urllib3/contrib/pyopenssl.py,sha256=XZY-QzyT7s8d43qisA4o-eSZYBeIkPyBMZhtj2kkLZs,19734
|
| 41 |
+
urllib3/contrib/socks.py,sha256=rSuklfha4-vto-8qSLhSPmf5lmRPIYuqdAxKe9bg2RA,7639
|
| 42 |
+
urllib3/exceptions.py,sha256=hQPHoqo4yw46esNSVkciVQXVUgAxWAglsh6MbyQta-Q,9945
|
| 43 |
+
urllib3/fields.py,sha256=aGLFAVZpVU-FbJlllve4Ahg0-pH9ZZBQ2Iz7lfpkjkM,10801
|
| 44 |
+
urllib3/filepost.py,sha256=U8eNZ-mpKKHhrlbHEEiTxxgK16IejhEa7uz42yqA_dI,2388
|
| 45 |
+
urllib3/http2/__init__.py,sha256=xzrASH7R5ANRkPJOot5lGnATOq3KKuyXzI42rcnwmqs,1741
|
| 46 |
+
urllib3/http2/__pycache__/__init__.cpython-312.pyc,,
|
| 47 |
+
urllib3/http2/__pycache__/connection.cpython-312.pyc,,
|
| 48 |
+
urllib3/http2/__pycache__/probe.cpython-312.pyc,,
|
| 49 |
+
urllib3/http2/connection.py,sha256=bHMH6fNvatwXPrKqrcn74yA3pUWcqPDppnK1LcKCbP8,12578
|
| 50 |
+
urllib3/http2/probe.py,sha256=nnAkqbhAakOiF75rz7W0udZ38Eeh_uD8fjV74N73FEI,3014
|
| 51 |
+
urllib3/poolmanager.py,sha256=c0rh0rcUC1t5tDGuUeGIgAzlErasPOwWwLiB67lX8pM,23895
|
| 52 |
+
urllib3/py.typed,sha256=UaCuPFa3H8UAakbt-5G8SPacldTOGvJv18pPjUJ5gDY,93
|
| 53 |
+
urllib3/response.py,sha256=9SX4BkkdoLgsx1ne6hpt-5PncrnDzPzeqC1DaShSc-M,53219
|
| 54 |
+
urllib3/util/__init__.py,sha256=-qeS0QceivazvBEKDNFCAI-6ACcdDOE4TMvo7SLNlAQ,1001
|
| 55 |
+
urllib3/util/__pycache__/__init__.cpython-312.pyc,,
|
| 56 |
+
urllib3/util/__pycache__/connection.cpython-312.pyc,,
|
| 57 |
+
urllib3/util/__pycache__/proxy.cpython-312.pyc,,
|
| 58 |
+
urllib3/util/__pycache__/request.cpython-312.pyc,,
|
| 59 |
+
urllib3/util/__pycache__/response.cpython-312.pyc,,
|
| 60 |
+
urllib3/util/__pycache__/retry.cpython-312.pyc,,
|
| 61 |
+
urllib3/util/__pycache__/ssl_.cpython-312.pyc,,
|
| 62 |
+
urllib3/util/__pycache__/ssl_match_hostname.cpython-312.pyc,,
|
| 63 |
+
urllib3/util/__pycache__/ssltransport.cpython-312.pyc,,
|
| 64 |
+
urllib3/util/__pycache__/timeout.cpython-312.pyc,,
|
| 65 |
+
urllib3/util/__pycache__/url.cpython-312.pyc,,
|
| 66 |
+
urllib3/util/__pycache__/util.cpython-312.pyc,,
|
| 67 |
+
urllib3/util/__pycache__/wait.cpython-312.pyc,,
|
| 68 |
+
urllib3/util/connection.py,sha256=JjO722lzHlzLXPTkr9ZWBdhseXnMVjMSb1DJLVrXSnQ,4444
|
| 69 |
+
urllib3/util/proxy.py,sha256=seP8-Q5B6bB0dMtwPj-YcZZQ30vHuLqRu-tI0JZ2fzs,1148
|
| 70 |
+
urllib3/util/request.py,sha256=itpnC8ug7D4nVfDmGUCRMlgkARUQ13r_XMxSnzTwmpE,8363
|
| 71 |
+
urllib3/util/response.py,sha256=vQE639uoEhj1vpjEdxu5lNIhJCSUZkd7pqllUI0BZOA,3374
|
| 72 |
+
urllib3/util/retry.py,sha256=2YnSX-_FecMShD61Mx5s68J0_btUHZrrc_BkFVRS1P4,19577
|
| 73 |
+
urllib3/util/ssl_.py,sha256=Oqe3rIhUU3e3GVgZob2hxmR8Q0ZDhrhESPlPP_GLaVQ,17742
|
| 74 |
+
urllib3/util/ssl_match_hostname.py,sha256=Ft44KJzTzGMmKff_ZXP91li2V4WhmvEy16PKBcv4vZk,5479
|
| 75 |
+
urllib3/util/ssltransport.py,sha256=Ez4O8pR_vT8dan_FvqBYS6dgDfBXEMfVfrzcdUoWfi4,8847
|
| 76 |
+
urllib3/util/timeout.py,sha256=4eT1FVeZZU7h7mYD1Jq2OXNe4fxekdNvhoWUkZusRpA,10346
|
| 77 |
+
urllib3/util/url.py,sha256=WRh-TMYXosmgp8m8lT4H5spoHw5yUjlcMCfU53AkoAs,15205
|
| 78 |
+
urllib3/util/util.py,sha256=j3lbZK1jPyiwD34T8IgJzdWEZVT-4E-0vYIJi9UjeNA,1146
|
| 79 |
+
urllib3/util/wait.py,sha256=_ph8IrUR3sqPqi0OopQgJUlH4wzkGeM5CiyA7XGGtmI,4423
|
.venv/Lib/site-packages/urllib3-2.7.0.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: hatchling 1.29.0
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
.venv/Lib/site-packages/urllib3-2.7.0.dist-info/licenses/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2008-2020 Andrey Petrov and contributors.
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
.venv/Lib/site-packages/urllib3/util/wait.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import select
|
| 4 |
+
import socket
|
| 5 |
+
from functools import partial
|
| 6 |
+
|
| 7 |
+
__all__ = ["wait_for_read", "wait_for_write"]
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# How should we wait on sockets?
|
| 11 |
+
#
|
| 12 |
+
# There are two types of APIs you can use for waiting on sockets: the fancy
|
| 13 |
+
# modern stateful APIs like epoll/kqueue, and the older stateless APIs like
|
| 14 |
+
# select/poll. The stateful APIs are more efficient when you have a lots of
|
| 15 |
+
# sockets to keep track of, because you can set them up once and then use them
|
| 16 |
+
# lots of times. But we only ever want to wait on a single socket at a time
|
| 17 |
+
# and don't want to keep track of state, so the stateless APIs are actually
|
| 18 |
+
# more efficient. So we want to use select() or poll().
|
| 19 |
+
#
|
| 20 |
+
# Now, how do we choose between select() and poll()? On traditional Unixes,
|
| 21 |
+
# select() has a strange calling convention that makes it slow, or fail
|
| 22 |
+
# altogether, for high-numbered file descriptors. The point of poll() is to fix
|
| 23 |
+
# that, so on Unixes, we prefer poll().
|
| 24 |
+
#
|
| 25 |
+
# On Windows, there is no poll() (or at least Python doesn't provide a wrapper
|
| 26 |
+
# for it), but that's OK, because on Windows, select() doesn't have this
|
| 27 |
+
# strange calling convention; plain select() works fine.
|
| 28 |
+
#
|
| 29 |
+
# So: on Windows we use select(), and everywhere else we use poll(). We also
|
| 30 |
+
# fall back to select() in case poll() is somehow broken or missing.
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def select_wait_for_socket(
|
| 34 |
+
sock: socket.socket,
|
| 35 |
+
read: bool = False,
|
| 36 |
+
write: bool = False,
|
| 37 |
+
timeout: float | None = None,
|
| 38 |
+
) -> bool:
|
| 39 |
+
if not read and not write:
|
| 40 |
+
raise RuntimeError("must specify at least one of read=True, write=True")
|
| 41 |
+
rcheck = []
|
| 42 |
+
wcheck = []
|
| 43 |
+
if read:
|
| 44 |
+
rcheck.append(sock)
|
| 45 |
+
if write:
|
| 46 |
+
wcheck.append(sock)
|
| 47 |
+
# When doing a non-blocking connect, most systems signal success by
|
| 48 |
+
# marking the socket writable. Windows, though, signals success by marked
|
| 49 |
+
# it as "exceptional". We paper over the difference by checking the write
|
| 50 |
+
# sockets for both conditions. (The stdlib selectors module does the same
|
| 51 |
+
# thing.)
|
| 52 |
+
fn = partial(select.select, rcheck, wcheck, wcheck)
|
| 53 |
+
rready, wready, xready = fn(timeout)
|
| 54 |
+
return bool(rready or wready or xready)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def poll_wait_for_socket(
|
| 58 |
+
sock: socket.socket,
|
| 59 |
+
read: bool = False,
|
| 60 |
+
write: bool = False,
|
| 61 |
+
timeout: float | None = None,
|
| 62 |
+
) -> bool:
|
| 63 |
+
if not read and not write:
|
| 64 |
+
raise RuntimeError("must specify at least one of read=True, write=True")
|
| 65 |
+
mask = 0
|
| 66 |
+
if read:
|
| 67 |
+
mask |= select.POLLIN
|
| 68 |
+
if write:
|
| 69 |
+
mask |= select.POLLOUT
|
| 70 |
+
poll_obj = select.poll()
|
| 71 |
+
poll_obj.register(sock, mask)
|
| 72 |
+
|
| 73 |
+
# For some reason, poll() takes timeout in milliseconds
|
| 74 |
+
def do_poll(t: float | None) -> list[tuple[int, int]]:
|
| 75 |
+
if t is not None:
|
| 76 |
+
t *= 1000
|
| 77 |
+
return poll_obj.poll(t)
|
| 78 |
+
|
| 79 |
+
return bool(do_poll(timeout))
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def _have_working_poll() -> bool:
|
| 83 |
+
# Apparently some systems have a select.poll that fails as soon as you try
|
| 84 |
+
# to use it, either due to strange configuration or broken monkeypatching
|
| 85 |
+
# from libraries like eventlet/greenlet.
|
| 86 |
+
try:
|
| 87 |
+
poll_obj = select.poll()
|
| 88 |
+
poll_obj.poll(0)
|
| 89 |
+
except (AttributeError, OSError):
|
| 90 |
+
return False
|
| 91 |
+
else:
|
| 92 |
+
return True
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def wait_for_socket(
|
| 96 |
+
sock: socket.socket,
|
| 97 |
+
read: bool = False,
|
| 98 |
+
write: bool = False,
|
| 99 |
+
timeout: float | None = None,
|
| 100 |
+
) -> bool:
|
| 101 |
+
# We delay choosing which implementation to use until the first time we're
|
| 102 |
+
# called. We could do it at import time, but then we might make the wrong
|
| 103 |
+
# decision if someone goes wild with monkeypatching select.poll after
|
| 104 |
+
# we're imported.
|
| 105 |
+
global wait_for_socket
|
| 106 |
+
if _have_working_poll():
|
| 107 |
+
wait_for_socket = poll_wait_for_socket
|
| 108 |
+
elif hasattr(select, "select"):
|
| 109 |
+
wait_for_socket = select_wait_for_socket
|
| 110 |
+
return wait_for_socket(sock, read, write, timeout)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def wait_for_read(sock: socket.socket, timeout: float | None = None) -> bool:
|
| 114 |
+
"""Waits for reading to be available on a given socket.
|
| 115 |
+
Returns True if the socket is readable, or False if the timeout expired.
|
| 116 |
+
"""
|
| 117 |
+
return wait_for_socket(sock, read=True, timeout=timeout)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def wait_for_write(sock: socket.socket, timeout: float | None = None) -> bool:
|
| 121 |
+
"""Waits for writing to be available on a given socket.
|
| 122 |
+
Returns True if the socket is readable, or False if the timeout expired.
|
| 123 |
+
"""
|
| 124 |
+
return wait_for_socket(sock, write=True, timeout=timeout)
|
.venv/Lib/site-packages/uvicorn-0.51.0.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
.venv/Lib/site-packages/uvicorn-0.51.0.dist-info/METADATA
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: uvicorn
|
| 3 |
+
Version: 0.51.0
|
| 4 |
+
Summary: The lightning-fast ASGI server.
|
| 5 |
+
Project-URL: Changelog, https://uvicorn.dev/release-notes
|
| 6 |
+
Project-URL: Funding, https://github.com/sponsors/encode
|
| 7 |
+
Project-URL: Homepage, https://uvicorn.dev/
|
| 8 |
+
Project-URL: Source, https://github.com/Kludex/uvicorn
|
| 9 |
+
Author-email: Tom Christie <tom@tomchristie.com>
|
| 10 |
+
Maintainer-email: Marcelo Trylesinski <marcelotryle@gmail.com>
|
| 11 |
+
License-Expression: BSD-3-Clause
|
| 12 |
+
License-File: LICENSE.md
|
| 13 |
+
Classifier: Development Status :: 4 - Beta
|
| 14 |
+
Classifier: Environment :: Web Environment
|
| 15 |
+
Classifier: Intended Audience :: Developers
|
| 16 |
+
Classifier: Operating System :: OS Independent
|
| 17 |
+
Classifier: Programming Language :: Python :: 3
|
| 18 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 19 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 21 |
+
Classifier: Programming Language :: Python :: 3.13
|
| 22 |
+
Classifier: Programming Language :: Python :: 3.14
|
| 23 |
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
| 24 |
+
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
| 25 |
+
Classifier: Topic :: Internet :: WWW/HTTP
|
| 26 |
+
Requires-Python: >=3.10
|
| 27 |
+
Requires-Dist: click>=7.0
|
| 28 |
+
Requires-Dist: h11>=0.8
|
| 29 |
+
Requires-Dist: typing-extensions>=4.0; python_version < '3.11'
|
| 30 |
+
Provides-Extra: standard
|
| 31 |
+
Requires-Dist: httptools>=0.8.0; extra == 'standard'
|
| 32 |
+
Requires-Dist: python-dotenv>=0.13; extra == 'standard'
|
| 33 |
+
Requires-Dist: pyyaml>=5.1; extra == 'standard'
|
| 34 |
+
Requires-Dist: uvloop>=0.15.1; (sys_platform != 'win32' and (sys_platform != 'cygwin' and platform_python_implementation != 'PyPy')) and extra == 'standard'
|
| 35 |
+
Requires-Dist: watchfiles>=0.20; extra == 'standard'
|
| 36 |
+
Requires-Dist: websockets>=13.0; extra == 'standard'
|
| 37 |
+
Description-Content-Type: text/markdown
|
| 38 |
+
|
| 39 |
+
<p align="center">
|
| 40 |
+
<img width="320" height="320" src="https://raw.githubusercontent.com/tomchristie/uvicorn/main/docs/uvicorn.png" alt='uvicorn'>
|
| 41 |
+
</p>
|
| 42 |
+
|
| 43 |
+
<p align="center">
|
| 44 |
+
<em>An ASGI web server, for Python.</em>
|
| 45 |
+
</p>
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
[](https://github.com/Kludex/uvicorn/actions)
|
| 50 |
+
[](https://pypi.python.org/pypi/uvicorn)
|
| 51 |
+
[](https://pypi.org/project/uvicorn)
|
| 52 |
+
[](https://discord.gg/RxKUF5JuHs)
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
**Documentation**: [https://uvicorn.dev](https://uvicorn.dev)
|
| 57 |
+
|
| 58 |
+
**Source Code**: [https://www.github.com/Kludex/uvicorn](https://www.github.com/Kludex/uvicorn)
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
Uvicorn is an ASGI web server implementation for Python.
|
| 63 |
+
|
| 64 |
+
Until recently Python has lacked a minimal low-level server/application interface for
|
| 65 |
+
async frameworks. The [ASGI specification][asgi] fills this gap, and means we're now able to
|
| 66 |
+
start building a common set of tooling usable across all async frameworks.
|
| 67 |
+
|
| 68 |
+
Uvicorn supports HTTP/1.1 and WebSockets.
|
| 69 |
+
|
| 70 |
+
## Quickstart
|
| 71 |
+
|
| 72 |
+
Install using `pip`:
|
| 73 |
+
|
| 74 |
+
```shell
|
| 75 |
+
$ pip install uvicorn
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
+
This will install uvicorn with minimal (pure Python) dependencies.
|
| 79 |
+
|
| 80 |
+
```shell
|
| 81 |
+
$ pip install 'uvicorn[standard]'
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
This will install uvicorn with "Cython-based" dependencies (where possible) and other "optional extras".
|
| 85 |
+
|
| 86 |
+
In this context, "Cython-based" means the following:
|
| 87 |
+
|
| 88 |
+
- the event loop `uvloop` will be installed and used if possible.
|
| 89 |
+
- the http protocol will be handled by `httptools` if possible.
|
| 90 |
+
|
| 91 |
+
Moreover, "optional extras" means that:
|
| 92 |
+
|
| 93 |
+
- the websocket protocol will be handled by `websockets` (should you want to use `wsproto` you'd need to install it manually) if possible.
|
| 94 |
+
- the `--reload` flag in development mode will use `watchfiles`.
|
| 95 |
+
- `python-dotenv` will be installed should you want to use the `--env-file` option.
|
| 96 |
+
- `PyYAML` will be installed to allow you to provide a `.yaml` file to `--log-config`, if desired.
|
| 97 |
+
|
| 98 |
+
Create an application, in `example.py`:
|
| 99 |
+
|
| 100 |
+
```python
|
| 101 |
+
async def app(scope, receive, send):
|
| 102 |
+
assert scope['type'] == 'http'
|
| 103 |
+
|
| 104 |
+
await send({
|
| 105 |
+
'type': 'http.response.start',
|
| 106 |
+
'status': 200,
|
| 107 |
+
'headers': [
|
| 108 |
+
(b'content-type', b'text/plain'),
|
| 109 |
+
],
|
| 110 |
+
})
|
| 111 |
+
await send({
|
| 112 |
+
'type': 'http.response.body',
|
| 113 |
+
'body': b'Hello, world!',
|
| 114 |
+
})
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Run the server:
|
| 118 |
+
|
| 119 |
+
```shell
|
| 120 |
+
$ uvicorn example:app
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
---
|
| 124 |
+
|
| 125 |
+
## Why ASGI?
|
| 126 |
+
|
| 127 |
+
Most well established Python Web frameworks started out as WSGI-based frameworks.
|
| 128 |
+
|
| 129 |
+
WSGI applications are a single, synchronous callable that takes a request and returns a response.
|
| 130 |
+
This doesn’t allow for long-lived connections, like you get with long-poll HTTP or WebSocket connections,
|
| 131 |
+
which WSGI doesn't support well.
|
| 132 |
+
|
| 133 |
+
Having an async concurrency model also allows for options such as lightweight background tasks,
|
| 134 |
+
and can be less of a limiting factor for endpoints that have long periods being blocked on network
|
| 135 |
+
I/O such as dealing with slow HTTP requests.
|
| 136 |
+
|
| 137 |
+
---
|
| 138 |
+
|
| 139 |
+
## Alternative ASGI servers
|
| 140 |
+
|
| 141 |
+
A strength of the ASGI protocol is that it decouples the server implementation
|
| 142 |
+
from the application framework. This allows for an ecosystem of interoperating
|
| 143 |
+
webservers and application frameworks.
|
| 144 |
+
|
| 145 |
+
### Daphne
|
| 146 |
+
|
| 147 |
+
The first ASGI server implementation, originally developed to power Django Channels, is [the Daphne webserver][daphne].
|
| 148 |
+
|
| 149 |
+
It is run widely in production, and supports HTTP/1.1, HTTP/2, and WebSockets.
|
| 150 |
+
|
| 151 |
+
Any of the example applications given here can equally well be run using `daphne` instead.
|
| 152 |
+
|
| 153 |
+
```
|
| 154 |
+
$ pip install daphne
|
| 155 |
+
$ daphne app:App
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
### Hypercorn
|
| 159 |
+
|
| 160 |
+
[Hypercorn][hypercorn] was initially part of the Quart web framework, before
|
| 161 |
+
being separated out into a standalone ASGI server.
|
| 162 |
+
|
| 163 |
+
Hypercorn supports HTTP/1.1, HTTP/2, and WebSockets.
|
| 164 |
+
|
| 165 |
+
It also supports [the excellent `trio` async framework][trio], as an alternative to `asyncio`.
|
| 166 |
+
|
| 167 |
+
```
|
| 168 |
+
$ pip install hypercorn
|
| 169 |
+
$ hypercorn app:App
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
### Mangum
|
| 173 |
+
|
| 174 |
+
[Mangum][mangum] is an adapter for using ASGI applications with AWS Lambda & API Gateway.
|
| 175 |
+
|
| 176 |
+
### Granian
|
| 177 |
+
|
| 178 |
+
[Granian][granian] is an ASGI compatible Rust HTTP server which supports HTTP/2, TLS and WebSockets.
|
| 179 |
+
|
| 180 |
+
---
|
| 181 |
+
|
| 182 |
+
<p align="center"><i>Uvicorn is <a href="https://github.com/Kludex/uvicorn/blob/main/LICENSE.md">BSD licensed</a> code.<br/>Designed & crafted with care.</i><br/>— 🦄 —</p>
|
| 183 |
+
|
| 184 |
+
[asgi]: https://asgi.readthedocs.io/en/latest/
|
| 185 |
+
[daphne]: https://github.com/django/daphne
|
| 186 |
+
[hypercorn]: https://github.com/pgjones/hypercorn
|
| 187 |
+
[trio]: https://trio.readthedocs.io
|
| 188 |
+
[mangum]: https://github.com/jordaneremieff/mangum
|
| 189 |
+
[granian]: https://github.com/emmett-framework/granian
|
.venv/Lib/site-packages/uvicorn-0.51.0.dist-info/RECORD
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
../../Scripts/uvicorn.exe,sha256=-SPhVkmoKomyN2m_cAAR7z-HvoWGTKbiaz4lKy7BacU,108462
|
| 2 |
+
uvicorn-0.51.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 3 |
+
uvicorn-0.51.0.dist-info/METADATA,sha256=B8cHkMDN5oEviRaPeqfBhYAKaQ9-ZX37VgOEjBN1AX4,6589
|
| 4 |
+
uvicorn-0.51.0.dist-info/RECORD,,
|
| 5 |
+
uvicorn-0.51.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
| 6 |
+
uvicorn-0.51.0.dist-info/entry_points.txt,sha256=FW1w-hkc9QgwaGoovMvm0ZY73w_NcycWdGAUfDsNGxw,46
|
| 7 |
+
uvicorn-0.51.0.dist-info/licenses/LICENSE.md,sha256=7-Gs8-YvuZwoiw7HPlp3O3Jo70Mg_nV-qZQhTktjw3E,1526
|
| 8 |
+
uvicorn/__init__.py,sha256=07oX9VGq6TnIs3mghqQk0uZIIdF08SJFBE0JXx3ps9g,147
|
| 9 |
+
uvicorn/__main__.py,sha256=DQizy6nKP0ywhPpnCHgmRDYIMfcqZKVEzNIWQZjqtVQ,62
|
| 10 |
+
uvicorn/__pycache__/__init__.cpython-312.pyc,,
|
| 11 |
+
uvicorn/__pycache__/__main__.cpython-312.pyc,,
|
| 12 |
+
uvicorn/__pycache__/_ansi.cpython-312.pyc,,
|
| 13 |
+
uvicorn/__pycache__/_compat.cpython-312.pyc,,
|
| 14 |
+
uvicorn/__pycache__/_subprocess.cpython-312.pyc,,
|
| 15 |
+
uvicorn/__pycache__/_types.cpython-312.pyc,,
|
| 16 |
+
uvicorn/__pycache__/config.cpython-312.pyc,,
|
| 17 |
+
uvicorn/__pycache__/importer.cpython-312.pyc,,
|
| 18 |
+
uvicorn/__pycache__/logging.cpython-312.pyc,,
|
| 19 |
+
uvicorn/__pycache__/main.cpython-312.pyc,,
|
| 20 |
+
uvicorn/__pycache__/server.cpython-312.pyc,,
|
| 21 |
+
uvicorn/__pycache__/workers.cpython-312.pyc,,
|
| 22 |
+
uvicorn/_ansi.py,sha256=s8RPpmPTO6QGRgEYbsNGeasSWqiEnCsTroyA8Z59Q00,617
|
| 23 |
+
uvicorn/_compat.py,sha256=6X49c9ovzmMHir2S3syinOQMzSPBcp8MNuI9ZQrbYiU,2916
|
| 24 |
+
uvicorn/_subprocess.py,sha256=fQ4JTajuXay6mVc8KQNGfXu6JHoab46aS6kyIMWJMPk,2776
|
| 25 |
+
uvicorn/_types.py,sha256=JDM6E-hqZ6o_cSZvjDULjAv77PFH2pLe_Vgqshe8qH4,7677
|
| 26 |
+
uvicorn/config.py,sha256=hjwBd2_PcHz92eeTvRgtbWIEvWKhMCITPT2M7fy0o68,24495
|
| 27 |
+
uvicorn/importer.py,sha256=nRt0QQ3qpi264-n_mR0l55C2ddM8nowTNzT1jsWaam8,1128
|
| 28 |
+
uvicorn/lifespan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 29 |
+
uvicorn/lifespan/__pycache__/__init__.cpython-312.pyc,,
|
| 30 |
+
uvicorn/lifespan/__pycache__/off.cpython-312.pyc,,
|
| 31 |
+
uvicorn/lifespan/__pycache__/on.cpython-312.pyc,,
|
| 32 |
+
uvicorn/lifespan/off.py,sha256=nfI6qHAUo_8-BEXMBKoHQ9wUbsXrPaXLCbDSS0vKSr8,332
|
| 33 |
+
uvicorn/lifespan/on.py,sha256=iETPFj6SFhPDPcdeGxfa43LKImJU7MI5i7QUUepBcPE,5173
|
| 34 |
+
uvicorn/logging.py,sha256=kCB1n45B5O55SSApiv2x6UQG3LnFqNlGvcohGyAwblU,4256
|
| 35 |
+
uvicorn/loops/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 36 |
+
uvicorn/loops/__pycache__/__init__.cpython-312.pyc,,
|
| 37 |
+
uvicorn/loops/__pycache__/asyncio.cpython-312.pyc,,
|
| 38 |
+
uvicorn/loops/__pycache__/auto.cpython-312.pyc,,
|
| 39 |
+
uvicorn/loops/__pycache__/uvloop.cpython-312.pyc,,
|
| 40 |
+
uvicorn/loops/asyncio.py,sha256=a8wO4fB4pCk-RLICq7CnGr_cxQI7MBLJj-Go4UQldD8,333
|
| 41 |
+
uvicorn/loops/auto.py,sha256=pdTsMcqkbE8fvdNgLs1y4c6hsuN598mZOSOlC0a23S4,566
|
| 42 |
+
uvicorn/loops/uvloop.py,sha256=Ca-TL-W8tbdEW5uvL_GrMih7G8e_t_vhNevi2NKYjEU,236
|
| 43 |
+
uvicorn/main.py,sha256=YitRjBbr9ECYXUYzV5_y0tVud-8RfkGXrk9dnJJqndg,18812
|
| 44 |
+
uvicorn/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 45 |
+
uvicorn/middleware/__pycache__/__init__.cpython-312.pyc,,
|
| 46 |
+
uvicorn/middleware/__pycache__/asgi2.cpython-312.pyc,,
|
| 47 |
+
uvicorn/middleware/__pycache__/message_logger.cpython-312.pyc,,
|
| 48 |
+
uvicorn/middleware/__pycache__/proxy_headers.cpython-312.pyc,,
|
| 49 |
+
uvicorn/middleware/__pycache__/wsgi.cpython-312.pyc,,
|
| 50 |
+
uvicorn/middleware/asgi2.py,sha256=YQrQNm3RehFts3mzk3k4yw8aD8Egtj0tRS3N45YkQa0,394
|
| 51 |
+
uvicorn/middleware/message_logger.py,sha256=IHEZUSnFNaMFUFdwtZO3AuFATnYcSor-gVtOjbCzt8M,2859
|
| 52 |
+
uvicorn/middleware/proxy_headers.py,sha256=pNZk80NNscLEwi8pGn6cIbaF0bSfJhCKn3pHjqqfW90,7421
|
| 53 |
+
uvicorn/middleware/wsgi.py,sha256=VnbnkdCL3_ctLWzk1j39xvxysuoAB3kSYmi8TsOLpNs,7125
|
| 54 |
+
uvicorn/protocols/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 55 |
+
uvicorn/protocols/__pycache__/__init__.cpython-312.pyc,,
|
| 56 |
+
uvicorn/protocols/__pycache__/utils.cpython-312.pyc,,
|
| 57 |
+
uvicorn/protocols/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 58 |
+
uvicorn/protocols/http/__pycache__/__init__.cpython-312.pyc,,
|
| 59 |
+
uvicorn/protocols/http/__pycache__/auto.cpython-312.pyc,,
|
| 60 |
+
uvicorn/protocols/http/__pycache__/flow_control.cpython-312.pyc,,
|
| 61 |
+
uvicorn/protocols/http/__pycache__/h11_impl.cpython-312.pyc,,
|
| 62 |
+
uvicorn/protocols/http/__pycache__/httptools_impl.cpython-312.pyc,,
|
| 63 |
+
uvicorn/protocols/http/auto.py,sha256=YfXGyzWTaaE2p_jkTPWrJCXsxEaQnC3NK0-G7Wgmnls,403
|
| 64 |
+
uvicorn/protocols/http/flow_control.py,sha256=050WVg31EvPOkHwynCoMP1zXFl_vO3U4durlc5vyp4U,1701
|
| 65 |
+
uvicorn/protocols/http/h11_impl.py,sha256=DBDE_HnnQtqW50bAb4OW3dKKDgEATUSOvNqfDCDBF2Q,21202
|
| 66 |
+
uvicorn/protocols/http/httptools_impl.py,sha256=Aq2MlNrL99irluvR673AfcVbo6qlogl8fiR9IODMxxA,22355
|
| 67 |
+
uvicorn/protocols/utils.py,sha256=1hKdDCmo_GtGrK1zZfa70jL7Hr3mDE8RnL69O4rHQRg,2058
|
| 68 |
+
uvicorn/protocols/websockets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 69 |
+
uvicorn/protocols/websockets/__pycache__/__init__.cpython-312.pyc,,
|
| 70 |
+
uvicorn/protocols/websockets/__pycache__/auto.cpython-312.pyc,,
|
| 71 |
+
uvicorn/protocols/websockets/__pycache__/websockets_impl.cpython-312.pyc,,
|
| 72 |
+
uvicorn/protocols/websockets/__pycache__/websockets_sansio_impl.cpython-312.pyc,,
|
| 73 |
+
uvicorn/protocols/websockets/__pycache__/wsproto_impl.cpython-312.pyc,,
|
| 74 |
+
uvicorn/protocols/websockets/auto.py,sha256=QOnMKZfXHfOcZHdq1OITHwv7Awx1BTS3eJfZswuGEU0,611
|
| 75 |
+
uvicorn/protocols/websockets/websockets_impl.py,sha256=7Bu_Ic_7AdxBcttG0GTLb3tzgQBsHx0Kyb_C49jBzxw,15176
|
| 76 |
+
uvicorn/protocols/websockets/websockets_sansio_impl.py,sha256=Tr54qVO78q0PiCUM-9-jnFabWahGZHLWa5ecKvkOsaY,20607
|
| 77 |
+
uvicorn/protocols/websockets/wsproto_impl.py,sha256=8088Bp8SOz4Z2L2gcqOqV3zgWDVxIN0eIkjhuR9oqYU,19821
|
| 78 |
+
uvicorn/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
| 79 |
+
uvicorn/server.py,sha256=ao_gfmmVQ_Ily8fQwAJ__Sb-yVeX1cahBEbTjHkp7Vc,13382
|
| 80 |
+
uvicorn/supervisors/__init__.py,sha256=wT8eOEIqT1yWQgytZtv5taWMul7xoTIY0xm1m4oyPTw,507
|
| 81 |
+
uvicorn/supervisors/__pycache__/__init__.cpython-312.pyc,,
|
| 82 |
+
uvicorn/supervisors/__pycache__/basereload.cpython-312.pyc,,
|
| 83 |
+
uvicorn/supervisors/__pycache__/multiprocess.cpython-312.pyc,,
|
| 84 |
+
uvicorn/supervisors/__pycache__/statreload.cpython-312.pyc,,
|
| 85 |
+
uvicorn/supervisors/__pycache__/watchfilesreload.cpython-312.pyc,,
|
| 86 |
+
uvicorn/supervisors/basereload.py,sha256=C-YD18flOKKVRNTRsSCOEvyqGf3ZxpvuUvR0z0Y1Wpg,4018
|
| 87 |
+
uvicorn/supervisors/multiprocess.py,sha256=U1QZdiJ_7Auazix6XOj1ku0Nj_N_PJPCoYuq6SNjZcI,10271
|
| 88 |
+
uvicorn/supervisors/statreload.py,sha256=XPaEtVXaasII1kkG32UUG8oD7-fS0FwyXkQLF9QYPbA,1548
|
| 89 |
+
uvicorn/supervisors/watchfilesreload.py,sha256=u-MuYm0LsZMN670u_TtSWiMBUA2z4j1sIe3O2A67Ep4,2923
|
| 90 |
+
uvicorn/workers.py,sha256=lMCubn1Wi-4h2jxrXNMmhOJZy7ZP8QxbZbP3_2O1iHE,3873
|
.venv/Lib/site-packages/uvicorn-0.51.0.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: hatchling 1.30.1
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
.venv/Lib/site-packages/uvicorn-0.51.0.dist-info/entry_points.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[console_scripts]
|
| 2 |
+
uvicorn = uvicorn.main:main
|
.venv/Lib/site-packages/uvicorn-0.51.0.dist-info/licenses/LICENSE.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright © 2017-present, [Encode OSS Ltd](https://www.encode.io/).
|
| 2 |
+
All rights reserved.
|
| 3 |
+
|
| 4 |
+
Redistribution and use in source and binary forms, with or without
|
| 5 |
+
modification, are permitted provided that the following conditions are met:
|
| 6 |
+
|
| 7 |
+
* Redistributions of source code must retain the above copyright notice, this
|
| 8 |
+
list of conditions and the following disclaimer.
|
| 9 |
+
|
| 10 |
+
* Redistributions in binary form must reproduce the above copyright notice,
|
| 11 |
+
this list of conditions and the following disclaimer in the documentation
|
| 12 |
+
and/or other materials provided with the distribution.
|
| 13 |
+
|
| 14 |
+
* Neither the name of the copyright holder nor the names of its
|
| 15 |
+
contributors may be used to endorse or promote products derived from
|
| 16 |
+
this software without specific prior written permission.
|
| 17 |
+
|
| 18 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
| 19 |
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
| 20 |
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
| 21 |
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
| 22 |
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
| 23 |
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
| 24 |
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
| 25 |
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
| 26 |
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| 27 |
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
.venv/Lib/site-packages/uvicorn/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from uvicorn.config import Config
|
| 2 |
+
from uvicorn.main import Server, main, run
|
| 3 |
+
|
| 4 |
+
__version__ = "0.51.0"
|
| 5 |
+
__all__ = ["main", "run", "Config", "Server"]
|
.venv/Lib/site-packages/uvicorn/__main__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uvicorn
|
| 2 |
+
|
| 3 |
+
if __name__ == "__main__":
|
| 4 |
+
uvicorn.main()
|
.venv/Lib/site-packages/uvicorn/_ansi.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
_ANSI_COLORS = {
|
| 4 |
+
"black": 30,
|
| 5 |
+
"red": 31,
|
| 6 |
+
"green": 32,
|
| 7 |
+
"yellow": 33,
|
| 8 |
+
"blue": 34,
|
| 9 |
+
"magenta": 35,
|
| 10 |
+
"cyan": 36,
|
| 11 |
+
"white": 37,
|
| 12 |
+
"bright_black": 90,
|
| 13 |
+
"bright_red": 91,
|
| 14 |
+
"bright_green": 92,
|
| 15 |
+
"bright_yellow": 93,
|
| 16 |
+
"bright_blue": 94,
|
| 17 |
+
"bright_magenta": 95,
|
| 18 |
+
"bright_cyan": 96,
|
| 19 |
+
"bright_white": 97,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def style(text: str, fg: str | None = None, bold: bool = False) -> str:
|
| 24 |
+
prefix = ""
|
| 25 |
+
if fg is not None:
|
| 26 |
+
prefix += f"\033[{_ANSI_COLORS[fg]}m"
|
| 27 |
+
if bold:
|
| 28 |
+
prefix += "\033[1m"
|
| 29 |
+
return f"{prefix}{text}\033[0m"
|
.venv/Lib/site-packages/uvicorn/_compat.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import sys
|
| 5 |
+
from collections.abc import Callable, Coroutine
|
| 6 |
+
from typing import Any, TypeVar
|
| 7 |
+
|
| 8 |
+
__all__ = ["asyncio_run", "iscoroutinefunction"]
|
| 9 |
+
|
| 10 |
+
if sys.version_info >= (3, 14):
|
| 11 |
+
from inspect import iscoroutinefunction
|
| 12 |
+
else:
|
| 13 |
+
from asyncio import iscoroutinefunction
|
| 14 |
+
|
| 15 |
+
_T = TypeVar("_T")
|
| 16 |
+
|
| 17 |
+
if sys.version_info >= (3, 12):
|
| 18 |
+
asyncio_run = asyncio.run
|
| 19 |
+
elif sys.version_info >= (3, 11):
|
| 20 |
+
|
| 21 |
+
def asyncio_run(
|
| 22 |
+
main: Coroutine[Any, Any, _T],
|
| 23 |
+
*,
|
| 24 |
+
debug: bool = False,
|
| 25 |
+
loop_factory: Callable[[], asyncio.AbstractEventLoop] | None = None,
|
| 26 |
+
) -> _T:
|
| 27 |
+
# asyncio.run from Python 3.12
|
| 28 |
+
# https://docs.python.org/3/license.html#psf-license
|
| 29 |
+
with asyncio.Runner(debug=debug, loop_factory=loop_factory) as runner:
|
| 30 |
+
return runner.run(main)
|
| 31 |
+
|
| 32 |
+
else:
|
| 33 |
+
# modified version of asyncio.run from Python 3.10 to add loop_factory kwarg
|
| 34 |
+
# https://docs.python.org/3/license.html#psf-license
|
| 35 |
+
def asyncio_run(
|
| 36 |
+
main: Coroutine[Any, Any, _T],
|
| 37 |
+
*,
|
| 38 |
+
debug: bool = False,
|
| 39 |
+
loop_factory: Callable[[], asyncio.AbstractEventLoop] | None = None,
|
| 40 |
+
) -> _T:
|
| 41 |
+
try:
|
| 42 |
+
asyncio.get_running_loop()
|
| 43 |
+
except RuntimeError:
|
| 44 |
+
pass
|
| 45 |
+
else:
|
| 46 |
+
raise RuntimeError("asyncio.run() cannot be called from a running event loop")
|
| 47 |
+
|
| 48 |
+
if not asyncio.iscoroutine(main):
|
| 49 |
+
raise ValueError(f"a coroutine was expected, got {main!r}")
|
| 50 |
+
|
| 51 |
+
if loop_factory is None:
|
| 52 |
+
loop = asyncio.new_event_loop()
|
| 53 |
+
else:
|
| 54 |
+
loop = loop_factory()
|
| 55 |
+
try:
|
| 56 |
+
if loop_factory is None:
|
| 57 |
+
asyncio.set_event_loop(loop)
|
| 58 |
+
if debug is not None:
|
| 59 |
+
loop.set_debug(debug)
|
| 60 |
+
return loop.run_until_complete(main)
|
| 61 |
+
finally:
|
| 62 |
+
try:
|
| 63 |
+
_cancel_all_tasks(loop)
|
| 64 |
+
loop.run_until_complete(loop.shutdown_asyncgens())
|
| 65 |
+
loop.run_until_complete(loop.shutdown_default_executor())
|
| 66 |
+
finally:
|
| 67 |
+
if loop_factory is None:
|
| 68 |
+
asyncio.set_event_loop(None)
|
| 69 |
+
loop.close()
|
| 70 |
+
|
| 71 |
+
def _cancel_all_tasks(loop: asyncio.AbstractEventLoop) -> None:
|
| 72 |
+
to_cancel = asyncio.all_tasks(loop)
|
| 73 |
+
if not to_cancel:
|
| 74 |
+
return
|
| 75 |
+
|
| 76 |
+
for task in to_cancel:
|
| 77 |
+
task.cancel()
|
| 78 |
+
|
| 79 |
+
loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True))
|
| 80 |
+
|
| 81 |
+
for task in to_cancel:
|
| 82 |
+
if task.cancelled():
|
| 83 |
+
continue
|
| 84 |
+
if task.exception() is not None:
|
| 85 |
+
loop.call_exception_handler(
|
| 86 |
+
{
|
| 87 |
+
"message": "unhandled exception during asyncio.run() shutdown",
|
| 88 |
+
"exception": task.exception(),
|
| 89 |
+
"task": task,
|
| 90 |
+
}
|
| 91 |
+
)
|
.venv/Lib/site-packages/uvicorn/_subprocess.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Some light wrappers around Python's multiprocessing, to deal with cleanly
|
| 3 |
+
starting child processes.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import multiprocessing
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
from collections.abc import Callable
|
| 12 |
+
from multiprocessing.context import SpawnProcess
|
| 13 |
+
from socket import socket
|
| 14 |
+
|
| 15 |
+
from uvicorn.config import Config
|
| 16 |
+
|
| 17 |
+
multiprocessing.allow_connection_pickling()
|
| 18 |
+
spawn = multiprocessing.get_context("spawn")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def get_subprocess(
|
| 22 |
+
config: Config,
|
| 23 |
+
target: Callable[..., None],
|
| 24 |
+
sockets: list[socket],
|
| 25 |
+
) -> SpawnProcess:
|
| 26 |
+
"""
|
| 27 |
+
Called in the parent process, to instantiate a new child process instance.
|
| 28 |
+
The child is not yet started at this point.
|
| 29 |
+
|
| 30 |
+
* config - The Uvicorn configuration instance.
|
| 31 |
+
* target - A callable that accepts a list of sockets. In practice this will
|
| 32 |
+
be the `Server.run()` method.
|
| 33 |
+
* sockets - A list of sockets to pass to the server. Sockets are bound once
|
| 34 |
+
by the parent process, and then passed to the child processes.
|
| 35 |
+
"""
|
| 36 |
+
# We pass across the stdin fileno, and reopen it in the child process.
|
| 37 |
+
# This is required for some debugging environments.
|
| 38 |
+
try:
|
| 39 |
+
stdin_fileno = sys.stdin.fileno()
|
| 40 |
+
# The `sys.stdin` can be `None`, see https://docs.python.org/3/library/sys.html#sys.__stdin__.
|
| 41 |
+
except (AttributeError, OSError):
|
| 42 |
+
stdin_fileno = None
|
| 43 |
+
|
| 44 |
+
kwargs = {
|
| 45 |
+
"config": config,
|
| 46 |
+
"target": target,
|
| 47 |
+
"sockets": sockets,
|
| 48 |
+
"stdin_fileno": stdin_fileno,
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
return spawn.Process(target=subprocess_started, kwargs=kwargs)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def subprocess_started(
|
| 55 |
+
config: Config,
|
| 56 |
+
target: Callable[..., None],
|
| 57 |
+
sockets: list[socket],
|
| 58 |
+
stdin_fileno: int | None,
|
| 59 |
+
) -> None:
|
| 60 |
+
"""
|
| 61 |
+
Called when the child process starts.
|
| 62 |
+
|
| 63 |
+
* config - The Uvicorn configuration instance.
|
| 64 |
+
* target - A callable that accepts a list of sockets. In practice this will
|
| 65 |
+
be the `Server.run()` method.
|
| 66 |
+
* sockets - A list of sockets to pass to the server. Sockets are bound once
|
| 67 |
+
by the parent process, and then passed to the child processes.
|
| 68 |
+
* stdin_fileno - The file number of sys.stdin, so that it can be reattached
|
| 69 |
+
to the child process.
|
| 70 |
+
"""
|
| 71 |
+
# Re-open stdin.
|
| 72 |
+
if stdin_fileno is not None:
|
| 73 |
+
sys.stdin = os.fdopen(stdin_fileno) # pragma: full coverage
|
| 74 |
+
|
| 75 |
+
# Logging needs to be setup again for each child.
|
| 76 |
+
config.configure_logging()
|
| 77 |
+
|
| 78 |
+
try:
|
| 79 |
+
# Now we can call into `Server.run(sockets=sockets)`
|
| 80 |
+
target(sockets=sockets)
|
| 81 |
+
except KeyboardInterrupt: # pragma: no cover
|
| 82 |
+
# suppress the exception to avoid a traceback from subprocess.Popen
|
| 83 |
+
# the parent already expects us to end, so no vital information is lost
|
| 84 |
+
pass
|
.venv/Lib/site-packages/uvicorn/_types.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Copyright (c) Django Software Foundation and individual contributors.
|
| 3 |
+
All rights reserved.
|
| 4 |
+
|
| 5 |
+
Redistribution and use in source and binary forms, with or without modification,
|
| 6 |
+
are permitted provided that the following conditions are met:
|
| 7 |
+
|
| 8 |
+
1. Redistributions of source code must retain the above copyright notice,
|
| 9 |
+
this list of conditions and the following disclaimer.
|
| 10 |
+
|
| 11 |
+
2. Redistributions in binary form must reproduce the above copyright
|
| 12 |
+
notice, this list of conditions and the following disclaimer in the
|
| 13 |
+
documentation and/or other materials provided with the distribution.
|
| 14 |
+
|
| 15 |
+
3. Neither the name of Django nor the names of its contributors may be used
|
| 16 |
+
to endorse or promote products derived from this software without
|
| 17 |
+
specific prior written permission.
|
| 18 |
+
|
| 19 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
| 20 |
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
| 21 |
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
| 22 |
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
| 23 |
+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
| 24 |
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
| 25 |
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
| 26 |
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
| 27 |
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
| 28 |
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
from __future__ import annotations
|
| 32 |
+
|
| 33 |
+
import sys
|
| 34 |
+
import types
|
| 35 |
+
from collections.abc import Awaitable, Callable, Iterable, MutableMapping
|
| 36 |
+
from typing import Any, Literal, Protocol, TypedDict
|
| 37 |
+
|
| 38 |
+
if sys.version_info >= (3, 11): # pragma: py-lt-311
|
| 39 |
+
from typing import NotRequired
|
| 40 |
+
else: # pragma: py-gte-311
|
| 41 |
+
from typing_extensions import NotRequired
|
| 42 |
+
|
| 43 |
+
# WSGI
|
| 44 |
+
Environ = MutableMapping[str, Any]
|
| 45 |
+
ExcInfo = tuple[type[BaseException], BaseException, types.TracebackType | None]
|
| 46 |
+
StartResponse = Callable[[str, Iterable[tuple[str, str]], ExcInfo | None], None]
|
| 47 |
+
WSGIApp = Callable[[Environ, StartResponse], Iterable[bytes] | BaseException]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ASGI
|
| 51 |
+
class ASGIVersions(TypedDict):
|
| 52 |
+
spec_version: str
|
| 53 |
+
version: Literal["2.0"] | Literal["3.0"]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class HTTPScope(TypedDict):
|
| 57 |
+
type: Literal["http"]
|
| 58 |
+
asgi: ASGIVersions
|
| 59 |
+
http_version: str
|
| 60 |
+
method: str
|
| 61 |
+
scheme: str
|
| 62 |
+
path: str
|
| 63 |
+
raw_path: bytes
|
| 64 |
+
query_string: bytes
|
| 65 |
+
root_path: str
|
| 66 |
+
headers: Iterable[tuple[bytes, bytes]]
|
| 67 |
+
client: tuple[str, int] | None
|
| 68 |
+
server: tuple[str, int | None] | None
|
| 69 |
+
state: NotRequired[dict[str, Any]]
|
| 70 |
+
extensions: NotRequired[dict[str, dict[object, object]]]
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class WebSocketScope(TypedDict):
|
| 74 |
+
type: Literal["websocket"]
|
| 75 |
+
asgi: ASGIVersions
|
| 76 |
+
http_version: str
|
| 77 |
+
scheme: str
|
| 78 |
+
path: str
|
| 79 |
+
raw_path: bytes
|
| 80 |
+
query_string: bytes
|
| 81 |
+
root_path: str
|
| 82 |
+
headers: Iterable[tuple[bytes, bytes]]
|
| 83 |
+
client: tuple[str, int] | None
|
| 84 |
+
server: tuple[str, int | None] | None
|
| 85 |
+
subprotocols: Iterable[str]
|
| 86 |
+
state: NotRequired[dict[str, Any]]
|
| 87 |
+
extensions: NotRequired[dict[str, dict[object, object]]]
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class LifespanScope(TypedDict):
|
| 91 |
+
type: Literal["lifespan"]
|
| 92 |
+
asgi: ASGIVersions
|
| 93 |
+
state: NotRequired[dict[str, Any]]
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
WWWScope = HTTPScope | WebSocketScope
|
| 97 |
+
Scope = HTTPScope | WebSocketScope | LifespanScope
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class HTTPRequestEvent(TypedDict):
|
| 101 |
+
type: Literal["http.request"]
|
| 102 |
+
body: bytes
|
| 103 |
+
more_body: bool
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class HTTPResponseDebugEvent(TypedDict):
|
| 107 |
+
type: Literal["http.response.debug"]
|
| 108 |
+
info: dict[str, object]
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class HTTPResponseStartEvent(TypedDict):
|
| 112 |
+
type: Literal["http.response.start"]
|
| 113 |
+
status: int
|
| 114 |
+
headers: NotRequired[Iterable[tuple[bytes, bytes]]]
|
| 115 |
+
trailers: NotRequired[bool]
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class HTTPResponseBodyEvent(TypedDict):
|
| 119 |
+
type: Literal["http.response.body"]
|
| 120 |
+
body: bytes
|
| 121 |
+
more_body: NotRequired[bool]
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
class HTTPResponseTrailersEvent(TypedDict):
|
| 125 |
+
type: Literal["http.response.trailers"]
|
| 126 |
+
headers: Iterable[tuple[bytes, bytes]]
|
| 127 |
+
more_trailers: bool
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class HTTPServerPushEvent(TypedDict):
|
| 131 |
+
type: Literal["http.response.push"]
|
| 132 |
+
path: str
|
| 133 |
+
headers: Iterable[tuple[bytes, bytes]]
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
class HTTPDisconnectEvent(TypedDict):
|
| 137 |
+
type: Literal["http.disconnect"]
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class WebSocketConnectEvent(TypedDict):
|
| 141 |
+
type: Literal["websocket.connect"]
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class WebSocketAcceptEvent(TypedDict):
|
| 145 |
+
type: Literal["websocket.accept"]
|
| 146 |
+
subprotocol: NotRequired[str | None]
|
| 147 |
+
headers: NotRequired[Iterable[tuple[bytes, bytes]]]
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class _WebSocketReceiveEventBytes(TypedDict):
|
| 151 |
+
type: Literal["websocket.receive"]
|
| 152 |
+
bytes: bytes
|
| 153 |
+
text: NotRequired[None]
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class _WebSocketReceiveEventText(TypedDict):
|
| 157 |
+
type: Literal["websocket.receive"]
|
| 158 |
+
bytes: NotRequired[None]
|
| 159 |
+
text: str
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
WebSocketReceiveEvent = _WebSocketReceiveEventBytes | _WebSocketReceiveEventText
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
class _WebSocketSendEventBytes(TypedDict):
|
| 166 |
+
type: Literal["websocket.send"]
|
| 167 |
+
bytes: bytes
|
| 168 |
+
text: NotRequired[None]
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class _WebSocketSendEventText(TypedDict):
|
| 172 |
+
type: Literal["websocket.send"]
|
| 173 |
+
bytes: NotRequired[None]
|
| 174 |
+
text: str
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
WebSocketSendEvent = _WebSocketSendEventBytes | _WebSocketSendEventText
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
class WebSocketResponseStartEvent(TypedDict):
|
| 181 |
+
type: Literal["websocket.http.response.start"]
|
| 182 |
+
status: int
|
| 183 |
+
headers: Iterable[tuple[bytes, bytes]]
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
class WebSocketResponseBodyEvent(TypedDict):
|
| 187 |
+
type: Literal["websocket.http.response.body"]
|
| 188 |
+
body: bytes
|
| 189 |
+
more_body: NotRequired[bool]
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
class WebSocketDisconnectEvent(TypedDict):
|
| 193 |
+
type: Literal["websocket.disconnect"]
|
| 194 |
+
code: int
|
| 195 |
+
reason: NotRequired[str | None]
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class WebSocketCloseEvent(TypedDict):
|
| 199 |
+
type: Literal["websocket.close"]
|
| 200 |
+
code: NotRequired[int]
|
| 201 |
+
reason: NotRequired[str | None]
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
class LifespanStartupEvent(TypedDict):
|
| 205 |
+
type: Literal["lifespan.startup"]
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
class LifespanShutdownEvent(TypedDict):
|
| 209 |
+
type: Literal["lifespan.shutdown"]
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
class LifespanStartupCompleteEvent(TypedDict):
|
| 213 |
+
type: Literal["lifespan.startup.complete"]
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class LifespanStartupFailedEvent(TypedDict):
|
| 217 |
+
type: Literal["lifespan.startup.failed"]
|
| 218 |
+
message: str
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
class LifespanShutdownCompleteEvent(TypedDict):
|
| 222 |
+
type: Literal["lifespan.shutdown.complete"]
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
class LifespanShutdownFailedEvent(TypedDict):
|
| 226 |
+
type: Literal["lifespan.shutdown.failed"]
|
| 227 |
+
message: str
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
WebSocketEvent = WebSocketReceiveEvent | WebSocketDisconnectEvent | WebSocketConnectEvent
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
ASGIReceiveEvent = (
|
| 234 |
+
HTTPRequestEvent
|
| 235 |
+
| HTTPDisconnectEvent
|
| 236 |
+
| WebSocketConnectEvent
|
| 237 |
+
| WebSocketReceiveEvent
|
| 238 |
+
| WebSocketDisconnectEvent
|
| 239 |
+
| LifespanStartupEvent
|
| 240 |
+
| LifespanShutdownEvent
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
ASGISendEvent = (
|
| 245 |
+
HTTPResponseStartEvent
|
| 246 |
+
| HTTPResponseBodyEvent
|
| 247 |
+
| HTTPResponseTrailersEvent
|
| 248 |
+
| HTTPServerPushEvent
|
| 249 |
+
| HTTPDisconnectEvent
|
| 250 |
+
| WebSocketAcceptEvent
|
| 251 |
+
| WebSocketSendEvent
|
| 252 |
+
| WebSocketResponseStartEvent
|
| 253 |
+
| WebSocketResponseBodyEvent
|
| 254 |
+
| WebSocketCloseEvent
|
| 255 |
+
| LifespanStartupCompleteEvent
|
| 256 |
+
| LifespanStartupFailedEvent
|
| 257 |
+
| LifespanShutdownCompleteEvent
|
| 258 |
+
| LifespanShutdownFailedEvent
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
|
| 262 |
+
ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]]
|
| 263 |
+
ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]]
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
class ASGI2Protocol(Protocol):
|
| 267 |
+
def __init__(self, scope: Scope) -> None: ... # pragma: no cover
|
| 268 |
+
|
| 269 |
+
async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None: ... # pragma: no cover
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
ASGI2Application = type[ASGI2Protocol]
|
| 273 |
+
ASGI3Application = Callable[[Scope, ASGIReceiveCallable, ASGISendCallable], Awaitable[None]]
|
| 274 |
+
ASGIApplication = ASGI2Application | ASGI3Application
|
.venv/Lib/site-packages/uvicorn/config.py
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import inspect
|
| 5 |
+
import json
|
| 6 |
+
import logging
|
| 7 |
+
import logging.config
|
| 8 |
+
import os
|
| 9 |
+
import socket
|
| 10 |
+
import ssl
|
| 11 |
+
import sys
|
| 12 |
+
from collections.abc import Awaitable, Callable
|
| 13 |
+
from configparser import RawConfigParser
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from typing import IO, Any, Literal
|
| 16 |
+
|
| 17 |
+
from uvicorn._ansi import style
|
| 18 |
+
from uvicorn._compat import iscoroutinefunction
|
| 19 |
+
from uvicorn._types import ASGIApplication
|
| 20 |
+
from uvicorn.importer import ImportFromStringError, import_from_string
|
| 21 |
+
from uvicorn.logging import TRACE_LOG_LEVEL
|
| 22 |
+
from uvicorn.middleware.asgi2 import ASGI2Middleware
|
| 23 |
+
from uvicorn.middleware.message_logger import MessageLoggerMiddleware
|
| 24 |
+
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
| 25 |
+
from uvicorn.middleware.wsgi import WSGIMiddleware
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class UvicornDeprecationWarning(UserWarning):
|
| 29 |
+
"""A custom deprecation warning for Uvicorn.
|
| 30 |
+
|
| 31 |
+
Unlike the built-in DeprecationWarning, this inherits from UserWarning to ensure it is visible by default, helping
|
| 32 |
+
users discover deprecated features without needing to enable warnings explicitly.
|
| 33 |
+
|
| 34 |
+
Reference: https://sethmlarson.dev/deprecations-via-warnings-dont-work-for-python-libraries
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
HTTPProtocolType = Literal["auto", "h11", "httptools"]
|
| 39 |
+
WSProtocolType = Literal["auto", "none", "websockets", "websockets-sansio", "wsproto"]
|
| 40 |
+
LifespanType = Literal["auto", "on", "off"]
|
| 41 |
+
LoopFactoryType = Literal["none", "auto", "asyncio", "uvloop"]
|
| 42 |
+
InterfaceType = Literal["auto", "asgi3", "asgi2", "wsgi"]
|
| 43 |
+
|
| 44 |
+
LOG_LEVELS: dict[str, int] = {
|
| 45 |
+
"critical": logging.CRITICAL,
|
| 46 |
+
"error": logging.ERROR,
|
| 47 |
+
"warning": logging.WARNING,
|
| 48 |
+
"info": logging.INFO,
|
| 49 |
+
"debug": logging.DEBUG,
|
| 50 |
+
"trace": TRACE_LOG_LEVEL,
|
| 51 |
+
}
|
| 52 |
+
HTTP_PROTOCOLS: dict[str, str] = {
|
| 53 |
+
"auto": "uvicorn.protocols.http.auto:AutoHTTPProtocol",
|
| 54 |
+
"h11": "uvicorn.protocols.http.h11_impl:H11Protocol",
|
| 55 |
+
"httptools": "uvicorn.protocols.http.httptools_impl:HttpToolsProtocol",
|
| 56 |
+
}
|
| 57 |
+
WS_PROTOCOLS: dict[str, str | None] = {
|
| 58 |
+
"auto": "uvicorn.protocols.websockets.auto:AutoWebSocketsProtocol",
|
| 59 |
+
"none": None,
|
| 60 |
+
"websockets": "uvicorn.protocols.websockets.websockets_impl:WebSocketProtocol",
|
| 61 |
+
"websockets-sansio": "uvicorn.protocols.websockets.websockets_sansio_impl:WebSocketsSansIOProtocol",
|
| 62 |
+
"wsproto": "uvicorn.protocols.websockets.wsproto_impl:WSProtocol",
|
| 63 |
+
}
|
| 64 |
+
LIFESPAN: dict[str, str] = {
|
| 65 |
+
"auto": "uvicorn.lifespan.on:LifespanOn",
|
| 66 |
+
"on": "uvicorn.lifespan.on:LifespanOn",
|
| 67 |
+
"off": "uvicorn.lifespan.off:LifespanOff",
|
| 68 |
+
}
|
| 69 |
+
LOOP_FACTORIES: dict[str, str | None] = {
|
| 70 |
+
"none": None,
|
| 71 |
+
"auto": "uvicorn.loops.auto:auto_loop_factory",
|
| 72 |
+
"asyncio": "uvicorn.loops.asyncio:asyncio_loop_factory",
|
| 73 |
+
"uvloop": "uvicorn.loops.uvloop:uvloop_loop_factory",
|
| 74 |
+
}
|
| 75 |
+
INTERFACES: list[InterfaceType] = ["auto", "asgi3", "asgi2", "wsgi"]
|
| 76 |
+
|
| 77 |
+
SSL_PROTOCOL_VERSION: int = ssl.PROTOCOL_TLS_SERVER
|
| 78 |
+
|
| 79 |
+
STARTUP_FAILURE = 3
|
| 80 |
+
|
| 81 |
+
LOGGING_CONFIG: dict[str, Any] = {
|
| 82 |
+
"version": 1,
|
| 83 |
+
"disable_existing_loggers": False,
|
| 84 |
+
"formatters": {
|
| 85 |
+
"default": {
|
| 86 |
+
"()": "uvicorn.logging.DefaultFormatter",
|
| 87 |
+
"fmt": "%(levelprefix)s %(message)s",
|
| 88 |
+
"use_colors": None,
|
| 89 |
+
},
|
| 90 |
+
"access": {
|
| 91 |
+
"()": "uvicorn.logging.AccessFormatter",
|
| 92 |
+
"fmt": '%(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s', # noqa: E501
|
| 93 |
+
},
|
| 94 |
+
},
|
| 95 |
+
"handlers": {
|
| 96 |
+
"default": {
|
| 97 |
+
"formatter": "default",
|
| 98 |
+
"class": "logging.StreamHandler",
|
| 99 |
+
"stream": "ext://sys.stderr",
|
| 100 |
+
},
|
| 101 |
+
"access": {
|
| 102 |
+
"formatter": "access",
|
| 103 |
+
"class": "logging.StreamHandler",
|
| 104 |
+
"stream": "ext://sys.stdout",
|
| 105 |
+
},
|
| 106 |
+
},
|
| 107 |
+
"loggers": {
|
| 108 |
+
"uvicorn": {"handlers": ["default"], "level": "INFO", "propagate": False},
|
| 109 |
+
"uvicorn.error": {"level": "INFO"},
|
| 110 |
+
"uvicorn.access": {"handlers": ["access"], "level": "INFO", "propagate": False},
|
| 111 |
+
},
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
logger = logging.getLogger("uvicorn.error")
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def create_ssl_context(
|
| 118 |
+
certfile: str | os.PathLike[str],
|
| 119 |
+
keyfile: str | os.PathLike[str] | None,
|
| 120 |
+
password: str | None,
|
| 121 |
+
ssl_version: int,
|
| 122 |
+
cert_reqs: int,
|
| 123 |
+
ca_certs: str | os.PathLike[str] | None,
|
| 124 |
+
ciphers: str | None,
|
| 125 |
+
) -> ssl.SSLContext:
|
| 126 |
+
ctx = ssl.SSLContext(ssl_version)
|
| 127 |
+
get_password = (lambda: password) if password else None
|
| 128 |
+
ctx.load_cert_chain(certfile, keyfile, get_password)
|
| 129 |
+
ctx.verify_mode = ssl.VerifyMode(cert_reqs)
|
| 130 |
+
if ca_certs:
|
| 131 |
+
ctx.load_verify_locations(ca_certs)
|
| 132 |
+
if ciphers:
|
| 133 |
+
ctx.set_ciphers(ciphers)
|
| 134 |
+
return ctx
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def is_dir(path: Path) -> bool:
|
| 138 |
+
try:
|
| 139 |
+
if not path.is_absolute():
|
| 140 |
+
path = path.resolve()
|
| 141 |
+
return path.is_dir()
|
| 142 |
+
except OSError: # pragma: full coverage
|
| 143 |
+
return False
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def resolve_reload_patterns(patterns_list: list[str], directories_list: list[str]) -> tuple[list[str], list[Path]]:
|
| 147 |
+
directories: list[Path] = list(set(map(Path, directories_list.copy())))
|
| 148 |
+
patterns: list[str] = patterns_list.copy()
|
| 149 |
+
|
| 150 |
+
current_working_directory = Path.cwd()
|
| 151 |
+
for pattern in patterns_list:
|
| 152 |
+
# Special case for the .* pattern, otherwise this would only match
|
| 153 |
+
# hidden directories which is probably undesired
|
| 154 |
+
if pattern == ".*":
|
| 155 |
+
continue # pragma: py-not-linux
|
| 156 |
+
patterns.append(pattern)
|
| 157 |
+
if is_dir(Path(pattern)):
|
| 158 |
+
directories.append(Path(pattern))
|
| 159 |
+
else:
|
| 160 |
+
for match in current_working_directory.glob(pattern):
|
| 161 |
+
if is_dir(match):
|
| 162 |
+
directories.append(match)
|
| 163 |
+
|
| 164 |
+
directories = list(set(directories))
|
| 165 |
+
directories = list(map(Path, directories))
|
| 166 |
+
directories = list(map(lambda x: x.resolve(), directories))
|
| 167 |
+
directories = list({reload_path for reload_path in directories if is_dir(reload_path)})
|
| 168 |
+
|
| 169 |
+
children = []
|
| 170 |
+
for j in range(len(directories)):
|
| 171 |
+
for k in range(j + 1, len(directories)): # pragma: full coverage
|
| 172 |
+
if directories[j] in directories[k].parents:
|
| 173 |
+
children.append(directories[k])
|
| 174 |
+
elif directories[k] in directories[j].parents:
|
| 175 |
+
children.append(directories[j])
|
| 176 |
+
|
| 177 |
+
directories = list(set(directories).difference(set(children)))
|
| 178 |
+
|
| 179 |
+
return list(set(patterns)), directories
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _normalize_dirs(dirs: list[str] | str | None) -> list[str]:
|
| 183 |
+
if dirs is None:
|
| 184 |
+
return []
|
| 185 |
+
if isinstance(dirs, str):
|
| 186 |
+
return [dirs]
|
| 187 |
+
return list(set(dirs))
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class Config:
|
| 191 |
+
def __init__(
|
| 192 |
+
self,
|
| 193 |
+
app: ASGIApplication | Callable[..., Any] | str,
|
| 194 |
+
host: str = "127.0.0.1",
|
| 195 |
+
port: int = 8000,
|
| 196 |
+
uds: str | None = None,
|
| 197 |
+
fd: int | None = None,
|
| 198 |
+
loop: LoopFactoryType | str = "auto",
|
| 199 |
+
http: type[asyncio.Protocol] | HTTPProtocolType | str = "auto",
|
| 200 |
+
ws: type[asyncio.Protocol] | WSProtocolType | str = "auto",
|
| 201 |
+
ws_max_size: int = 16 * 1024 * 1024,
|
| 202 |
+
ws_max_queue: int = 32,
|
| 203 |
+
ws_ping_interval: float | None = 20.0,
|
| 204 |
+
ws_ping_timeout: float | None = 20.0,
|
| 205 |
+
ws_per_message_deflate: bool = True,
|
| 206 |
+
lifespan: LifespanType = "auto",
|
| 207 |
+
env_file: str | os.PathLike[str] | None = None,
|
| 208 |
+
log_config: dict[str, Any] | str | os.PathLike[str] | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
|
| 209 |
+
log_level: str | int | None = None,
|
| 210 |
+
access_log: bool = True,
|
| 211 |
+
use_colors: bool | None = None,
|
| 212 |
+
interface: InterfaceType = "auto",
|
| 213 |
+
reload: bool = False,
|
| 214 |
+
reload_dirs: list[str] | str | None = None,
|
| 215 |
+
reload_delay: float = 0.25,
|
| 216 |
+
reload_includes: list[str] | str | None = None,
|
| 217 |
+
reload_excludes: list[str] | str | None = None,
|
| 218 |
+
workers: int | None = None,
|
| 219 |
+
proxy_headers: bool = True,
|
| 220 |
+
server_header: bool = True,
|
| 221 |
+
date_header: bool = True,
|
| 222 |
+
forwarded_allow_ips: list[str] | str | None = None,
|
| 223 |
+
root_path: str = "",
|
| 224 |
+
limit_concurrency: int | None = None,
|
| 225 |
+
limit_max_requests: int | None = None,
|
| 226 |
+
limit_max_requests_jitter: int = 0,
|
| 227 |
+
backlog: int = 2048,
|
| 228 |
+
timeout_keep_alive: int = 5,
|
| 229 |
+
timeout_notify: int = 30,
|
| 230 |
+
timeout_graceful_shutdown: int | None = None,
|
| 231 |
+
timeout_worker_healthcheck: int = 5,
|
| 232 |
+
callback_notify: Callable[..., Awaitable[None]] | None = None,
|
| 233 |
+
ssl_keyfile: str | os.PathLike[str] | None = None,
|
| 234 |
+
ssl_certfile: str | os.PathLike[str] | None = None,
|
| 235 |
+
ssl_keyfile_password: str | None = None,
|
| 236 |
+
ssl_version: int = SSL_PROTOCOL_VERSION,
|
| 237 |
+
ssl_cert_reqs: int = ssl.CERT_NONE,
|
| 238 |
+
ssl_ca_certs: str | os.PathLike[str] | None = None,
|
| 239 |
+
ssl_ciphers: str | None = None,
|
| 240 |
+
ssl_context_factory: Callable[[Config, Callable[[], ssl.SSLContext]], ssl.SSLContext] | None = None,
|
| 241 |
+
headers: list[tuple[str, str]] | None = None,
|
| 242 |
+
factory: bool = False,
|
| 243 |
+
h11_max_incomplete_event_size: int | None = None,
|
| 244 |
+
reset_contextvars: bool = False,
|
| 245 |
+
):
|
| 246 |
+
self.app = app
|
| 247 |
+
self.host = host
|
| 248 |
+
self.port = port
|
| 249 |
+
self.uds = uds
|
| 250 |
+
self.fd = fd
|
| 251 |
+
self.loop = loop
|
| 252 |
+
self.http = http
|
| 253 |
+
self.ws = ws
|
| 254 |
+
self.ws_max_size = ws_max_size
|
| 255 |
+
self.ws_max_queue = ws_max_queue
|
| 256 |
+
self.ws_ping_interval = ws_ping_interval
|
| 257 |
+
self.ws_ping_timeout = ws_ping_timeout
|
| 258 |
+
self.ws_per_message_deflate = ws_per_message_deflate
|
| 259 |
+
self.lifespan = lifespan
|
| 260 |
+
self.log_config = log_config
|
| 261 |
+
self.log_level = log_level
|
| 262 |
+
self.access_log = access_log
|
| 263 |
+
self.use_colors = use_colors
|
| 264 |
+
self.interface = interface
|
| 265 |
+
self.reload = reload
|
| 266 |
+
self.reload_delay = reload_delay
|
| 267 |
+
self.workers = workers or 1
|
| 268 |
+
self.proxy_headers = proxy_headers
|
| 269 |
+
self.server_header = server_header
|
| 270 |
+
self.date_header = date_header
|
| 271 |
+
self.root_path = root_path
|
| 272 |
+
self.limit_concurrency = limit_concurrency
|
| 273 |
+
self.limit_max_requests = limit_max_requests
|
| 274 |
+
self.limit_max_requests_jitter = limit_max_requests_jitter
|
| 275 |
+
self.backlog = backlog
|
| 276 |
+
self.timeout_keep_alive = timeout_keep_alive
|
| 277 |
+
self.timeout_notify = timeout_notify
|
| 278 |
+
self.timeout_graceful_shutdown = timeout_graceful_shutdown
|
| 279 |
+
self.timeout_worker_healthcheck = timeout_worker_healthcheck
|
| 280 |
+
self.callback_notify = callback_notify
|
| 281 |
+
self.ssl_keyfile = ssl_keyfile
|
| 282 |
+
self.ssl_certfile = ssl_certfile
|
| 283 |
+
self.ssl_keyfile_password = ssl_keyfile_password
|
| 284 |
+
self.ssl_version = ssl_version
|
| 285 |
+
self.ssl_cert_reqs = ssl_cert_reqs
|
| 286 |
+
self.ssl_ca_certs = ssl_ca_certs
|
| 287 |
+
self.ssl_ciphers = ssl_ciphers
|
| 288 |
+
self.ssl_context_factory = ssl_context_factory
|
| 289 |
+
self.headers: list[tuple[str, str]] = headers or []
|
| 290 |
+
self.encoded_headers: list[tuple[bytes, bytes]] = []
|
| 291 |
+
self.factory = factory
|
| 292 |
+
self.h11_max_incomplete_event_size = h11_max_incomplete_event_size
|
| 293 |
+
self.reset_contextvars = reset_contextvars
|
| 294 |
+
|
| 295 |
+
self.loaded = False
|
| 296 |
+
self.configure_logging()
|
| 297 |
+
|
| 298 |
+
self.reload_dirs: list[Path] = []
|
| 299 |
+
self.reload_dirs_excludes: list[Path] = []
|
| 300 |
+
self.reload_includes: list[str] = []
|
| 301 |
+
self.reload_excludes: list[str] = []
|
| 302 |
+
|
| 303 |
+
if (reload_dirs or reload_includes or reload_excludes) and not self.should_reload:
|
| 304 |
+
logger.warning(
|
| 305 |
+
"Current configuration will not reload as not all conditions are met, please refer to documentation."
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
if self.should_reload:
|
| 309 |
+
reload_dirs = _normalize_dirs(reload_dirs)
|
| 310 |
+
reload_includes = _normalize_dirs(reload_includes)
|
| 311 |
+
reload_excludes = _normalize_dirs(reload_excludes)
|
| 312 |
+
|
| 313 |
+
self.reload_includes, self.reload_dirs = resolve_reload_patterns(reload_includes, reload_dirs)
|
| 314 |
+
|
| 315 |
+
self.reload_excludes, self.reload_dirs_excludes = resolve_reload_patterns(reload_excludes, [])
|
| 316 |
+
|
| 317 |
+
reload_dirs_tmp = self.reload_dirs.copy()
|
| 318 |
+
|
| 319 |
+
for directory in self.reload_dirs_excludes:
|
| 320 |
+
for reload_directory in reload_dirs_tmp:
|
| 321 |
+
if directory == reload_directory or directory in reload_directory.parents:
|
| 322 |
+
try:
|
| 323 |
+
self.reload_dirs.remove(reload_directory)
|
| 324 |
+
except ValueError: # pragma: full coverage
|
| 325 |
+
pass
|
| 326 |
+
|
| 327 |
+
for pattern in self.reload_excludes:
|
| 328 |
+
if pattern in self.reload_includes:
|
| 329 |
+
self.reload_includes.remove(pattern) # pragma: full coverage
|
| 330 |
+
|
| 331 |
+
if not self.reload_dirs:
|
| 332 |
+
if reload_dirs:
|
| 333 |
+
logger.warning(
|
| 334 |
+
"Provided reload directories %s did not contain valid "
|
| 335 |
+
+ "directories, watching current working directory.",
|
| 336 |
+
reload_dirs,
|
| 337 |
+
)
|
| 338 |
+
self.reload_dirs = [Path.cwd()]
|
| 339 |
+
|
| 340 |
+
logger.info(
|
| 341 |
+
"Will watch for changes in these directories: %s",
|
| 342 |
+
sorted(list(map(str, self.reload_dirs))),
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
if env_file is not None:
|
| 346 |
+
from dotenv import load_dotenv
|
| 347 |
+
|
| 348 |
+
logger.info("Loading environment from '%s'", env_file)
|
| 349 |
+
load_dotenv(dotenv_path=env_file)
|
| 350 |
+
|
| 351 |
+
if workers is None and "WEB_CONCURRENCY" in os.environ:
|
| 352 |
+
self.workers = int(os.environ["WEB_CONCURRENCY"])
|
| 353 |
+
|
| 354 |
+
self.forwarded_allow_ips: list[str] | str
|
| 355 |
+
if forwarded_allow_ips is None:
|
| 356 |
+
self.forwarded_allow_ips = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1")
|
| 357 |
+
else:
|
| 358 |
+
self.forwarded_allow_ips = forwarded_allow_ips # pragma: full coverage
|
| 359 |
+
|
| 360 |
+
if self.reload and self.workers > 1:
|
| 361 |
+
logger.warning('"workers" flag is ignored when reloading is enabled.')
|
| 362 |
+
|
| 363 |
+
@property
|
| 364 |
+
def asgi_version(self) -> Literal["2.0", "3.0"]:
|
| 365 |
+
mapping: dict[str, Literal["2.0", "3.0"]] = {
|
| 366 |
+
"asgi2": "2.0",
|
| 367 |
+
"asgi3": "3.0",
|
| 368 |
+
"wsgi": "3.0",
|
| 369 |
+
}
|
| 370 |
+
return mapping[self.interface]
|
| 371 |
+
|
| 372 |
+
@property
|
| 373 |
+
def is_ssl(self) -> bool:
|
| 374 |
+
return bool(self.ssl_keyfile or self.ssl_certfile or self.ssl_context_factory)
|
| 375 |
+
|
| 376 |
+
@property
|
| 377 |
+
def use_subprocess(self) -> bool:
|
| 378 |
+
return bool(self.reload or self.workers > 1)
|
| 379 |
+
|
| 380 |
+
def configure_logging(self) -> None:
|
| 381 |
+
logging.addLevelName(TRACE_LOG_LEVEL, "TRACE")
|
| 382 |
+
|
| 383 |
+
if self.log_config is not None:
|
| 384 |
+
if isinstance(self.log_config, os.PathLike):
|
| 385 |
+
self.log_config = os.fspath(self.log_config)
|
| 386 |
+
|
| 387 |
+
if isinstance(self.log_config, dict):
|
| 388 |
+
if self.use_colors in (True, False):
|
| 389 |
+
self.log_config["formatters"]["default"]["use_colors"] = self.use_colors
|
| 390 |
+
self.log_config["formatters"]["access"]["use_colors"] = self.use_colors
|
| 391 |
+
logging.config.dictConfig(self.log_config)
|
| 392 |
+
elif isinstance(self.log_config, str) and self.log_config.endswith(".json"):
|
| 393 |
+
with open(self.log_config) as file:
|
| 394 |
+
loaded_config = json.load(file)
|
| 395 |
+
logging.config.dictConfig(loaded_config)
|
| 396 |
+
elif isinstance(self.log_config, str) and self.log_config.endswith((".yaml", ".yml")):
|
| 397 |
+
try:
|
| 398 |
+
import yaml
|
| 399 |
+
except ImportError as e:
|
| 400 |
+
raise ImportError(
|
| 401 |
+
"Install the PyYAML package or uvicorn[standard] to use `--log-config` with YAML files."
|
| 402 |
+
) from e
|
| 403 |
+
|
| 404 |
+
with open(self.log_config) as file:
|
| 405 |
+
loaded_config = yaml.safe_load(file)
|
| 406 |
+
logging.config.dictConfig(loaded_config)
|
| 407 |
+
else:
|
| 408 |
+
# See the note about fileConfig() here:
|
| 409 |
+
# https://docs.python.org/3/library/logging.config.html#configuration-file-format
|
| 410 |
+
logging.config.fileConfig(self.log_config, disable_existing_loggers=False)
|
| 411 |
+
|
| 412 |
+
if self.log_level is not None:
|
| 413 |
+
if isinstance(self.log_level, str):
|
| 414 |
+
log_level = LOG_LEVELS[self.log_level.lower()]
|
| 415 |
+
else:
|
| 416 |
+
log_level = self.log_level
|
| 417 |
+
logging.getLogger("uvicorn.error").setLevel(log_level)
|
| 418 |
+
logging.getLogger("uvicorn.access").setLevel(log_level)
|
| 419 |
+
logging.getLogger("uvicorn.asgi").setLevel(log_level)
|
| 420 |
+
if self.access_log is False:
|
| 421 |
+
logging.getLogger("uvicorn.access").handlers = []
|
| 422 |
+
logging.getLogger("uvicorn.access").propagate = False
|
| 423 |
+
|
| 424 |
+
def load_app(self) -> Any:
|
| 425 |
+
"""Import the app and return it. Exits on failure."""
|
| 426 |
+
try:
|
| 427 |
+
return import_from_string(self.app)
|
| 428 |
+
except ImportFromStringError as exc:
|
| 429 |
+
logger.error("Error loading ASGI app. %s" % exc)
|
| 430 |
+
sys.exit(STARTUP_FAILURE)
|
| 431 |
+
|
| 432 |
+
def load(self) -> None:
|
| 433 |
+
assert not self.loaded
|
| 434 |
+
|
| 435 |
+
if self.ssl_context_factory is not None:
|
| 436 |
+
|
| 437 |
+
def default_factory() -> ssl.SSLContext:
|
| 438 |
+
if not self.ssl_certfile:
|
| 439 |
+
raise RuntimeError(
|
| 440 |
+
"`default_ssl_context_factory()` requires `ssl_certfile` to be set on `Config`. "
|
| 441 |
+
"Either pass `ssl_certfile` (and optionally `ssl_keyfile`) or build the `SSLContext` "
|
| 442 |
+
"directly inside `ssl_context_factory` without calling the default factory."
|
| 443 |
+
)
|
| 444 |
+
return create_ssl_context(
|
| 445 |
+
keyfile=self.ssl_keyfile,
|
| 446 |
+
certfile=self.ssl_certfile,
|
| 447 |
+
password=self.ssl_keyfile_password,
|
| 448 |
+
ssl_version=self.ssl_version,
|
| 449 |
+
cert_reqs=self.ssl_cert_reqs,
|
| 450 |
+
ca_certs=self.ssl_ca_certs,
|
| 451 |
+
ciphers=self.ssl_ciphers,
|
| 452 |
+
)
|
| 453 |
+
|
| 454 |
+
context = self.ssl_context_factory(self, default_factory)
|
| 455 |
+
if not isinstance(context, ssl.SSLContext):
|
| 456 |
+
raise TypeError(f"`ssl_context_factory` must return an `ssl.SSLContext`, got {type(context).__name__}")
|
| 457 |
+
self.ssl: ssl.SSLContext | None = context
|
| 458 |
+
elif self.is_ssl:
|
| 459 |
+
assert self.ssl_certfile
|
| 460 |
+
self.ssl = create_ssl_context(
|
| 461 |
+
keyfile=self.ssl_keyfile,
|
| 462 |
+
certfile=self.ssl_certfile,
|
| 463 |
+
password=self.ssl_keyfile_password,
|
| 464 |
+
ssl_version=self.ssl_version,
|
| 465 |
+
cert_reqs=self.ssl_cert_reqs,
|
| 466 |
+
ca_certs=self.ssl_ca_certs,
|
| 467 |
+
ciphers=self.ssl_ciphers,
|
| 468 |
+
)
|
| 469 |
+
else:
|
| 470 |
+
self.ssl = None
|
| 471 |
+
|
| 472 |
+
encoded_headers = [(key.lower().encode("latin1"), value.encode("latin1")) for key, value in self.headers]
|
| 473 |
+
self.encoded_headers = (
|
| 474 |
+
[(b"server", b"uvicorn")] + encoded_headers
|
| 475 |
+
if b"server" not in dict(encoded_headers) and self.server_header
|
| 476 |
+
else encoded_headers
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
if isinstance(self.http, str):
|
| 480 |
+
http_protocol_class = import_from_string(HTTP_PROTOCOLS.get(self.http, self.http))
|
| 481 |
+
self.http_protocol_class: type[asyncio.Protocol] = http_protocol_class
|
| 482 |
+
else:
|
| 483 |
+
self.http_protocol_class = self.http
|
| 484 |
+
|
| 485 |
+
if isinstance(self.ws, str):
|
| 486 |
+
ws_protocol_class = import_from_string(WS_PROTOCOLS.get(self.ws, self.ws))
|
| 487 |
+
self.ws_protocol_class: type[asyncio.Protocol] | None = ws_protocol_class
|
| 488 |
+
else:
|
| 489 |
+
self.ws_protocol_class = self.ws
|
| 490 |
+
|
| 491 |
+
self.lifespan_class = import_from_string(LIFESPAN[self.lifespan])
|
| 492 |
+
|
| 493 |
+
self.loaded_app = self.load_app()
|
| 494 |
+
|
| 495 |
+
try:
|
| 496 |
+
self.loaded_app = self.loaded_app()
|
| 497 |
+
except TypeError as exc:
|
| 498 |
+
if self.factory:
|
| 499 |
+
logger.error("Error loading ASGI app factory: %s", exc)
|
| 500 |
+
sys.exit(STARTUP_FAILURE)
|
| 501 |
+
else:
|
| 502 |
+
if not self.factory:
|
| 503 |
+
logger.warning(
|
| 504 |
+
"ASGI app factory detected. Using it, but please consider setting the --factory flag explicitly."
|
| 505 |
+
)
|
| 506 |
+
|
| 507 |
+
if self.interface == "auto":
|
| 508 |
+
if inspect.isclass(self.loaded_app):
|
| 509 |
+
use_asgi_3 = hasattr(self.loaded_app, "__await__")
|
| 510 |
+
elif inspect.isfunction(self.loaded_app):
|
| 511 |
+
use_asgi_3 = iscoroutinefunction(self.loaded_app)
|
| 512 |
+
else:
|
| 513 |
+
call = getattr(self.loaded_app, "__call__", None)
|
| 514 |
+
use_asgi_3 = iscoroutinefunction(call)
|
| 515 |
+
self.interface = "asgi3" if use_asgi_3 else "asgi2"
|
| 516 |
+
|
| 517 |
+
if self.interface == "wsgi":
|
| 518 |
+
self.loaded_app = WSGIMiddleware(self.loaded_app)
|
| 519 |
+
self.ws_protocol_class = None
|
| 520 |
+
elif self.interface == "asgi2":
|
| 521 |
+
self.loaded_app = ASGI2Middleware(self.loaded_app)
|
| 522 |
+
|
| 523 |
+
if logger.getEffectiveLevel() <= TRACE_LOG_LEVEL:
|
| 524 |
+
self.loaded_app = MessageLoggerMiddleware(self.loaded_app)
|
| 525 |
+
if self.proxy_headers:
|
| 526 |
+
self.loaded_app = ProxyHeadersMiddleware(self.loaded_app, trusted_hosts=self.forwarded_allow_ips)
|
| 527 |
+
|
| 528 |
+
self.loaded = True
|
| 529 |
+
|
| 530 |
+
def setup_event_loop(self) -> None:
|
| 531 |
+
raise AttributeError(
|
| 532 |
+
"The `setup_event_loop` method was replaced by `get_loop_factory` in uvicorn 0.36.0.\n"
|
| 533 |
+
"None of those methods are supposed to be used directly. If you are doing it, please let me know here: "
|
| 534 |
+
"https://github.com/Kludex/uvicorn/discussions/2706. Thank you, and sorry for the inconvenience."
|
| 535 |
+
)
|
| 536 |
+
|
| 537 |
+
def get_loop_factory(self) -> Callable[[], asyncio.AbstractEventLoop] | None:
|
| 538 |
+
if self.loop in LOOP_FACTORIES:
|
| 539 |
+
loop_factory: Callable[..., Any] | None = import_from_string(LOOP_FACTORIES[self.loop])
|
| 540 |
+
else:
|
| 541 |
+
try:
|
| 542 |
+
return import_from_string(self.loop)
|
| 543 |
+
except ImportFromStringError as exc:
|
| 544 |
+
logger.error("Error loading custom loop setup function. %s" % exc)
|
| 545 |
+
sys.exit(STARTUP_FAILURE)
|
| 546 |
+
if loop_factory is None:
|
| 547 |
+
return None
|
| 548 |
+
return loop_factory(use_subprocess=self.use_subprocess)
|
| 549 |
+
|
| 550 |
+
def bind_socket(self) -> socket.socket:
|
| 551 |
+
logger_args: list[str | int]
|
| 552 |
+
if self.uds is not None: # pragma: py-win32
|
| 553 |
+
path = self.uds
|
| 554 |
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
| 555 |
+
try:
|
| 556 |
+
sock.bind(path)
|
| 557 |
+
uds_perms = 0o666
|
| 558 |
+
os.chmod(self.uds, uds_perms)
|
| 559 |
+
except OSError as exc: # pragma: full coverage
|
| 560 |
+
logger.error(exc)
|
| 561 |
+
sys.exit(STARTUP_FAILURE)
|
| 562 |
+
|
| 563 |
+
message = "Uvicorn running on unix socket %s (Press CTRL+C to quit)"
|
| 564 |
+
sock_name_format = "%s"
|
| 565 |
+
color_message = "Uvicorn running on " + style(sock_name_format, bold=True) + " (Press CTRL+C to quit)"
|
| 566 |
+
logger_args = [self.uds]
|
| 567 |
+
elif self.fd is not None: # pragma: py-win32
|
| 568 |
+
sock = socket.fromfd(self.fd, socket.AF_UNIX, socket.SOCK_STREAM)
|
| 569 |
+
message = "Uvicorn running on socket %s (Press CTRL+C to quit)"
|
| 570 |
+
fd_name_format = "%s"
|
| 571 |
+
color_message = "Uvicorn running on " + style(fd_name_format, bold=True) + " (Press CTRL+C to quit)"
|
| 572 |
+
logger_args = [sock.getsockname()]
|
| 573 |
+
else:
|
| 574 |
+
family = socket.AF_INET
|
| 575 |
+
addr_format = "%s://%s:%d"
|
| 576 |
+
|
| 577 |
+
if self.host and ":" in self.host: # pragma: full coverage
|
| 578 |
+
# It's an IPv6 address.
|
| 579 |
+
family = socket.AF_INET6
|
| 580 |
+
addr_format = "%s://[%s]:%d"
|
| 581 |
+
|
| 582 |
+
sock = socket.socket(family=family)
|
| 583 |
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
| 584 |
+
try:
|
| 585 |
+
sock.bind((self.host, self.port))
|
| 586 |
+
except OSError as exc: # pragma: full coverage
|
| 587 |
+
logger.error(exc)
|
| 588 |
+
sys.exit(STARTUP_FAILURE)
|
| 589 |
+
|
| 590 |
+
message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)"
|
| 591 |
+
color_message = "Uvicorn running on " + style(addr_format, bold=True) + " (Press CTRL+C to quit)"
|
| 592 |
+
protocol_name = "https" if self.is_ssl else "http"
|
| 593 |
+
logger_args = [protocol_name, self.host, sock.getsockname()[1]]
|
| 594 |
+
logger.info(message, *logger_args, extra={"color_message": color_message})
|
| 595 |
+
sock.set_inheritable(True)
|
| 596 |
+
return sock
|
| 597 |
+
|
| 598 |
+
@property
|
| 599 |
+
def should_reload(self) -> bool:
|
| 600 |
+
return isinstance(self.app, str) and self.reload
|
.venv/Lib/site-packages/uvicorn/importer.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import importlib
|
| 2 |
+
from typing import Any
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class ImportFromStringError(Exception):
|
| 6 |
+
pass
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def import_from_string(import_str: Any) -> Any:
|
| 10 |
+
if not isinstance(import_str, str):
|
| 11 |
+
return import_str
|
| 12 |
+
|
| 13 |
+
module_str, _, attrs_str = import_str.partition(":")
|
| 14 |
+
if not module_str or not attrs_str:
|
| 15 |
+
message = 'Import string "{import_str}" must be in format "<module>:<attribute>".'
|
| 16 |
+
raise ImportFromStringError(message.format(import_str=import_str))
|
| 17 |
+
|
| 18 |
+
try:
|
| 19 |
+
module = importlib.import_module(module_str)
|
| 20 |
+
except ModuleNotFoundError as exc:
|
| 21 |
+
if exc.name != module_str:
|
| 22 |
+
raise exc from None
|
| 23 |
+
message = 'Could not import module "{module_str}".'
|
| 24 |
+
raise ImportFromStringError(message.format(module_str=module_str))
|
| 25 |
+
|
| 26 |
+
instance = module
|
| 27 |
+
try:
|
| 28 |
+
for attr_str in attrs_str.split("."):
|
| 29 |
+
instance = getattr(instance, attr_str)
|
| 30 |
+
except AttributeError:
|
| 31 |
+
message = 'Attribute "{attrs_str}" not found in module "{module_str}".'
|
| 32 |
+
raise ImportFromStringError(message.format(attrs_str=attrs_str, module_str=module_str))
|
| 33 |
+
|
| 34 |
+
return instance
|
.venv/Lib/site-packages/uvicorn/lifespan/__init__.py
ADDED
|
File without changes
|
.venv/Lib/site-packages/uvicorn/lifespan/off.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import Any
|
| 4 |
+
|
| 5 |
+
from uvicorn import Config
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class LifespanOff:
|
| 9 |
+
def __init__(self, config: Config) -> None:
|
| 10 |
+
self.should_exit = False
|
| 11 |
+
self.state: dict[str, Any] = {}
|
| 12 |
+
|
| 13 |
+
async def startup(self) -> None:
|
| 14 |
+
pass
|
| 15 |
+
|
| 16 |
+
async def shutdown(self) -> None:
|
| 17 |
+
pass
|
.venv/Lib/site-packages/uvicorn/lifespan/on.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
from asyncio import Queue
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
from uvicorn import Config
|
| 9 |
+
from uvicorn._types import (
|
| 10 |
+
LifespanScope,
|
| 11 |
+
LifespanShutdownCompleteEvent,
|
| 12 |
+
LifespanShutdownEvent,
|
| 13 |
+
LifespanShutdownFailedEvent,
|
| 14 |
+
LifespanStartupCompleteEvent,
|
| 15 |
+
LifespanStartupEvent,
|
| 16 |
+
LifespanStartupFailedEvent,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
LifespanReceiveMessage = LifespanStartupEvent | LifespanShutdownEvent
|
| 20 |
+
LifespanSendMessage = (
|
| 21 |
+
LifespanStartupFailedEvent
|
| 22 |
+
| LifespanShutdownFailedEvent
|
| 23 |
+
| LifespanStartupCompleteEvent
|
| 24 |
+
| LifespanShutdownCompleteEvent
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
STATE_TRANSITION_ERROR = "Got invalid state transition on lifespan protocol."
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class LifespanOn:
|
| 32 |
+
def __init__(self, config: Config) -> None:
|
| 33 |
+
if not config.loaded:
|
| 34 |
+
config.load()
|
| 35 |
+
|
| 36 |
+
self.config = config
|
| 37 |
+
self.logger = logging.getLogger("uvicorn.error")
|
| 38 |
+
self.startup_event = asyncio.Event()
|
| 39 |
+
self.shutdown_event = asyncio.Event()
|
| 40 |
+
self.receive_queue: Queue[LifespanReceiveMessage] = asyncio.Queue()
|
| 41 |
+
self.error_occurred = False
|
| 42 |
+
self.startup_failed = False
|
| 43 |
+
self.shutdown_failed = False
|
| 44 |
+
self.should_exit = False
|
| 45 |
+
self.state: dict[str, Any] = {}
|
| 46 |
+
|
| 47 |
+
async def startup(self) -> None:
|
| 48 |
+
self.logger.info("Waiting for application startup.")
|
| 49 |
+
|
| 50 |
+
loop = asyncio.get_event_loop()
|
| 51 |
+
main_lifespan_task = loop.create_task(self.main()) # noqa: F841
|
| 52 |
+
# Keep a hard reference to prevent garbage collection
|
| 53 |
+
# See https://github.com/Kludex/uvicorn/pull/972
|
| 54 |
+
startup_event: LifespanStartupEvent = {"type": "lifespan.startup"}
|
| 55 |
+
await self.receive_queue.put(startup_event)
|
| 56 |
+
await self.startup_event.wait()
|
| 57 |
+
|
| 58 |
+
if self.startup_failed or (self.error_occurred and self.config.lifespan == "on"):
|
| 59 |
+
self.logger.error("Application startup failed. Exiting.")
|
| 60 |
+
self.should_exit = True
|
| 61 |
+
else:
|
| 62 |
+
self.logger.info("Application startup complete.")
|
| 63 |
+
|
| 64 |
+
async def shutdown(self) -> None:
|
| 65 |
+
if self.error_occurred:
|
| 66 |
+
return
|
| 67 |
+
self.logger.info("Waiting for application shutdown.")
|
| 68 |
+
shutdown_event: LifespanShutdownEvent = {"type": "lifespan.shutdown"}
|
| 69 |
+
await self.receive_queue.put(shutdown_event)
|
| 70 |
+
await self.shutdown_event.wait()
|
| 71 |
+
|
| 72 |
+
if self.shutdown_failed or (self.error_occurred and self.config.lifespan == "on"):
|
| 73 |
+
self.logger.error("Application shutdown failed. Exiting.")
|
| 74 |
+
self.should_exit = True
|
| 75 |
+
else:
|
| 76 |
+
self.logger.info("Application shutdown complete.")
|
| 77 |
+
|
| 78 |
+
async def main(self) -> None:
|
| 79 |
+
try:
|
| 80 |
+
app = self.config.loaded_app
|
| 81 |
+
scope: LifespanScope = {
|
| 82 |
+
"type": "lifespan",
|
| 83 |
+
"asgi": {"version": self.config.asgi_version, "spec_version": "2.0"},
|
| 84 |
+
"state": self.state,
|
| 85 |
+
}
|
| 86 |
+
await app(scope, self.receive, self.send)
|
| 87 |
+
except BaseException as exc:
|
| 88 |
+
self.asgi = None
|
| 89 |
+
self.error_occurred = True
|
| 90 |
+
if self.startup_failed or self.shutdown_failed:
|
| 91 |
+
return
|
| 92 |
+
if self.config.lifespan == "auto":
|
| 93 |
+
msg = "ASGI 'lifespan' protocol appears unsupported."
|
| 94 |
+
self.logger.info(msg)
|
| 95 |
+
else:
|
| 96 |
+
msg = "Exception in 'lifespan' protocol\n"
|
| 97 |
+
self.logger.error(msg, exc_info=exc)
|
| 98 |
+
finally:
|
| 99 |
+
self.startup_event.set()
|
| 100 |
+
self.shutdown_event.set()
|
| 101 |
+
|
| 102 |
+
async def send(self, message: LifespanSendMessage) -> None:
|
| 103 |
+
assert message["type"] in (
|
| 104 |
+
"lifespan.startup.complete",
|
| 105 |
+
"lifespan.startup.failed",
|
| 106 |
+
"lifespan.shutdown.complete",
|
| 107 |
+
"lifespan.shutdown.failed",
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
if message["type"] == "lifespan.startup.complete":
|
| 111 |
+
assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR
|
| 112 |
+
assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR
|
| 113 |
+
self.startup_event.set()
|
| 114 |
+
|
| 115 |
+
elif message["type"] == "lifespan.startup.failed":
|
| 116 |
+
assert not self.startup_event.is_set(), STATE_TRANSITION_ERROR
|
| 117 |
+
assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR
|
| 118 |
+
self.startup_event.set()
|
| 119 |
+
self.startup_failed = True
|
| 120 |
+
if message.get("message"):
|
| 121 |
+
self.logger.error(message["message"])
|
| 122 |
+
|
| 123 |
+
elif message["type"] == "lifespan.shutdown.complete":
|
| 124 |
+
assert self.startup_event.is_set(), STATE_TRANSITION_ERROR
|
| 125 |
+
assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR
|
| 126 |
+
self.shutdown_event.set()
|
| 127 |
+
|
| 128 |
+
elif message["type"] == "lifespan.shutdown.failed":
|
| 129 |
+
assert self.startup_event.is_set(), STATE_TRANSITION_ERROR
|
| 130 |
+
assert not self.shutdown_event.is_set(), STATE_TRANSITION_ERROR
|
| 131 |
+
self.shutdown_event.set()
|
| 132 |
+
self.shutdown_failed = True
|
| 133 |
+
if message.get("message"):
|
| 134 |
+
self.logger.error(message["message"])
|
| 135 |
+
|
| 136 |
+
async def receive(self) -> LifespanReceiveMessage:
|
| 137 |
+
return await self.receive_queue.get()
|
.venv/Lib/site-packages/uvicorn/logging.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import http
|
| 4 |
+
import logging
|
| 5 |
+
import sys
|
| 6 |
+
from copy import copy
|
| 7 |
+
from typing import Literal
|
| 8 |
+
|
| 9 |
+
from uvicorn._ansi import style as ansi_style
|
| 10 |
+
|
| 11 |
+
TRACE_LOG_LEVEL = 5
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class ColourizedFormatter(logging.Formatter):
|
| 15 |
+
"""
|
| 16 |
+
A custom log formatter class that:
|
| 17 |
+
|
| 18 |
+
* Outputs the LOG_LEVEL with an appropriate color.
|
| 19 |
+
* If a log call includes an `extra={"color_message": ...}` it will be used
|
| 20 |
+
for formatting the output, instead of the plain text message.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
level_name_colors = {
|
| 24 |
+
TRACE_LOG_LEVEL: lambda level_name: ansi_style(str(level_name), fg="blue"),
|
| 25 |
+
logging.DEBUG: lambda level_name: ansi_style(str(level_name), fg="cyan"),
|
| 26 |
+
logging.INFO: lambda level_name: ansi_style(str(level_name), fg="green"),
|
| 27 |
+
logging.WARNING: lambda level_name: ansi_style(str(level_name), fg="yellow"),
|
| 28 |
+
logging.ERROR: lambda level_name: ansi_style(str(level_name), fg="red"),
|
| 29 |
+
logging.CRITICAL: lambda level_name: ansi_style(str(level_name), fg="bright_red"),
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
def __init__(
|
| 33 |
+
self,
|
| 34 |
+
fmt: str | None = None,
|
| 35 |
+
datefmt: str | None = None,
|
| 36 |
+
style: Literal["%", "{", "$"] = "%",
|
| 37 |
+
use_colors: bool | None = None,
|
| 38 |
+
):
|
| 39 |
+
if use_colors in (True, False):
|
| 40 |
+
self.use_colors = use_colors
|
| 41 |
+
else:
|
| 42 |
+
self.use_colors = sys.stdout.isatty()
|
| 43 |
+
super().__init__(fmt=fmt, datefmt=datefmt, style=style)
|
| 44 |
+
|
| 45 |
+
def color_level_name(self, level_name: str, level_no: int) -> str:
|
| 46 |
+
def default(level_name: str) -> str:
|
| 47 |
+
return str(level_name) # pragma: no cover
|
| 48 |
+
|
| 49 |
+
func = self.level_name_colors.get(level_no, default)
|
| 50 |
+
return func(level_name)
|
| 51 |
+
|
| 52 |
+
def should_use_colors(self) -> bool:
|
| 53 |
+
return True # pragma: no cover
|
| 54 |
+
|
| 55 |
+
def formatMessage(self, record: logging.LogRecord) -> str:
|
| 56 |
+
recordcopy = copy(record)
|
| 57 |
+
levelname = recordcopy.levelname
|
| 58 |
+
separator = " " * (8 - len(recordcopy.levelname))
|
| 59 |
+
if self.use_colors:
|
| 60 |
+
levelname = self.color_level_name(levelname, recordcopy.levelno)
|
| 61 |
+
if "color_message" in recordcopy.__dict__:
|
| 62 |
+
recordcopy.msg = recordcopy.__dict__["color_message"]
|
| 63 |
+
recordcopy.__dict__["message"] = recordcopy.getMessage()
|
| 64 |
+
recordcopy.__dict__["levelprefix"] = levelname + ":" + separator
|
| 65 |
+
return super().formatMessage(recordcopy)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class DefaultFormatter(ColourizedFormatter):
|
| 69 |
+
def should_use_colors(self) -> bool:
|
| 70 |
+
return sys.stderr.isatty() # pragma: no cover
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
class AccessFormatter(ColourizedFormatter):
|
| 74 |
+
status_code_colours = {
|
| 75 |
+
1: lambda code: ansi_style(str(code), fg="bright_white"),
|
| 76 |
+
2: lambda code: ansi_style(str(code), fg="green"),
|
| 77 |
+
3: lambda code: ansi_style(str(code), fg="yellow"),
|
| 78 |
+
4: lambda code: ansi_style(str(code), fg="red"),
|
| 79 |
+
5: lambda code: ansi_style(str(code), fg="bright_red"),
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
def get_status_code(self, status_code: int) -> str:
|
| 83 |
+
try:
|
| 84 |
+
status_phrase = http.HTTPStatus(status_code).phrase
|
| 85 |
+
except ValueError:
|
| 86 |
+
status_phrase = ""
|
| 87 |
+
status_and_phrase = f"{status_code} {status_phrase}"
|
| 88 |
+
if self.use_colors:
|
| 89 |
+
|
| 90 |
+
def default(code: int) -> str:
|
| 91 |
+
return status_and_phrase # pragma: no cover
|
| 92 |
+
|
| 93 |
+
func = self.status_code_colours.get(status_code // 100, default)
|
| 94 |
+
return func(status_and_phrase)
|
| 95 |
+
return status_and_phrase
|
| 96 |
+
|
| 97 |
+
def formatMessage(self, record: logging.LogRecord) -> str:
|
| 98 |
+
recordcopy = copy(record)
|
| 99 |
+
(
|
| 100 |
+
client_addr,
|
| 101 |
+
method,
|
| 102 |
+
full_path,
|
| 103 |
+
http_version,
|
| 104 |
+
status_code,
|
| 105 |
+
) = recordcopy.args # type: ignore[misc]
|
| 106 |
+
status_code = self.get_status_code(int(status_code)) # type: ignore[arg-type]
|
| 107 |
+
request_line = f"{method} {full_path} HTTP/{http_version}"
|
| 108 |
+
if self.use_colors:
|
| 109 |
+
request_line = ansi_style(request_line, bold=True)
|
| 110 |
+
recordcopy.__dict__.update(
|
| 111 |
+
{
|
| 112 |
+
"client_addr": client_addr,
|
| 113 |
+
"request_line": request_line,
|
| 114 |
+
"status_code": status_code,
|
| 115 |
+
}
|
| 116 |
+
)
|
| 117 |
+
return super().formatMessage(recordcopy)
|
.venv/Lib/site-packages/uvicorn/loops/__init__.py
ADDED
|
File without changes
|
.venv/Lib/site-packages/uvicorn/loops/asyncio.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import sys
|
| 5 |
+
from collections.abc import Callable
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def asyncio_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]:
|
| 9 |
+
if sys.platform == "win32" and not use_subprocess:
|
| 10 |
+
return asyncio.ProactorEventLoop
|
| 11 |
+
return asyncio.SelectorEventLoop
|
.venv/Lib/site-packages/uvicorn/loops/auto.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
from collections.abc import Callable
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def auto_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]:
|
| 8 |
+
try:
|
| 9 |
+
import uvloop # noqa
|
| 10 |
+
except ImportError: # pragma: no cover
|
| 11 |
+
from uvicorn.loops.asyncio import asyncio_loop_factory as loop_factory
|
| 12 |
+
|
| 13 |
+
return loop_factory(use_subprocess=use_subprocess)
|
| 14 |
+
else: # pragma: no cover
|
| 15 |
+
from uvicorn.loops.uvloop import uvloop_loop_factory
|
| 16 |
+
|
| 17 |
+
return uvloop_loop_factory(use_subprocess=use_subprocess)
|
.venv/Lib/site-packages/uvicorn/loops/uvloop.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
from collections.abc import Callable
|
| 5 |
+
|
| 6 |
+
import uvloop
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def uvloop_loop_factory(use_subprocess: bool = False) -> Callable[[], asyncio.AbstractEventLoop]:
|
| 10 |
+
return uvloop.new_event_loop
|
.venv/Lib/site-packages/uvicorn/main.py
ADDED
|
@@ -0,0 +1,645 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
import os
|
| 6 |
+
import platform
|
| 7 |
+
import ssl
|
| 8 |
+
import sys
|
| 9 |
+
import warnings
|
| 10 |
+
from collections.abc import Callable
|
| 11 |
+
from configparser import RawConfigParser
|
| 12 |
+
from typing import IO, Any, get_args
|
| 13 |
+
|
| 14 |
+
import click
|
| 15 |
+
|
| 16 |
+
import uvicorn
|
| 17 |
+
from uvicorn._types import ASGIApplication
|
| 18 |
+
from uvicorn.config import (
|
| 19 |
+
INTERFACES,
|
| 20 |
+
LIFESPAN,
|
| 21 |
+
LOG_LEVELS,
|
| 22 |
+
LOGGING_CONFIG,
|
| 23 |
+
SSL_PROTOCOL_VERSION,
|
| 24 |
+
STARTUP_FAILURE,
|
| 25 |
+
Config,
|
| 26 |
+
HTTPProtocolType,
|
| 27 |
+
InterfaceType,
|
| 28 |
+
LifespanType,
|
| 29 |
+
LoopFactoryType,
|
| 30 |
+
WSProtocolType,
|
| 31 |
+
)
|
| 32 |
+
from uvicorn.server import Server
|
| 33 |
+
from uvicorn.supervisors import ChangeReload, Multiprocess
|
| 34 |
+
|
| 35 |
+
LEVEL_CHOICES = click.Choice(list(LOG_LEVELS.keys()))
|
| 36 |
+
LIFESPAN_CHOICES = click.Choice(list(LIFESPAN.keys()))
|
| 37 |
+
INTERFACE_CHOICES = click.Choice(INTERFACES)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _metavar_from_type(_type: Any) -> str:
|
| 41 |
+
return f"[{'|'.join(key for key in get_args(_type) if key != 'none')}]"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
logger = logging.getLogger("uvicorn.error")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def print_version(ctx: click.Context, param: click.Parameter, value: bool) -> None:
|
| 48 |
+
if not value or ctx.resilient_parsing:
|
| 49 |
+
return
|
| 50 |
+
click.echo(
|
| 51 |
+
"Running uvicorn {version} with {py_implementation} {py_version} on {system}".format( # noqa: UP032
|
| 52 |
+
version=uvicorn.__version__,
|
| 53 |
+
py_implementation=platform.python_implementation(),
|
| 54 |
+
py_version=platform.python_version(),
|
| 55 |
+
system=platform.system(),
|
| 56 |
+
)
|
| 57 |
+
)
|
| 58 |
+
ctx.exit()
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@click.command(context_settings={"auto_envvar_prefix": "UVICORN"})
|
| 62 |
+
@click.argument("app", envvar="UVICORN_APP")
|
| 63 |
+
@click.option(
|
| 64 |
+
"--host",
|
| 65 |
+
type=str,
|
| 66 |
+
default="127.0.0.1",
|
| 67 |
+
help="Bind socket to this host.",
|
| 68 |
+
show_default=True,
|
| 69 |
+
)
|
| 70 |
+
@click.option(
|
| 71 |
+
"--port",
|
| 72 |
+
type=int,
|
| 73 |
+
default=8000,
|
| 74 |
+
help="Bind socket to this port. If 0, an available port will be picked.",
|
| 75 |
+
show_default=True,
|
| 76 |
+
)
|
| 77 |
+
@click.option("--uds", type=str, default=None, help="Bind to a UNIX domain socket.")
|
| 78 |
+
@click.option("--fd", type=int, default=None, help="Bind to socket from this file descriptor.")
|
| 79 |
+
@click.option("--reload", is_flag=True, default=False, help="Enable auto-reload.")
|
| 80 |
+
@click.option(
|
| 81 |
+
"--reload-dir",
|
| 82 |
+
"reload_dirs",
|
| 83 |
+
multiple=True,
|
| 84 |
+
help="Set reload directories explicitly, instead of using the current working directory.",
|
| 85 |
+
type=click.Path(exists=True),
|
| 86 |
+
)
|
| 87 |
+
@click.option(
|
| 88 |
+
"--reload-include",
|
| 89 |
+
"reload_includes",
|
| 90 |
+
multiple=True,
|
| 91 |
+
help="Set glob patterns to include while watching for files. Includes '*.py' "
|
| 92 |
+
"by default; these defaults can be overridden with `--reload-exclude`. "
|
| 93 |
+
"This option has no effect unless watchfiles is installed.",
|
| 94 |
+
)
|
| 95 |
+
@click.option(
|
| 96 |
+
"--reload-exclude",
|
| 97 |
+
"reload_excludes",
|
| 98 |
+
multiple=True,
|
| 99 |
+
help="Set glob patterns to exclude while watching for files. Includes "
|
| 100 |
+
"'.*, .py[cod], .sw.*, ~*' by default; these defaults can be overridden "
|
| 101 |
+
"with `--reload-include`. This option has no effect unless watchfiles is "
|
| 102 |
+
"installed.",
|
| 103 |
+
)
|
| 104 |
+
@click.option(
|
| 105 |
+
"--reload-delay",
|
| 106 |
+
type=float,
|
| 107 |
+
default=0.25,
|
| 108 |
+
show_default=True,
|
| 109 |
+
help="Delay between previous and next check if application needs to be. Defaults to 0.25s.",
|
| 110 |
+
)
|
| 111 |
+
@click.option(
|
| 112 |
+
"--workers",
|
| 113 |
+
default=None,
|
| 114 |
+
type=int,
|
| 115 |
+
help="Number of worker processes. Defaults to the $WEB_CONCURRENCY environment"
|
| 116 |
+
" variable if available, or 1. Not valid with --reload.",
|
| 117 |
+
)
|
| 118 |
+
@click.option(
|
| 119 |
+
"--loop",
|
| 120 |
+
type=str,
|
| 121 |
+
metavar=_metavar_from_type(LoopFactoryType),
|
| 122 |
+
default="auto",
|
| 123 |
+
help="Event loop factory implementation.",
|
| 124 |
+
show_default=True,
|
| 125 |
+
)
|
| 126 |
+
@click.option(
|
| 127 |
+
"--http",
|
| 128 |
+
type=str,
|
| 129 |
+
metavar=_metavar_from_type(HTTPProtocolType),
|
| 130 |
+
default="auto",
|
| 131 |
+
help="HTTP protocol implementation.",
|
| 132 |
+
show_default=True,
|
| 133 |
+
)
|
| 134 |
+
@click.option(
|
| 135 |
+
"--ws",
|
| 136 |
+
type=str,
|
| 137 |
+
metavar=_metavar_from_type(WSProtocolType),
|
| 138 |
+
default="auto",
|
| 139 |
+
help="WebSocket protocol implementation.",
|
| 140 |
+
show_default=True,
|
| 141 |
+
)
|
| 142 |
+
@click.option(
|
| 143 |
+
"--ws-max-size",
|
| 144 |
+
type=int,
|
| 145 |
+
default=16777216,
|
| 146 |
+
help="WebSocket max size message in bytes",
|
| 147 |
+
show_default=True,
|
| 148 |
+
)
|
| 149 |
+
@click.option(
|
| 150 |
+
"--ws-max-queue",
|
| 151 |
+
type=int,
|
| 152 |
+
default=32,
|
| 153 |
+
help="The maximum length of the WebSocket message queue.",
|
| 154 |
+
show_default=True,
|
| 155 |
+
)
|
| 156 |
+
@click.option(
|
| 157 |
+
"--ws-ping-interval",
|
| 158 |
+
type=float,
|
| 159 |
+
default=20.0,
|
| 160 |
+
help="WebSocket ping interval in seconds.",
|
| 161 |
+
show_default=True,
|
| 162 |
+
)
|
| 163 |
+
@click.option(
|
| 164 |
+
"--ws-ping-timeout",
|
| 165 |
+
type=float,
|
| 166 |
+
default=20.0,
|
| 167 |
+
help="WebSocket ping timeout in seconds.",
|
| 168 |
+
show_default=True,
|
| 169 |
+
)
|
| 170 |
+
@click.option(
|
| 171 |
+
"--ws-per-message-deflate",
|
| 172 |
+
type=bool,
|
| 173 |
+
default=True,
|
| 174 |
+
help="WebSocket per-message-deflate compression",
|
| 175 |
+
show_default=True,
|
| 176 |
+
)
|
| 177 |
+
@click.option(
|
| 178 |
+
"--lifespan",
|
| 179 |
+
type=LIFESPAN_CHOICES,
|
| 180 |
+
default="auto",
|
| 181 |
+
help="Lifespan implementation.",
|
| 182 |
+
show_default=True,
|
| 183 |
+
)
|
| 184 |
+
@click.option(
|
| 185 |
+
"--interface",
|
| 186 |
+
type=INTERFACE_CHOICES,
|
| 187 |
+
default="auto",
|
| 188 |
+
help="Select ASGI3, ASGI2, or WSGI as the application interface.",
|
| 189 |
+
show_default=True,
|
| 190 |
+
)
|
| 191 |
+
@click.option(
|
| 192 |
+
"--env-file",
|
| 193 |
+
type=click.Path(exists=True),
|
| 194 |
+
default=None,
|
| 195 |
+
help="Environment configuration file.",
|
| 196 |
+
show_default=True,
|
| 197 |
+
)
|
| 198 |
+
@click.option(
|
| 199 |
+
"--log-config",
|
| 200 |
+
type=click.Path(exists=True),
|
| 201 |
+
default=None,
|
| 202 |
+
help="Logging configuration file. Supported formats: .ini, .json, .yaml.",
|
| 203 |
+
show_default=True,
|
| 204 |
+
)
|
| 205 |
+
@click.option(
|
| 206 |
+
"--log-level",
|
| 207 |
+
type=LEVEL_CHOICES,
|
| 208 |
+
default=None,
|
| 209 |
+
help="Log level. [default: info]",
|
| 210 |
+
show_default=True,
|
| 211 |
+
)
|
| 212 |
+
@click.option(
|
| 213 |
+
"--access-log/--no-access-log",
|
| 214 |
+
is_flag=True,
|
| 215 |
+
default=True,
|
| 216 |
+
help="Enable/Disable access log.",
|
| 217 |
+
)
|
| 218 |
+
@click.option(
|
| 219 |
+
"--use-colors/--no-use-colors",
|
| 220 |
+
is_flag=True,
|
| 221 |
+
default=None,
|
| 222 |
+
help="Enable/Disable colorized logging.",
|
| 223 |
+
)
|
| 224 |
+
@click.option(
|
| 225 |
+
"--proxy-headers/--no-proxy-headers",
|
| 226 |
+
is_flag=True,
|
| 227 |
+
default=True,
|
| 228 |
+
help="Enable/Disable X-Forwarded-Proto, X-Forwarded-For to populate url scheme and remote address info.",
|
| 229 |
+
)
|
| 230 |
+
@click.option(
|
| 231 |
+
"--server-header/--no-server-header",
|
| 232 |
+
is_flag=True,
|
| 233 |
+
default=True,
|
| 234 |
+
help="Enable/Disable default Server header.",
|
| 235 |
+
)
|
| 236 |
+
@click.option(
|
| 237 |
+
"--date-header/--no-date-header",
|
| 238 |
+
is_flag=True,
|
| 239 |
+
default=True,
|
| 240 |
+
help="Enable/Disable default Date header.",
|
| 241 |
+
)
|
| 242 |
+
@click.option(
|
| 243 |
+
"--forwarded-allow-ips",
|
| 244 |
+
type=str,
|
| 245 |
+
default=None,
|
| 246 |
+
help="Comma separated list of IP Addresses, IP Networks, or literals "
|
| 247 |
+
"(e.g. UNIX Socket path) to trust with proxy headers. Defaults to the "
|
| 248 |
+
"$FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'. "
|
| 249 |
+
"The literal '*' means trust everything.",
|
| 250 |
+
)
|
| 251 |
+
@click.option(
|
| 252 |
+
"--root-path",
|
| 253 |
+
type=str,
|
| 254 |
+
default="",
|
| 255 |
+
help="Set the ASGI 'root_path' for applications submounted below a given URL path.",
|
| 256 |
+
)
|
| 257 |
+
@click.option(
|
| 258 |
+
"--limit-concurrency",
|
| 259 |
+
type=int,
|
| 260 |
+
default=None,
|
| 261 |
+
help="Maximum number of concurrent connections or tasks to allow, before issuing HTTP 503 responses.",
|
| 262 |
+
)
|
| 263 |
+
@click.option(
|
| 264 |
+
"--backlog",
|
| 265 |
+
type=int,
|
| 266 |
+
default=2048,
|
| 267 |
+
help="Maximum number of connections to hold in backlog",
|
| 268 |
+
)
|
| 269 |
+
@click.option(
|
| 270 |
+
"--limit-max-requests",
|
| 271 |
+
type=int,
|
| 272 |
+
default=None,
|
| 273 |
+
help="Maximum number of requests to service before terminating the process.",
|
| 274 |
+
)
|
| 275 |
+
@click.option(
|
| 276 |
+
"--limit-max-requests-jitter",
|
| 277 |
+
type=int,
|
| 278 |
+
default=0,
|
| 279 |
+
help="Maximum jitter to add to limit_max_requests."
|
| 280 |
+
" Staggers worker restarts to avoid all workers restarting simultaneously.",
|
| 281 |
+
show_default=True,
|
| 282 |
+
)
|
| 283 |
+
@click.option(
|
| 284 |
+
"--timeout-keep-alive",
|
| 285 |
+
type=int,
|
| 286 |
+
default=5,
|
| 287 |
+
help="Close Keep-Alive connections if no new data is received within this timeout (in seconds).",
|
| 288 |
+
show_default=True,
|
| 289 |
+
)
|
| 290 |
+
@click.option(
|
| 291 |
+
"--timeout-graceful-shutdown",
|
| 292 |
+
type=int,
|
| 293 |
+
default=None,
|
| 294 |
+
help="Maximum number of seconds to wait for graceful shutdown.",
|
| 295 |
+
)
|
| 296 |
+
@click.option(
|
| 297 |
+
"--timeout-worker-healthcheck",
|
| 298 |
+
type=int,
|
| 299 |
+
default=5,
|
| 300 |
+
help="Maximum number of seconds to wait for a worker to respond to a healthcheck.",
|
| 301 |
+
show_default=True,
|
| 302 |
+
)
|
| 303 |
+
@click.option("--ssl-keyfile", type=str, default=None, help="SSL key file", show_default=True)
|
| 304 |
+
@click.option(
|
| 305 |
+
"--ssl-certfile",
|
| 306 |
+
type=str,
|
| 307 |
+
default=None,
|
| 308 |
+
help="SSL certificate file",
|
| 309 |
+
show_default=True,
|
| 310 |
+
)
|
| 311 |
+
@click.option(
|
| 312 |
+
"--ssl-keyfile-password",
|
| 313 |
+
type=str,
|
| 314 |
+
default=None,
|
| 315 |
+
help="SSL keyfile password",
|
| 316 |
+
show_default=True,
|
| 317 |
+
)
|
| 318 |
+
@click.option(
|
| 319 |
+
"--ssl-version",
|
| 320 |
+
type=int,
|
| 321 |
+
default=int(SSL_PROTOCOL_VERSION),
|
| 322 |
+
help="SSL version to use (see stdlib ssl module's)",
|
| 323 |
+
show_default=True,
|
| 324 |
+
)
|
| 325 |
+
@click.option(
|
| 326 |
+
"--ssl-cert-reqs",
|
| 327 |
+
type=int,
|
| 328 |
+
default=int(ssl.CERT_NONE),
|
| 329 |
+
help="Whether client certificate is required (see stdlib ssl module's)",
|
| 330 |
+
show_default=True,
|
| 331 |
+
)
|
| 332 |
+
@click.option(
|
| 333 |
+
"--ssl-ca-certs",
|
| 334 |
+
type=str,
|
| 335 |
+
default=None,
|
| 336 |
+
help="CA certificates file",
|
| 337 |
+
show_default=True,
|
| 338 |
+
)
|
| 339 |
+
@click.option(
|
| 340 |
+
"--ssl-ciphers",
|
| 341 |
+
type=str,
|
| 342 |
+
default=None,
|
| 343 |
+
help="Ciphers to use (see stdlib ssl module's). Defaults to OpenSSL's safe defaults.",
|
| 344 |
+
show_default=True,
|
| 345 |
+
)
|
| 346 |
+
@click.option(
|
| 347 |
+
"--header",
|
| 348 |
+
"headers",
|
| 349 |
+
multiple=True,
|
| 350 |
+
help="Specify custom default HTTP response headers as a Name:Value pair",
|
| 351 |
+
)
|
| 352 |
+
@click.option(
|
| 353 |
+
"--version",
|
| 354 |
+
is_flag=True,
|
| 355 |
+
callback=print_version,
|
| 356 |
+
expose_value=False,
|
| 357 |
+
is_eager=True,
|
| 358 |
+
help="Display the uvicorn version and exit.",
|
| 359 |
+
)
|
| 360 |
+
@click.option(
|
| 361 |
+
"--app-dir",
|
| 362 |
+
default="",
|
| 363 |
+
show_default=True,
|
| 364 |
+
help="Look for APP in the specified directory, by adding this to the PYTHONPATH."
|
| 365 |
+
" Defaults to the current working directory.",
|
| 366 |
+
)
|
| 367 |
+
@click.option(
|
| 368 |
+
"--h11-max-incomplete-event-size",
|
| 369 |
+
"h11_max_incomplete_event_size",
|
| 370 |
+
type=int,
|
| 371 |
+
default=None,
|
| 372 |
+
help="For h11, the maximum number of bytes to buffer of an incomplete event.",
|
| 373 |
+
)
|
| 374 |
+
@click.option(
|
| 375 |
+
"--reset-contextvars",
|
| 376 |
+
is_flag=True,
|
| 377 |
+
default=False,
|
| 378 |
+
help="Run each ASGI request in a fresh contextvars.Context. Hides context set in the lifespan.",
|
| 379 |
+
show_default=True,
|
| 380 |
+
)
|
| 381 |
+
@click.option(
|
| 382 |
+
"--factory",
|
| 383 |
+
is_flag=True,
|
| 384 |
+
default=False,
|
| 385 |
+
help="Treat APP as an application factory, i.e. a () -> <ASGI app> callable.",
|
| 386 |
+
show_default=True,
|
| 387 |
+
)
|
| 388 |
+
def main(
|
| 389 |
+
app: str,
|
| 390 |
+
host: str,
|
| 391 |
+
port: int,
|
| 392 |
+
uds: str,
|
| 393 |
+
fd: int,
|
| 394 |
+
loop: LoopFactoryType | str,
|
| 395 |
+
http: HTTPProtocolType | str,
|
| 396 |
+
ws: WSProtocolType | str,
|
| 397 |
+
ws_max_size: int,
|
| 398 |
+
ws_max_queue: int,
|
| 399 |
+
ws_ping_interval: float,
|
| 400 |
+
ws_ping_timeout: float,
|
| 401 |
+
ws_per_message_deflate: bool,
|
| 402 |
+
lifespan: LifespanType,
|
| 403 |
+
interface: InterfaceType,
|
| 404 |
+
reload: bool,
|
| 405 |
+
reload_dirs: list[str],
|
| 406 |
+
reload_includes: list[str],
|
| 407 |
+
reload_excludes: list[str],
|
| 408 |
+
reload_delay: float,
|
| 409 |
+
workers: int,
|
| 410 |
+
env_file: str,
|
| 411 |
+
log_config: str,
|
| 412 |
+
log_level: str,
|
| 413 |
+
access_log: bool,
|
| 414 |
+
proxy_headers: bool,
|
| 415 |
+
server_header: bool,
|
| 416 |
+
date_header: bool,
|
| 417 |
+
forwarded_allow_ips: str,
|
| 418 |
+
root_path: str,
|
| 419 |
+
limit_concurrency: int,
|
| 420 |
+
backlog: int,
|
| 421 |
+
limit_max_requests: int,
|
| 422 |
+
limit_max_requests_jitter: int,
|
| 423 |
+
timeout_keep_alive: int,
|
| 424 |
+
timeout_graceful_shutdown: int | None,
|
| 425 |
+
timeout_worker_healthcheck: int,
|
| 426 |
+
ssl_keyfile: str,
|
| 427 |
+
ssl_certfile: str,
|
| 428 |
+
ssl_keyfile_password: str,
|
| 429 |
+
ssl_version: int,
|
| 430 |
+
ssl_cert_reqs: int,
|
| 431 |
+
ssl_ca_certs: str,
|
| 432 |
+
ssl_ciphers: str | None,
|
| 433 |
+
headers: list[str],
|
| 434 |
+
use_colors: bool,
|
| 435 |
+
app_dir: str,
|
| 436 |
+
h11_max_incomplete_event_size: int | None,
|
| 437 |
+
reset_contextvars: bool,
|
| 438 |
+
factory: bool,
|
| 439 |
+
) -> None:
|
| 440 |
+
run(
|
| 441 |
+
app,
|
| 442 |
+
host=host,
|
| 443 |
+
port=port,
|
| 444 |
+
uds=uds,
|
| 445 |
+
fd=fd,
|
| 446 |
+
loop=loop,
|
| 447 |
+
http=http,
|
| 448 |
+
ws=ws,
|
| 449 |
+
ws_max_size=ws_max_size,
|
| 450 |
+
ws_max_queue=ws_max_queue,
|
| 451 |
+
ws_ping_interval=ws_ping_interval,
|
| 452 |
+
ws_ping_timeout=ws_ping_timeout,
|
| 453 |
+
ws_per_message_deflate=ws_per_message_deflate,
|
| 454 |
+
lifespan=lifespan,
|
| 455 |
+
env_file=env_file,
|
| 456 |
+
log_config=LOGGING_CONFIG if log_config is None else log_config,
|
| 457 |
+
log_level=log_level,
|
| 458 |
+
access_log=access_log,
|
| 459 |
+
interface=interface,
|
| 460 |
+
reload=reload,
|
| 461 |
+
reload_dirs=reload_dirs or None,
|
| 462 |
+
reload_includes=reload_includes or None,
|
| 463 |
+
reload_excludes=reload_excludes or None,
|
| 464 |
+
reload_delay=reload_delay,
|
| 465 |
+
workers=workers,
|
| 466 |
+
proxy_headers=proxy_headers,
|
| 467 |
+
server_header=server_header,
|
| 468 |
+
date_header=date_header,
|
| 469 |
+
forwarded_allow_ips=forwarded_allow_ips,
|
| 470 |
+
root_path=root_path,
|
| 471 |
+
limit_concurrency=limit_concurrency,
|
| 472 |
+
backlog=backlog,
|
| 473 |
+
limit_max_requests=limit_max_requests,
|
| 474 |
+
limit_max_requests_jitter=limit_max_requests_jitter,
|
| 475 |
+
timeout_keep_alive=timeout_keep_alive,
|
| 476 |
+
timeout_graceful_shutdown=timeout_graceful_shutdown,
|
| 477 |
+
timeout_worker_healthcheck=timeout_worker_healthcheck,
|
| 478 |
+
ssl_keyfile=ssl_keyfile,
|
| 479 |
+
ssl_certfile=ssl_certfile,
|
| 480 |
+
ssl_keyfile_password=ssl_keyfile_password,
|
| 481 |
+
ssl_version=ssl_version,
|
| 482 |
+
ssl_cert_reqs=ssl_cert_reqs,
|
| 483 |
+
ssl_ca_certs=ssl_ca_certs,
|
| 484 |
+
ssl_ciphers=ssl_ciphers,
|
| 485 |
+
headers=[header.split(":", 1) for header in headers], # type: ignore[misc]
|
| 486 |
+
use_colors=use_colors,
|
| 487 |
+
factory=factory,
|
| 488 |
+
app_dir=app_dir,
|
| 489 |
+
h11_max_incomplete_event_size=h11_max_incomplete_event_size,
|
| 490 |
+
reset_contextvars=reset_contextvars,
|
| 491 |
+
)
|
| 492 |
+
|
| 493 |
+
|
| 494 |
+
def run(
|
| 495 |
+
app: ASGIApplication | Callable[..., Any] | str,
|
| 496 |
+
*,
|
| 497 |
+
host: str = "127.0.0.1",
|
| 498 |
+
port: int = 8000,
|
| 499 |
+
uds: str | None = None,
|
| 500 |
+
fd: int | None = None,
|
| 501 |
+
loop: LoopFactoryType | str = "auto",
|
| 502 |
+
http: type[asyncio.Protocol] | HTTPProtocolType | str = "auto",
|
| 503 |
+
ws: type[asyncio.Protocol] | WSProtocolType | str = "auto",
|
| 504 |
+
ws_max_size: int = 16777216,
|
| 505 |
+
ws_max_queue: int = 32,
|
| 506 |
+
ws_ping_interval: float | None = 20.0,
|
| 507 |
+
ws_ping_timeout: float | None = 20.0,
|
| 508 |
+
ws_per_message_deflate: bool = True,
|
| 509 |
+
lifespan: LifespanType = "auto",
|
| 510 |
+
interface: InterfaceType = "auto",
|
| 511 |
+
reload: bool = False,
|
| 512 |
+
reload_dirs: list[str] | str | None = None,
|
| 513 |
+
reload_includes: list[str] | str | None = None,
|
| 514 |
+
reload_excludes: list[str] | str | None = None,
|
| 515 |
+
reload_delay: float = 0.25,
|
| 516 |
+
workers: int | None = None,
|
| 517 |
+
env_file: str | os.PathLike[str] | None = None,
|
| 518 |
+
log_config: dict[str, Any] | str | os.PathLike[str] | RawConfigParser | IO[Any] | None = LOGGING_CONFIG,
|
| 519 |
+
log_level: str | int | None = None,
|
| 520 |
+
access_log: bool = True,
|
| 521 |
+
proxy_headers: bool = True,
|
| 522 |
+
server_header: bool = True,
|
| 523 |
+
date_header: bool = True,
|
| 524 |
+
forwarded_allow_ips: list[str] | str | None = None,
|
| 525 |
+
root_path: str = "",
|
| 526 |
+
limit_concurrency: int | None = None,
|
| 527 |
+
backlog: int = 2048,
|
| 528 |
+
limit_max_requests: int | None = None,
|
| 529 |
+
limit_max_requests_jitter: int = 0,
|
| 530 |
+
timeout_keep_alive: int = 5,
|
| 531 |
+
timeout_graceful_shutdown: int | None = None,
|
| 532 |
+
timeout_worker_healthcheck: int = 5,
|
| 533 |
+
ssl_keyfile: str | os.PathLike[str] | None = None,
|
| 534 |
+
ssl_certfile: str | os.PathLike[str] | None = None,
|
| 535 |
+
ssl_keyfile_password: str | None = None,
|
| 536 |
+
ssl_version: int = SSL_PROTOCOL_VERSION,
|
| 537 |
+
ssl_cert_reqs: int = ssl.CERT_NONE,
|
| 538 |
+
ssl_ca_certs: str | os.PathLike[str] | None = None,
|
| 539 |
+
ssl_ciphers: str | None = None,
|
| 540 |
+
ssl_context_factory: Callable[[Config, Callable[[], ssl.SSLContext]], ssl.SSLContext] | None = None,
|
| 541 |
+
headers: list[tuple[str, str]] | None = None,
|
| 542 |
+
use_colors: bool | None = None,
|
| 543 |
+
app_dir: str | None = None,
|
| 544 |
+
factory: bool = False,
|
| 545 |
+
h11_max_incomplete_event_size: int | None = None,
|
| 546 |
+
reset_contextvars: bool = False,
|
| 547 |
+
) -> None:
|
| 548 |
+
if app_dir is not None:
|
| 549 |
+
sys.path.insert(0, app_dir)
|
| 550 |
+
|
| 551 |
+
config = Config(
|
| 552 |
+
app,
|
| 553 |
+
host=host,
|
| 554 |
+
port=port,
|
| 555 |
+
uds=uds,
|
| 556 |
+
fd=fd,
|
| 557 |
+
loop=loop,
|
| 558 |
+
http=http,
|
| 559 |
+
ws=ws,
|
| 560 |
+
ws_max_size=ws_max_size,
|
| 561 |
+
ws_max_queue=ws_max_queue,
|
| 562 |
+
ws_ping_interval=ws_ping_interval,
|
| 563 |
+
ws_ping_timeout=ws_ping_timeout,
|
| 564 |
+
ws_per_message_deflate=ws_per_message_deflate,
|
| 565 |
+
lifespan=lifespan,
|
| 566 |
+
interface=interface,
|
| 567 |
+
reload=reload,
|
| 568 |
+
reload_dirs=reload_dirs,
|
| 569 |
+
reload_includes=reload_includes,
|
| 570 |
+
reload_excludes=reload_excludes,
|
| 571 |
+
reload_delay=reload_delay,
|
| 572 |
+
workers=workers,
|
| 573 |
+
env_file=env_file,
|
| 574 |
+
log_config=log_config,
|
| 575 |
+
log_level=log_level,
|
| 576 |
+
access_log=access_log,
|
| 577 |
+
proxy_headers=proxy_headers,
|
| 578 |
+
server_header=server_header,
|
| 579 |
+
date_header=date_header,
|
| 580 |
+
forwarded_allow_ips=forwarded_allow_ips,
|
| 581 |
+
root_path=root_path,
|
| 582 |
+
limit_concurrency=limit_concurrency,
|
| 583 |
+
backlog=backlog,
|
| 584 |
+
limit_max_requests=limit_max_requests,
|
| 585 |
+
limit_max_requests_jitter=limit_max_requests_jitter,
|
| 586 |
+
timeout_keep_alive=timeout_keep_alive,
|
| 587 |
+
timeout_graceful_shutdown=timeout_graceful_shutdown,
|
| 588 |
+
timeout_worker_healthcheck=timeout_worker_healthcheck,
|
| 589 |
+
ssl_keyfile=ssl_keyfile,
|
| 590 |
+
ssl_certfile=ssl_certfile,
|
| 591 |
+
ssl_keyfile_password=ssl_keyfile_password,
|
| 592 |
+
ssl_version=ssl_version,
|
| 593 |
+
ssl_cert_reqs=ssl_cert_reqs,
|
| 594 |
+
ssl_ca_certs=ssl_ca_certs,
|
| 595 |
+
ssl_ciphers=ssl_ciphers,
|
| 596 |
+
ssl_context_factory=ssl_context_factory,
|
| 597 |
+
headers=headers,
|
| 598 |
+
use_colors=use_colors,
|
| 599 |
+
factory=factory,
|
| 600 |
+
h11_max_incomplete_event_size=h11_max_incomplete_event_size,
|
| 601 |
+
reset_contextvars=reset_contextvars,
|
| 602 |
+
)
|
| 603 |
+
if config.reload or config.workers > 1:
|
| 604 |
+
if not isinstance(app, str):
|
| 605 |
+
logger = logging.getLogger("uvicorn.error")
|
| 606 |
+
logger.warning("You must pass the application as an import string to enable 'reload' or 'workers'.")
|
| 607 |
+
sys.exit(STARTUP_FAILURE)
|
| 608 |
+
else:
|
| 609 |
+
config.load_app()
|
| 610 |
+
|
| 611 |
+
server = Server(config=config)
|
| 612 |
+
|
| 613 |
+
try:
|
| 614 |
+
if config.should_reload:
|
| 615 |
+
sock = config.bind_socket()
|
| 616 |
+
ChangeReload(config, target=server.run, sockets=[sock]).run()
|
| 617 |
+
elif config.workers > 1:
|
| 618 |
+
sock = config.bind_socket()
|
| 619 |
+
Multiprocess(config, sockets=[sock]).run()
|
| 620 |
+
else:
|
| 621 |
+
server.run()
|
| 622 |
+
except KeyboardInterrupt: # pragma: full coverage
|
| 623 |
+
pass
|
| 624 |
+
finally:
|
| 625 |
+
if config.uds and os.path.exists(config.uds):
|
| 626 |
+
os.remove(config.uds) # pragma: py-win32
|
| 627 |
+
|
| 628 |
+
if not server.started and not config.should_reload and config.workers == 1:
|
| 629 |
+
sys.exit(STARTUP_FAILURE)
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
def __getattr__(name: str) -> Any:
|
| 633 |
+
if name == "ServerState":
|
| 634 |
+
warnings.warn(
|
| 635 |
+
"uvicorn.main.ServerState is deprecated, use uvicorn.server.ServerState instead.",
|
| 636 |
+
DeprecationWarning,
|
| 637 |
+
)
|
| 638 |
+
from uvicorn.server import ServerState
|
| 639 |
+
|
| 640 |
+
return ServerState
|
| 641 |
+
raise AttributeError(f"module {__name__} has no attribute {name}")
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
if __name__ == "__main__":
|
| 645 |
+
main() # pragma: no cover
|
.venv/Lib/site-packages/uvicorn/middleware/__init__.py
ADDED
|
File without changes
|
.venv/Lib/site-packages/uvicorn/middleware/asgi2.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from uvicorn._types import (
|
| 2 |
+
ASGI2Application,
|
| 3 |
+
ASGIReceiveCallable,
|
| 4 |
+
ASGISendCallable,
|
| 5 |
+
Scope,
|
| 6 |
+
)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ASGI2Middleware:
|
| 10 |
+
def __init__(self, app: "ASGI2Application"):
|
| 11 |
+
self.app = app
|
| 12 |
+
|
| 13 |
+
async def __call__(self, scope: "Scope", receive: "ASGIReceiveCallable", send: "ASGISendCallable") -> None:
|
| 14 |
+
instance = self.app(scope)
|
| 15 |
+
await instance(receive, send)
|
.venv/Lib/site-packages/uvicorn/middleware/message_logger.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Any
|
| 3 |
+
|
| 4 |
+
from uvicorn._types import (
|
| 5 |
+
ASGI3Application,
|
| 6 |
+
ASGIReceiveCallable,
|
| 7 |
+
ASGIReceiveEvent,
|
| 8 |
+
ASGISendCallable,
|
| 9 |
+
ASGISendEvent,
|
| 10 |
+
WWWScope,
|
| 11 |
+
)
|
| 12 |
+
from uvicorn.logging import TRACE_LOG_LEVEL
|
| 13 |
+
|
| 14 |
+
PLACEHOLDER_FORMAT = {
|
| 15 |
+
"body": "<{length} bytes>",
|
| 16 |
+
"bytes": "<{length} bytes>",
|
| 17 |
+
"text": "<{length} chars>",
|
| 18 |
+
"headers": "<...>",
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def message_with_placeholders(message: Any) -> Any:
|
| 23 |
+
"""
|
| 24 |
+
Return an ASGI message, with any body-type content omitted and replaced
|
| 25 |
+
with a placeholder.
|
| 26 |
+
"""
|
| 27 |
+
new_message = message.copy()
|
| 28 |
+
for attr in PLACEHOLDER_FORMAT.keys():
|
| 29 |
+
if message.get(attr) is not None:
|
| 30 |
+
content = message[attr]
|
| 31 |
+
placeholder = PLACEHOLDER_FORMAT[attr].format(length=len(content))
|
| 32 |
+
new_message[attr] = placeholder
|
| 33 |
+
return new_message
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class MessageLoggerMiddleware:
|
| 37 |
+
def __init__(self, app: "ASGI3Application"):
|
| 38 |
+
self.task_counter = 0
|
| 39 |
+
self.app = app
|
| 40 |
+
self.logger = logging.getLogger("uvicorn.asgi")
|
| 41 |
+
|
| 42 |
+
def trace(message: Any, *args: Any, **kwargs: Any) -> None:
|
| 43 |
+
self.logger.log(TRACE_LOG_LEVEL, message, *args, **kwargs)
|
| 44 |
+
|
| 45 |
+
self.logger.trace = trace # type: ignore
|
| 46 |
+
|
| 47 |
+
async def __call__(
|
| 48 |
+
self,
|
| 49 |
+
scope: "WWWScope",
|
| 50 |
+
receive: "ASGIReceiveCallable",
|
| 51 |
+
send: "ASGISendCallable",
|
| 52 |
+
) -> None:
|
| 53 |
+
self.task_counter += 1
|
| 54 |
+
|
| 55 |
+
task_counter = self.task_counter
|
| 56 |
+
client = scope.get("client")
|
| 57 |
+
prefix = "%s:%d - ASGI" % (client[0], client[1]) if client else "ASGI"
|
| 58 |
+
|
| 59 |
+
async def inner_receive() -> "ASGIReceiveEvent":
|
| 60 |
+
message = await receive()
|
| 61 |
+
logged_message = message_with_placeholders(message)
|
| 62 |
+
log_text = "%s [%d] Receive %s"
|
| 63 |
+
self.logger.trace( # type: ignore
|
| 64 |
+
log_text, prefix, task_counter, logged_message
|
| 65 |
+
)
|
| 66 |
+
return message
|
| 67 |
+
|
| 68 |
+
async def inner_send(message: "ASGISendEvent") -> None:
|
| 69 |
+
logged_message = message_with_placeholders(message)
|
| 70 |
+
log_text = "%s [%d] Send %s"
|
| 71 |
+
self.logger.trace( # type: ignore
|
| 72 |
+
log_text, prefix, task_counter, logged_message
|
| 73 |
+
)
|
| 74 |
+
await send(message)
|
| 75 |
+
|
| 76 |
+
logged_scope = message_with_placeholders(scope)
|
| 77 |
+
log_text = "%s [%d] Started scope=%s"
|
| 78 |
+
self.logger.trace(log_text, prefix, task_counter, logged_scope) # type: ignore
|
| 79 |
+
try:
|
| 80 |
+
await self.app(scope, inner_receive, inner_send)
|
| 81 |
+
except BaseException as exc:
|
| 82 |
+
log_text = "%s [%d] Raised exception"
|
| 83 |
+
self.logger.trace(log_text, prefix, task_counter) # type: ignore
|
| 84 |
+
raise exc from None
|
| 85 |
+
else:
|
| 86 |
+
log_text = "%s [%d] Completed"
|
| 87 |
+
self.logger.trace(log_text, prefix, task_counter) # type: ignore
|
.venv/Lib/site-packages/uvicorn/middleware/proxy_headers.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import functools
|
| 4 |
+
import ipaddress
|
| 5 |
+
|
| 6 |
+
from uvicorn._types import ASGI3Application, ASGIReceiveCallable, ASGISendCallable, Scope
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class ProxyHeadersMiddleware:
|
| 10 |
+
"""Middleware for handling known proxy headers
|
| 11 |
+
|
| 12 |
+
This middleware can be used when a known proxy is fronting the application,
|
| 13 |
+
and is trusted to be properly setting the `X-Forwarded-Proto` and
|
| 14 |
+
`X-Forwarded-For` headers with the connecting client information.
|
| 15 |
+
|
| 16 |
+
Modifies the `client` and `scheme` information so that they reference
|
| 17 |
+
the connecting client, rather that the connecting proxy.
|
| 18 |
+
|
| 19 |
+
References:
|
| 20 |
+
- <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#Proxies>
|
| 21 |
+
- <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For>
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self, app: ASGI3Application, trusted_hosts: list[str] | str = "127.0.0.1") -> None:
|
| 25 |
+
self.app = app
|
| 26 |
+
self.trusted_hosts = _TrustedHosts(trusted_hosts)
|
| 27 |
+
|
| 28 |
+
async def __call__(self, scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
| 29 |
+
if scope["type"] == "lifespan":
|
| 30 |
+
return await self.app(scope, receive, send)
|
| 31 |
+
|
| 32 |
+
client_addr = scope.get("client")
|
| 33 |
+
client_host = client_addr[0] if client_addr else None
|
| 34 |
+
|
| 35 |
+
if client_host in self.trusted_hosts:
|
| 36 |
+
x_forwarded_proto_value: bytes | None = None
|
| 37 |
+
x_forwarded_for_values: list[bytes] = []
|
| 38 |
+
for name, value in scope["headers"]:
|
| 39 |
+
if name == b"x-forwarded-proto":
|
| 40 |
+
x_forwarded_proto_value = value
|
| 41 |
+
elif name == b"x-forwarded-for":
|
| 42 |
+
x_forwarded_for_values.append(value)
|
| 43 |
+
|
| 44 |
+
if x_forwarded_proto_value is not None:
|
| 45 |
+
x_forwarded_proto = x_forwarded_proto_value.decode("latin1").strip()
|
| 46 |
+
|
| 47 |
+
if x_forwarded_proto in {"http", "https", "ws", "wss"}:
|
| 48 |
+
if scope["type"] == "websocket":
|
| 49 |
+
scope["scheme"] = x_forwarded_proto.replace("http", "ws")
|
| 50 |
+
else:
|
| 51 |
+
scope["scheme"] = x_forwarded_proto
|
| 52 |
+
|
| 53 |
+
if x_forwarded_for_values:
|
| 54 |
+
x_forwarded_for = b", ".join(x_forwarded_for_values).decode("latin1")
|
| 55 |
+
host, port = self.trusted_hosts.get_trusted_client_address(x_forwarded_for)
|
| 56 |
+
|
| 57 |
+
if host:
|
| 58 |
+
# If the x-forwarded-for header is empty then host is an empty string.
|
| 59 |
+
# Only set the client if we actually got something usable.
|
| 60 |
+
# See: https://github.com/Kludex/uvicorn/issues/1068
|
| 61 |
+
scope["client"] = (host, port)
|
| 62 |
+
|
| 63 |
+
return await self.app(scope, receive, send)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _parse_raw_hosts(value: str) -> list[str]:
|
| 67 |
+
return [item.strip() for item in value.split(",")]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _parse_host_port(value: str) -> tuple[str, int]:
|
| 71 |
+
"""Parse a forwarded host value into host and optional port.
|
| 72 |
+
|
| 73 |
+
Accepts bare IPs, IPv4 `host:port`, and bracketed IPv6 `[host]:port`.
|
| 74 |
+
Any unrecognized or malformed value is treated conservatively and returned
|
| 75 |
+
without a port so trust checks do not silently normalize arbitrary input.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
if value.startswith("["):
|
| 79 |
+
bracket_end = value.find("]")
|
| 80 |
+
if bracket_end == -1:
|
| 81 |
+
return value, 0
|
| 82 |
+
|
| 83 |
+
host = value[1:bracket_end]
|
| 84 |
+
remainder = value[bracket_end + 1 :]
|
| 85 |
+
if not remainder:
|
| 86 |
+
return host, 0
|
| 87 |
+
if not remainder.startswith(":"):
|
| 88 |
+
return value, 0
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
return host, int(remainder[1:])
|
| 92 |
+
except ValueError:
|
| 93 |
+
return host, 0
|
| 94 |
+
|
| 95 |
+
if value.count(":") == 1:
|
| 96 |
+
host, port = value.rsplit(":", 1)
|
| 97 |
+
try:
|
| 98 |
+
return host, int(port)
|
| 99 |
+
except ValueError:
|
| 100 |
+
return value, 0
|
| 101 |
+
|
| 102 |
+
return value, 0
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class _TrustedHosts:
|
| 106 |
+
"""Container for trusted hosts and networks"""
|
| 107 |
+
|
| 108 |
+
def __init__(self, trusted_hosts: list[str] | str) -> None:
|
| 109 |
+
self.always_trust: bool = trusted_hosts in ("*", ["*"])
|
| 110 |
+
|
| 111 |
+
self.trusted_literals: set[str] = set()
|
| 112 |
+
self.trusted_hosts: set[ipaddress.IPv4Address | ipaddress.IPv6Address] = set()
|
| 113 |
+
self.trusted_networks: set[ipaddress.IPv4Network | ipaddress.IPv6Network] = set()
|
| 114 |
+
|
| 115 |
+
# Notes:
|
| 116 |
+
# - We separate hosts from literals as there are many ways to write
|
| 117 |
+
# an IPv6 Address so we need to compare by object.
|
| 118 |
+
# - We don't convert IP Address to single host networks (e.g. /32 / 128) as
|
| 119 |
+
# it more efficient to do an address lookup in a set than check for
|
| 120 |
+
# membership in each network.
|
| 121 |
+
# - We still allow literals as it might be possible that we receive a
|
| 122 |
+
# something that isn't an IP Address e.g. a unix socket.
|
| 123 |
+
|
| 124 |
+
if not self.always_trust:
|
| 125 |
+
if isinstance(trusted_hosts, str):
|
| 126 |
+
trusted_hosts = _parse_raw_hosts(trusted_hosts)
|
| 127 |
+
|
| 128 |
+
for host in trusted_hosts:
|
| 129 |
+
# Note: because we always convert invalid IP types to literals it
|
| 130 |
+
# is not possible for the user to know they provided a malformed IP
|
| 131 |
+
# type - this may lead to unexpected / difficult to debug behaviour.
|
| 132 |
+
|
| 133 |
+
if "/" in host:
|
| 134 |
+
# Looks like a network
|
| 135 |
+
try:
|
| 136 |
+
self.trusted_networks.add(ipaddress.ip_network(host))
|
| 137 |
+
except ValueError:
|
| 138 |
+
# Was not a valid IP Network
|
| 139 |
+
self.trusted_literals.add(host)
|
| 140 |
+
else:
|
| 141 |
+
try:
|
| 142 |
+
self.trusted_hosts.add(ipaddress.ip_address(host))
|
| 143 |
+
except ValueError:
|
| 144 |
+
# Was not a valid IP Address
|
| 145 |
+
self.trusted_literals.add(host)
|
| 146 |
+
|
| 147 |
+
self._trusts = functools.lru_cache(maxsize=4096)(self._compute_trust)
|
| 148 |
+
|
| 149 |
+
def __contains__(self, host: str | None) -> bool:
|
| 150 |
+
if self.always_trust:
|
| 151 |
+
return True
|
| 152 |
+
|
| 153 |
+
if not host:
|
| 154 |
+
return False
|
| 155 |
+
|
| 156 |
+
# Don't cache hosts longer than a DNS name (253); they can't be trusted and would pin huge cache keys.
|
| 157 |
+
if len(host) > 253:
|
| 158 |
+
return self._compute_trust(host) # pragma: no cover
|
| 159 |
+
|
| 160 |
+
return self._trusts(host)
|
| 161 |
+
|
| 162 |
+
def _compute_trust(self, host: str) -> bool:
|
| 163 |
+
try:
|
| 164 |
+
ip = ipaddress.ip_address(host)
|
| 165 |
+
return ip in self.trusted_hosts or any(ip in net for net in self.trusted_networks)
|
| 166 |
+
except ValueError:
|
| 167 |
+
return host in self.trusted_literals
|
| 168 |
+
|
| 169 |
+
def get_trusted_client_address(self, x_forwarded_for: str) -> tuple[str, int]:
|
| 170 |
+
"""Extract the client address from x_forwarded_for header.
|
| 171 |
+
|
| 172 |
+
In general this is the first "untrusted" host in the forwarded for list.
|
| 173 |
+
"""
|
| 174 |
+
x_forwarded_for_hosts = _parse_raw_hosts(x_forwarded_for)
|
| 175 |
+
|
| 176 |
+
if self.always_trust:
|
| 177 |
+
return _parse_host_port(x_forwarded_for_hosts[0])
|
| 178 |
+
|
| 179 |
+
# Note: each proxy appends to the header list so check it in reverse order
|
| 180 |
+
for host_port in reversed(x_forwarded_for_hosts):
|
| 181 |
+
host, port = _parse_host_port(host_port)
|
| 182 |
+
if host not in self:
|
| 183 |
+
return host, port
|
| 184 |
+
|
| 185 |
+
# All hosts are trusted meaning that the client was also a trusted proxy
|
| 186 |
+
# See https://github.com/Kludex/uvicorn/issues/1068#issuecomment-855371576
|
| 187 |
+
return _parse_host_port(x_forwarded_for_hosts[0])
|
.venv/Lib/site-packages/uvicorn/middleware/wsgi.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import concurrent.futures
|
| 5 |
+
import io
|
| 6 |
+
import sys
|
| 7 |
+
import warnings
|
| 8 |
+
from collections import deque
|
| 9 |
+
from collections.abc import Iterable
|
| 10 |
+
|
| 11 |
+
from uvicorn._types import (
|
| 12 |
+
ASGIReceiveCallable,
|
| 13 |
+
ASGIReceiveEvent,
|
| 14 |
+
ASGISendCallable,
|
| 15 |
+
ASGISendEvent,
|
| 16 |
+
Environ,
|
| 17 |
+
ExcInfo,
|
| 18 |
+
HTTPRequestEvent,
|
| 19 |
+
HTTPResponseBodyEvent,
|
| 20 |
+
HTTPResponseStartEvent,
|
| 21 |
+
HTTPScope,
|
| 22 |
+
StartResponse,
|
| 23 |
+
WSGIApp,
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def build_environ(scope: HTTPScope, message: ASGIReceiveEvent, body: io.BytesIO) -> Environ:
|
| 28 |
+
"""
|
| 29 |
+
Builds a scope and request message into a WSGI environ object.
|
| 30 |
+
"""
|
| 31 |
+
script_name = scope.get("root_path", "").encode("utf8").decode("latin1")
|
| 32 |
+
path_info = scope["path"].encode("utf8").decode("latin1")
|
| 33 |
+
if path_info.startswith(script_name):
|
| 34 |
+
path_info = path_info[len(script_name) :]
|
| 35 |
+
environ = {
|
| 36 |
+
"REQUEST_METHOD": scope["method"],
|
| 37 |
+
"SCRIPT_NAME": script_name,
|
| 38 |
+
"PATH_INFO": path_info,
|
| 39 |
+
"QUERY_STRING": scope["query_string"].decode("ascii"),
|
| 40 |
+
"SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"],
|
| 41 |
+
"wsgi.version": (1, 0),
|
| 42 |
+
"wsgi.url_scheme": scope.get("scheme", "http"),
|
| 43 |
+
"wsgi.input": body,
|
| 44 |
+
"wsgi.errors": sys.stdout,
|
| 45 |
+
"wsgi.multithread": True,
|
| 46 |
+
"wsgi.multiprocess": True,
|
| 47 |
+
"wsgi.run_once": False,
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
# Get server name and port - required in WSGI, not in ASGI
|
| 51 |
+
server = scope.get("server")
|
| 52 |
+
if server is None:
|
| 53 |
+
server = ("localhost", 80)
|
| 54 |
+
environ["SERVER_NAME"] = server[0]
|
| 55 |
+
environ["SERVER_PORT"] = server[1]
|
| 56 |
+
|
| 57 |
+
# Get client IP address
|
| 58 |
+
client = scope.get("client")
|
| 59 |
+
if client is not None:
|
| 60 |
+
environ["REMOTE_ADDR"] = client[0]
|
| 61 |
+
|
| 62 |
+
# Go through headers and make them into environ entries
|
| 63 |
+
for name, value in scope.get("headers", []):
|
| 64 |
+
name_str: str = name.decode("latin1")
|
| 65 |
+
if name_str == "content-length":
|
| 66 |
+
corrected_name = "CONTENT_LENGTH"
|
| 67 |
+
elif name_str == "content-type":
|
| 68 |
+
corrected_name = "CONTENT_TYPE"
|
| 69 |
+
else:
|
| 70 |
+
corrected_name = "HTTP_%s" % name_str.upper().replace("-", "_")
|
| 71 |
+
# HTTPbis say only ASCII chars are allowed in headers, but we latin1
|
| 72 |
+
# just in case
|
| 73 |
+
value_str: str = value.decode("latin1")
|
| 74 |
+
if corrected_name in environ:
|
| 75 |
+
corrected_name_environ = environ[corrected_name]
|
| 76 |
+
assert isinstance(corrected_name_environ, str)
|
| 77 |
+
value_str = corrected_name_environ + "," + value_str
|
| 78 |
+
environ[corrected_name] = value_str
|
| 79 |
+
return environ
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class _WSGIMiddleware:
|
| 83 |
+
def __init__(self, app: WSGIApp, workers: int = 10):
|
| 84 |
+
warnings.warn(
|
| 85 |
+
"Uvicorn's native WSGI implementation is deprecated, you should switch to a2wsgi (`pip install a2wsgi`).",
|
| 86 |
+
DeprecationWarning,
|
| 87 |
+
)
|
| 88 |
+
self.app = app
|
| 89 |
+
self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=workers)
|
| 90 |
+
|
| 91 |
+
async def __call__(
|
| 92 |
+
self,
|
| 93 |
+
scope: HTTPScope,
|
| 94 |
+
receive: ASGIReceiveCallable,
|
| 95 |
+
send: ASGISendCallable,
|
| 96 |
+
) -> None:
|
| 97 |
+
assert scope["type"] == "http"
|
| 98 |
+
instance = WSGIResponder(self.app, self.executor, scope)
|
| 99 |
+
await instance(receive, send)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class WSGIResponder:
|
| 103 |
+
def __init__(
|
| 104 |
+
self,
|
| 105 |
+
app: WSGIApp,
|
| 106 |
+
executor: concurrent.futures.ThreadPoolExecutor,
|
| 107 |
+
scope: HTTPScope,
|
| 108 |
+
):
|
| 109 |
+
self.app = app
|
| 110 |
+
self.executor = executor
|
| 111 |
+
self.scope = scope
|
| 112 |
+
self.status = None
|
| 113 |
+
self.response_headers = None
|
| 114 |
+
self.send_event = asyncio.Event()
|
| 115 |
+
self.send_queue: deque[ASGISendEvent | None] = deque()
|
| 116 |
+
self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
|
| 117 |
+
self.response_started = False
|
| 118 |
+
self.exc_info: ExcInfo | None = None
|
| 119 |
+
|
| 120 |
+
async def __call__(self, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
| 121 |
+
message: HTTPRequestEvent = await receive() # type: ignore[assignment]
|
| 122 |
+
body = io.BytesIO(message.get("body", b""))
|
| 123 |
+
more_body = message.get("more_body", False)
|
| 124 |
+
if more_body:
|
| 125 |
+
body.seek(0, io.SEEK_END)
|
| 126 |
+
while more_body:
|
| 127 |
+
body_message: HTTPRequestEvent = (
|
| 128 |
+
await receive() # type: ignore[assignment]
|
| 129 |
+
)
|
| 130 |
+
body.write(body_message.get("body", b""))
|
| 131 |
+
more_body = body_message.get("more_body", False)
|
| 132 |
+
body.seek(0)
|
| 133 |
+
environ = build_environ(self.scope, message, body)
|
| 134 |
+
self.loop = asyncio.get_event_loop()
|
| 135 |
+
wsgi = self.loop.run_in_executor(self.executor, self.wsgi, environ, self.start_response)
|
| 136 |
+
sender = self.loop.create_task(self.sender(send))
|
| 137 |
+
try:
|
| 138 |
+
await asyncio.wait_for(wsgi, None)
|
| 139 |
+
finally:
|
| 140 |
+
self.send_queue.append(None)
|
| 141 |
+
self.send_event.set()
|
| 142 |
+
await asyncio.wait_for(sender, None)
|
| 143 |
+
if self.exc_info is not None:
|
| 144 |
+
raise self.exc_info[0].with_traceback(self.exc_info[1], self.exc_info[2])
|
| 145 |
+
|
| 146 |
+
async def sender(self, send: ASGISendCallable) -> None:
|
| 147 |
+
while True:
|
| 148 |
+
if self.send_queue:
|
| 149 |
+
message = self.send_queue.popleft()
|
| 150 |
+
if message is None:
|
| 151 |
+
return
|
| 152 |
+
await send(message)
|
| 153 |
+
else: # pragma: no cover
|
| 154 |
+
await self.send_event.wait()
|
| 155 |
+
self.send_event.clear()
|
| 156 |
+
|
| 157 |
+
def start_response(
|
| 158 |
+
self,
|
| 159 |
+
status: str,
|
| 160 |
+
response_headers: Iterable[tuple[str, str]],
|
| 161 |
+
exc_info: ExcInfo | None = None,
|
| 162 |
+
) -> None:
|
| 163 |
+
self.exc_info = exc_info
|
| 164 |
+
if not self.response_started:
|
| 165 |
+
self.response_started = True
|
| 166 |
+
status_code_str, _ = status.split(" ", 1)
|
| 167 |
+
status_code = int(status_code_str)
|
| 168 |
+
headers = [(name.encode("ascii"), value.encode("ascii")) for name, value in response_headers]
|
| 169 |
+
http_response_start_event: HTTPResponseStartEvent = {
|
| 170 |
+
"type": "http.response.start",
|
| 171 |
+
"status": status_code,
|
| 172 |
+
"headers": headers,
|
| 173 |
+
}
|
| 174 |
+
self.send_queue.append(http_response_start_event)
|
| 175 |
+
self.loop.call_soon_threadsafe(self.send_event.set)
|
| 176 |
+
|
| 177 |
+
def wsgi(self, environ: Environ, start_response: StartResponse) -> None:
|
| 178 |
+
for chunk in self.app(environ, start_response): # type: ignore
|
| 179 |
+
response_body: HTTPResponseBodyEvent = {
|
| 180 |
+
"type": "http.response.body",
|
| 181 |
+
"body": chunk,
|
| 182 |
+
"more_body": True,
|
| 183 |
+
}
|
| 184 |
+
self.send_queue.append(response_body)
|
| 185 |
+
self.loop.call_soon_threadsafe(self.send_event.set)
|
| 186 |
+
|
| 187 |
+
empty_body: HTTPResponseBodyEvent = {
|
| 188 |
+
"type": "http.response.body",
|
| 189 |
+
"body": b"",
|
| 190 |
+
"more_body": False,
|
| 191 |
+
}
|
| 192 |
+
self.send_queue.append(empty_body)
|
| 193 |
+
self.loop.call_soon_threadsafe(self.send_event.set)
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
try:
|
| 197 |
+
from a2wsgi import WSGIMiddleware
|
| 198 |
+
except ModuleNotFoundError: # pragma: no cover
|
| 199 |
+
WSGIMiddleware = _WSGIMiddleware # type: ignore[misc, assignment]
|
.venv/Lib/site-packages/uvicorn/protocols/__init__.py
ADDED
|
File without changes
|
.venv/Lib/site-packages/uvicorn/protocols/http/__init__.py
ADDED
|
File without changes
|
.venv/Lib/site-packages/uvicorn/protocols/http/auto.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
|
| 5 |
+
AutoHTTPProtocol: type[asyncio.Protocol]
|
| 6 |
+
try:
|
| 7 |
+
import httptools # noqa
|
| 8 |
+
except ImportError: # pragma: no cover
|
| 9 |
+
from uvicorn.protocols.http.h11_impl import H11Protocol
|
| 10 |
+
|
| 11 |
+
AutoHTTPProtocol = H11Protocol
|
| 12 |
+
else: # pragma: no cover
|
| 13 |
+
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
| 14 |
+
|
| 15 |
+
AutoHTTPProtocol = HttpToolsProtocol
|
.venv/Lib/site-packages/uvicorn/protocols/http/flow_control.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
|
| 3 |
+
from uvicorn._types import ASGIReceiveCallable, ASGISendCallable, Scope
|
| 4 |
+
|
| 5 |
+
CLOSE_HEADER = (b"connection", b"close")
|
| 6 |
+
|
| 7 |
+
HIGH_WATER_LIMIT = 65536
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class FlowControl:
|
| 11 |
+
def __init__(self, transport: asyncio.Transport) -> None:
|
| 12 |
+
self._transport = transport
|
| 13 |
+
self.read_paused = False
|
| 14 |
+
self.write_paused = False
|
| 15 |
+
self._is_writable_event = asyncio.Event()
|
| 16 |
+
self._is_writable_event.set()
|
| 17 |
+
|
| 18 |
+
async def drain(self) -> None:
|
| 19 |
+
await self._is_writable_event.wait() # pragma: full coverage
|
| 20 |
+
|
| 21 |
+
def pause_reading(self) -> None:
|
| 22 |
+
if not self.read_paused:
|
| 23 |
+
self.read_paused = True
|
| 24 |
+
self._transport.pause_reading()
|
| 25 |
+
|
| 26 |
+
def resume_reading(self) -> None:
|
| 27 |
+
if self.read_paused:
|
| 28 |
+
self.read_paused = False
|
| 29 |
+
self._transport.resume_reading()
|
| 30 |
+
|
| 31 |
+
def pause_writing(self) -> None:
|
| 32 |
+
if not self.write_paused: # pragma: full coverage
|
| 33 |
+
self.write_paused = True
|
| 34 |
+
self._is_writable_event.clear()
|
| 35 |
+
|
| 36 |
+
def resume_writing(self) -> None:
|
| 37 |
+
if self.write_paused: # pragma: full coverage
|
| 38 |
+
self.write_paused = False
|
| 39 |
+
self._is_writable_event.set()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
async def service_unavailable(scope: Scope, receive: ASGIReceiveCallable, send: ASGISendCallable) -> None:
|
| 43 |
+
await send(
|
| 44 |
+
{
|
| 45 |
+
"type": "http.response.start",
|
| 46 |
+
"status": 503,
|
| 47 |
+
"headers": [
|
| 48 |
+
(b"content-type", b"text/plain; charset=utf-8"),
|
| 49 |
+
(b"content-length", b"19"),
|
| 50 |
+
(b"connection", b"close"),
|
| 51 |
+
],
|
| 52 |
+
}
|
| 53 |
+
)
|
| 54 |
+
await send({"type": "http.response.body", "body": b"Service Unavailable", "more_body": False})
|
.venv/Lib/site-packages/uvicorn/protocols/http/h11_impl.py
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import contextvars
|
| 5 |
+
import http
|
| 6 |
+
import logging
|
| 7 |
+
import sys
|
| 8 |
+
from collections.abc import Callable
|
| 9 |
+
from typing import Any, Literal
|
| 10 |
+
from urllib.parse import unquote
|
| 11 |
+
|
| 12 |
+
import h11
|
| 13 |
+
from h11._connection import DEFAULT_MAX_INCOMPLETE_EVENT_SIZE
|
| 14 |
+
|
| 15 |
+
from uvicorn._types import (
|
| 16 |
+
ASGI3Application,
|
| 17 |
+
ASGIReceiveEvent,
|
| 18 |
+
ASGISendEvent,
|
| 19 |
+
HTTPRequestEvent,
|
| 20 |
+
HTTPResponseBodyEvent,
|
| 21 |
+
HTTPResponseStartEvent,
|
| 22 |
+
HTTPScope,
|
| 23 |
+
)
|
| 24 |
+
from uvicorn.config import Config
|
| 25 |
+
from uvicorn.logging import TRACE_LOG_LEVEL
|
| 26 |
+
from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable
|
| 27 |
+
from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl
|
| 28 |
+
from uvicorn.server import ServerState
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _get_status_phrase(status_code: int) -> bytes:
|
| 32 |
+
try:
|
| 33 |
+
return http.HTTPStatus(status_code).phrase.encode()
|
| 34 |
+
except ValueError:
|
| 35 |
+
return b""
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
STATUS_PHRASES = {status_code: _get_status_phrase(status_code) for status_code in range(100, 600)}
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class H11Protocol(asyncio.Protocol):
|
| 42 |
+
def __init__(
|
| 43 |
+
self,
|
| 44 |
+
config: Config,
|
| 45 |
+
server_state: ServerState,
|
| 46 |
+
app_state: dict[str, Any],
|
| 47 |
+
_loop: asyncio.AbstractEventLoop | None = None,
|
| 48 |
+
) -> None:
|
| 49 |
+
if not config.loaded:
|
| 50 |
+
config.load()
|
| 51 |
+
|
| 52 |
+
self.config = config
|
| 53 |
+
self.app = config.loaded_app
|
| 54 |
+
self.loop = _loop or asyncio.get_event_loop()
|
| 55 |
+
self.logger = logging.getLogger("uvicorn.error")
|
| 56 |
+
self.access_logger = logging.getLogger("uvicorn.access")
|
| 57 |
+
self.access_log = self.access_logger.hasHandlers()
|
| 58 |
+
self.conn = h11.Connection(
|
| 59 |
+
h11.SERVER,
|
| 60 |
+
config.h11_max_incomplete_event_size
|
| 61 |
+
if config.h11_max_incomplete_event_size is not None
|
| 62 |
+
else DEFAULT_MAX_INCOMPLETE_EVENT_SIZE,
|
| 63 |
+
)
|
| 64 |
+
self.ws_protocol_class = config.ws_protocol_class
|
| 65 |
+
self.root_path = config.root_path
|
| 66 |
+
self.asgi_version = config.asgi_version
|
| 67 |
+
self.limit_concurrency = config.limit_concurrency
|
| 68 |
+
self.app_state = app_state
|
| 69 |
+
|
| 70 |
+
# Timeouts
|
| 71 |
+
self.timeout_keep_alive_task: asyncio.TimerHandle | None = None
|
| 72 |
+
self.timeout_keep_alive = config.timeout_keep_alive
|
| 73 |
+
|
| 74 |
+
# Shared server state
|
| 75 |
+
self.server_state = server_state
|
| 76 |
+
self.connections = server_state.connections
|
| 77 |
+
self.tasks = server_state.tasks
|
| 78 |
+
|
| 79 |
+
# Per-connection state
|
| 80 |
+
self.transport: asyncio.Transport = None # type: ignore[assignment]
|
| 81 |
+
self.flow: FlowControl = None # type: ignore[assignment]
|
| 82 |
+
self.server: tuple[str, int | None] | None = None
|
| 83 |
+
self.client: tuple[str, int] | None = None
|
| 84 |
+
self.scheme: Literal["http", "https"] | None = None
|
| 85 |
+
|
| 86 |
+
# Per-request state
|
| 87 |
+
self.scope: HTTPScope = None # type: ignore[assignment]
|
| 88 |
+
self.headers: list[tuple[bytes, bytes]] = None # type: ignore[assignment]
|
| 89 |
+
self.cycle: RequestResponseCycle = None # type: ignore[assignment]
|
| 90 |
+
|
| 91 |
+
# Protocol interface
|
| 92 |
+
def connection_made( # type: ignore[override]
|
| 93 |
+
self, transport: asyncio.Transport
|
| 94 |
+
) -> None:
|
| 95 |
+
self.connections.add(self)
|
| 96 |
+
|
| 97 |
+
self.transport = transport
|
| 98 |
+
self.flow = FlowControl(transport)
|
| 99 |
+
self.server = get_local_addr(transport)
|
| 100 |
+
self.client = get_remote_addr(transport)
|
| 101 |
+
self.scheme = "https" if is_ssl(transport) else "http"
|
| 102 |
+
|
| 103 |
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
| 104 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 105 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix)
|
| 106 |
+
|
| 107 |
+
def connection_lost(self, exc: Exception | None) -> None:
|
| 108 |
+
self.connections.discard(self)
|
| 109 |
+
|
| 110 |
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
| 111 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 112 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection lost", prefix)
|
| 113 |
+
|
| 114 |
+
if self.cycle and not self.cycle.response_complete:
|
| 115 |
+
self.cycle.disconnected = True
|
| 116 |
+
if self.conn.our_state != h11.ERROR:
|
| 117 |
+
event = h11.ConnectionClosed()
|
| 118 |
+
try:
|
| 119 |
+
self.conn.send(event)
|
| 120 |
+
except h11.LocalProtocolError:
|
| 121 |
+
# Premature client disconnect
|
| 122 |
+
pass
|
| 123 |
+
|
| 124 |
+
if self.cycle is not None:
|
| 125 |
+
self.cycle.message_event.set()
|
| 126 |
+
if self.flow is not None:
|
| 127 |
+
self.flow.resume_writing()
|
| 128 |
+
if exc is None:
|
| 129 |
+
self.transport.close()
|
| 130 |
+
self._unset_keepalive_if_required()
|
| 131 |
+
|
| 132 |
+
def eof_received(self) -> None:
|
| 133 |
+
pass
|
| 134 |
+
|
| 135 |
+
def _unset_keepalive_if_required(self) -> None:
|
| 136 |
+
if self.timeout_keep_alive_task is not None:
|
| 137 |
+
self.timeout_keep_alive_task.cancel()
|
| 138 |
+
self.timeout_keep_alive_task = None
|
| 139 |
+
|
| 140 |
+
def _get_upgrade(self) -> bytes | None:
|
| 141 |
+
connection = []
|
| 142 |
+
upgrade = None
|
| 143 |
+
for name, value in self.headers:
|
| 144 |
+
if name == b"connection":
|
| 145 |
+
connection = [token.lower().strip() for token in value.split(b",")]
|
| 146 |
+
if name == b"upgrade":
|
| 147 |
+
upgrade = value.lower()
|
| 148 |
+
if b"upgrade" in connection:
|
| 149 |
+
return upgrade
|
| 150 |
+
return None
|
| 151 |
+
|
| 152 |
+
def _should_upgrade_to_ws(self) -> bool:
|
| 153 |
+
if self.ws_protocol_class is None:
|
| 154 |
+
return False
|
| 155 |
+
return True
|
| 156 |
+
|
| 157 |
+
def _unsupported_upgrade_warning(self) -> None:
|
| 158 |
+
msg = "Unsupported upgrade request."
|
| 159 |
+
self.logger.warning(msg)
|
| 160 |
+
if not self._should_upgrade_to_ws():
|
| 161 |
+
msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501
|
| 162 |
+
self.logger.warning(msg)
|
| 163 |
+
|
| 164 |
+
def _should_upgrade(self) -> bool:
|
| 165 |
+
upgrade = self._get_upgrade()
|
| 166 |
+
if upgrade == b"websocket" and self._should_upgrade_to_ws():
|
| 167 |
+
return True
|
| 168 |
+
if upgrade is not None:
|
| 169 |
+
self._unsupported_upgrade_warning()
|
| 170 |
+
return False
|
| 171 |
+
|
| 172 |
+
def data_received(self, data: bytes) -> None:
|
| 173 |
+
self._unset_keepalive_if_required()
|
| 174 |
+
|
| 175 |
+
self.conn.receive_data(data)
|
| 176 |
+
self.handle_events()
|
| 177 |
+
|
| 178 |
+
def handle_events(self) -> None:
|
| 179 |
+
while True:
|
| 180 |
+
try:
|
| 181 |
+
event = self.conn.next_event()
|
| 182 |
+
except h11.RemoteProtocolError:
|
| 183 |
+
msg = "Invalid HTTP request received."
|
| 184 |
+
self.logger.warning(msg)
|
| 185 |
+
self.send_400_response(msg)
|
| 186 |
+
return
|
| 187 |
+
|
| 188 |
+
if event is h11.NEED_DATA:
|
| 189 |
+
break
|
| 190 |
+
|
| 191 |
+
elif event is h11.PAUSED:
|
| 192 |
+
# This case can occur in HTTP pipelining, so we need to
|
| 193 |
+
# stop reading any more data, and ensure that at the end
|
| 194 |
+
# of the active request/response cycle we handle any
|
| 195 |
+
# events that have been buffered up.
|
| 196 |
+
self.flow.pause_reading()
|
| 197 |
+
break
|
| 198 |
+
|
| 199 |
+
elif isinstance(event, h11.Request):
|
| 200 |
+
self.headers = [(key.lower(), value) for key, value in event.headers]
|
| 201 |
+
raw_path, _, query_string = event.target.partition(b"?")
|
| 202 |
+
path = unquote(raw_path.decode("ascii"))
|
| 203 |
+
full_path = self.root_path + path
|
| 204 |
+
full_raw_path = self.root_path.encode("ascii") + raw_path
|
| 205 |
+
self.scope = {
|
| 206 |
+
"type": "http",
|
| 207 |
+
"asgi": {"version": self.asgi_version, "spec_version": "2.3"},
|
| 208 |
+
"http_version": event.http_version.decode("ascii"),
|
| 209 |
+
"server": self.server,
|
| 210 |
+
"client": self.client,
|
| 211 |
+
"scheme": self.scheme, # type: ignore[typeddict-item]
|
| 212 |
+
"method": event.method.decode("ascii"),
|
| 213 |
+
"root_path": self.root_path,
|
| 214 |
+
"path": full_path,
|
| 215 |
+
"raw_path": full_raw_path,
|
| 216 |
+
"query_string": query_string,
|
| 217 |
+
"headers": self.headers,
|
| 218 |
+
"state": self.app_state.copy(),
|
| 219 |
+
}
|
| 220 |
+
if self._should_upgrade():
|
| 221 |
+
self.handle_websocket_upgrade(event)
|
| 222 |
+
return
|
| 223 |
+
|
| 224 |
+
# Handle 503 responses when 'limit_concurrency' is exceeded.
|
| 225 |
+
if self.limit_concurrency is not None and (
|
| 226 |
+
len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency
|
| 227 |
+
):
|
| 228 |
+
app = service_unavailable
|
| 229 |
+
message = "Exceeded concurrency limit."
|
| 230 |
+
self.logger.warning(message)
|
| 231 |
+
else:
|
| 232 |
+
app = self.app
|
| 233 |
+
|
| 234 |
+
# When starting to process a request, disable the keep-alive
|
| 235 |
+
# timeout. Normally we disable this when receiving data from
|
| 236 |
+
# client and set back when finishing processing its request.
|
| 237 |
+
# However, for pipelined requests processing finishes after
|
| 238 |
+
# already receiving the next request and thus the timer may
|
| 239 |
+
# be set here, which we don't want.
|
| 240 |
+
self._unset_keepalive_if_required()
|
| 241 |
+
|
| 242 |
+
self.cycle = RequestResponseCycle(
|
| 243 |
+
scope=self.scope,
|
| 244 |
+
conn=self.conn,
|
| 245 |
+
transport=self.transport,
|
| 246 |
+
flow=self.flow,
|
| 247 |
+
logger=self.logger,
|
| 248 |
+
access_logger=self.access_logger,
|
| 249 |
+
access_log=self.access_log,
|
| 250 |
+
default_headers=self.server_state.default_headers,
|
| 251 |
+
message_event=asyncio.Event(),
|
| 252 |
+
on_response=self.on_response_complete,
|
| 253 |
+
)
|
| 254 |
+
if self.config.reset_contextvars:
|
| 255 |
+
# Opt-in workaround for https://github.com/python/cpython/issues/140947:
|
| 256 |
+
# asyncio can leak context vars between tasks. Hides context set in the
|
| 257 |
+
# lifespan or by external instrumentation.
|
| 258 |
+
if sys.version_info >= (3, 11): # pragma: py-lt-311
|
| 259 |
+
task = self.loop.create_task(self.cycle.run_asgi(app), context=contextvars.Context())
|
| 260 |
+
else: # pragma: py-gte-311
|
| 261 |
+
task = contextvars.Context().run(self.loop.create_task, self.cycle.run_asgi(app))
|
| 262 |
+
else:
|
| 263 |
+
task = self.loop.create_task(self.cycle.run_asgi(app))
|
| 264 |
+
task.add_done_callback(self.tasks.discard)
|
| 265 |
+
self.tasks.add(task)
|
| 266 |
+
|
| 267 |
+
elif isinstance(event, h11.Data):
|
| 268 |
+
if self.conn.our_state is h11.DONE:
|
| 269 |
+
continue
|
| 270 |
+
self.cycle.body += event.data
|
| 271 |
+
if len(self.cycle.body) > HIGH_WATER_LIMIT:
|
| 272 |
+
self.flow.pause_reading()
|
| 273 |
+
self.cycle.message_event.set()
|
| 274 |
+
|
| 275 |
+
elif isinstance(event, h11.EndOfMessage):
|
| 276 |
+
if self.conn.our_state is h11.DONE:
|
| 277 |
+
self.transport.resume_reading()
|
| 278 |
+
self.conn.start_next_cycle()
|
| 279 |
+
continue
|
| 280 |
+
self.cycle.more_body = False
|
| 281 |
+
self.cycle.message_event.set()
|
| 282 |
+
if self.conn.their_state == h11.MUST_CLOSE:
|
| 283 |
+
break
|
| 284 |
+
|
| 285 |
+
def handle_websocket_upgrade(self, event: h11.Request) -> None:
|
| 286 |
+
if self.logger.level <= TRACE_LOG_LEVEL: # pragma: full coverage
|
| 287 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 288 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sUpgrading to WebSocket", prefix)
|
| 289 |
+
|
| 290 |
+
self.connections.discard(self)
|
| 291 |
+
output = [event.method, b" ", event.target, b" HTTP/1.1\r\n"]
|
| 292 |
+
for name, value in self.headers:
|
| 293 |
+
output += [name, b": ", value, b"\r\n"]
|
| 294 |
+
output.append(b"\r\n")
|
| 295 |
+
protocol = self.ws_protocol_class( # type: ignore[call-arg, misc]
|
| 296 |
+
config=self.config,
|
| 297 |
+
server_state=self.server_state,
|
| 298 |
+
app_state=self.app_state,
|
| 299 |
+
)
|
| 300 |
+
protocol.connection_made(self.transport)
|
| 301 |
+
protocol.data_received(b"".join(output))
|
| 302 |
+
self.transport.set_protocol(protocol)
|
| 303 |
+
|
| 304 |
+
def send_400_response(self, msg: str) -> None:
|
| 305 |
+
reason = STATUS_PHRASES[400]
|
| 306 |
+
headers: list[tuple[bytes, bytes]] = [
|
| 307 |
+
(b"content-type", b"text/plain; charset=utf-8"),
|
| 308 |
+
(b"connection", b"close"),
|
| 309 |
+
]
|
| 310 |
+
event = h11.Response(status_code=400, headers=headers, reason=reason)
|
| 311 |
+
output = self.conn.send(event)
|
| 312 |
+
self.transport.write(output)
|
| 313 |
+
|
| 314 |
+
output = self.conn.send(event=h11.Data(data=msg.encode("ascii")))
|
| 315 |
+
self.transport.write(output)
|
| 316 |
+
|
| 317 |
+
output = self.conn.send(event=h11.EndOfMessage())
|
| 318 |
+
self.transport.write(output)
|
| 319 |
+
|
| 320 |
+
self.transport.close()
|
| 321 |
+
|
| 322 |
+
def on_response_complete(self) -> None:
|
| 323 |
+
self.server_state.total_requests += 1
|
| 324 |
+
|
| 325 |
+
if self.transport.is_closing():
|
| 326 |
+
return
|
| 327 |
+
|
| 328 |
+
# Set a short Keep-Alive timeout.
|
| 329 |
+
self._unset_keepalive_if_required()
|
| 330 |
+
|
| 331 |
+
self.timeout_keep_alive_task = self.loop.call_later(self.timeout_keep_alive, self.timeout_keep_alive_handler)
|
| 332 |
+
|
| 333 |
+
# Unpause data reads if needed.
|
| 334 |
+
self.flow.resume_reading()
|
| 335 |
+
|
| 336 |
+
# Unblock any pipelined events.
|
| 337 |
+
if self.conn.our_state is h11.DONE and self.conn.their_state is h11.DONE:
|
| 338 |
+
self.conn.start_next_cycle()
|
| 339 |
+
self.handle_events()
|
| 340 |
+
|
| 341 |
+
def shutdown(self) -> None:
|
| 342 |
+
"""
|
| 343 |
+
Called by the server to commence a graceful shutdown.
|
| 344 |
+
"""
|
| 345 |
+
if self.cycle is None or self.cycle.response_complete:
|
| 346 |
+
event = h11.ConnectionClosed()
|
| 347 |
+
self.conn.send(event)
|
| 348 |
+
self.transport.close()
|
| 349 |
+
else:
|
| 350 |
+
self.cycle.keep_alive = False
|
| 351 |
+
|
| 352 |
+
def pause_writing(self) -> None:
|
| 353 |
+
"""
|
| 354 |
+
Called by the transport when the write buffer exceeds the high water mark.
|
| 355 |
+
"""
|
| 356 |
+
self.flow.pause_writing() # pragma: full coverage
|
| 357 |
+
|
| 358 |
+
def resume_writing(self) -> None:
|
| 359 |
+
"""
|
| 360 |
+
Called by the transport when the write buffer drops below the low water mark.
|
| 361 |
+
"""
|
| 362 |
+
self.flow.resume_writing() # pragma: full coverage
|
| 363 |
+
|
| 364 |
+
def timeout_keep_alive_handler(self) -> None:
|
| 365 |
+
"""
|
| 366 |
+
Called on a keep-alive connection if no new data is received after a short
|
| 367 |
+
delay.
|
| 368 |
+
"""
|
| 369 |
+
if not self.transport.is_closing():
|
| 370 |
+
event = h11.ConnectionClosed()
|
| 371 |
+
self.conn.send(event)
|
| 372 |
+
self.transport.close()
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
class RequestResponseCycle:
|
| 376 |
+
def __init__(
|
| 377 |
+
self,
|
| 378 |
+
scope: HTTPScope,
|
| 379 |
+
conn: h11.Connection,
|
| 380 |
+
transport: asyncio.Transport,
|
| 381 |
+
flow: FlowControl,
|
| 382 |
+
logger: logging.Logger,
|
| 383 |
+
access_logger: logging.Logger,
|
| 384 |
+
access_log: bool,
|
| 385 |
+
default_headers: list[tuple[bytes, bytes]],
|
| 386 |
+
message_event: asyncio.Event,
|
| 387 |
+
on_response: Callable[..., None],
|
| 388 |
+
) -> None:
|
| 389 |
+
self.scope = scope
|
| 390 |
+
self.conn = conn
|
| 391 |
+
self.transport = transport
|
| 392 |
+
self.flow = flow
|
| 393 |
+
self.logger = logger
|
| 394 |
+
self.access_logger = access_logger
|
| 395 |
+
self.access_log = access_log
|
| 396 |
+
self.default_headers = default_headers
|
| 397 |
+
self.message_event = message_event
|
| 398 |
+
self.on_response = on_response
|
| 399 |
+
|
| 400 |
+
# Connection state
|
| 401 |
+
self.disconnected = False
|
| 402 |
+
self.keep_alive = True
|
| 403 |
+
self.waiting_for_100_continue = conn.they_are_waiting_for_100_continue
|
| 404 |
+
|
| 405 |
+
# Request state
|
| 406 |
+
self.body = bytearray()
|
| 407 |
+
self.more_body = True
|
| 408 |
+
|
| 409 |
+
# Response state
|
| 410 |
+
self.response_started = False
|
| 411 |
+
self.response_complete = False
|
| 412 |
+
|
| 413 |
+
# ASGI exception wrapper
|
| 414 |
+
async def run_asgi(self, app: ASGI3Application) -> None:
|
| 415 |
+
try:
|
| 416 |
+
result = await app( # type: ignore[func-returns-value]
|
| 417 |
+
self.scope, self.receive, self.send
|
| 418 |
+
)
|
| 419 |
+
except BaseException as exc:
|
| 420 |
+
msg = "Exception in ASGI application\n"
|
| 421 |
+
self.logger.error(msg, exc_info=exc)
|
| 422 |
+
if not self.response_started:
|
| 423 |
+
await self.send_500_response()
|
| 424 |
+
else:
|
| 425 |
+
self.transport.close()
|
| 426 |
+
else:
|
| 427 |
+
if result is not None:
|
| 428 |
+
msg = "ASGI callable should return None, but returned '%s'."
|
| 429 |
+
self.logger.error(msg, result)
|
| 430 |
+
self.transport.close()
|
| 431 |
+
elif not self.response_started and not self.disconnected:
|
| 432 |
+
msg = "ASGI callable returned without starting response."
|
| 433 |
+
self.logger.error(msg)
|
| 434 |
+
await self.send_500_response()
|
| 435 |
+
elif not self.response_complete and not self.disconnected:
|
| 436 |
+
msg = "ASGI callable returned without completing response."
|
| 437 |
+
self.logger.error(msg)
|
| 438 |
+
self.transport.close()
|
| 439 |
+
finally:
|
| 440 |
+
self.on_response = lambda: None
|
| 441 |
+
|
| 442 |
+
async def send_500_response(self) -> None:
|
| 443 |
+
response_start_event: HTTPResponseStartEvent = {
|
| 444 |
+
"type": "http.response.start",
|
| 445 |
+
"status": 500,
|
| 446 |
+
"headers": [
|
| 447 |
+
(b"content-type", b"text/plain; charset=utf-8"),
|
| 448 |
+
(b"connection", b"close"),
|
| 449 |
+
],
|
| 450 |
+
}
|
| 451 |
+
await self.send(response_start_event)
|
| 452 |
+
response_body_event: HTTPResponseBodyEvent = {
|
| 453 |
+
"type": "http.response.body",
|
| 454 |
+
"body": b"Internal Server Error",
|
| 455 |
+
"more_body": False,
|
| 456 |
+
}
|
| 457 |
+
await self.send(response_body_event)
|
| 458 |
+
|
| 459 |
+
# ASGI interface
|
| 460 |
+
async def send(self, message: ASGISendEvent) -> None:
|
| 461 |
+
if self.flow.write_paused and not self.disconnected:
|
| 462 |
+
await self.flow.drain() # pragma: full coverage
|
| 463 |
+
|
| 464 |
+
if self.disconnected:
|
| 465 |
+
return # pragma: full coverage
|
| 466 |
+
|
| 467 |
+
if not self.response_started:
|
| 468 |
+
# Sending response status line and headers
|
| 469 |
+
if message["type"] != "http.response.start":
|
| 470 |
+
raise RuntimeError(f"Expected ASGI message 'http.response.start', but got '{message['type']}'.")
|
| 471 |
+
|
| 472 |
+
self.response_started = True
|
| 473 |
+
self.waiting_for_100_continue = False
|
| 474 |
+
|
| 475 |
+
status = message["status"]
|
| 476 |
+
headers = self.default_headers + list(message.get("headers", []))
|
| 477 |
+
|
| 478 |
+
if CLOSE_HEADER in self.scope["headers"] and CLOSE_HEADER not in headers:
|
| 479 |
+
headers = headers + [CLOSE_HEADER]
|
| 480 |
+
|
| 481 |
+
if self.access_log:
|
| 482 |
+
self.access_logger.info(
|
| 483 |
+
'%s - "%s %s HTTP/%s" %d',
|
| 484 |
+
get_client_addr(self.scope),
|
| 485 |
+
self.scope["method"],
|
| 486 |
+
get_path_with_query_string(self.scope),
|
| 487 |
+
self.scope["http_version"],
|
| 488 |
+
status,
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
# Write response status line and headers
|
| 492 |
+
reason = STATUS_PHRASES[status]
|
| 493 |
+
response = h11.Response(status_code=status, headers=headers, reason=reason)
|
| 494 |
+
output = self.conn.send(event=response)
|
| 495 |
+
self.transport.write(output)
|
| 496 |
+
|
| 497 |
+
elif not self.response_complete:
|
| 498 |
+
# Sending response body
|
| 499 |
+
if message["type"] != "http.response.body":
|
| 500 |
+
raise RuntimeError(f"Expected ASGI message 'http.response.body', but got '{message['type']}'.")
|
| 501 |
+
|
| 502 |
+
body = message.get("body", b"")
|
| 503 |
+
more_body = message.get("more_body", False)
|
| 504 |
+
|
| 505 |
+
# Write response body
|
| 506 |
+
data = b"" if self.scope["method"] == "HEAD" else body
|
| 507 |
+
output = self.conn.send(event=h11.Data(data=data))
|
| 508 |
+
self.transport.write(output)
|
| 509 |
+
|
| 510 |
+
# Handle response completion
|
| 511 |
+
if not more_body:
|
| 512 |
+
self.response_complete = True
|
| 513 |
+
self.message_event.set()
|
| 514 |
+
output = self.conn.send(event=h11.EndOfMessage())
|
| 515 |
+
self.transport.write(output)
|
| 516 |
+
|
| 517 |
+
else:
|
| 518 |
+
# Response already sent
|
| 519 |
+
raise RuntimeError(f"Unexpected ASGI message '{message['type']}' sent, after response already completed.")
|
| 520 |
+
|
| 521 |
+
if self.response_complete:
|
| 522 |
+
if self.conn.our_state is h11.MUST_CLOSE or not self.keep_alive:
|
| 523 |
+
self.conn.send(event=h11.ConnectionClosed())
|
| 524 |
+
self.transport.close()
|
| 525 |
+
self.on_response()
|
| 526 |
+
|
| 527 |
+
async def receive(self) -> ASGIReceiveEvent:
|
| 528 |
+
if self.waiting_for_100_continue and not self.transport.is_closing():
|
| 529 |
+
headers: list[tuple[str, str]] = []
|
| 530 |
+
event = h11.InformationalResponse(status_code=100, headers=headers, reason="Continue")
|
| 531 |
+
output = self.conn.send(event=event)
|
| 532 |
+
self.transport.write(output)
|
| 533 |
+
self.waiting_for_100_continue = False
|
| 534 |
+
|
| 535 |
+
if not self.disconnected and not self.response_complete:
|
| 536 |
+
self.flow.resume_reading()
|
| 537 |
+
await self.message_event.wait()
|
| 538 |
+
self.message_event.clear()
|
| 539 |
+
|
| 540 |
+
if self.disconnected or self.response_complete:
|
| 541 |
+
return {"type": "http.disconnect"}
|
| 542 |
+
|
| 543 |
+
message: HTTPRequestEvent = {"type": "http.request", "body": bytes(self.body), "more_body": self.more_body}
|
| 544 |
+
self.body = bytearray()
|
| 545 |
+
return message
|
.venv/Lib/site-packages/uvicorn/protocols/http/httptools_impl.py
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import contextvars
|
| 5 |
+
import http
|
| 6 |
+
import logging
|
| 7 |
+
import re
|
| 8 |
+
import sys
|
| 9 |
+
import urllib
|
| 10 |
+
from asyncio.events import TimerHandle
|
| 11 |
+
from collections import deque
|
| 12 |
+
from collections.abc import Callable
|
| 13 |
+
from typing import Any, Literal
|
| 14 |
+
|
| 15 |
+
import httptools
|
| 16 |
+
|
| 17 |
+
from uvicorn._types import (
|
| 18 |
+
ASGI3Application,
|
| 19 |
+
ASGIReceiveEvent,
|
| 20 |
+
ASGISendEvent,
|
| 21 |
+
HTTPRequestEvent,
|
| 22 |
+
HTTPScope,
|
| 23 |
+
)
|
| 24 |
+
from uvicorn.config import Config
|
| 25 |
+
from uvicorn.logging import TRACE_LOG_LEVEL
|
| 26 |
+
from uvicorn.protocols.http.flow_control import CLOSE_HEADER, HIGH_WATER_LIMIT, FlowControl, service_unavailable
|
| 27 |
+
from uvicorn.protocols.utils import get_client_addr, get_local_addr, get_path_with_query_string, get_remote_addr, is_ssl
|
| 28 |
+
from uvicorn.server import ServerState
|
| 29 |
+
|
| 30 |
+
HEADER_RE = re.compile(b'[\x00-\x1f\x7f()<>@,;:\\[\\]={} \t\\\\"]')
|
| 31 |
+
HEADER_VALUE_RE = re.compile(b"[\x00-\x08\x0a-\x1f\x7f]")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _get_status_line(status_code: int) -> bytes:
|
| 35 |
+
try:
|
| 36 |
+
phrase = http.HTTPStatus(status_code).phrase.encode()
|
| 37 |
+
except ValueError:
|
| 38 |
+
phrase = b""
|
| 39 |
+
return b"".join([b"HTTP/1.1 ", str(status_code).encode(), b" ", phrase, b"\r\n"])
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
STATUS_LINE = {status_code: _get_status_line(status_code) for status_code in range(100, 600)}
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class HttpToolsProtocol(asyncio.Protocol):
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
config: Config,
|
| 49 |
+
server_state: ServerState,
|
| 50 |
+
app_state: dict[str, Any],
|
| 51 |
+
_loop: asyncio.AbstractEventLoop | None = None,
|
| 52 |
+
) -> None:
|
| 53 |
+
if not config.loaded:
|
| 54 |
+
config.load()
|
| 55 |
+
|
| 56 |
+
self.config = config
|
| 57 |
+
self.app = config.loaded_app
|
| 58 |
+
self.loop = _loop or asyncio.get_event_loop()
|
| 59 |
+
self.logger = logging.getLogger("uvicorn.error")
|
| 60 |
+
self.access_logger = logging.getLogger("uvicorn.access")
|
| 61 |
+
self.access_log = self.access_logger.hasHandlers()
|
| 62 |
+
self.parser = httptools.HttpRequestParser(self)
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
# Enable dangerous leniencies to allow server to a response on the first request from a pipelined request.
|
| 66 |
+
self.parser.set_dangerous_leniencies(lenient_data_after_close=True)
|
| 67 |
+
except AttributeError: # pragma: no cover
|
| 68 |
+
# httptools < 0.6.3
|
| 69 |
+
pass
|
| 70 |
+
|
| 71 |
+
self.ws_protocol_class = config.ws_protocol_class
|
| 72 |
+
self.root_path = config.root_path
|
| 73 |
+
self.asgi_version = config.asgi_version
|
| 74 |
+
self.limit_concurrency = config.limit_concurrency
|
| 75 |
+
self.app_state = app_state
|
| 76 |
+
|
| 77 |
+
# Timeouts
|
| 78 |
+
self.timeout_keep_alive_task: TimerHandle | None = None
|
| 79 |
+
self.timeout_keep_alive = config.timeout_keep_alive
|
| 80 |
+
|
| 81 |
+
# Global state
|
| 82 |
+
self.server_state = server_state
|
| 83 |
+
self.connections = server_state.connections
|
| 84 |
+
self.tasks = server_state.tasks
|
| 85 |
+
|
| 86 |
+
# Per-connection state
|
| 87 |
+
self.transport: asyncio.Transport = None # type: ignore[assignment]
|
| 88 |
+
self.flow: FlowControl = None # type: ignore[assignment]
|
| 89 |
+
self.server: tuple[str, int | None] | None = None
|
| 90 |
+
self.client: tuple[str, int] | None = None
|
| 91 |
+
self.scheme: Literal["http", "https"] | None = None
|
| 92 |
+
self.pipeline: deque[tuple[RequestResponseCycle, ASGI3Application]] = deque()
|
| 93 |
+
|
| 94 |
+
# Per-request state
|
| 95 |
+
self.scope: HTTPScope = None # type: ignore[assignment]
|
| 96 |
+
self.headers: list[tuple[bytes, bytes]] = None # type: ignore[assignment]
|
| 97 |
+
self.expect_100_continue = False
|
| 98 |
+
self.cycle: RequestResponseCycle = None # type: ignore[assignment]
|
| 99 |
+
|
| 100 |
+
# Protocol interface
|
| 101 |
+
def connection_made( # type: ignore[override]
|
| 102 |
+
self, transport: asyncio.Transport
|
| 103 |
+
) -> None:
|
| 104 |
+
self.connections.add(self)
|
| 105 |
+
|
| 106 |
+
self.transport = transport
|
| 107 |
+
self.flow = FlowControl(transport)
|
| 108 |
+
self.server = get_local_addr(transport)
|
| 109 |
+
self.client = get_remote_addr(transport)
|
| 110 |
+
self.scheme = "https" if is_ssl(transport) else "http"
|
| 111 |
+
|
| 112 |
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
| 113 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 114 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection made", prefix)
|
| 115 |
+
|
| 116 |
+
def connection_lost(self, exc: Exception | None) -> None:
|
| 117 |
+
self.connections.discard(self)
|
| 118 |
+
|
| 119 |
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
| 120 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 121 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sHTTP connection lost", prefix)
|
| 122 |
+
|
| 123 |
+
if self.cycle and not self.cycle.response_complete:
|
| 124 |
+
self.cycle.disconnected = True
|
| 125 |
+
if self.cycle is not None:
|
| 126 |
+
self.cycle.message_event.set()
|
| 127 |
+
if self.flow is not None:
|
| 128 |
+
self.flow.resume_writing()
|
| 129 |
+
if exc is None:
|
| 130 |
+
self.transport.close()
|
| 131 |
+
self._unset_keepalive_if_required()
|
| 132 |
+
|
| 133 |
+
self.parser = None # type: ignore[assignment]
|
| 134 |
+
|
| 135 |
+
def eof_received(self) -> None:
|
| 136 |
+
pass
|
| 137 |
+
|
| 138 |
+
def _unset_keepalive_if_required(self) -> None:
|
| 139 |
+
if self.timeout_keep_alive_task is not None:
|
| 140 |
+
self.timeout_keep_alive_task.cancel()
|
| 141 |
+
self.timeout_keep_alive_task = None
|
| 142 |
+
|
| 143 |
+
def _get_upgrade(self) -> bytes | None:
|
| 144 |
+
connection = []
|
| 145 |
+
upgrade = None
|
| 146 |
+
for name, value in self.headers:
|
| 147 |
+
if name == b"connection":
|
| 148 |
+
connection = [token.lower().strip() for token in value.split(b",")]
|
| 149 |
+
if name == b"upgrade":
|
| 150 |
+
upgrade = value.lower()
|
| 151 |
+
if b"upgrade" in connection:
|
| 152 |
+
return upgrade
|
| 153 |
+
return None # pragma: full coverage
|
| 154 |
+
|
| 155 |
+
def _should_upgrade_to_ws(self) -> bool:
|
| 156 |
+
if self.ws_protocol_class is None:
|
| 157 |
+
return False
|
| 158 |
+
return True
|
| 159 |
+
|
| 160 |
+
def _unsupported_upgrade_warning(self) -> None:
|
| 161 |
+
self.logger.warning("Unsupported upgrade request.")
|
| 162 |
+
if not self._should_upgrade_to_ws():
|
| 163 |
+
msg = "No supported WebSocket library detected. Please use \"pip install 'uvicorn[standard]'\", or install 'websockets' or 'wsproto' manually." # noqa: E501
|
| 164 |
+
self.logger.warning(msg)
|
| 165 |
+
|
| 166 |
+
def _should_upgrade(self) -> bool:
|
| 167 |
+
upgrade = self._get_upgrade()
|
| 168 |
+
return upgrade == b"websocket" and self._should_upgrade_to_ws()
|
| 169 |
+
|
| 170 |
+
def data_received(self, data: bytes) -> None:
|
| 171 |
+
self._unset_keepalive_if_required()
|
| 172 |
+
|
| 173 |
+
try:
|
| 174 |
+
self.parser.feed_data(data)
|
| 175 |
+
except httptools.HttpParserError:
|
| 176 |
+
msg = "Invalid HTTP request received."
|
| 177 |
+
self.logger.warning(msg)
|
| 178 |
+
self.send_400_response(msg)
|
| 179 |
+
return
|
| 180 |
+
except httptools.HttpParserUpgrade:
|
| 181 |
+
if self._should_upgrade():
|
| 182 |
+
self.handle_websocket_upgrade()
|
| 183 |
+
else:
|
| 184 |
+
self._unsupported_upgrade_warning()
|
| 185 |
+
|
| 186 |
+
def handle_websocket_upgrade(self) -> None:
|
| 187 |
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
| 188 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 189 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sUpgrading to WebSocket", prefix)
|
| 190 |
+
|
| 191 |
+
self.connections.discard(self)
|
| 192 |
+
method = self.scope["method"].encode()
|
| 193 |
+
output = [method, b" ", self.url, b" HTTP/1.1\r\n"]
|
| 194 |
+
for name, value in self.scope["headers"]:
|
| 195 |
+
output += [name, b": ", value, b"\r\n"]
|
| 196 |
+
output.append(b"\r\n")
|
| 197 |
+
protocol = self.ws_protocol_class( # type: ignore[call-arg, misc]
|
| 198 |
+
config=self.config,
|
| 199 |
+
server_state=self.server_state,
|
| 200 |
+
app_state=self.app_state,
|
| 201 |
+
)
|
| 202 |
+
protocol.connection_made(self.transport)
|
| 203 |
+
protocol.data_received(b"".join(output))
|
| 204 |
+
self.transport.set_protocol(protocol)
|
| 205 |
+
|
| 206 |
+
def send_400_response(self, msg: str) -> None:
|
| 207 |
+
content = [STATUS_LINE[400]]
|
| 208 |
+
for name, value in self.server_state.default_headers:
|
| 209 |
+
content.extend([name, b": ", value, b"\r\n"]) # pragma: full coverage
|
| 210 |
+
content.extend(
|
| 211 |
+
[
|
| 212 |
+
b"content-type: text/plain; charset=utf-8\r\n",
|
| 213 |
+
b"content-length: " + str(len(msg)).encode("ascii") + b"\r\n",
|
| 214 |
+
b"connection: close\r\n",
|
| 215 |
+
b"\r\n",
|
| 216 |
+
msg.encode("ascii"),
|
| 217 |
+
]
|
| 218 |
+
)
|
| 219 |
+
self.transport.write(b"".join(content))
|
| 220 |
+
self.transport.close()
|
| 221 |
+
|
| 222 |
+
def on_message_begin(self) -> None:
|
| 223 |
+
self.url = b""
|
| 224 |
+
self.expect_100_continue = False
|
| 225 |
+
self.headers = []
|
| 226 |
+
self.scope = { # type: ignore[typeddict-item]
|
| 227 |
+
"type": "http",
|
| 228 |
+
"asgi": {"version": self.asgi_version, "spec_version": "2.3"},
|
| 229 |
+
"http_version": "1.1",
|
| 230 |
+
"server": self.server,
|
| 231 |
+
"client": self.client,
|
| 232 |
+
"scheme": self.scheme, # type: ignore[typeddict-item]
|
| 233 |
+
"root_path": self.root_path,
|
| 234 |
+
"headers": self.headers,
|
| 235 |
+
"state": self.app_state.copy(),
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
# Parser callbacks
|
| 239 |
+
def on_url(self, url: bytes) -> None:
|
| 240 |
+
self.url += url
|
| 241 |
+
|
| 242 |
+
def on_header(self, name: bytes, value: bytes) -> None:
|
| 243 |
+
name = name.lower()
|
| 244 |
+
if name == b"expect" and value.lower() == b"100-continue":
|
| 245 |
+
self.expect_100_continue = True
|
| 246 |
+
self.headers.append((name, value))
|
| 247 |
+
|
| 248 |
+
def on_headers_complete(self) -> None:
|
| 249 |
+
http_version = self.parser.get_http_version()
|
| 250 |
+
method = self.parser.get_method()
|
| 251 |
+
self.scope["method"] = method.decode("ascii")
|
| 252 |
+
if http_version != "1.1":
|
| 253 |
+
self.scope["http_version"] = http_version
|
| 254 |
+
if self.parser.should_upgrade() and self._should_upgrade():
|
| 255 |
+
return
|
| 256 |
+
parsed_url = httptools.parse_url(self.url)
|
| 257 |
+
raw_path = parsed_url.path
|
| 258 |
+
path = raw_path.decode("ascii")
|
| 259 |
+
if "%" in path:
|
| 260 |
+
path = urllib.parse.unquote(path)
|
| 261 |
+
full_path = self.root_path + path
|
| 262 |
+
full_raw_path = self.root_path.encode("ascii") + raw_path
|
| 263 |
+
self.scope["path"] = full_path
|
| 264 |
+
self.scope["raw_path"] = full_raw_path
|
| 265 |
+
self.scope["query_string"] = parsed_url.query or b""
|
| 266 |
+
|
| 267 |
+
# Handle 503 responses when 'limit_concurrency' is exceeded.
|
| 268 |
+
if self.limit_concurrency is not None and (
|
| 269 |
+
len(self.connections) >= self.limit_concurrency or len(self.tasks) >= self.limit_concurrency
|
| 270 |
+
):
|
| 271 |
+
app = service_unavailable
|
| 272 |
+
message = "Exceeded concurrency limit."
|
| 273 |
+
self.logger.warning(message)
|
| 274 |
+
else:
|
| 275 |
+
app = self.app
|
| 276 |
+
|
| 277 |
+
existing_cycle = self.cycle
|
| 278 |
+
self.cycle = RequestResponseCycle(
|
| 279 |
+
scope=self.scope,
|
| 280 |
+
transport=self.transport,
|
| 281 |
+
flow=self.flow,
|
| 282 |
+
logger=self.logger,
|
| 283 |
+
access_logger=self.access_logger,
|
| 284 |
+
access_log=self.access_log,
|
| 285 |
+
default_headers=self.server_state.default_headers,
|
| 286 |
+
message_event=asyncio.Event(),
|
| 287 |
+
expect_100_continue=self.expect_100_continue,
|
| 288 |
+
keep_alive=http_version != "1.0",
|
| 289 |
+
on_response=self.on_response_complete,
|
| 290 |
+
)
|
| 291 |
+
if existing_cycle is None or existing_cycle.response_complete:
|
| 292 |
+
# Standard case - start processing the request.
|
| 293 |
+
self._start_asgi_task(self.cycle, app)
|
| 294 |
+
else:
|
| 295 |
+
# Pipelined HTTP requests need to be queued up.
|
| 296 |
+
self.flow.pause_reading()
|
| 297 |
+
self.pipeline.appendleft((self.cycle, app))
|
| 298 |
+
|
| 299 |
+
def _start_asgi_task(self, cycle: RequestResponseCycle, app: ASGI3Application) -> None:
|
| 300 |
+
if self.config.reset_contextvars:
|
| 301 |
+
# Opt-in workaround for https://github.com/python/cpython/issues/140947:
|
| 302 |
+
# asyncio can leak context vars between tasks. Hides context set in the
|
| 303 |
+
# lifespan or by external instrumentation.
|
| 304 |
+
if sys.version_info >= (3, 11): # pragma: py-lt-311
|
| 305 |
+
task = self.loop.create_task(cycle.run_asgi(app), context=contextvars.Context())
|
| 306 |
+
else: # pragma: py-gte-311
|
| 307 |
+
task = contextvars.Context().run(self.loop.create_task, cycle.run_asgi(app))
|
| 308 |
+
else:
|
| 309 |
+
task = self.loop.create_task(cycle.run_asgi(app))
|
| 310 |
+
task.add_done_callback(self.tasks.discard)
|
| 311 |
+
self.tasks.add(task)
|
| 312 |
+
|
| 313 |
+
def on_body(self, body: bytes) -> None:
|
| 314 |
+
if (self.parser.should_upgrade() and self._should_upgrade()) or self.cycle.response_complete:
|
| 315 |
+
return
|
| 316 |
+
self.cycle.body += body
|
| 317 |
+
if len(self.cycle.body) > HIGH_WATER_LIMIT:
|
| 318 |
+
self.flow.pause_reading()
|
| 319 |
+
self.cycle.message_event.set()
|
| 320 |
+
|
| 321 |
+
def on_message_complete(self) -> None:
|
| 322 |
+
if (self.parser.should_upgrade() and self._should_upgrade()) or self.cycle.response_complete:
|
| 323 |
+
return
|
| 324 |
+
self.cycle.more_body = False
|
| 325 |
+
self.cycle.message_event.set()
|
| 326 |
+
|
| 327 |
+
def on_response_complete(self) -> None:
|
| 328 |
+
# Callback for pipelined HTTP requests to be started.
|
| 329 |
+
self.server_state.total_requests += 1
|
| 330 |
+
|
| 331 |
+
if self.transport.is_closing():
|
| 332 |
+
return
|
| 333 |
+
|
| 334 |
+
self._unset_keepalive_if_required()
|
| 335 |
+
|
| 336 |
+
# Unpause data reads if needed.
|
| 337 |
+
self.flow.resume_reading()
|
| 338 |
+
|
| 339 |
+
# Unblock any pipelined events. If there are none, arm the
|
| 340 |
+
# Keep-Alive timeout instead.
|
| 341 |
+
if self.pipeline:
|
| 342 |
+
cycle, app = self.pipeline.pop()
|
| 343 |
+
self._start_asgi_task(cycle, app)
|
| 344 |
+
else:
|
| 345 |
+
self.timeout_keep_alive_task = self.loop.call_later(
|
| 346 |
+
self.timeout_keep_alive, self.timeout_keep_alive_handler
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
def shutdown(self) -> None:
|
| 350 |
+
"""
|
| 351 |
+
Called by the server to commence a graceful shutdown.
|
| 352 |
+
"""
|
| 353 |
+
if self.cycle is None or self.cycle.response_complete:
|
| 354 |
+
self.transport.close()
|
| 355 |
+
else:
|
| 356 |
+
self.cycle.keep_alive = False
|
| 357 |
+
|
| 358 |
+
def pause_writing(self) -> None:
|
| 359 |
+
"""
|
| 360 |
+
Called by the transport when the write buffer exceeds the high water mark.
|
| 361 |
+
"""
|
| 362 |
+
self.flow.pause_writing() # pragma: full coverage
|
| 363 |
+
|
| 364 |
+
def resume_writing(self) -> None:
|
| 365 |
+
"""
|
| 366 |
+
Called by the transport when the write buffer drops below the low water mark.
|
| 367 |
+
"""
|
| 368 |
+
self.flow.resume_writing() # pragma: full coverage
|
| 369 |
+
|
| 370 |
+
def timeout_keep_alive_handler(self) -> None:
|
| 371 |
+
"""
|
| 372 |
+
Called on a keep-alive connection if no new data is received after a short
|
| 373 |
+
delay.
|
| 374 |
+
"""
|
| 375 |
+
if not self.transport.is_closing():
|
| 376 |
+
self.transport.close()
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
class RequestResponseCycle:
|
| 380 |
+
def __init__(
|
| 381 |
+
self,
|
| 382 |
+
scope: HTTPScope,
|
| 383 |
+
transport: asyncio.Transport,
|
| 384 |
+
flow: FlowControl,
|
| 385 |
+
logger: logging.Logger,
|
| 386 |
+
access_logger: logging.Logger,
|
| 387 |
+
access_log: bool,
|
| 388 |
+
default_headers: list[tuple[bytes, bytes]],
|
| 389 |
+
message_event: asyncio.Event,
|
| 390 |
+
expect_100_continue: bool,
|
| 391 |
+
keep_alive: bool,
|
| 392 |
+
on_response: Callable[..., None],
|
| 393 |
+
):
|
| 394 |
+
self.scope = scope
|
| 395 |
+
self.transport = transport
|
| 396 |
+
self.flow = flow
|
| 397 |
+
self.logger = logger
|
| 398 |
+
self.access_logger = access_logger
|
| 399 |
+
self.access_log = access_log
|
| 400 |
+
self.default_headers = default_headers
|
| 401 |
+
self.message_event = message_event
|
| 402 |
+
self.on_response = on_response
|
| 403 |
+
|
| 404 |
+
# Connection state
|
| 405 |
+
self.disconnected = False
|
| 406 |
+
self.keep_alive = keep_alive
|
| 407 |
+
self.waiting_for_100_continue = expect_100_continue
|
| 408 |
+
|
| 409 |
+
# Request state
|
| 410 |
+
self.body = bytearray()
|
| 411 |
+
self.more_body = True
|
| 412 |
+
|
| 413 |
+
# Response state
|
| 414 |
+
self.response_started = False
|
| 415 |
+
self.response_complete = False
|
| 416 |
+
self.chunked_encoding: bool | None = None
|
| 417 |
+
self.expected_content_length = 0
|
| 418 |
+
|
| 419 |
+
# ASGI exception wrapper
|
| 420 |
+
async def run_asgi(self, app: ASGI3Application) -> None:
|
| 421 |
+
try:
|
| 422 |
+
result = await app( # type: ignore[func-returns-value]
|
| 423 |
+
self.scope, self.receive, self.send
|
| 424 |
+
)
|
| 425 |
+
except BaseException as exc:
|
| 426 |
+
msg = "Exception in ASGI application\n"
|
| 427 |
+
self.logger.error(msg, exc_info=exc)
|
| 428 |
+
if not self.response_started:
|
| 429 |
+
await self.send_500_response()
|
| 430 |
+
else:
|
| 431 |
+
self.transport.close()
|
| 432 |
+
else:
|
| 433 |
+
if result is not None:
|
| 434 |
+
msg = "ASGI callable should return None, but returned '%s'."
|
| 435 |
+
self.logger.error(msg, result)
|
| 436 |
+
self.transport.close()
|
| 437 |
+
elif not self.response_started and not self.disconnected:
|
| 438 |
+
msg = "ASGI callable returned without starting response."
|
| 439 |
+
self.logger.error(msg)
|
| 440 |
+
await self.send_500_response()
|
| 441 |
+
elif not self.response_complete and not self.disconnected:
|
| 442 |
+
msg = "ASGI callable returned without completing response."
|
| 443 |
+
self.logger.error(msg)
|
| 444 |
+
self.transport.close()
|
| 445 |
+
finally:
|
| 446 |
+
self.on_response = lambda: None
|
| 447 |
+
|
| 448 |
+
async def send_500_response(self) -> None:
|
| 449 |
+
await self.send(
|
| 450 |
+
{
|
| 451 |
+
"type": "http.response.start",
|
| 452 |
+
"status": 500,
|
| 453 |
+
"headers": [
|
| 454 |
+
(b"content-type", b"text/plain; charset=utf-8"),
|
| 455 |
+
(b"content-length", b"21"),
|
| 456 |
+
(b"connection", b"close"),
|
| 457 |
+
],
|
| 458 |
+
}
|
| 459 |
+
)
|
| 460 |
+
await self.send({"type": "http.response.body", "body": b"Internal Server Error", "more_body": False})
|
| 461 |
+
|
| 462 |
+
# ASGI interface
|
| 463 |
+
async def send(self, message: ASGISendEvent) -> None:
|
| 464 |
+
if self.flow.write_paused and not self.disconnected:
|
| 465 |
+
await self.flow.drain() # pragma: full coverage
|
| 466 |
+
|
| 467 |
+
if self.disconnected:
|
| 468 |
+
return # pragma: full coverage
|
| 469 |
+
|
| 470 |
+
if not self.response_started:
|
| 471 |
+
# Sending response status line and headers
|
| 472 |
+
if message["type"] != "http.response.start":
|
| 473 |
+
raise RuntimeError(f"Expected ASGI message 'http.response.start', but got '{message['type']}'.")
|
| 474 |
+
|
| 475 |
+
self.response_started = True
|
| 476 |
+
self.waiting_for_100_continue = False
|
| 477 |
+
|
| 478 |
+
status_code = message["status"]
|
| 479 |
+
headers = self.default_headers + list(message.get("headers", []))
|
| 480 |
+
|
| 481 |
+
if CLOSE_HEADER in self.scope["headers"] and CLOSE_HEADER not in headers:
|
| 482 |
+
headers = headers + [CLOSE_HEADER]
|
| 483 |
+
|
| 484 |
+
if self.access_log:
|
| 485 |
+
self.access_logger.info(
|
| 486 |
+
'%s - "%s %s HTTP/%s" %d',
|
| 487 |
+
get_client_addr(self.scope),
|
| 488 |
+
self.scope["method"],
|
| 489 |
+
get_path_with_query_string(self.scope),
|
| 490 |
+
self.scope["http_version"],
|
| 491 |
+
status_code,
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
# Write response status line and headers
|
| 495 |
+
content = [STATUS_LINE[status_code]]
|
| 496 |
+
|
| 497 |
+
for name, value in headers:
|
| 498 |
+
if HEADER_RE.search(name):
|
| 499 |
+
raise RuntimeError("Invalid HTTP header name.") # pragma: full coverage
|
| 500 |
+
if HEADER_VALUE_RE.search(value):
|
| 501 |
+
raise RuntimeError("Invalid HTTP header value.")
|
| 502 |
+
|
| 503 |
+
name = name.lower()
|
| 504 |
+
if name == b"content-length" and self.chunked_encoding is None:
|
| 505 |
+
self.expected_content_length = int(value.decode())
|
| 506 |
+
self.chunked_encoding = False
|
| 507 |
+
elif name == b"transfer-encoding" and value.lower() == b"chunked":
|
| 508 |
+
self.expected_content_length = 0
|
| 509 |
+
self.chunked_encoding = True
|
| 510 |
+
elif name == b"connection" and value.lower() == b"close":
|
| 511 |
+
self.keep_alive = False
|
| 512 |
+
content.extend([name, b": ", value, b"\r\n"])
|
| 513 |
+
|
| 514 |
+
if self.chunked_encoding is None and self.scope["method"] != "HEAD" and status_code not in (204, 304):
|
| 515 |
+
# Neither content-length nor transfer-encoding specified
|
| 516 |
+
self.chunked_encoding = True
|
| 517 |
+
content.append(b"transfer-encoding: chunked\r\n")
|
| 518 |
+
|
| 519 |
+
content.append(b"\r\n")
|
| 520 |
+
self.transport.write(b"".join(content))
|
| 521 |
+
|
| 522 |
+
elif not self.response_complete:
|
| 523 |
+
# Sending response body
|
| 524 |
+
if message["type"] != "http.response.body":
|
| 525 |
+
raise RuntimeError(f"Expected ASGI message 'http.response.body', but got '{message['type']}'.")
|
| 526 |
+
|
| 527 |
+
body = message.get("body", b"")
|
| 528 |
+
more_body = message.get("more_body", False)
|
| 529 |
+
|
| 530 |
+
# Write response body
|
| 531 |
+
if self.scope["method"] == "HEAD":
|
| 532 |
+
self.expected_content_length = 0
|
| 533 |
+
elif self.chunked_encoding:
|
| 534 |
+
if body:
|
| 535 |
+
content = [b"%x\r\n" % len(body), body, b"\r\n"]
|
| 536 |
+
else:
|
| 537 |
+
content = []
|
| 538 |
+
if not more_body:
|
| 539 |
+
content.append(b"0\r\n\r\n")
|
| 540 |
+
self.transport.write(b"".join(content))
|
| 541 |
+
else:
|
| 542 |
+
num_bytes = len(body)
|
| 543 |
+
if num_bytes > self.expected_content_length:
|
| 544 |
+
raise RuntimeError("Response content longer than Content-Length")
|
| 545 |
+
else:
|
| 546 |
+
self.expected_content_length -= num_bytes
|
| 547 |
+
self.transport.write(body)
|
| 548 |
+
|
| 549 |
+
# Handle response completion
|
| 550 |
+
if not more_body:
|
| 551 |
+
if self.expected_content_length != 0:
|
| 552 |
+
raise RuntimeError("Response content shorter than Content-Length")
|
| 553 |
+
self.response_complete = True
|
| 554 |
+
self.message_event.set()
|
| 555 |
+
if not self.keep_alive:
|
| 556 |
+
self.transport.close()
|
| 557 |
+
self.on_response()
|
| 558 |
+
|
| 559 |
+
else:
|
| 560 |
+
# Response already sent
|
| 561 |
+
raise RuntimeError(f"Unexpected ASGI message '{message['type']}' sent, after response already completed.")
|
| 562 |
+
|
| 563 |
+
async def receive(self) -> ASGIReceiveEvent:
|
| 564 |
+
if self.waiting_for_100_continue and not self.transport.is_closing():
|
| 565 |
+
self.transport.write(b"HTTP/1.1 100 Continue\r\n\r\n")
|
| 566 |
+
self.waiting_for_100_continue = False
|
| 567 |
+
|
| 568 |
+
if not self.disconnected and not self.response_complete:
|
| 569 |
+
self.flow.resume_reading()
|
| 570 |
+
await self.message_event.wait()
|
| 571 |
+
self.message_event.clear()
|
| 572 |
+
|
| 573 |
+
if self.disconnected or self.response_complete:
|
| 574 |
+
return {"type": "http.disconnect"}
|
| 575 |
+
message: HTTPRequestEvent = {"type": "http.request", "body": bytes(self.body), "more_body": self.more_body}
|
| 576 |
+
self.body = bytearray()
|
| 577 |
+
return message
|
.venv/Lib/site-packages/uvicorn/protocols/utils.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import socket
|
| 5 |
+
import urllib.parse
|
| 6 |
+
|
| 7 |
+
from uvicorn._types import WWWScope
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class ClientDisconnected(OSError): ...
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def get_remote_addr(transport: asyncio.Transport) -> tuple[str, int] | None:
|
| 14 |
+
socket_info: socket.socket | None = transport.get_extra_info("socket")
|
| 15 |
+
if socket_info is not None:
|
| 16 |
+
try:
|
| 17 |
+
info = socket_info.getpeername()
|
| 18 |
+
return (str(info[0]), int(info[1])) if isinstance(info, tuple) else None
|
| 19 |
+
except OSError: # pragma: no cover
|
| 20 |
+
# This case appears to inconsistently occur with uvloop
|
| 21 |
+
# bound to a unix domain socket.
|
| 22 |
+
return None
|
| 23 |
+
|
| 24 |
+
info = transport.get_extra_info("peername")
|
| 25 |
+
if info is not None and isinstance(info, list | tuple) and len(info) == 2:
|
| 26 |
+
return (str(info[0]), int(info[1]))
|
| 27 |
+
return None
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def get_local_addr(transport: asyncio.Transport) -> tuple[str, int | None] | None:
|
| 31 |
+
socket_info: socket.socket | None = transport.get_extra_info("socket")
|
| 32 |
+
if socket_info is not None:
|
| 33 |
+
info = socket_info.getsockname()
|
| 34 |
+
if isinstance(info, tuple):
|
| 35 |
+
return (str(info[0]), int(info[1]))
|
| 36 |
+
if isinstance(info, str):
|
| 37 |
+
return (info, None)
|
| 38 |
+
return None
|
| 39 |
+
info = transport.get_extra_info("sockname")
|
| 40 |
+
if info is not None and isinstance(info, list | tuple) and len(info) == 2:
|
| 41 |
+
return (str(info[0]), int(info[1]))
|
| 42 |
+
if isinstance(info, str):
|
| 43 |
+
return (info, None)
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def is_ssl(transport: asyncio.Transport) -> bool:
|
| 48 |
+
return bool(transport.get_extra_info("sslcontext"))
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def get_client_addr(scope: WWWScope) -> str:
|
| 52 |
+
client = scope.get("client")
|
| 53 |
+
if not client:
|
| 54 |
+
return ""
|
| 55 |
+
return "%s:%d" % client
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def get_path_with_query_string(scope: WWWScope) -> str:
|
| 59 |
+
path_with_query_string = urllib.parse.quote(scope["path"])
|
| 60 |
+
if scope["query_string"]:
|
| 61 |
+
path_with_query_string = "{}?{}".format(path_with_query_string, scope["query_string"].decode("ascii"))
|
| 62 |
+
return path_with_query_string
|
.venv/Lib/site-packages/uvicorn/protocols/websockets/__init__.py
ADDED
|
File without changes
|
.venv/Lib/site-packages/uvicorn/protocols/websockets/auto.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
from collections.abc import Callable
|
| 5 |
+
|
| 6 |
+
AutoWebSocketsProtocol: Callable[..., asyncio.Protocol] | None
|
| 7 |
+
try:
|
| 8 |
+
import websockets # noqa
|
| 9 |
+
except ImportError: # pragma: no cover
|
| 10 |
+
try:
|
| 11 |
+
import wsproto # noqa
|
| 12 |
+
except ImportError:
|
| 13 |
+
AutoWebSocketsProtocol = None
|
| 14 |
+
else:
|
| 15 |
+
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
|
| 16 |
+
|
| 17 |
+
AutoWebSocketsProtocol = WSProtocol
|
| 18 |
+
else:
|
| 19 |
+
from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol
|
| 20 |
+
|
| 21 |
+
AutoWebSocketsProtocol = WebSocketsSansIOProtocol
|
.venv/Lib/site-packages/uvicorn/protocols/websockets/websockets_impl.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import http
|
| 5 |
+
import logging
|
| 6 |
+
import warnings
|
| 7 |
+
from collections.abc import Sequence
|
| 8 |
+
from typing import Any, Literal, cast
|
| 9 |
+
from urllib.parse import unquote
|
| 10 |
+
|
| 11 |
+
import websockets
|
| 12 |
+
import websockets.legacy.handshake
|
| 13 |
+
from websockets.datastructures import Headers
|
| 14 |
+
from websockets.exceptions import ConnectionClosed
|
| 15 |
+
from websockets.extensions.base import ServerExtensionFactory
|
| 16 |
+
from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory
|
| 17 |
+
from websockets.legacy.server import HTTPResponse, WebSocketServerProtocol
|
| 18 |
+
from websockets.typing import Subprotocol
|
| 19 |
+
|
| 20 |
+
from uvicorn._types import (
|
| 21 |
+
ASGI3Application,
|
| 22 |
+
ASGISendEvent,
|
| 23 |
+
WebSocketConnectEvent,
|
| 24 |
+
WebSocketDisconnectEvent,
|
| 25 |
+
WebSocketReceiveEvent,
|
| 26 |
+
WebSocketScope,
|
| 27 |
+
)
|
| 28 |
+
from uvicorn.config import Config, UvicornDeprecationWarning
|
| 29 |
+
from uvicorn.logging import TRACE_LOG_LEVEL
|
| 30 |
+
from uvicorn.protocols.utils import (
|
| 31 |
+
ClientDisconnected,
|
| 32 |
+
get_client_addr,
|
| 33 |
+
get_local_addr,
|
| 34 |
+
get_path_with_query_string,
|
| 35 |
+
get_remote_addr,
|
| 36 |
+
is_ssl,
|
| 37 |
+
)
|
| 38 |
+
from uvicorn.server import ServerState
|
| 39 |
+
|
| 40 |
+
warnings.warn(
|
| 41 |
+
"The `websockets` implementation is deprecated, and `--ws websockets` will point at the "
|
| 42 |
+
"`websockets-sansio` implementation in a future release. "
|
| 43 |
+
"Use `--ws websockets-sansio` or `--ws auto` to switch now.",
|
| 44 |
+
UvicornDeprecationWarning,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class Server:
|
| 49 |
+
closing = False
|
| 50 |
+
|
| 51 |
+
def register(self, ws: WebSocketServerProtocol) -> None:
|
| 52 |
+
pass
|
| 53 |
+
|
| 54 |
+
def unregister(self, ws: WebSocketServerProtocol) -> None:
|
| 55 |
+
pass
|
| 56 |
+
|
| 57 |
+
def is_serving(self) -> bool:
|
| 58 |
+
return not self.closing
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class WebSocketProtocol(WebSocketServerProtocol):
|
| 62 |
+
extra_headers: list[tuple[str, str]]
|
| 63 |
+
logger: logging.Logger | logging.LoggerAdapter[Any]
|
| 64 |
+
|
| 65 |
+
def __init__(
|
| 66 |
+
self,
|
| 67 |
+
config: Config,
|
| 68 |
+
server_state: ServerState,
|
| 69 |
+
app_state: dict[str, Any],
|
| 70 |
+
_loop: asyncio.AbstractEventLoop | None = None,
|
| 71 |
+
):
|
| 72 |
+
if not config.loaded:
|
| 73 |
+
config.load()
|
| 74 |
+
|
| 75 |
+
self.config = config
|
| 76 |
+
self.app = cast(ASGI3Application, config.loaded_app)
|
| 77 |
+
self.loop = _loop or asyncio.get_event_loop()
|
| 78 |
+
self.root_path = config.root_path
|
| 79 |
+
self.asgi_version = config.asgi_version
|
| 80 |
+
self.app_state = app_state
|
| 81 |
+
|
| 82 |
+
# Shared server state
|
| 83 |
+
self.connections = server_state.connections
|
| 84 |
+
self.tasks = server_state.tasks
|
| 85 |
+
|
| 86 |
+
# Connection state
|
| 87 |
+
self.transport: asyncio.Transport = None # type: ignore[assignment]
|
| 88 |
+
self.server: tuple[str, int | None] | None = None
|
| 89 |
+
self.client: tuple[str, int] | None = None
|
| 90 |
+
self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment]
|
| 91 |
+
|
| 92 |
+
# Connection events
|
| 93 |
+
self.scope: WebSocketScope
|
| 94 |
+
self.handshake_started_event = asyncio.Event()
|
| 95 |
+
self.handshake_completed_event = asyncio.Event()
|
| 96 |
+
self.closed_event = asyncio.Event()
|
| 97 |
+
self.initial_response: HTTPResponse | None = None
|
| 98 |
+
self.connect_sent = False
|
| 99 |
+
self.lost_connection_before_handshake = False
|
| 100 |
+
self.accepted_subprotocol: Subprotocol | None = None
|
| 101 |
+
|
| 102 |
+
self.ws_server: Server = Server() # type: ignore[assignment]
|
| 103 |
+
|
| 104 |
+
extensions: list[ServerExtensionFactory] = []
|
| 105 |
+
if self.config.ws_per_message_deflate:
|
| 106 |
+
extensions.append(ServerPerMessageDeflateFactory())
|
| 107 |
+
|
| 108 |
+
super().__init__(
|
| 109 |
+
ws_handler=self.ws_handler,
|
| 110 |
+
ws_server=self.ws_server, # type: ignore[arg-type]
|
| 111 |
+
max_size=self.config.ws_max_size,
|
| 112 |
+
max_queue=self.config.ws_max_queue,
|
| 113 |
+
ping_interval=self.config.ws_ping_interval,
|
| 114 |
+
ping_timeout=self.config.ws_ping_timeout,
|
| 115 |
+
extensions=extensions,
|
| 116 |
+
logger=logging.getLogger("uvicorn.error"),
|
| 117 |
+
)
|
| 118 |
+
self.server_header = None
|
| 119 |
+
self.extra_headers = [
|
| 120 |
+
(name.decode("latin-1"), value.decode("latin-1")) for name, value in server_state.default_headers
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
+
def connection_made( # type: ignore[override]
|
| 124 |
+
self, transport: asyncio.Transport
|
| 125 |
+
) -> None:
|
| 126 |
+
self.connections.add(self)
|
| 127 |
+
self.transport = transport
|
| 128 |
+
self.server = get_local_addr(transport)
|
| 129 |
+
self.client = get_remote_addr(transport)
|
| 130 |
+
self.scheme = "wss" if is_ssl(transport) else "ws"
|
| 131 |
+
|
| 132 |
+
if self.logger.isEnabledFor(TRACE_LOG_LEVEL):
|
| 133 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 134 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix)
|
| 135 |
+
|
| 136 |
+
super().connection_made(transport)
|
| 137 |
+
|
| 138 |
+
def connection_lost(self, exc: Exception | None) -> None:
|
| 139 |
+
self.connections.remove(self)
|
| 140 |
+
|
| 141 |
+
if self.logger.isEnabledFor(TRACE_LOG_LEVEL):
|
| 142 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 143 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix)
|
| 144 |
+
|
| 145 |
+
self.lost_connection_before_handshake = not self.handshake_completed_event.is_set()
|
| 146 |
+
self.handshake_completed_event.set()
|
| 147 |
+
super().connection_lost(exc)
|
| 148 |
+
if exc is None:
|
| 149 |
+
self.transport.close()
|
| 150 |
+
|
| 151 |
+
def shutdown(self) -> None:
|
| 152 |
+
self.ws_server.closing = True
|
| 153 |
+
if self.handshake_completed_event.is_set():
|
| 154 |
+
self.fail_connection(1012)
|
| 155 |
+
else:
|
| 156 |
+
self.send_500_response()
|
| 157 |
+
self.transport.close()
|
| 158 |
+
|
| 159 |
+
def on_task_complete(self, task: asyncio.Task[None]) -> None:
|
| 160 |
+
self.tasks.discard(task)
|
| 161 |
+
|
| 162 |
+
async def process_request(self, path: str, request_headers: Headers) -> HTTPResponse | None:
|
| 163 |
+
"""
|
| 164 |
+
This hook is called to determine if the websocket should return
|
| 165 |
+
an HTTP response and close.
|
| 166 |
+
|
| 167 |
+
Our behavior here is to start the ASGI application, and then wait
|
| 168 |
+
for either `accept` or `close` in order to determine if we should
|
| 169 |
+
close the connection.
|
| 170 |
+
"""
|
| 171 |
+
path_portion, _, query_string = path.partition("?")
|
| 172 |
+
|
| 173 |
+
websockets.legacy.handshake.check_request(request_headers)
|
| 174 |
+
|
| 175 |
+
subprotocols: list[str] = []
|
| 176 |
+
for header in request_headers.get_all("Sec-WebSocket-Protocol"):
|
| 177 |
+
subprotocols.extend([token.strip() for token in header.split(",")])
|
| 178 |
+
|
| 179 |
+
asgi_headers = [
|
| 180 |
+
(name.encode("ascii"), value.encode("ascii", errors="surrogateescape"))
|
| 181 |
+
for name, value in request_headers.raw_items()
|
| 182 |
+
]
|
| 183 |
+
path = unquote(path_portion)
|
| 184 |
+
full_path = self.root_path + path
|
| 185 |
+
full_raw_path = self.root_path.encode("ascii") + path_portion.encode("ascii")
|
| 186 |
+
|
| 187 |
+
self.scope = {
|
| 188 |
+
"type": "websocket",
|
| 189 |
+
"asgi": {"version": self.asgi_version, "spec_version": "2.4"},
|
| 190 |
+
"http_version": "1.1",
|
| 191 |
+
"scheme": self.scheme,
|
| 192 |
+
"server": self.server,
|
| 193 |
+
"client": self.client,
|
| 194 |
+
"root_path": self.root_path,
|
| 195 |
+
"path": full_path,
|
| 196 |
+
"raw_path": full_raw_path,
|
| 197 |
+
"query_string": query_string.encode("ascii"),
|
| 198 |
+
"headers": asgi_headers,
|
| 199 |
+
"subprotocols": subprotocols,
|
| 200 |
+
"state": self.app_state.copy(),
|
| 201 |
+
"extensions": {"websocket.http.response": {}},
|
| 202 |
+
}
|
| 203 |
+
task = self.loop.create_task(self.run_asgi())
|
| 204 |
+
task.add_done_callback(self.on_task_complete)
|
| 205 |
+
self.tasks.add(task)
|
| 206 |
+
await self.handshake_started_event.wait()
|
| 207 |
+
return self.initial_response
|
| 208 |
+
|
| 209 |
+
def process_subprotocol(
|
| 210 |
+
self, headers: Headers, available_subprotocols: Sequence[Subprotocol] | None
|
| 211 |
+
) -> Subprotocol | None:
|
| 212 |
+
"""
|
| 213 |
+
We override the standard 'process_subprotocol' behavior here so that
|
| 214 |
+
we return whatever subprotocol is sent in the 'accept' message.
|
| 215 |
+
"""
|
| 216 |
+
return self.accepted_subprotocol
|
| 217 |
+
|
| 218 |
+
def send_500_response(self) -> None:
|
| 219 |
+
msg = b"Internal Server Error"
|
| 220 |
+
content = [
|
| 221 |
+
b"HTTP/1.1 500 Internal Server Error\r\ncontent-type: text/plain; charset=utf-8\r\n",
|
| 222 |
+
b"content-length: " + str(len(msg)).encode("ascii") + b"\r\n",
|
| 223 |
+
b"connection: close\r\n",
|
| 224 |
+
b"\r\n",
|
| 225 |
+
msg,
|
| 226 |
+
]
|
| 227 |
+
self.transport.write(b"".join(content))
|
| 228 |
+
# Allow handler task to terminate cleanly, as websockets doesn't cancel it by
|
| 229 |
+
# itself (see https://github.com/Kludex/uvicorn/issues/920)
|
| 230 |
+
self.handshake_started_event.set()
|
| 231 |
+
|
| 232 |
+
async def ws_handler(self, protocol: WebSocketServerProtocol, path: str) -> Any: # type: ignore[override]
|
| 233 |
+
"""
|
| 234 |
+
This is the main handler function for the 'websockets' implementation
|
| 235 |
+
to call into. We just wait for close then return, and instead allow
|
| 236 |
+
'send' and 'receive' events to drive the flow.
|
| 237 |
+
"""
|
| 238 |
+
self.handshake_completed_event.set()
|
| 239 |
+
await self.wait_closed()
|
| 240 |
+
|
| 241 |
+
async def run_asgi(self) -> None:
|
| 242 |
+
"""
|
| 243 |
+
Wrapper around the ASGI callable, handling exceptions and unexpected
|
| 244 |
+
termination states.
|
| 245 |
+
"""
|
| 246 |
+
try:
|
| 247 |
+
result = await self.app(self.scope, self.asgi_receive, self.asgi_send) # type: ignore[func-returns-value]
|
| 248 |
+
except ClientDisconnected: # pragma: full coverage
|
| 249 |
+
self.closed_event.set()
|
| 250 |
+
except BaseException:
|
| 251 |
+
self.closed_event.set()
|
| 252 |
+
self.logger.exception("Exception in ASGI application\n")
|
| 253 |
+
if not self.handshake_started_event.is_set():
|
| 254 |
+
self.send_500_response()
|
| 255 |
+
else:
|
| 256 |
+
await self.handshake_completed_event.wait()
|
| 257 |
+
else:
|
| 258 |
+
self.closed_event.set()
|
| 259 |
+
if not self.handshake_started_event.is_set():
|
| 260 |
+
self.logger.error("ASGI callable returned without sending handshake.")
|
| 261 |
+
self.send_500_response()
|
| 262 |
+
elif result is not None:
|
| 263 |
+
self.logger.error("ASGI callable should return None, but returned '%s'.", result)
|
| 264 |
+
await self.handshake_completed_event.wait()
|
| 265 |
+
self.transport.close()
|
| 266 |
+
|
| 267 |
+
async def asgi_send(self, message: ASGISendEvent) -> None:
|
| 268 |
+
if not self.handshake_started_event.is_set():
|
| 269 |
+
if message["type"] == "websocket.accept":
|
| 270 |
+
self.logger.info(
|
| 271 |
+
'%s - "WebSocket %s" [accepted]',
|
| 272 |
+
get_client_addr(self.scope),
|
| 273 |
+
get_path_with_query_string(self.scope),
|
| 274 |
+
)
|
| 275 |
+
self.initial_response = None
|
| 276 |
+
self.accepted_subprotocol = cast(Subprotocol | None, message.get("subprotocol"))
|
| 277 |
+
if "headers" in message:
|
| 278 |
+
self.extra_headers.extend(
|
| 279 |
+
# ASGI spec requires bytes
|
| 280 |
+
# But for compatibility we need to convert it to strings
|
| 281 |
+
(name.decode("latin-1"), value.decode("latin-1"))
|
| 282 |
+
for name, value in message["headers"]
|
| 283 |
+
)
|
| 284 |
+
self.handshake_started_event.set()
|
| 285 |
+
|
| 286 |
+
elif message["type"] == "websocket.close":
|
| 287 |
+
self.logger.info(
|
| 288 |
+
'%s - "WebSocket %s" 403',
|
| 289 |
+
get_client_addr(self.scope),
|
| 290 |
+
get_path_with_query_string(self.scope),
|
| 291 |
+
)
|
| 292 |
+
self.initial_response = (http.HTTPStatus.FORBIDDEN, [], b"")
|
| 293 |
+
self.handshake_started_event.set()
|
| 294 |
+
self.closed_event.set()
|
| 295 |
+
|
| 296 |
+
elif message["type"] == "websocket.http.response.start":
|
| 297 |
+
self.logger.info(
|
| 298 |
+
'%s - "WebSocket %s" %d',
|
| 299 |
+
get_client_addr(self.scope),
|
| 300 |
+
get_path_with_query_string(self.scope),
|
| 301 |
+
message["status"],
|
| 302 |
+
)
|
| 303 |
+
# websockets requires the status to be an enum. look it up.
|
| 304 |
+
status = http.HTTPStatus(message["status"])
|
| 305 |
+
headers = [
|
| 306 |
+
(name.decode("latin-1"), value.decode("latin-1")) for name, value in message.get("headers", [])
|
| 307 |
+
]
|
| 308 |
+
self.initial_response = (status, headers, b"")
|
| 309 |
+
self.handshake_started_event.set()
|
| 310 |
+
|
| 311 |
+
else:
|
| 312 |
+
raise RuntimeError(
|
| 313 |
+
"Expected ASGI message 'websocket.accept', 'websocket.close', "
|
| 314 |
+
f"or 'websocket.http.response.start' but got '{message['type']}'."
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
elif not self.closed_event.is_set() and self.initial_response is None:
|
| 318 |
+
await self.handshake_completed_event.wait()
|
| 319 |
+
|
| 320 |
+
try:
|
| 321 |
+
if message["type"] == "websocket.send":
|
| 322 |
+
bytes_data = message.get("bytes")
|
| 323 |
+
text_data = message.get("text")
|
| 324 |
+
data = text_data if bytes_data is None else bytes_data
|
| 325 |
+
await self.send(data) # type: ignore[arg-type]
|
| 326 |
+
|
| 327 |
+
elif message["type"] == "websocket.close":
|
| 328 |
+
code = message.get("code", 1000)
|
| 329 |
+
reason = message.get("reason", "") or ""
|
| 330 |
+
await self.close(code, reason)
|
| 331 |
+
self.closed_event.set()
|
| 332 |
+
|
| 333 |
+
else:
|
| 334 |
+
raise RuntimeError(
|
| 335 |
+
f"Expected ASGI message 'websocket.send' or 'websocket.close', but got '{message['type']}'."
|
| 336 |
+
)
|
| 337 |
+
except ConnectionClosed as exc:
|
| 338 |
+
raise ClientDisconnected from exc
|
| 339 |
+
|
| 340 |
+
elif self.initial_response is not None:
|
| 341 |
+
if message["type"] == "websocket.http.response.body":
|
| 342 |
+
body = self.initial_response[2] + message["body"]
|
| 343 |
+
self.initial_response = self.initial_response[:2] + (body,)
|
| 344 |
+
if not message.get("more_body", False):
|
| 345 |
+
self.closed_event.set()
|
| 346 |
+
else:
|
| 347 |
+
raise RuntimeError(f"Expected ASGI message 'websocket.http.response.body' but got '{message['type']}'.")
|
| 348 |
+
|
| 349 |
+
else:
|
| 350 |
+
raise RuntimeError(
|
| 351 |
+
f"Unexpected ASGI message '{message['type']}', after sending 'websocket.close' "
|
| 352 |
+
"or response already completed."
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
async def asgi_receive(self) -> WebSocketDisconnectEvent | WebSocketConnectEvent | WebSocketReceiveEvent:
|
| 356 |
+
if not self.connect_sent:
|
| 357 |
+
self.connect_sent = True
|
| 358 |
+
return {"type": "websocket.connect"}
|
| 359 |
+
|
| 360 |
+
await self.handshake_completed_event.wait()
|
| 361 |
+
|
| 362 |
+
if self.lost_connection_before_handshake:
|
| 363 |
+
# If the handshake failed or the app closed before handshake completion,
|
| 364 |
+
# use 1006 Abnormal Closure.
|
| 365 |
+
return {"type": "websocket.disconnect", "code": 1006}
|
| 366 |
+
|
| 367 |
+
if self.closed_event.is_set():
|
| 368 |
+
return {"type": "websocket.disconnect", "code": 1005}
|
| 369 |
+
|
| 370 |
+
try:
|
| 371 |
+
data = await self.recv()
|
| 372 |
+
except ConnectionClosed:
|
| 373 |
+
self.closed_event.set()
|
| 374 |
+
if self.ws_server.closing:
|
| 375 |
+
return {"type": "websocket.disconnect", "code": 1012}
|
| 376 |
+
return {"type": "websocket.disconnect", "code": self.close_code or 1005, "reason": self.close_reason}
|
| 377 |
+
|
| 378 |
+
if isinstance(data, str):
|
| 379 |
+
return {"type": "websocket.receive", "text": data}
|
| 380 |
+
return {"type": "websocket.receive", "bytes": data}
|
.venv/Lib/site-packages/uvicorn/protocols/websockets/websockets_sansio_impl.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
import random
|
| 6 |
+
import struct
|
| 7 |
+
import sys
|
| 8 |
+
from asyncio import TimerHandle
|
| 9 |
+
from asyncio.transports import BaseTransport, Transport
|
| 10 |
+
from http import HTTPStatus
|
| 11 |
+
from typing import Any, Literal, cast
|
| 12 |
+
from urllib.parse import unquote
|
| 13 |
+
|
| 14 |
+
from websockets.exceptions import InvalidState
|
| 15 |
+
from websockets.extensions.permessage_deflate import ServerPerMessageDeflateFactory
|
| 16 |
+
from websockets.frames import Frame, Opcode
|
| 17 |
+
from websockets.http11 import Request
|
| 18 |
+
from websockets.server import ServerProtocol
|
| 19 |
+
|
| 20 |
+
from uvicorn._types import (
|
| 21 |
+
ASGIReceiveEvent,
|
| 22 |
+
ASGISendEvent,
|
| 23 |
+
WebSocketScope,
|
| 24 |
+
)
|
| 25 |
+
from uvicorn.config import Config
|
| 26 |
+
from uvicorn.logging import TRACE_LOG_LEVEL
|
| 27 |
+
from uvicorn.protocols.utils import (
|
| 28 |
+
ClientDisconnected,
|
| 29 |
+
get_client_addr,
|
| 30 |
+
get_local_addr,
|
| 31 |
+
get_path_with_query_string,
|
| 32 |
+
get_remote_addr,
|
| 33 |
+
is_ssl,
|
| 34 |
+
)
|
| 35 |
+
from uvicorn.server import ServerState
|
| 36 |
+
|
| 37 |
+
if sys.version_info >= (3, 11): # pragma: no cover
|
| 38 |
+
from typing import assert_never
|
| 39 |
+
else: # pragma: no cover
|
| 40 |
+
from typing_extensions import assert_never
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class WebSocketsSansIOProtocol(asyncio.Protocol):
|
| 44 |
+
def __init__(
|
| 45 |
+
self,
|
| 46 |
+
config: Config,
|
| 47 |
+
server_state: ServerState,
|
| 48 |
+
app_state: dict[str, Any],
|
| 49 |
+
_loop: asyncio.AbstractEventLoop | None = None,
|
| 50 |
+
) -> None:
|
| 51 |
+
if not config.loaded:
|
| 52 |
+
config.load() # pragma: no cover
|
| 53 |
+
|
| 54 |
+
self.config = config
|
| 55 |
+
self.app = config.loaded_app
|
| 56 |
+
self.loop = _loop or asyncio.get_event_loop()
|
| 57 |
+
self.logger = logging.getLogger("uvicorn.error")
|
| 58 |
+
self.root_path = config.root_path
|
| 59 |
+
self.asgi_version = config.asgi_version
|
| 60 |
+
self.app_state = app_state
|
| 61 |
+
|
| 62 |
+
# Shared server state
|
| 63 |
+
self.connections = server_state.connections
|
| 64 |
+
self.tasks = server_state.tasks
|
| 65 |
+
self.default_headers = server_state.default_headers
|
| 66 |
+
|
| 67 |
+
# Connection state
|
| 68 |
+
self.transport: asyncio.Transport = None # type: ignore[assignment]
|
| 69 |
+
self.server: tuple[str, int | None] | None = None
|
| 70 |
+
self.client: tuple[str, int] | None = None
|
| 71 |
+
self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment]
|
| 72 |
+
|
| 73 |
+
# WebSocket state
|
| 74 |
+
self.queue: asyncio.Queue[ASGIReceiveEvent] = asyncio.Queue()
|
| 75 |
+
self.handshake_initiated = False
|
| 76 |
+
self.handshake_complete = False
|
| 77 |
+
self.close_sent = False
|
| 78 |
+
self.initial_response: tuple[int, list[tuple[str, str]], bytes] | None = None
|
| 79 |
+
|
| 80 |
+
extensions = []
|
| 81 |
+
if self.config.ws_per_message_deflate:
|
| 82 |
+
extensions = [
|
| 83 |
+
ServerPerMessageDeflateFactory(
|
| 84 |
+
server_max_window_bits=12,
|
| 85 |
+
client_max_window_bits=12,
|
| 86 |
+
compress_settings={"memLevel": 5},
|
| 87 |
+
)
|
| 88 |
+
]
|
| 89 |
+
self.conn = ServerProtocol(
|
| 90 |
+
extensions=extensions,
|
| 91 |
+
max_size=self.config.ws_max_size,
|
| 92 |
+
logger=logging.getLogger("uvicorn.error"),
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
self.read_paused = False
|
| 96 |
+
self.writable = asyncio.Event()
|
| 97 |
+
self.writable.set()
|
| 98 |
+
|
| 99 |
+
# Keepalive state
|
| 100 |
+
self.ping_interval = config.ws_ping_interval
|
| 101 |
+
self.ping_timeout = config.ws_ping_timeout
|
| 102 |
+
self.ping_timer: TimerHandle | None = None
|
| 103 |
+
self.pong_timer: TimerHandle | None = None
|
| 104 |
+
self.pending_ping_payload: bytes | None = None
|
| 105 |
+
self.ping_sent_at: float = 0.0
|
| 106 |
+
self.last_ping_rtt: float = 0.0
|
| 107 |
+
|
| 108 |
+
# Incoming message state
|
| 109 |
+
self.frames: list[bytes] = []
|
| 110 |
+
self.curr_msg_data_type: Literal["text", "bytes"] = "bytes"
|
| 111 |
+
|
| 112 |
+
def connection_made(self, transport: BaseTransport) -> None:
|
| 113 |
+
"""Called when a connection is made."""
|
| 114 |
+
transport = cast(Transport, transport)
|
| 115 |
+
self.connections.add(self)
|
| 116 |
+
self.transport = transport
|
| 117 |
+
self.server = get_local_addr(transport)
|
| 118 |
+
self.client = get_remote_addr(transport)
|
| 119 |
+
self.scheme = "wss" if is_ssl(transport) else "ws"
|
| 120 |
+
|
| 121 |
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
| 122 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 123 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix)
|
| 124 |
+
|
| 125 |
+
def connection_lost(self, exc: Exception | None) -> None:
|
| 126 |
+
self.stop_keepalive()
|
| 127 |
+
code = 1005 if self.handshake_complete else 1006
|
| 128 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": code})
|
| 129 |
+
self.connections.remove(self)
|
| 130 |
+
|
| 131 |
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
| 132 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 133 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix)
|
| 134 |
+
|
| 135 |
+
self.handshake_complete = True
|
| 136 |
+
if exc is None:
|
| 137 |
+
self.transport.close()
|
| 138 |
+
|
| 139 |
+
def eof_received(self) -> None:
|
| 140 |
+
pass
|
| 141 |
+
|
| 142 |
+
def shutdown(self) -> None:
|
| 143 |
+
self.stop_keepalive()
|
| 144 |
+
if self.handshake_complete:
|
| 145 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012})
|
| 146 |
+
self.conn.send_close(1012)
|
| 147 |
+
output = self.conn.data_to_send()
|
| 148 |
+
self.transport.write(b"".join(output))
|
| 149 |
+
else:
|
| 150 |
+
self.send_500_response()
|
| 151 |
+
self.transport.close()
|
| 152 |
+
|
| 153 |
+
def data_received(self, data: bytes) -> None:
|
| 154 |
+
self.conn.receive_data(data)
|
| 155 |
+
if self.conn.parser_exc is not None: # pragma: no cover
|
| 156 |
+
self.handle_parser_exception()
|
| 157 |
+
return
|
| 158 |
+
self.handle_events()
|
| 159 |
+
|
| 160 |
+
def handle_events(self) -> None:
|
| 161 |
+
for event in self.conn.events_received():
|
| 162 |
+
if isinstance(event, Request):
|
| 163 |
+
self.handle_connect(event)
|
| 164 |
+
if isinstance(event, Frame):
|
| 165 |
+
if event.opcode == Opcode.CONT:
|
| 166 |
+
self.handle_cont(event) # pragma: no cover
|
| 167 |
+
elif event.opcode == Opcode.TEXT:
|
| 168 |
+
self.handle_text(event)
|
| 169 |
+
elif event.opcode == Opcode.BINARY:
|
| 170 |
+
self.handle_bytes(event)
|
| 171 |
+
elif event.opcode == Opcode.PING:
|
| 172 |
+
self.handle_ping()
|
| 173 |
+
elif event.opcode == Opcode.PONG:
|
| 174 |
+
self.handle_pong(event)
|
| 175 |
+
elif event.opcode == Opcode.CLOSE:
|
| 176 |
+
self.handle_close(event)
|
| 177 |
+
else:
|
| 178 |
+
assert_never(event.opcode) # pragma: no cover
|
| 179 |
+
|
| 180 |
+
# Event handlers
|
| 181 |
+
|
| 182 |
+
def handle_connect(self, event: Request) -> None:
|
| 183 |
+
self.request = event
|
| 184 |
+
self.response = self.conn.accept(event)
|
| 185 |
+
self.handshake_initiated = True
|
| 186 |
+
if self.response.status_code != 101:
|
| 187 |
+
self.handshake_complete = True
|
| 188 |
+
self.close_sent = True
|
| 189 |
+
self.conn.send_response(self.response)
|
| 190 |
+
output = self.conn.data_to_send()
|
| 191 |
+
self.transport.write(b"".join(output))
|
| 192 |
+
self.transport.close()
|
| 193 |
+
return
|
| 194 |
+
|
| 195 |
+
headers = [
|
| 196 |
+
(key.encode("ascii"), value.encode("ascii", errors="surrogateescape"))
|
| 197 |
+
for key, value in event.headers.raw_items()
|
| 198 |
+
]
|
| 199 |
+
raw_path, _, query_string = event.path.partition("?")
|
| 200 |
+
subprotocols: list[str] = []
|
| 201 |
+
for header in event.headers.get_all("Sec-WebSocket-Protocol"):
|
| 202 |
+
subprotocols.extend([token.strip() for token in header.split(",")])
|
| 203 |
+
self.scope: WebSocketScope = {
|
| 204 |
+
"type": "websocket",
|
| 205 |
+
"asgi": {"version": self.asgi_version, "spec_version": "2.4"},
|
| 206 |
+
"http_version": "1.1",
|
| 207 |
+
"scheme": self.scheme,
|
| 208 |
+
"server": self.server,
|
| 209 |
+
"client": self.client,
|
| 210 |
+
"root_path": self.root_path,
|
| 211 |
+
"path": self.root_path + unquote(raw_path),
|
| 212 |
+
"raw_path": self.root_path.encode("ascii") + raw_path.encode("ascii"),
|
| 213 |
+
"query_string": query_string.encode("ascii"),
|
| 214 |
+
"headers": headers,
|
| 215 |
+
"subprotocols": subprotocols,
|
| 216 |
+
"state": self.app_state.copy(),
|
| 217 |
+
"extensions": {"websocket.http.response": {}},
|
| 218 |
+
}
|
| 219 |
+
self.queue.put_nowait({"type": "websocket.connect"})
|
| 220 |
+
task = self.loop.create_task(self.run_asgi())
|
| 221 |
+
task.add_done_callback(self.on_task_complete)
|
| 222 |
+
self.tasks.add(task)
|
| 223 |
+
|
| 224 |
+
def handle_cont(self, event: Frame) -> None:
|
| 225 |
+
self.frames.append(event.data) # type: ignore[arg-type]
|
| 226 |
+
if event.fin:
|
| 227 |
+
self.send_receive_event_to_app()
|
| 228 |
+
|
| 229 |
+
def handle_text(self, event: Frame) -> None:
|
| 230 |
+
self.curr_msg_data_type = "text"
|
| 231 |
+
self.frames = [event.data] # type: ignore[list-item]
|
| 232 |
+
if event.fin:
|
| 233 |
+
self.send_receive_event_to_app()
|
| 234 |
+
|
| 235 |
+
def handle_bytes(self, event: Frame) -> None:
|
| 236 |
+
self.curr_msg_data_type = "bytes"
|
| 237 |
+
self.frames = [event.data] # type: ignore[list-item]
|
| 238 |
+
if event.fin:
|
| 239 |
+
self.send_receive_event_to_app()
|
| 240 |
+
|
| 241 |
+
def send_receive_event_to_app(self) -> None:
|
| 242 |
+
data = self.frames[0] if len(self.frames) == 1 else b"".join(self.frames)
|
| 243 |
+
self.frames = []
|
| 244 |
+
if self.curr_msg_data_type == "text":
|
| 245 |
+
try:
|
| 246 |
+
self.queue.put_nowait({"type": "websocket.receive", "text": data.decode()})
|
| 247 |
+
except UnicodeDecodeError: # pragma: no cover
|
| 248 |
+
self.logger.exception("Invalid UTF-8 sequence received from client.")
|
| 249 |
+
self.conn.send_close(1007)
|
| 250 |
+
self.handle_parser_exception()
|
| 251 |
+
return
|
| 252 |
+
else:
|
| 253 |
+
self.queue.put_nowait({"type": "websocket.receive", "bytes": data})
|
| 254 |
+
if not self.read_paused:
|
| 255 |
+
self.read_paused = True
|
| 256 |
+
self.transport.pause_reading()
|
| 257 |
+
|
| 258 |
+
def handle_ping(self) -> None:
|
| 259 |
+
output = self.conn.data_to_send()
|
| 260 |
+
self.transport.write(b"".join(output))
|
| 261 |
+
|
| 262 |
+
def handle_pong(self, event: Frame) -> None:
|
| 263 |
+
# Ignore unsolicited pongs and stale pongs whose payload doesn't match the ping currently in flight
|
| 264 |
+
if self.pending_ping_payload is None or bytes(event.data) != self.pending_ping_payload:
|
| 265 |
+
return # pragma: no cover
|
| 266 |
+
|
| 267 |
+
self.last_ping_rtt = self.loop.time() - self.ping_sent_at
|
| 268 |
+
self.pending_ping_payload = None
|
| 269 |
+
# The peer answered in time; cancel the pong deadline and chain the next ping. This `schedule_ping()` call is
|
| 270 |
+
# what keeps the keepalive loop running when ping_timeout is set. When ping_timeout is None the next ping is
|
| 271 |
+
# already scheduled by `send_keepalive_ping`, so we must not schedule a duplicate here.
|
| 272 |
+
if self.pong_timer is not None:
|
| 273 |
+
self.pong_timer.cancel()
|
| 274 |
+
self.pong_timer = None
|
| 275 |
+
self.schedule_ping()
|
| 276 |
+
|
| 277 |
+
def start_keepalive(self) -> None:
|
| 278 |
+
if self.ping_interval is not None and self.ping_interval > 0:
|
| 279 |
+
self.schedule_ping()
|
| 280 |
+
|
| 281 |
+
def stop_keepalive(self) -> None:
|
| 282 |
+
if self.ping_timer is not None:
|
| 283 |
+
self.ping_timer.cancel()
|
| 284 |
+
self.ping_timer = None
|
| 285 |
+
if self.pong_timer is not None: # pragma: no cover
|
| 286 |
+
self.pong_timer.cancel()
|
| 287 |
+
self.pong_timer = None
|
| 288 |
+
self.pending_ping_payload = None
|
| 289 |
+
|
| 290 |
+
def schedule_ping(self) -> None:
|
| 291 |
+
assert self.ping_interval is not None
|
| 292 |
+
delay = max(0.0, self.ping_interval - self.last_ping_rtt)
|
| 293 |
+
self.ping_timer = self.loop.call_later(delay, self.send_keepalive_ping)
|
| 294 |
+
|
| 295 |
+
def send_keepalive_ping(self) -> None:
|
| 296 |
+
self.ping_timer = None
|
| 297 |
+
if self.close_sent or self.transport.is_closing(): # pragma: no cover
|
| 298 |
+
return
|
| 299 |
+
# Random 4-byte payload identifies this ping; `handle_pong` uses it to ignore stale or unsolicited pongs.
|
| 300 |
+
# See https://github.com/python-websockets/websockets/blob/4d229bf9f583d593aa103287aee0a77c9fbc3a79/src/websockets/asyncio/connection.py#L624
|
| 301 |
+
self.pending_ping_payload = struct.pack("!I", random.getrandbits(32))
|
| 302 |
+
self.ping_sent_at = self.loop.time()
|
| 303 |
+
self.conn.send_ping(self.pending_ping_payload)
|
| 304 |
+
self.transport.write(b"".join(self.conn.data_to_send()))
|
| 305 |
+
if self.ping_timeout is not None:
|
| 306 |
+
self.pong_timer = self.loop.call_later(self.ping_timeout, self.keepalive_timeout)
|
| 307 |
+
else: # pragma: no cover
|
| 308 |
+
self.schedule_ping()
|
| 309 |
+
|
| 310 |
+
def keepalive_timeout(self) -> None:
|
| 311 |
+
self.pong_timer = None
|
| 312 |
+
self.pending_ping_payload = None
|
| 313 |
+
if self.close_sent or self.transport.is_closing(): # pragma: no cover
|
| 314 |
+
return
|
| 315 |
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
| 316 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 317 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket keepalive ping timeout", prefix)
|
| 318 |
+
self.conn.fail(1011, "keepalive ping timeout")
|
| 319 |
+
self.transport.write(b"".join(self.conn.data_to_send()))
|
| 320 |
+
self.close_sent = True
|
| 321 |
+
self.transport.close()
|
| 322 |
+
|
| 323 |
+
def handle_close(self, event: Frame) -> None:
|
| 324 |
+
if not self.close_sent and not self.transport.is_closing():
|
| 325 |
+
assert self.conn.close_rcvd is not None
|
| 326 |
+
code = self.conn.close_rcvd.code
|
| 327 |
+
reason = self.conn.close_rcvd.reason
|
| 328 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
|
| 329 |
+
|
| 330 |
+
output = self.conn.data_to_send()
|
| 331 |
+
self.transport.write(b"".join(output))
|
| 332 |
+
self.transport.close()
|
| 333 |
+
|
| 334 |
+
def handle_parser_exception(self) -> None: # pragma: no cover
|
| 335 |
+
assert self.conn.close_sent is not None
|
| 336 |
+
code = self.conn.close_sent.code
|
| 337 |
+
reason = self.conn.close_sent.reason
|
| 338 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
|
| 339 |
+
|
| 340 |
+
output = self.conn.data_to_send()
|
| 341 |
+
self.transport.write(b"".join(output))
|
| 342 |
+
self.close_sent = True
|
| 343 |
+
self.transport.close()
|
| 344 |
+
|
| 345 |
+
def on_task_complete(self, task: asyncio.Task[None]) -> None:
|
| 346 |
+
self.tasks.discard(task)
|
| 347 |
+
|
| 348 |
+
async def run_asgi(self) -> None:
|
| 349 |
+
try:
|
| 350 |
+
result = await self.app(self.scope, self.receive, self.send)
|
| 351 |
+
except ClientDisconnected:
|
| 352 |
+
pass # pragma: full coverage
|
| 353 |
+
except BaseException:
|
| 354 |
+
self.logger.exception("Exception in ASGI application\n")
|
| 355 |
+
self.send_500_response()
|
| 356 |
+
else:
|
| 357 |
+
if not self.handshake_complete:
|
| 358 |
+
self.logger.error("ASGI callable returned without completing handshake.")
|
| 359 |
+
self.send_500_response()
|
| 360 |
+
elif result is not None:
|
| 361 |
+
self.logger.error("ASGI callable should return None, but returned '%s'.", result)
|
| 362 |
+
self.transport.close()
|
| 363 |
+
|
| 364 |
+
def send_500_response(self) -> None:
|
| 365 |
+
if self.initial_response or self.handshake_complete:
|
| 366 |
+
return
|
| 367 |
+
response = self.conn.reject(500, "Internal Server Error")
|
| 368 |
+
self.conn.send_response(response)
|
| 369 |
+
output = self.conn.data_to_send()
|
| 370 |
+
self.transport.write(b"".join(output))
|
| 371 |
+
|
| 372 |
+
async def send(self, message: ASGISendEvent) -> None:
|
| 373 |
+
await self.writable.wait()
|
| 374 |
+
|
| 375 |
+
if not self.handshake_complete and self.initial_response is None:
|
| 376 |
+
if message["type"] == "websocket.accept":
|
| 377 |
+
self.logger.info(
|
| 378 |
+
'%s - "WebSocket %s" [accepted]',
|
| 379 |
+
get_client_addr(self.scope),
|
| 380 |
+
get_path_with_query_string(self.scope),
|
| 381 |
+
)
|
| 382 |
+
headers = [
|
| 383 |
+
(name.decode("latin-1").lower(), value.decode("latin-1"))
|
| 384 |
+
for name, value in (self.default_headers + list(message.get("headers", [])))
|
| 385 |
+
]
|
| 386 |
+
accepted_subprotocol = message.get("subprotocol")
|
| 387 |
+
if accepted_subprotocol:
|
| 388 |
+
headers.append(("Sec-WebSocket-Protocol", accepted_subprotocol))
|
| 389 |
+
self.response.headers.update(headers)
|
| 390 |
+
|
| 391 |
+
if not self.transport.is_closing():
|
| 392 |
+
self.handshake_complete = True
|
| 393 |
+
self.conn.send_response(self.response)
|
| 394 |
+
output = self.conn.data_to_send()
|
| 395 |
+
self.transport.write(b"".join(output))
|
| 396 |
+
self.start_keepalive()
|
| 397 |
+
|
| 398 |
+
elif message["type"] == "websocket.close":
|
| 399 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
|
| 400 |
+
self.logger.info(
|
| 401 |
+
'%s - "WebSocket %s" 403',
|
| 402 |
+
get_client_addr(self.scope),
|
| 403 |
+
get_path_with_query_string(self.scope),
|
| 404 |
+
)
|
| 405 |
+
response = self.conn.reject(HTTPStatus.FORBIDDEN, "")
|
| 406 |
+
self.conn.send_response(response)
|
| 407 |
+
output = self.conn.data_to_send()
|
| 408 |
+
self.close_sent = True
|
| 409 |
+
self.handshake_complete = True
|
| 410 |
+
self.transport.write(b"".join(output))
|
| 411 |
+
self.transport.close()
|
| 412 |
+
elif message["type"] == "websocket.http.response.start" and self.initial_response is None:
|
| 413 |
+
if not (100 <= message["status"] < 600):
|
| 414 |
+
raise RuntimeError("Invalid HTTP status code '%d' in response." % message["status"])
|
| 415 |
+
self.logger.info(
|
| 416 |
+
'%s - "WebSocket %s" %d',
|
| 417 |
+
get_client_addr(self.scope),
|
| 418 |
+
get_path_with_query_string(self.scope),
|
| 419 |
+
message["status"],
|
| 420 |
+
)
|
| 421 |
+
headers = [
|
| 422 |
+
(name.decode("latin-1"), value.decode("latin-1"))
|
| 423 |
+
for name, value in list(message.get("headers", []))
|
| 424 |
+
]
|
| 425 |
+
self.initial_response = (message["status"], headers, b"")
|
| 426 |
+
else:
|
| 427 |
+
raise RuntimeError(
|
| 428 |
+
"Expected ASGI message 'websocket.accept', 'websocket.close' "
|
| 429 |
+
f"or 'websocket.http.response.start' but got '{message['type']}'."
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
elif not self.close_sent and self.initial_response is None:
|
| 433 |
+
try:
|
| 434 |
+
if message["type"] == "websocket.send":
|
| 435 |
+
bytes_data = message.get("bytes")
|
| 436 |
+
text_data = message.get("text")
|
| 437 |
+
if bytes_data is not None:
|
| 438 |
+
self.conn.send_binary(bytes_data)
|
| 439 |
+
elif text_data is not None:
|
| 440 |
+
self.conn.send_text(text_data.encode())
|
| 441 |
+
output = self.conn.data_to_send()
|
| 442 |
+
self.transport.write(b"".join(output))
|
| 443 |
+
|
| 444 |
+
elif message["type"] == "websocket.close":
|
| 445 |
+
if not self.transport.is_closing():
|
| 446 |
+
code = message.get("code", 1000)
|
| 447 |
+
reason = message.get("reason", "") or ""
|
| 448 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
|
| 449 |
+
self.conn.send_close(code, reason)
|
| 450 |
+
output = self.conn.data_to_send()
|
| 451 |
+
self.transport.write(b"".join(output))
|
| 452 |
+
self.close_sent = True
|
| 453 |
+
self.transport.close()
|
| 454 |
+
else:
|
| 455 |
+
raise RuntimeError(
|
| 456 |
+
f"Expected ASGI message 'websocket.send' or 'websocket.close', but got '{message['type']}'."
|
| 457 |
+
)
|
| 458 |
+
except InvalidState:
|
| 459 |
+
raise ClientDisconnected()
|
| 460 |
+
elif self.initial_response is not None:
|
| 461 |
+
if message["type"] == "websocket.http.response.body":
|
| 462 |
+
body = self.initial_response[2] + message["body"]
|
| 463 |
+
self.initial_response = self.initial_response[:2] + (body,)
|
| 464 |
+
if not message.get("more_body", False):
|
| 465 |
+
response = self.conn.reject(self.initial_response[0], body.decode())
|
| 466 |
+
response.headers.update(self.initial_response[1])
|
| 467 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
|
| 468 |
+
self.conn.send_response(response)
|
| 469 |
+
output = self.conn.data_to_send()
|
| 470 |
+
self.close_sent = True
|
| 471 |
+
self.transport.write(b"".join(output))
|
| 472 |
+
self.transport.close()
|
| 473 |
+
else: # pragma: no cover
|
| 474 |
+
raise RuntimeError(f"Expected ASGI message 'websocket.http.response.body' but got '{message['type']}'.")
|
| 475 |
+
|
| 476 |
+
else:
|
| 477 |
+
raise RuntimeError(f"Unexpected ASGI message '{message['type']}', after sending 'websocket.close'.")
|
| 478 |
+
|
| 479 |
+
async def receive(self) -> ASGIReceiveEvent:
|
| 480 |
+
message = await self.queue.get()
|
| 481 |
+
if self.read_paused and self.queue.empty():
|
| 482 |
+
self.read_paused = False
|
| 483 |
+
self.transport.resume_reading()
|
| 484 |
+
return message
|
.venv/Lib/site-packages/uvicorn/protocols/websockets/wsproto_impl.py
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import logging
|
| 5 |
+
import random
|
| 6 |
+
import struct
|
| 7 |
+
from asyncio import TimerHandle
|
| 8 |
+
from io import BytesIO, StringIO
|
| 9 |
+
from typing import Any, Literal, cast
|
| 10 |
+
from urllib.parse import unquote
|
| 11 |
+
|
| 12 |
+
import wsproto
|
| 13 |
+
from wsproto import ConnectionType, events
|
| 14 |
+
from wsproto.connection import ConnectionState
|
| 15 |
+
from wsproto.extensions import Extension, PerMessageDeflate
|
| 16 |
+
from wsproto.utilities import LocalProtocolError, RemoteProtocolError
|
| 17 |
+
|
| 18 |
+
from uvicorn._types import (
|
| 19 |
+
ASGI3Application,
|
| 20 |
+
ASGISendEvent,
|
| 21 |
+
WebSocketEvent,
|
| 22 |
+
WebSocketReceiveEvent,
|
| 23 |
+
WebSocketScope,
|
| 24 |
+
)
|
| 25 |
+
from uvicorn.config import Config
|
| 26 |
+
from uvicorn.logging import TRACE_LOG_LEVEL
|
| 27 |
+
from uvicorn.protocols.utils import (
|
| 28 |
+
ClientDisconnected,
|
| 29 |
+
get_client_addr,
|
| 30 |
+
get_local_addr,
|
| 31 |
+
get_path_with_query_string,
|
| 32 |
+
get_remote_addr,
|
| 33 |
+
is_ssl,
|
| 34 |
+
)
|
| 35 |
+
from uvicorn.server import ServerState
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class FrameTooLargeError(Exception):
|
| 39 |
+
"""Raised when accumulated websocket message bytes exceed `ws_max_size`."""
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class WebsocketBuffer:
|
| 43 |
+
def __init__(self, max_length: int) -> None:
|
| 44 |
+
self.value: BytesIO | StringIO | None = None
|
| 45 |
+
self.length = 0
|
| 46 |
+
self.max_length = max_length
|
| 47 |
+
|
| 48 |
+
def extend(self, event: events.TextMessage | events.BytesMessage) -> None:
|
| 49 |
+
if self.value is None:
|
| 50 |
+
self.value = StringIO() if isinstance(event, events.TextMessage) else BytesIO()
|
| 51 |
+
self.value.write(event.data) # type: ignore[arg-type]
|
| 52 |
+
# `ws_max_size` is a byte budget, so count UTF-8 bytes for text.
|
| 53 |
+
self.length += len(event.data.encode()) if isinstance(event, events.TextMessage) else len(event.data)
|
| 54 |
+
if self.length > self.max_length:
|
| 55 |
+
raise FrameTooLargeError
|
| 56 |
+
|
| 57 |
+
def clear(self) -> None:
|
| 58 |
+
self.value = None
|
| 59 |
+
self.length = 0
|
| 60 |
+
|
| 61 |
+
def to_message(self) -> WebSocketReceiveEvent:
|
| 62 |
+
if isinstance(self.value, StringIO):
|
| 63 |
+
return {"type": "websocket.receive", "text": self.value.getvalue()}
|
| 64 |
+
assert isinstance(self.value, BytesIO)
|
| 65 |
+
return {"type": "websocket.receive", "bytes": self.value.getvalue()}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class WSProtocol(asyncio.Protocol):
|
| 69 |
+
def __init__(
|
| 70 |
+
self,
|
| 71 |
+
config: Config,
|
| 72 |
+
server_state: ServerState,
|
| 73 |
+
app_state: dict[str, Any],
|
| 74 |
+
_loop: asyncio.AbstractEventLoop | None = None,
|
| 75 |
+
) -> None:
|
| 76 |
+
if not config.loaded:
|
| 77 |
+
config.load() # pragma: full coverage
|
| 78 |
+
|
| 79 |
+
self.config = config
|
| 80 |
+
self.app = cast(ASGI3Application, config.loaded_app)
|
| 81 |
+
self.loop = _loop or asyncio.get_event_loop()
|
| 82 |
+
self.logger = logging.getLogger("uvicorn.error")
|
| 83 |
+
self.root_path = config.root_path
|
| 84 |
+
self.asgi_version = config.asgi_version
|
| 85 |
+
self.app_state = app_state
|
| 86 |
+
|
| 87 |
+
# Shared server state
|
| 88 |
+
self.connections = server_state.connections
|
| 89 |
+
self.tasks = server_state.tasks
|
| 90 |
+
self.default_headers = server_state.default_headers
|
| 91 |
+
|
| 92 |
+
# Connection state
|
| 93 |
+
self.transport: asyncio.Transport = None # type: ignore[assignment]
|
| 94 |
+
self.server: tuple[str, int | None] | None = None
|
| 95 |
+
self.client: tuple[str, int] | None = None
|
| 96 |
+
self.scheme: Literal["wss", "ws"] = None # type: ignore[assignment]
|
| 97 |
+
|
| 98 |
+
# WebSocket state
|
| 99 |
+
self.queue: asyncio.Queue[WebSocketEvent] = asyncio.Queue()
|
| 100 |
+
self.handshake_complete = False
|
| 101 |
+
self.close_sent = False
|
| 102 |
+
|
| 103 |
+
# Rejection state
|
| 104 |
+
self.response_started = False
|
| 105 |
+
|
| 106 |
+
self.conn = wsproto.WSConnection(connection_type=ConnectionType.SERVER)
|
| 107 |
+
|
| 108 |
+
self.read_paused = False
|
| 109 |
+
self.writable = asyncio.Event()
|
| 110 |
+
self.writable.set()
|
| 111 |
+
|
| 112 |
+
# Keepalive state
|
| 113 |
+
self.ping_interval = config.ws_ping_interval
|
| 114 |
+
self.ping_timeout = config.ws_ping_timeout
|
| 115 |
+
self.ping_timer: TimerHandle | None = None
|
| 116 |
+
self.pong_timer: TimerHandle | None = None
|
| 117 |
+
self.pending_ping_payload: bytes | None = None
|
| 118 |
+
self.ping_sent_at: float = 0.0
|
| 119 |
+
self.last_ping_rtt: float = 0.0
|
| 120 |
+
|
| 121 |
+
# Buffer
|
| 122 |
+
self.buffer = WebsocketBuffer(self.config.ws_max_size)
|
| 123 |
+
|
| 124 |
+
# Protocol interface
|
| 125 |
+
|
| 126 |
+
def connection_made(self, transport: asyncio.Transport) -> None: # type: ignore[override]
|
| 127 |
+
self.connections.add(self)
|
| 128 |
+
self.transport = transport
|
| 129 |
+
self.server = get_local_addr(transport)
|
| 130 |
+
self.client = get_remote_addr(transport)
|
| 131 |
+
self.scheme = "wss" if is_ssl(transport) else "ws"
|
| 132 |
+
|
| 133 |
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
| 134 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 135 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection made", prefix)
|
| 136 |
+
|
| 137 |
+
def connection_lost(self, exc: Exception | None) -> None:
|
| 138 |
+
self.stop_keepalive()
|
| 139 |
+
code = 1005 if self.handshake_complete else 1006
|
| 140 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": code})
|
| 141 |
+
self.connections.remove(self)
|
| 142 |
+
|
| 143 |
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
| 144 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 145 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket connection lost", prefix)
|
| 146 |
+
|
| 147 |
+
self.handshake_complete = True
|
| 148 |
+
if exc is None:
|
| 149 |
+
self.transport.close()
|
| 150 |
+
|
| 151 |
+
def eof_received(self) -> None:
|
| 152 |
+
pass
|
| 153 |
+
|
| 154 |
+
def data_received(self, data: bytes) -> None:
|
| 155 |
+
try:
|
| 156 |
+
self.conn.receive_data(data)
|
| 157 |
+
except RemoteProtocolError as err:
|
| 158 |
+
# TODO: Remove `type: ignore` when wsproto fixes the type annotation.
|
| 159 |
+
self.transport.write(self.conn.send(err.event_hint)) # type: ignore[arg-type] # noqa: E501
|
| 160 |
+
self.transport.close()
|
| 161 |
+
except LocalProtocolError:
|
| 162 |
+
# Data received after the close handshake completed, e.g. a pong the client
|
| 163 |
+
# sent while the server's close was in flight.
|
| 164 |
+
self.transport.close()
|
| 165 |
+
else:
|
| 166 |
+
self.handle_events()
|
| 167 |
+
|
| 168 |
+
def handle_events(self) -> None:
|
| 169 |
+
for event in self.conn.events():
|
| 170 |
+
if self.close_sent:
|
| 171 |
+
return
|
| 172 |
+
if isinstance(event, events.Request):
|
| 173 |
+
self.handle_connect(event)
|
| 174 |
+
elif isinstance(event, (events.TextMessage, events.BytesMessage)):
|
| 175 |
+
self.handle_message(event)
|
| 176 |
+
elif isinstance(event, events.CloseConnection):
|
| 177 |
+
self.handle_close(event)
|
| 178 |
+
elif isinstance(event, events.Ping):
|
| 179 |
+
self.handle_ping(event)
|
| 180 |
+
elif isinstance(event, events.Pong):
|
| 181 |
+
self.handle_pong(event)
|
| 182 |
+
|
| 183 |
+
def pause_writing(self) -> None:
|
| 184 |
+
"""
|
| 185 |
+
Called by the transport when the write buffer exceeds the high water mark.
|
| 186 |
+
"""
|
| 187 |
+
self.writable.clear() # pragma: full coverage
|
| 188 |
+
|
| 189 |
+
def resume_writing(self) -> None:
|
| 190 |
+
"""
|
| 191 |
+
Called by the transport when the write buffer drops below the low water mark.
|
| 192 |
+
"""
|
| 193 |
+
self.writable.set() # pragma: full coverage
|
| 194 |
+
|
| 195 |
+
def shutdown(self) -> None:
|
| 196 |
+
self.stop_keepalive()
|
| 197 |
+
if self.handshake_complete:
|
| 198 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1012})
|
| 199 |
+
output = self.conn.send(wsproto.events.CloseConnection(code=1012))
|
| 200 |
+
self.transport.write(output)
|
| 201 |
+
else:
|
| 202 |
+
self.send_500_response()
|
| 203 |
+
self.transport.close()
|
| 204 |
+
|
| 205 |
+
def on_task_complete(self, task: asyncio.Task[None]) -> None:
|
| 206 |
+
self.tasks.discard(task)
|
| 207 |
+
|
| 208 |
+
# Event handlers
|
| 209 |
+
|
| 210 |
+
def handle_connect(self, event: events.Request) -> None:
|
| 211 |
+
headers = [(b"host", event.host.encode())]
|
| 212 |
+
headers += [(key.lower(), value) for key, value in event.extra_headers]
|
| 213 |
+
raw_path, _, query_string = event.target.partition("?")
|
| 214 |
+
path = unquote(raw_path)
|
| 215 |
+
full_path = self.root_path + path
|
| 216 |
+
full_raw_path = self.root_path.encode("ascii") + raw_path.encode("ascii")
|
| 217 |
+
self.scope: WebSocketScope = {
|
| 218 |
+
"type": "websocket",
|
| 219 |
+
"asgi": {"version": self.asgi_version, "spec_version": "2.4"},
|
| 220 |
+
"http_version": "1.1",
|
| 221 |
+
"scheme": self.scheme,
|
| 222 |
+
"server": self.server,
|
| 223 |
+
"client": self.client,
|
| 224 |
+
"root_path": self.root_path,
|
| 225 |
+
"path": full_path,
|
| 226 |
+
"raw_path": full_raw_path,
|
| 227 |
+
"query_string": query_string.encode("ascii"),
|
| 228 |
+
"headers": headers,
|
| 229 |
+
"subprotocols": event.subprotocols,
|
| 230 |
+
"state": self.app_state.copy(),
|
| 231 |
+
"extensions": {"websocket.http.response": {}},
|
| 232 |
+
}
|
| 233 |
+
self.queue.put_nowait({"type": "websocket.connect"})
|
| 234 |
+
task = self.loop.create_task(self.run_asgi())
|
| 235 |
+
task.add_done_callback(self.on_task_complete)
|
| 236 |
+
self.tasks.add(task)
|
| 237 |
+
|
| 238 |
+
def handle_message(self, event: events.TextMessage | events.BytesMessage) -> None:
|
| 239 |
+
try:
|
| 240 |
+
self.buffer.extend(event)
|
| 241 |
+
except FrameTooLargeError:
|
| 242 |
+
self.close_sent = True
|
| 243 |
+
reason = f"Message exceeds the maximum size ({self.config.ws_max_size} bytes)"
|
| 244 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1009, "reason": reason})
|
| 245 |
+
if not self.transport.is_closing():
|
| 246 |
+
self.transport.write(self.conn.send(wsproto.events.CloseConnection(code=1009, reason=reason)))
|
| 247 |
+
self.transport.close()
|
| 248 |
+
return
|
| 249 |
+
if event.message_finished:
|
| 250 |
+
self.queue.put_nowait(self.buffer.to_message())
|
| 251 |
+
self.buffer.clear()
|
| 252 |
+
if not self.read_paused:
|
| 253 |
+
self.read_paused = True
|
| 254 |
+
self.transport.pause_reading()
|
| 255 |
+
|
| 256 |
+
def handle_close(self, event: events.CloseConnection) -> None:
|
| 257 |
+
if self.conn.state == ConnectionState.REMOTE_CLOSING:
|
| 258 |
+
self.transport.write(self.conn.send(event.response()))
|
| 259 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": event.code, "reason": event.reason})
|
| 260 |
+
self.transport.close()
|
| 261 |
+
|
| 262 |
+
def handle_ping(self, event: events.Ping) -> None:
|
| 263 |
+
self.transport.write(self.conn.send(event.response()))
|
| 264 |
+
|
| 265 |
+
def handle_pong(self, event: events.Pong) -> None:
|
| 266 |
+
# Ignore unsolicited pongs and stale pongs whose payload doesn't match the ping currently in flight.
|
| 267 |
+
if self.pending_ping_payload is None or bytes(event.payload) != self.pending_ping_payload:
|
| 268 |
+
return # pragma: no cover
|
| 269 |
+
|
| 270 |
+
self.last_ping_rtt = self.loop.time() - self.ping_sent_at
|
| 271 |
+
self.pending_ping_payload = None
|
| 272 |
+
# The peer answered in time; cancel the pong deadline and chain the next ping. This `schedule_ping()` call is
|
| 273 |
+
# what keeps the keepalive loop running when ping_timeout is set. When ping_timeout is None the next ping is
|
| 274 |
+
# already scheduled by `send_keepalive_ping`, so we must not schedule a duplicate here.
|
| 275 |
+
if self.pong_timer is not None:
|
| 276 |
+
self.pong_timer.cancel()
|
| 277 |
+
self.pong_timer = None
|
| 278 |
+
self.schedule_ping()
|
| 279 |
+
|
| 280 |
+
def start_keepalive(self) -> None:
|
| 281 |
+
if self.ping_interval is not None and self.ping_interval > 0:
|
| 282 |
+
self.schedule_ping()
|
| 283 |
+
|
| 284 |
+
def stop_keepalive(self) -> None:
|
| 285 |
+
if self.ping_timer is not None:
|
| 286 |
+
self.ping_timer.cancel()
|
| 287 |
+
self.ping_timer = None
|
| 288 |
+
if self.pong_timer is not None: # pragma: no cover
|
| 289 |
+
self.pong_timer.cancel()
|
| 290 |
+
self.pong_timer = None
|
| 291 |
+
self.pending_ping_payload = None
|
| 292 |
+
|
| 293 |
+
def schedule_ping(self) -> None:
|
| 294 |
+
assert self.ping_interval is not None
|
| 295 |
+
delay = max(0.0, self.ping_interval - self.last_ping_rtt)
|
| 296 |
+
self.ping_timer = self.loop.call_later(delay, self.send_keepalive_ping)
|
| 297 |
+
|
| 298 |
+
def send_keepalive_ping(self) -> None:
|
| 299 |
+
self.ping_timer = None
|
| 300 |
+
if self.close_sent or self.transport.is_closing(): # pragma: no cover
|
| 301 |
+
return
|
| 302 |
+
# Random 4-byte payload identifies this ping; `handle_pong` uses it to ignore stale or unsolicited pongs.
|
| 303 |
+
self.pending_ping_payload = struct.pack("!I", random.getrandbits(32))
|
| 304 |
+
self.ping_sent_at = self.loop.time()
|
| 305 |
+
self.transport.write(self.conn.send(wsproto.events.Ping(payload=self.pending_ping_payload)))
|
| 306 |
+
if self.ping_timeout is not None:
|
| 307 |
+
self.pong_timer = self.loop.call_later(self.ping_timeout, self.keepalive_timeout)
|
| 308 |
+
else: # pragma: no cover
|
| 309 |
+
self.schedule_ping()
|
| 310 |
+
|
| 311 |
+
def keepalive_timeout(self) -> None:
|
| 312 |
+
self.pong_timer = None
|
| 313 |
+
self.pending_ping_payload = None
|
| 314 |
+
if self.close_sent or self.transport.is_closing(): # pragma: no cover
|
| 315 |
+
return
|
| 316 |
+
if self.logger.level <= TRACE_LOG_LEVEL:
|
| 317 |
+
prefix = "%s:%d - " % self.client if self.client else ""
|
| 318 |
+
self.logger.log(TRACE_LOG_LEVEL, "%sWebSocket keepalive ping timeout", prefix)
|
| 319 |
+
reason = "keepalive ping timeout"
|
| 320 |
+
self.transport.write(self.conn.send(wsproto.events.CloseConnection(code=1011, reason=reason)))
|
| 321 |
+
self.close_sent = True
|
| 322 |
+
self.transport.close()
|
| 323 |
+
|
| 324 |
+
def send_500_response(self) -> None:
|
| 325 |
+
if self.response_started or self.handshake_complete:
|
| 326 |
+
return # we cannot send responses anymore
|
| 327 |
+
headers: list[tuple[bytes, bytes]] = [
|
| 328 |
+
(b"content-type", b"text/plain; charset=utf-8"),
|
| 329 |
+
(b"connection", b"close"),
|
| 330 |
+
(b"content-length", b"21"),
|
| 331 |
+
]
|
| 332 |
+
output = self.conn.send(wsproto.events.RejectConnection(status_code=500, headers=headers, has_body=True))
|
| 333 |
+
output += self.conn.send(wsproto.events.RejectData(data=b"Internal Server Error"))
|
| 334 |
+
self.transport.write(output)
|
| 335 |
+
|
| 336 |
+
async def run_asgi(self) -> None:
|
| 337 |
+
try:
|
| 338 |
+
result = await self.app(self.scope, self.receive, self.send) # type: ignore[func-returns-value]
|
| 339 |
+
except ClientDisconnected:
|
| 340 |
+
pass # pragma: full coverage
|
| 341 |
+
except BaseException:
|
| 342 |
+
self.logger.exception("Exception in ASGI application\n")
|
| 343 |
+
self.send_500_response()
|
| 344 |
+
else:
|
| 345 |
+
if not self.handshake_complete:
|
| 346 |
+
self.logger.error("ASGI callable returned without completing handshake.")
|
| 347 |
+
self.send_500_response()
|
| 348 |
+
elif result is not None:
|
| 349 |
+
self.logger.error("ASGI callable should return None, but returned '%s'.", result)
|
| 350 |
+
self.transport.close()
|
| 351 |
+
|
| 352 |
+
async def send(self, message: ASGISendEvent) -> None:
|
| 353 |
+
await self.writable.wait()
|
| 354 |
+
|
| 355 |
+
if not self.handshake_complete:
|
| 356 |
+
if message["type"] == "websocket.accept":
|
| 357 |
+
self.logger.info(
|
| 358 |
+
'%s - "WebSocket %s" [accepted]',
|
| 359 |
+
get_client_addr(self.scope),
|
| 360 |
+
get_path_with_query_string(self.scope),
|
| 361 |
+
)
|
| 362 |
+
subprotocol = message.get("subprotocol")
|
| 363 |
+
extra_headers = self.default_headers + list(message.get("headers", []))
|
| 364 |
+
extensions: list[Extension] = []
|
| 365 |
+
if self.config.ws_per_message_deflate:
|
| 366 |
+
extensions.append(PerMessageDeflate())
|
| 367 |
+
if not self.transport.is_closing():
|
| 368 |
+
self.handshake_complete = True
|
| 369 |
+
output = self.conn.send(
|
| 370 |
+
wsproto.events.AcceptConnection(
|
| 371 |
+
subprotocol=subprotocol,
|
| 372 |
+
extensions=extensions,
|
| 373 |
+
extra_headers=extra_headers,
|
| 374 |
+
)
|
| 375 |
+
)
|
| 376 |
+
self.transport.write(output)
|
| 377 |
+
self.start_keepalive()
|
| 378 |
+
|
| 379 |
+
elif message["type"] == "websocket.close":
|
| 380 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
|
| 381 |
+
self.logger.info(
|
| 382 |
+
'%s - "WebSocket %s" 403',
|
| 383 |
+
get_client_addr(self.scope),
|
| 384 |
+
get_path_with_query_string(self.scope),
|
| 385 |
+
)
|
| 386 |
+
self.handshake_complete = True
|
| 387 |
+
self.close_sent = True
|
| 388 |
+
event = events.RejectConnection(status_code=403, headers=[])
|
| 389 |
+
output = self.conn.send(event)
|
| 390 |
+
self.transport.write(output)
|
| 391 |
+
self.transport.close()
|
| 392 |
+
|
| 393 |
+
elif message["type"] == "websocket.http.response.start":
|
| 394 |
+
# ensure status code is in the valid range
|
| 395 |
+
if not (100 <= message["status"] < 600):
|
| 396 |
+
msg = "Invalid HTTP status code '%d' in response."
|
| 397 |
+
raise RuntimeError(msg % message["status"])
|
| 398 |
+
self.logger.info(
|
| 399 |
+
'%s - "WebSocket %s" %d',
|
| 400 |
+
get_client_addr(self.scope),
|
| 401 |
+
get_path_with_query_string(self.scope),
|
| 402 |
+
message["status"],
|
| 403 |
+
)
|
| 404 |
+
self.handshake_complete = True
|
| 405 |
+
event = events.RejectConnection(
|
| 406 |
+
status_code=message["status"],
|
| 407 |
+
headers=list(message["headers"]),
|
| 408 |
+
has_body=True,
|
| 409 |
+
)
|
| 410 |
+
output = self.conn.send(event)
|
| 411 |
+
self.transport.write(output)
|
| 412 |
+
self.response_started = True
|
| 413 |
+
|
| 414 |
+
else:
|
| 415 |
+
raise RuntimeError(
|
| 416 |
+
"Expected ASGI message 'websocket.accept', 'websocket.close' "
|
| 417 |
+
f"or 'websocket.http.response.start' but got '{message['type']}'."
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
elif not self.close_sent and not self.response_started:
|
| 421 |
+
try:
|
| 422 |
+
if message["type"] == "websocket.send":
|
| 423 |
+
bytes_data = message.get("bytes")
|
| 424 |
+
text_data = message.get("text")
|
| 425 |
+
data = text_data if bytes_data is None else bytes_data
|
| 426 |
+
output = self.conn.send(wsproto.events.Message(data=data)) # type: ignore
|
| 427 |
+
if not self.transport.is_closing():
|
| 428 |
+
self.transport.write(output)
|
| 429 |
+
|
| 430 |
+
elif message["type"] == "websocket.close":
|
| 431 |
+
self.close_sent = True
|
| 432 |
+
code = message.get("code", 1000)
|
| 433 |
+
reason = message.get("reason", "") or ""
|
| 434 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": code, "reason": reason})
|
| 435 |
+
output = self.conn.send(wsproto.events.CloseConnection(code=code, reason=reason))
|
| 436 |
+
if not self.transport.is_closing():
|
| 437 |
+
self.transport.write(output)
|
| 438 |
+
self.transport.close()
|
| 439 |
+
|
| 440 |
+
else:
|
| 441 |
+
raise RuntimeError(
|
| 442 |
+
f"Expected ASGI message 'websocket.send' or 'websocket.close', but got '{message['type']}'."
|
| 443 |
+
)
|
| 444 |
+
except LocalProtocolError as exc:
|
| 445 |
+
raise ClientDisconnected from exc
|
| 446 |
+
elif self.response_started:
|
| 447 |
+
if message["type"] == "websocket.http.response.body":
|
| 448 |
+
body_finished = not message.get("more_body", False)
|
| 449 |
+
reject_data = events.RejectData(data=message["body"], body_finished=body_finished)
|
| 450 |
+
output = self.conn.send(reject_data)
|
| 451 |
+
self.transport.write(output)
|
| 452 |
+
|
| 453 |
+
if body_finished:
|
| 454 |
+
self.queue.put_nowait({"type": "websocket.disconnect", "code": 1006})
|
| 455 |
+
self.close_sent = True
|
| 456 |
+
self.transport.close()
|
| 457 |
+
|
| 458 |
+
else:
|
| 459 |
+
raise RuntimeError(f"Expected ASGI message 'websocket.http.response.body' but got '{message['type']}'.")
|
| 460 |
+
|
| 461 |
+
else:
|
| 462 |
+
raise RuntimeError(f"Unexpected ASGI message '{message['type']}', after sending 'websocket.close'.")
|
| 463 |
+
|
| 464 |
+
async def receive(self) -> WebSocketEvent:
|
| 465 |
+
message = await self.queue.get()
|
| 466 |
+
if self.read_paused and self.queue.empty():
|
| 467 |
+
self.read_paused = False
|
| 468 |
+
self.transport.resume_reading()
|
| 469 |
+
return message
|
.venv/Lib/site-packages/uvicorn/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
.venv/Lib/site-packages/uvicorn/server.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import contextlib
|
| 5 |
+
import functools
|
| 6 |
+
import logging
|
| 7 |
+
import os
|
| 8 |
+
import platform
|
| 9 |
+
import random
|
| 10 |
+
import signal
|
| 11 |
+
import socket
|
| 12 |
+
import sys
|
| 13 |
+
import threading
|
| 14 |
+
import time
|
| 15 |
+
from collections.abc import Generator, Sequence
|
| 16 |
+
from email.utils import formatdate
|
| 17 |
+
from types import FrameType
|
| 18 |
+
from typing import TYPE_CHECKING, TypeAlias
|
| 19 |
+
|
| 20 |
+
from uvicorn._ansi import style
|
| 21 |
+
from uvicorn._compat import asyncio_run
|
| 22 |
+
from uvicorn.config import STARTUP_FAILURE, Config
|
| 23 |
+
|
| 24 |
+
if TYPE_CHECKING:
|
| 25 |
+
from uvicorn.protocols.http.h11_impl import H11Protocol
|
| 26 |
+
from uvicorn.protocols.http.httptools_impl import HttpToolsProtocol
|
| 27 |
+
from uvicorn.protocols.websockets.websockets_impl import WebSocketProtocol
|
| 28 |
+
from uvicorn.protocols.websockets.websockets_sansio_impl import WebSocketsSansIOProtocol
|
| 29 |
+
from uvicorn.protocols.websockets.wsproto_impl import WSProtocol
|
| 30 |
+
|
| 31 |
+
Protocols: TypeAlias = H11Protocol | HttpToolsProtocol | WSProtocol | WebSocketProtocol | WebSocketsSansIOProtocol
|
| 32 |
+
|
| 33 |
+
HANDLED_SIGNALS = (
|
| 34 |
+
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
| 35 |
+
signal.SIGTERM, # Unix signal 15. Sent by `kill <pid>`.
|
| 36 |
+
)
|
| 37 |
+
if sys.platform == "win32": # pragma: py-not-win32
|
| 38 |
+
HANDLED_SIGNALS += (signal.SIGBREAK,) # Windows signal 21. Sent by Ctrl+Break.
|
| 39 |
+
|
| 40 |
+
logger = logging.getLogger("uvicorn.error")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class ServerState:
|
| 44 |
+
"""
|
| 45 |
+
Shared servers state that is available between all protocol instances.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(self) -> None:
|
| 49 |
+
self.total_requests = 0
|
| 50 |
+
self.connections: set[Protocols] = set()
|
| 51 |
+
self.tasks: set[asyncio.Task[None]] = set()
|
| 52 |
+
self.default_headers: list[tuple[bytes, bytes]] = []
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class Server:
|
| 56 |
+
def __init__(self, config: Config) -> None:
|
| 57 |
+
self.config = config
|
| 58 |
+
self.server_state = ServerState()
|
| 59 |
+
|
| 60 |
+
self.started = False
|
| 61 |
+
self.should_exit = False
|
| 62 |
+
self.force_exit = False
|
| 63 |
+
self.last_notified = 0.0
|
| 64 |
+
|
| 65 |
+
self._captured_signals: list[int] = []
|
| 66 |
+
|
| 67 |
+
@functools.cached_property
|
| 68 |
+
def limit_max_requests(self) -> int | None:
|
| 69 |
+
if self.config.limit_max_requests is None:
|
| 70 |
+
return None
|
| 71 |
+
return self.config.limit_max_requests + random.randint(0, self.config.limit_max_requests_jitter)
|
| 72 |
+
|
| 73 |
+
def run(self, sockets: list[socket.socket] | None = None) -> None:
|
| 74 |
+
return asyncio_run(self.serve(sockets=sockets), loop_factory=self.config.get_loop_factory())
|
| 75 |
+
|
| 76 |
+
async def serve(self, sockets: list[socket.socket] | None = None) -> None:
|
| 77 |
+
with self.capture_signals():
|
| 78 |
+
await self._serve(sockets)
|
| 79 |
+
|
| 80 |
+
async def _serve(self, sockets: list[socket.socket] | None = None) -> None:
|
| 81 |
+
process_id = os.getpid()
|
| 82 |
+
|
| 83 |
+
config = self.config
|
| 84 |
+
if not config.loaded:
|
| 85 |
+
config.load()
|
| 86 |
+
|
| 87 |
+
self.lifespan = config.lifespan_class(config)
|
| 88 |
+
|
| 89 |
+
message = "Started server process [%d]"
|
| 90 |
+
color_message = "Started server process [" + style("%d", fg="cyan") + "]"
|
| 91 |
+
logger.info(message, process_id, extra={"color_message": color_message})
|
| 92 |
+
|
| 93 |
+
await self.startup(sockets=sockets)
|
| 94 |
+
if not self.should_exit:
|
| 95 |
+
await self.main_loop()
|
| 96 |
+
if self.started:
|
| 97 |
+
await self.shutdown(sockets=sockets)
|
| 98 |
+
|
| 99 |
+
message = "Finished server process [%d]"
|
| 100 |
+
color_message = "Finished server process [" + style("%d", fg="cyan") + "]"
|
| 101 |
+
logger.info(message, process_id, extra={"color_message": color_message})
|
| 102 |
+
|
| 103 |
+
async def startup(self, sockets: list[socket.socket] | None = None) -> None:
|
| 104 |
+
await self.lifespan.startup()
|
| 105 |
+
if self.lifespan.should_exit:
|
| 106 |
+
sys.exit(STARTUP_FAILURE)
|
| 107 |
+
|
| 108 |
+
config = self.config
|
| 109 |
+
|
| 110 |
+
def create_protocol(
|
| 111 |
+
_loop: asyncio.AbstractEventLoop | None = None,
|
| 112 |
+
) -> asyncio.Protocol:
|
| 113 |
+
return config.http_protocol_class( # type: ignore[call-arg]
|
| 114 |
+
config=config,
|
| 115 |
+
server_state=self.server_state,
|
| 116 |
+
app_state=self.lifespan.state,
|
| 117 |
+
_loop=_loop,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
loop = asyncio.get_running_loop()
|
| 121 |
+
|
| 122 |
+
listeners: Sequence[socket.SocketType]
|
| 123 |
+
if sockets is not None: # pragma: full coverage
|
| 124 |
+
# Explicitly passed a list of open sockets.
|
| 125 |
+
# We use this when the server is run from a Gunicorn worker.
|
| 126 |
+
|
| 127 |
+
def _share_socket(
|
| 128 |
+
sock: socket.SocketType,
|
| 129 |
+
) -> socket.SocketType: # pragma py-not-win32
|
| 130 |
+
# Windows requires the socket be explicitly shared across
|
| 131 |
+
# multiple workers (processes).
|
| 132 |
+
from socket import fromshare # type: ignore[attr-defined]
|
| 133 |
+
|
| 134 |
+
sock_data = sock.share(os.getpid()) # type: ignore[attr-defined]
|
| 135 |
+
return fromshare(sock_data)
|
| 136 |
+
|
| 137 |
+
self.servers: list[asyncio.base_events.Server] = []
|
| 138 |
+
for sock in sockets:
|
| 139 |
+
is_windows = platform.system() == "Windows"
|
| 140 |
+
if config.workers > 1 and is_windows: # pragma: py-not-win32
|
| 141 |
+
sock = _share_socket(sock) # type: ignore[assignment]
|
| 142 |
+
server = await loop.create_server(create_protocol, sock=sock, ssl=config.ssl, backlog=config.backlog)
|
| 143 |
+
self.servers.append(server)
|
| 144 |
+
listeners = sockets
|
| 145 |
+
|
| 146 |
+
elif config.fd is not None: # pragma: py-win32
|
| 147 |
+
# Use an existing socket, from a file descriptor.
|
| 148 |
+
sock = socket.fromfd(config.fd, socket.AF_UNIX, socket.SOCK_STREAM)
|
| 149 |
+
server = await loop.create_server(create_protocol, sock=sock, ssl=config.ssl, backlog=config.backlog)
|
| 150 |
+
assert server.sockets is not None # mypy
|
| 151 |
+
listeners = server.sockets
|
| 152 |
+
self.servers = [server]
|
| 153 |
+
|
| 154 |
+
elif config.uds is not None: # pragma: py-win32
|
| 155 |
+
# Create a socket using UNIX domain socket.
|
| 156 |
+
uds_perms = 0o666
|
| 157 |
+
if os.path.exists(config.uds):
|
| 158 |
+
uds_perms = os.stat(config.uds).st_mode # pragma: full coverage
|
| 159 |
+
server = await loop.create_unix_server(
|
| 160 |
+
create_protocol, path=config.uds, ssl=config.ssl, backlog=config.backlog
|
| 161 |
+
)
|
| 162 |
+
os.chmod(config.uds, uds_perms)
|
| 163 |
+
assert server.sockets is not None # mypy
|
| 164 |
+
listeners = server.sockets
|
| 165 |
+
self.servers = [server]
|
| 166 |
+
|
| 167 |
+
else:
|
| 168 |
+
# Standard case. Create a socket from a host/port pair.
|
| 169 |
+
try:
|
| 170 |
+
server = await loop.create_server(
|
| 171 |
+
create_protocol,
|
| 172 |
+
host=config.host,
|
| 173 |
+
port=config.port,
|
| 174 |
+
ssl=config.ssl,
|
| 175 |
+
backlog=config.backlog,
|
| 176 |
+
)
|
| 177 |
+
except OSError as exc:
|
| 178 |
+
logger.error(exc)
|
| 179 |
+
await self.lifespan.shutdown()
|
| 180 |
+
sys.exit(STARTUP_FAILURE)
|
| 181 |
+
|
| 182 |
+
assert server.sockets is not None
|
| 183 |
+
listeners = server.sockets
|
| 184 |
+
self.servers = [server]
|
| 185 |
+
|
| 186 |
+
if sockets is None:
|
| 187 |
+
self._log_started_message(listeners)
|
| 188 |
+
else:
|
| 189 |
+
# We're most likely running multiple workers, so a message has already been
|
| 190 |
+
# logged by `config.bind_socket()`.
|
| 191 |
+
pass # pragma: full coverage
|
| 192 |
+
|
| 193 |
+
self.started = True
|
| 194 |
+
|
| 195 |
+
def _log_started_message(self, listeners: Sequence[socket.SocketType]) -> None:
|
| 196 |
+
config = self.config
|
| 197 |
+
|
| 198 |
+
if config.fd is not None: # pragma: py-win32
|
| 199 |
+
sock = listeners[0]
|
| 200 |
+
logger.info(
|
| 201 |
+
"Uvicorn running on socket %s (Press CTRL+C to quit)",
|
| 202 |
+
sock.getsockname(),
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
elif config.uds is not None: # pragma: py-win32
|
| 206 |
+
logger.info("Uvicorn running on unix socket %s (Press CTRL+C to quit)", config.uds)
|
| 207 |
+
|
| 208 |
+
else:
|
| 209 |
+
addr_format = "%s://%s:%d"
|
| 210 |
+
host = "0.0.0.0" if config.host is None else config.host
|
| 211 |
+
if ":" in host:
|
| 212 |
+
# It's an IPv6 address.
|
| 213 |
+
addr_format = "%s://[%s]:%d"
|
| 214 |
+
|
| 215 |
+
port = config.port
|
| 216 |
+
if port == 0:
|
| 217 |
+
port = listeners[0].getsockname()[1]
|
| 218 |
+
|
| 219 |
+
protocol_name = "https" if config.ssl else "http"
|
| 220 |
+
message = f"Uvicorn running on {addr_format} (Press CTRL+C to quit)"
|
| 221 |
+
color_message = "Uvicorn running on " + style(addr_format, bold=True) + " (Press CTRL+C to quit)"
|
| 222 |
+
logger.info(
|
| 223 |
+
message,
|
| 224 |
+
protocol_name,
|
| 225 |
+
host,
|
| 226 |
+
port,
|
| 227 |
+
extra={"color_message": color_message},
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
async def main_loop(self) -> None:
|
| 231 |
+
counter = 0
|
| 232 |
+
should_exit = await self.on_tick(counter)
|
| 233 |
+
while not should_exit:
|
| 234 |
+
counter += 1
|
| 235 |
+
counter = counter % 864000
|
| 236 |
+
await asyncio.sleep(0.1)
|
| 237 |
+
should_exit = await self.on_tick(counter)
|
| 238 |
+
|
| 239 |
+
async def on_tick(self, counter: int) -> bool:
|
| 240 |
+
# Update the default headers, once per second.
|
| 241 |
+
if counter % 10 == 0:
|
| 242 |
+
current_time = time.time()
|
| 243 |
+
current_date = formatdate(current_time, usegmt=True).encode()
|
| 244 |
+
|
| 245 |
+
if self.config.date_header:
|
| 246 |
+
date_header = [(b"date", current_date)]
|
| 247 |
+
else:
|
| 248 |
+
date_header = []
|
| 249 |
+
|
| 250 |
+
self.server_state.default_headers = date_header + self.config.encoded_headers
|
| 251 |
+
|
| 252 |
+
# Callback to `callback_notify` once every `timeout_notify` seconds.
|
| 253 |
+
if self.config.callback_notify is not None:
|
| 254 |
+
if current_time - self.last_notified > self.config.timeout_notify: # pragma: full coverage
|
| 255 |
+
self.last_notified = current_time
|
| 256 |
+
await self.config.callback_notify()
|
| 257 |
+
|
| 258 |
+
# Determine if we should exit.
|
| 259 |
+
if self.should_exit:
|
| 260 |
+
return True
|
| 261 |
+
|
| 262 |
+
max_requests = self.limit_max_requests
|
| 263 |
+
if max_requests is not None and self.server_state.total_requests >= max_requests:
|
| 264 |
+
logger.info("Maximum request limit of %d exceeded. Terminating process.", max_requests)
|
| 265 |
+
return True
|
| 266 |
+
|
| 267 |
+
return False
|
| 268 |
+
|
| 269 |
+
async def shutdown(self, sockets: list[socket.socket] | None = None) -> None:
|
| 270 |
+
logger.info("Shutting down")
|
| 271 |
+
|
| 272 |
+
# Stop accepting new connections.
|
| 273 |
+
for server in self.servers:
|
| 274 |
+
server.close()
|
| 275 |
+
for sock in sockets or []:
|
| 276 |
+
sock.close() # pragma: full coverage
|
| 277 |
+
|
| 278 |
+
# Request shutdown on all existing connections.
|
| 279 |
+
for connection in list(self.server_state.connections):
|
| 280 |
+
connection.shutdown()
|
| 281 |
+
await asyncio.sleep(0.1)
|
| 282 |
+
|
| 283 |
+
# When 3.10 is not supported anymore, use `async with asyncio.timeout(...):`.
|
| 284 |
+
try:
|
| 285 |
+
await asyncio.wait_for(
|
| 286 |
+
self._wait_tasks_to_complete(),
|
| 287 |
+
timeout=self.config.timeout_graceful_shutdown,
|
| 288 |
+
)
|
| 289 |
+
except asyncio.TimeoutError:
|
| 290 |
+
logger.error(
|
| 291 |
+
"Cancel %s running task(s), timeout graceful shutdown exceeded",
|
| 292 |
+
len(self.server_state.tasks),
|
| 293 |
+
)
|
| 294 |
+
for t in self.server_state.tasks:
|
| 295 |
+
t.cancel(msg="Task cancelled, timeout graceful shutdown exceeded")
|
| 296 |
+
|
| 297 |
+
# Send the lifespan shutdown event, and wait for application shutdown.
|
| 298 |
+
if not self.force_exit:
|
| 299 |
+
await self.lifespan.shutdown()
|
| 300 |
+
|
| 301 |
+
async def _wait_tasks_to_complete(self) -> None:
|
| 302 |
+
# Wait for existing connections to finish sending responses.
|
| 303 |
+
if self.server_state.connections and not self.force_exit:
|
| 304 |
+
msg = "Waiting for connections to close. (CTRL+C to force quit)"
|
| 305 |
+
logger.info(msg)
|
| 306 |
+
while self.server_state.connections and not self.force_exit:
|
| 307 |
+
await asyncio.sleep(0.1)
|
| 308 |
+
|
| 309 |
+
# Wait for existing tasks to complete.
|
| 310 |
+
if self.server_state.tasks and not self.force_exit:
|
| 311 |
+
msg = "Waiting for background tasks to complete. (CTRL+C to force quit)"
|
| 312 |
+
logger.info(msg)
|
| 313 |
+
while self.server_state.tasks and not self.force_exit:
|
| 314 |
+
await asyncio.sleep(0.1)
|
| 315 |
+
|
| 316 |
+
for server in self.servers:
|
| 317 |
+
await server.wait_closed()
|
| 318 |
+
|
| 319 |
+
@contextlib.contextmanager
|
| 320 |
+
def capture_signals(self) -> Generator[None, None, None]:
|
| 321 |
+
# Signals can only be listened to from the main thread.
|
| 322 |
+
if threading.current_thread() is not threading.main_thread():
|
| 323 |
+
yield
|
| 324 |
+
return
|
| 325 |
+
# always use signal.signal, even if loop.add_signal_handler is available
|
| 326 |
+
# this allows to restore previous signal handlers later on
|
| 327 |
+
original_handlers = {sig: signal.signal(sig, self.handle_exit) for sig in HANDLED_SIGNALS}
|
| 328 |
+
try:
|
| 329 |
+
yield
|
| 330 |
+
finally:
|
| 331 |
+
for sig, handler in original_handlers.items():
|
| 332 |
+
signal.signal(sig, handler)
|
| 333 |
+
# If we did gracefully shut down due to a signal, try to
|
| 334 |
+
# trigger the expected behaviour now; multiple signals would be
|
| 335 |
+
# done LIFO, see https://stackoverflow.com/questions/48434964
|
| 336 |
+
for captured_signal in reversed(self._captured_signals):
|
| 337 |
+
signal.raise_signal(captured_signal)
|
| 338 |
+
|
| 339 |
+
def handle_exit(self, sig: int, frame: FrameType | None) -> None:
|
| 340 |
+
self._captured_signals.append(sig)
|
| 341 |
+
if self.should_exit and sig == signal.SIGINT:
|
| 342 |
+
self.force_exit = True # pragma: full coverage
|
| 343 |
+
else:
|
| 344 |
+
self.should_exit = True
|
.venv/Lib/site-packages/uvicorn/supervisors/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from typing import TYPE_CHECKING
|
| 4 |
+
|
| 5 |
+
from uvicorn.supervisors.basereload import BaseReload
|
| 6 |
+
from uvicorn.supervisors.multiprocess import Multiprocess
|
| 7 |
+
|
| 8 |
+
if TYPE_CHECKING:
|
| 9 |
+
ChangeReload: type[BaseReload]
|
| 10 |
+
else:
|
| 11 |
+
try:
|
| 12 |
+
from uvicorn.supervisors.watchfilesreload import WatchFilesReload as ChangeReload
|
| 13 |
+
except ImportError: # pragma: no cover
|
| 14 |
+
from uvicorn.supervisors.statreload import StatReload as ChangeReload
|
| 15 |
+
|
| 16 |
+
__all__ = ["Multiprocess", "ChangeReload"]
|