Upload folder using huggingface_hub (part 4)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +3 -0
- venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/INSTALLER +1 -0
- venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/METADATA +625 -0
- venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/RECORD +68 -0
- venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/WHEEL +4 -0
- venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/licenses/LICENSE.md +27 -0
- venv/lib/python3.12/site-packages/httpcore/_async/http2.py +592 -0
- venv/lib/python3.12/site-packages/httpcore/_async/http_proxy.py +367 -0
- venv/lib/python3.12/site-packages/httpcore/_async/interfaces.py +137 -0
- venv/lib/python3.12/site-packages/httpcore/_async/socks_proxy.py +341 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/__init__.py +0 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/__init__.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/anyio.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/auto.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/base.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/mock.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/sync.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/trio.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/anyio.py +146 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/auto.py +52 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/base.py +101 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/mock.py +143 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/sync.py +241 -0
- venv/lib/python3.12/site-packages/httpcore/_backends/trio.py +159 -0
- venv/lib/python3.12/site-packages/httpcore/_exceptions.py +81 -0
- venv/lib/python3.12/site-packages/httpcore/_models.py +516 -0
- venv/lib/python3.12/site-packages/httpcore/_ssl.py +9 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/__init__.py +39 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/__init__.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/connection.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/connection_pool.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http11.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http2.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http_proxy.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/interfaces.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/socks_proxy.cpython-312.pyc +0 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/connection.py +222 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py +420 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/http11.py +379 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/http2.py +592 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/http_proxy.py +367 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/interfaces.py +137 -0
- venv/lib/python3.12/site-packages/httpcore/_sync/socks_proxy.py +341 -0
- venv/lib/python3.12/site-packages/httpcore/_synchronization.py +318 -0
- venv/lib/python3.12/site-packages/httpcore/_trace.py +107 -0
- venv/lib/python3.12/site-packages/httpcore/_utils.py +37 -0
- venv/lib/python3.12/site-packages/httpcore/py.typed +0 -0
- venv/lib/python3.12/site-packages/httpx-0.28.1.dist-info/INSTALLER +1 -0
- venv/lib/python3.12/site-packages/httpx-0.28.1.dist-info/METADATA +203 -0
- venv/lib/python3.12/site-packages/httpx-0.28.1.dist-info/RECORD +54 -0
.gitattributes
CHANGED
|
@@ -59,3 +59,6 @@ venv/lib/python3.12/site-packages/cuda/bindings/nvrtc.cpython-312-x86_64-linux-g
|
|
| 59 |
venv/lib/python3.12/site-packages/cuda/bindings/nvvm.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 60 |
venv/lib/python3.12/site-packages/cuda/bindings/runtime.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 61 |
venv/lib/python3.12/site-packages/hf_xet/hf_xet.abi3.so filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
venv/lib/python3.12/site-packages/cuda/bindings/nvvm.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 60 |
venv/lib/python3.12/site-packages/cuda/bindings/runtime.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 61 |
venv/lib/python3.12/site-packages/hf_xet/hf_xet.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 62 |
+
venv/lib/python3.12/site-packages/huggingface_hub/__pycache__/hf_api.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 63 |
+
venv/lib/python3.12/site-packages/huggingface_hub/inference/__pycache__/_client.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 64 |
+
venv/lib/python3.12/site-packages/huggingface_hub/inference/_generated/__pycache__/_async_client.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/METADATA
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.4
|
| 2 |
+
Name: httpcore
|
| 3 |
+
Version: 1.0.9
|
| 4 |
+
Summary: A minimal low-level HTTP client.
|
| 5 |
+
Project-URL: Documentation, https://www.encode.io/httpcore
|
| 6 |
+
Project-URL: Homepage, https://www.encode.io/httpcore/
|
| 7 |
+
Project-URL: Source, https://github.com/encode/httpcore
|
| 8 |
+
Author-email: Tom Christie <tom@tomchristie.com>
|
| 9 |
+
License-Expression: BSD-3-Clause
|
| 10 |
+
License-File: LICENSE.md
|
| 11 |
+
Classifier: Development Status :: 3 - Alpha
|
| 12 |
+
Classifier: Environment :: Web Environment
|
| 13 |
+
Classifier: Framework :: AsyncIO
|
| 14 |
+
Classifier: Framework :: Trio
|
| 15 |
+
Classifier: Intended Audience :: Developers
|
| 16 |
+
Classifier: License :: OSI Approved :: BSD License
|
| 17 |
+
Classifier: Operating System :: OS Independent
|
| 18 |
+
Classifier: Programming Language :: Python :: 3
|
| 19 |
+
Classifier: Programming Language :: Python :: 3 :: Only
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.8
|
| 21 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 22 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 23 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 24 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 25 |
+
Classifier: Topic :: Internet :: WWW/HTTP
|
| 26 |
+
Requires-Python: >=3.8
|
| 27 |
+
Requires-Dist: certifi
|
| 28 |
+
Requires-Dist: h11>=0.16
|
| 29 |
+
Provides-Extra: asyncio
|
| 30 |
+
Requires-Dist: anyio<5.0,>=4.0; extra == 'asyncio'
|
| 31 |
+
Provides-Extra: http2
|
| 32 |
+
Requires-Dist: h2<5,>=3; extra == 'http2'
|
| 33 |
+
Provides-Extra: socks
|
| 34 |
+
Requires-Dist: socksio==1.*; extra == 'socks'
|
| 35 |
+
Provides-Extra: trio
|
| 36 |
+
Requires-Dist: trio<1.0,>=0.22.0; extra == 'trio'
|
| 37 |
+
Description-Content-Type: text/markdown
|
| 38 |
+
|
| 39 |
+
# HTTP Core
|
| 40 |
+
|
| 41 |
+
[](https://github.com/encode/httpcore/actions)
|
| 42 |
+
[](https://pypi.org/project/httpcore/)
|
| 43 |
+
|
| 44 |
+
> *Do one thing, and do it well.*
|
| 45 |
+
|
| 46 |
+
The HTTP Core package provides a minimal low-level HTTP client, which does
|
| 47 |
+
one thing only. Sending HTTP requests.
|
| 48 |
+
|
| 49 |
+
It does not provide any high level model abstractions over the API,
|
| 50 |
+
does not handle redirects, multipart uploads, building authentication headers,
|
| 51 |
+
transparent HTTP caching, URL parsing, session cookie handling,
|
| 52 |
+
content or charset decoding, handling JSON, environment based configuration
|
| 53 |
+
defaults, or any of that Jazz.
|
| 54 |
+
|
| 55 |
+
Some things HTTP Core does do:
|
| 56 |
+
|
| 57 |
+
* Sending HTTP requests.
|
| 58 |
+
* Thread-safe / task-safe connection pooling.
|
| 59 |
+
* HTTP(S) proxy & SOCKS proxy support.
|
| 60 |
+
* Supports HTTP/1.1 and HTTP/2.
|
| 61 |
+
* Provides both sync and async interfaces.
|
| 62 |
+
* Async backend support for `asyncio` and `trio`.
|
| 63 |
+
|
| 64 |
+
## Requirements
|
| 65 |
+
|
| 66 |
+
Python 3.8+
|
| 67 |
+
|
| 68 |
+
## Installation
|
| 69 |
+
|
| 70 |
+
For HTTP/1.1 only support, install with:
|
| 71 |
+
|
| 72 |
+
```shell
|
| 73 |
+
$ pip install httpcore
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
There are also a number of optional extras available...
|
| 77 |
+
|
| 78 |
+
```shell
|
| 79 |
+
$ pip install httpcore['asyncio,trio,http2,socks']
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
## Sending requests
|
| 83 |
+
|
| 84 |
+
Send an HTTP request:
|
| 85 |
+
|
| 86 |
+
```python
|
| 87 |
+
import httpcore
|
| 88 |
+
|
| 89 |
+
response = httpcore.request("GET", "https://www.example.com/")
|
| 90 |
+
|
| 91 |
+
print(response)
|
| 92 |
+
# <Response [200]>
|
| 93 |
+
print(response.status)
|
| 94 |
+
# 200
|
| 95 |
+
print(response.headers)
|
| 96 |
+
# [(b'Accept-Ranges', b'bytes'), (b'Age', b'557328'), (b'Cache-Control', b'max-age=604800'), ...]
|
| 97 |
+
print(response.content)
|
| 98 |
+
# b'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>\n\n<meta charset="utf-8"/>\n ...'
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
The top-level `httpcore.request()` function is provided for convenience. In practice whenever you're working with `httpcore` you'll want to use the connection pooling functionality that it provides.
|
| 102 |
+
|
| 103 |
+
```python
|
| 104 |
+
import httpcore
|
| 105 |
+
|
| 106 |
+
http = httpcore.ConnectionPool()
|
| 107 |
+
response = http.request("GET", "https://www.example.com/")
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
Once you're ready to get going, [head over to the documentation](https://www.encode.io/httpcore/).
|
| 111 |
+
|
| 112 |
+
## Motivation
|
| 113 |
+
|
| 114 |
+
You *probably* don't want to be using HTTP Core directly. It might make sense if
|
| 115 |
+
you're writing something like a proxy service in Python, and you just want
|
| 116 |
+
something at the lowest possible level, but more typically you'll want to use
|
| 117 |
+
a higher level client library, such as `httpx`.
|
| 118 |
+
|
| 119 |
+
The motivation for `httpcore` is:
|
| 120 |
+
|
| 121 |
+
* To provide a reusable low-level client library, that other packages can then build on top of.
|
| 122 |
+
* To provide a *really clear interface split* between the networking code and client logic,
|
| 123 |
+
so that each is easier to understand and reason about in isolation.
|
| 124 |
+
|
| 125 |
+
## Dependencies
|
| 126 |
+
|
| 127 |
+
The `httpcore` package has the following dependencies...
|
| 128 |
+
|
| 129 |
+
* `h11`
|
| 130 |
+
* `certifi`
|
| 131 |
+
|
| 132 |
+
And the following optional extras...
|
| 133 |
+
|
| 134 |
+
* `anyio` - Required by `pip install httpcore['asyncio']`.
|
| 135 |
+
* `trio` - Required by `pip install httpcore['trio']`.
|
| 136 |
+
* `h2` - Required by `pip install httpcore['http2']`.
|
| 137 |
+
* `socksio` - Required by `pip install httpcore['socks']`.
|
| 138 |
+
|
| 139 |
+
## Versioning
|
| 140 |
+
|
| 141 |
+
We use [SEMVER for our versioning policy](https://semver.org/).
|
| 142 |
+
|
| 143 |
+
For changes between package versions please see our [project changelog](CHANGELOG.md).
|
| 144 |
+
|
| 145 |
+
We recommend pinning your requirements either the most current major version, or a more specific version range:
|
| 146 |
+
|
| 147 |
+
```python
|
| 148 |
+
pip install 'httpcore==1.*'
|
| 149 |
+
```
|
| 150 |
+
# Changelog
|
| 151 |
+
|
| 152 |
+
All notable changes to this project will be documented in this file.
|
| 153 |
+
|
| 154 |
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
| 155 |
+
|
| 156 |
+
## Version 1.0.9 (April 24th, 2025)
|
| 157 |
+
|
| 158 |
+
- Resolve https://github.com/advisories/GHSA-vqfr-h8mv-ghfj with h11 dependency update. (#1008)
|
| 159 |
+
|
| 160 |
+
## Version 1.0.8 (April 11th, 2025)
|
| 161 |
+
|
| 162 |
+
- Fix `AttributeError` when importing on Python 3.14. (#1005)
|
| 163 |
+
|
| 164 |
+
## Version 1.0.7 (November 15th, 2024)
|
| 165 |
+
|
| 166 |
+
- Support `proxy=…` configuration on `ConnectionPool()`. (#974)
|
| 167 |
+
|
| 168 |
+
## Version 1.0.6 (October 1st, 2024)
|
| 169 |
+
|
| 170 |
+
- Relax `trio` dependency pinning. (#956)
|
| 171 |
+
- Handle `trio` raising `NotImplementedError` on unsupported platforms. (#955)
|
| 172 |
+
- Handle mapping `ssl.SSLError` to `httpcore.ConnectError`. (#918)
|
| 173 |
+
|
| 174 |
+
## 1.0.5 (March 27th, 2024)
|
| 175 |
+
|
| 176 |
+
- Handle `EndOfStream` exception for anyio backend. (#899)
|
| 177 |
+
- Allow trio `0.25.*` series in package dependancies. (#903)
|
| 178 |
+
|
| 179 |
+
## 1.0.4 (February 21st, 2024)
|
| 180 |
+
|
| 181 |
+
- Add `target` request extension. (#888)
|
| 182 |
+
- Fix support for connection `Upgrade` and `CONNECT` when some data in the stream has been read. (#882)
|
| 183 |
+
|
| 184 |
+
## 1.0.3 (February 13th, 2024)
|
| 185 |
+
|
| 186 |
+
- Fix support for async cancellations. (#880)
|
| 187 |
+
- Fix trace extension when used with socks proxy. (#849)
|
| 188 |
+
- Fix SSL context for connections using the "wss" scheme (#869)
|
| 189 |
+
|
| 190 |
+
## 1.0.2 (November 10th, 2023)
|
| 191 |
+
|
| 192 |
+
- Fix `float("inf")` timeouts in `Event.wait` function. (#846)
|
| 193 |
+
|
| 194 |
+
## 1.0.1 (November 3rd, 2023)
|
| 195 |
+
|
| 196 |
+
- Fix pool timeout to account for the total time spent retrying. (#823)
|
| 197 |
+
- Raise a neater RuntimeError when the correct async deps are not installed. (#826)
|
| 198 |
+
- Add support for synchronous TLS-in-TLS streams. (#840)
|
| 199 |
+
|
| 200 |
+
## 1.0.0 (October 6th, 2023)
|
| 201 |
+
|
| 202 |
+
From version 1.0 our async support is now optional, as the package has minimal dependencies by default.
|
| 203 |
+
|
| 204 |
+
For async support use either `pip install 'httpcore[asyncio]'` or `pip install 'httpcore[trio]'`.
|
| 205 |
+
|
| 206 |
+
The project versioning policy is now explicitly governed by SEMVER. See https://semver.org/.
|
| 207 |
+
|
| 208 |
+
- Async support becomes fully optional. (#809)
|
| 209 |
+
- Add support for Python 3.12. (#807)
|
| 210 |
+
|
| 211 |
+
## 0.18.0 (September 8th, 2023)
|
| 212 |
+
|
| 213 |
+
- Add support for HTTPS proxies. (#745, #786)
|
| 214 |
+
- Drop Python 3.7 support. (#727)
|
| 215 |
+
- Handle `sni_hostname` extension with SOCKS proxy. (#774)
|
| 216 |
+
- Handle HTTP/1.1 half-closed connections gracefully. (#641)
|
| 217 |
+
- Change the type of `Extensions` from `Mapping[Str, Any]` to `MutableMapping[Str, Any]`. (#762)
|
| 218 |
+
|
| 219 |
+
## 0.17.3 (July 5th, 2023)
|
| 220 |
+
|
| 221 |
+
- Support async cancellations, ensuring that the connection pool is left in a clean state when cancellations occur. (#726)
|
| 222 |
+
- The networking backend interface has [been added to the public API](https://www.encode.io/httpcore/network-backends). Some classes which were previously private implementation detail are now part of the top-level public API. (#699)
|
| 223 |
+
- Graceful handling of HTTP/2 GoAway frames, with requests being transparently retried on a new connection. (#730)
|
| 224 |
+
- Add exceptions when a synchronous `trace callback` is passed to an asynchronous request or an asynchronous `trace callback` is passed to a synchronous request. (#717)
|
| 225 |
+
- Drop Python 3.7 support. (#727)
|
| 226 |
+
|
| 227 |
+
## 0.17.2 (May 23th, 2023)
|
| 228 |
+
|
| 229 |
+
- Add `socket_options` argument to `ConnectionPool` and `HTTProxy` classes. (#668)
|
| 230 |
+
- Improve logging with per-module logger names. (#690)
|
| 231 |
+
- Add `sni_hostname` request extension. (#696)
|
| 232 |
+
- Resolve race condition during import of `anyio` package. (#692)
|
| 233 |
+
- Enable TCP_NODELAY for all synchronous sockets. (#651)
|
| 234 |
+
|
| 235 |
+
## 0.17.1 (May 17th, 2023)
|
| 236 |
+
|
| 237 |
+
- If 'retries' is set, then allow retries if an SSL handshake error occurs. (#669)
|
| 238 |
+
- Improve correctness of tracebacks on network exceptions, by raising properly chained exceptions. (#678)
|
| 239 |
+
- Prevent connection-hanging behaviour when HTTP/2 connections are closed by a server-sent 'GoAway' frame. (#679)
|
| 240 |
+
- Fix edge-case exception when removing requests from the connection pool. (#680)
|
| 241 |
+
- Fix pool timeout edge-case. (#688)
|
| 242 |
+
|
| 243 |
+
## 0.17.0 (March 16th, 2023)
|
| 244 |
+
|
| 245 |
+
- Add DEBUG level logging. (#648)
|
| 246 |
+
- Respect HTTP/2 max concurrent streams when settings updates are sent by server. (#652)
|
| 247 |
+
- Increase the allowable HTTP header size to 100kB. (#647)
|
| 248 |
+
- Add `retries` option to SOCKS proxy classes. (#643)
|
| 249 |
+
|
| 250 |
+
## 0.16.3 (December 20th, 2022)
|
| 251 |
+
|
| 252 |
+
- Allow `ws` and `wss` schemes. Allows us to properly support websocket upgrade connections. (#625)
|
| 253 |
+
- Forwarding HTTP proxies use a connection-per-remote-host. Required by some proxy implementations. (#637)
|
| 254 |
+
- Don't raise `RuntimeError` when closing a connection pool with active connections. Removes some error cases when cancellations are used. (#631)
|
| 255 |
+
- Lazy import `anyio`, so that it's no longer a hard dependancy, and isn't imported if unused. (#639)
|
| 256 |
+
|
| 257 |
+
## 0.16.2 (November 25th, 2022)
|
| 258 |
+
|
| 259 |
+
- Revert 'Fix async cancellation behaviour', which introduced race conditions. (#627)
|
| 260 |
+
- Raise `RuntimeError` if attempting to us UNIX domain sockets on Windows. (#619)
|
| 261 |
+
|
| 262 |
+
## 0.16.1 (November 17th, 2022)
|
| 263 |
+
|
| 264 |
+
- Fix HTTP/1.1 interim informational responses, such as "100 Continue". (#605)
|
| 265 |
+
|
| 266 |
+
## 0.16.0 (October 11th, 2022)
|
| 267 |
+
|
| 268 |
+
- Support HTTP/1.1 informational responses. (#581)
|
| 269 |
+
- Fix async cancellation behaviour. (#580)
|
| 270 |
+
- Support `h11` 0.14. (#579)
|
| 271 |
+
|
| 272 |
+
## 0.15.0 (May 17th, 2022)
|
| 273 |
+
|
| 274 |
+
- Drop Python 3.6 support (#535)
|
| 275 |
+
- Ensure HTTP proxy CONNECT requests include `timeout` configuration. (#506)
|
| 276 |
+
- Switch to explicit `typing.Optional` for type hints. (#513)
|
| 277 |
+
- For `trio` map OSError exceptions to `ConnectError`. (#543)
|
| 278 |
+
|
| 279 |
+
## 0.14.7 (February 4th, 2022)
|
| 280 |
+
|
| 281 |
+
- Requests which raise a PoolTimeout need to be removed from the pool queue. (#502)
|
| 282 |
+
- Fix AttributeError that happened when Socks5Connection were terminated. (#501)
|
| 283 |
+
|
| 284 |
+
## 0.14.6 (February 1st, 2022)
|
| 285 |
+
|
| 286 |
+
- Fix SOCKS support for `http://` URLs. (#492)
|
| 287 |
+
- Resolve race condition around exceptions during streaming a response. (#491)
|
| 288 |
+
|
| 289 |
+
## 0.14.5 (January 18th, 2022)
|
| 290 |
+
|
| 291 |
+
- SOCKS proxy support. (#478)
|
| 292 |
+
- Add proxy_auth argument to HTTPProxy. (#481)
|
| 293 |
+
- Improve error message on 'RemoteProtocolError' exception when server disconnects without sending a response. (#479)
|
| 294 |
+
|
| 295 |
+
## 0.14.4 (January 5th, 2022)
|
| 296 |
+
|
| 297 |
+
- Support HTTP/2 on HTTPS tunnelling proxies. (#468)
|
| 298 |
+
- Fix proxy headers missing on HTTP forwarding. (#456)
|
| 299 |
+
- Only instantiate SSL context if required. (#457)
|
| 300 |
+
- More robust HTTP/2 handling. (#253, #439, #440, #441)
|
| 301 |
+
|
| 302 |
+
## 0.14.3 (November 17th, 2021)
|
| 303 |
+
|
| 304 |
+
- Fix race condition when removing closed connections from the pool. (#437)
|
| 305 |
+
|
| 306 |
+
## 0.14.2 (November 16th, 2021)
|
| 307 |
+
|
| 308 |
+
- Failed connections no longer remain in the pool. (Pull #433)
|
| 309 |
+
|
| 310 |
+
## 0.14.1 (November 12th, 2021)
|
| 311 |
+
|
| 312 |
+
- `max_connections` becomes optional. (Pull #429)
|
| 313 |
+
- `certifi` is now included in the install dependancies. (Pull #428)
|
| 314 |
+
- `h2` is now strictly optional. (Pull #428)
|
| 315 |
+
|
| 316 |
+
## 0.14.0 (November 11th, 2021)
|
| 317 |
+
|
| 318 |
+
The 0.14 release is a complete reworking of `httpcore`, comprehensively addressing some underlying issues in the connection pooling, as well as substantially redesigning the API to be more user friendly.
|
| 319 |
+
|
| 320 |
+
Some of the lower-level API design also makes the components more easily testable in isolation, and the package now has 100% test coverage.
|
| 321 |
+
|
| 322 |
+
See [discussion #419](https://github.com/encode/httpcore/discussions/419) for a little more background.
|
| 323 |
+
|
| 324 |
+
There's some other neat bits in there too, such as the "trace" extension, which gives a hook into inspecting the internal events that occur during the request/response cycle. This extension is needed for the HTTPX cli, in order to...
|
| 325 |
+
|
| 326 |
+
* Log the point at which the connection is established, and the IP/port on which it is made.
|
| 327 |
+
* Determine if the outgoing request should log as HTTP/1.1 or HTTP/2, rather than having to assume it's HTTP/2 if the --http2 flag was passed. (Which may not actually be true.)
|
| 328 |
+
* Log SSL version info / certificate info.
|
| 329 |
+
|
| 330 |
+
Note that `curio` support is not currently available in 0.14.0. If you're using `httpcore` with `curio` please get in touch, so we can assess if we ought to prioritize it as a feature or not.
|
| 331 |
+
|
| 332 |
+
## 0.13.7 (September 13th, 2021)
|
| 333 |
+
|
| 334 |
+
- Fix broken error messaging when URL scheme is missing, or a non HTTP(S) scheme is used. (Pull #403)
|
| 335 |
+
|
| 336 |
+
## 0.13.6 (June 15th, 2021)
|
| 337 |
+
|
| 338 |
+
### Fixed
|
| 339 |
+
|
| 340 |
+
- Close sockets when read or write timeouts occur. (Pull #365)
|
| 341 |
+
|
| 342 |
+
## 0.13.5 (June 14th, 2021)
|
| 343 |
+
|
| 344 |
+
### Fixed
|
| 345 |
+
|
| 346 |
+
- Resolved niggles with AnyIO EOF behaviours. (Pull #358, #362)
|
| 347 |
+
|
| 348 |
+
## 0.13.4 (June 9th, 2021)
|
| 349 |
+
|
| 350 |
+
### Added
|
| 351 |
+
|
| 352 |
+
- Improved error messaging when URL scheme is missing, or a non HTTP(S) scheme is used. (Pull #354)
|
| 353 |
+
|
| 354 |
+
### Fixed
|
| 355 |
+
|
| 356 |
+
- Switched to `anyio` as the default backend implementation when running with `asyncio`. Resolves some awkward [TLS timeout issues](https://github.com/encode/httpx/discussions/1511).
|
| 357 |
+
|
| 358 |
+
## 0.13.3 (May 6th, 2021)
|
| 359 |
+
|
| 360 |
+
### Added
|
| 361 |
+
|
| 362 |
+
- Support HTTP/2 prior knowledge, using `httpcore.SyncConnectionPool(http1=False)`. (Pull #333)
|
| 363 |
+
|
| 364 |
+
### Fixed
|
| 365 |
+
|
| 366 |
+
- Handle cases where environment does not provide `select.poll` support. (Pull #331)
|
| 367 |
+
|
| 368 |
+
## 0.13.2 (April 29th, 2021)
|
| 369 |
+
|
| 370 |
+
### Added
|
| 371 |
+
|
| 372 |
+
- Improve error message for specific case of `RemoteProtocolError` where server disconnects without sending a response. (Pull #313)
|
| 373 |
+
|
| 374 |
+
## 0.13.1 (April 28th, 2021)
|
| 375 |
+
|
| 376 |
+
### Fixed
|
| 377 |
+
|
| 378 |
+
- More resiliant testing for closed connections. (Pull #311)
|
| 379 |
+
- Don't raise exceptions on ungraceful connection closes. (Pull #310)
|
| 380 |
+
|
| 381 |
+
## 0.13.0 (April 21st, 2021)
|
| 382 |
+
|
| 383 |
+
The 0.13 release updates the core API in order to match the HTTPX Transport API,
|
| 384 |
+
introduced in HTTPX 0.18 onwards.
|
| 385 |
+
|
| 386 |
+
An example of making requests with the new interface is:
|
| 387 |
+
|
| 388 |
+
```python
|
| 389 |
+
with httpcore.SyncConnectionPool() as http:
|
| 390 |
+
status_code, headers, stream, extensions = http.handle_request(
|
| 391 |
+
method=b'GET',
|
| 392 |
+
url=(b'https', b'example.org', 443, b'/'),
|
| 393 |
+
headers=[(b'host', b'example.org'), (b'user-agent', b'httpcore')]
|
| 394 |
+
stream=httpcore.ByteStream(b''),
|
| 395 |
+
extensions={}
|
| 396 |
+
)
|
| 397 |
+
body = stream.read()
|
| 398 |
+
print(status_code, body)
|
| 399 |
+
```
|
| 400 |
+
|
| 401 |
+
### Changed
|
| 402 |
+
|
| 403 |
+
- The `.request()` method is now `handle_request()`. (Pull #296)
|
| 404 |
+
- The `.arequest()` method is now `.handle_async_request()`. (Pull #296)
|
| 405 |
+
- The `headers` argument is no longer optional. (Pull #296)
|
| 406 |
+
- The `stream` argument is no longer optional. (Pull #296)
|
| 407 |
+
- The `ext` argument is now named `extensions`, and is no longer optional. (Pull #296)
|
| 408 |
+
- The `"reason"` extension keyword is now named `"reason_phrase"`. (Pull #296)
|
| 409 |
+
- The `"reason_phrase"` and `"http_version"` extensions now use byte strings for their values. (Pull #296)
|
| 410 |
+
- The `httpcore.PlainByteStream()` class becomes `httpcore.ByteStream()`. (Pull #296)
|
| 411 |
+
|
| 412 |
+
### Added
|
| 413 |
+
|
| 414 |
+
- Streams now support a `.read()` interface. (Pull #296)
|
| 415 |
+
|
| 416 |
+
### Fixed
|
| 417 |
+
|
| 418 |
+
- Task cancellation no longer leaks connections from the connection pool. (Pull #305)
|
| 419 |
+
|
| 420 |
+
## 0.12.3 (December 7th, 2020)
|
| 421 |
+
|
| 422 |
+
### Fixed
|
| 423 |
+
|
| 424 |
+
- Abort SSL connections on close rather than waiting for remote EOF when using `asyncio`. (Pull #167)
|
| 425 |
+
- Fix exception raised in case of connect timeouts when using the `anyio` backend. (Pull #236)
|
| 426 |
+
- Fix `Host` header precedence for `:authority` in HTTP/2. (Pull #241, #243)
|
| 427 |
+
- Handle extra edge case when detecting for socket readability when using `asyncio`. (Pull #242, #244)
|
| 428 |
+
- Fix `asyncio` SSL warning when using proxy tunneling. (Pull #249)
|
| 429 |
+
|
| 430 |
+
## 0.12.2 (November 20th, 2020)
|
| 431 |
+
|
| 432 |
+
### Fixed
|
| 433 |
+
|
| 434 |
+
- Properly wrap connect errors on the asyncio backend. (Pull #235)
|
| 435 |
+
- Fix `ImportError` occurring on Python 3.9 when using the HTTP/1.1 sync client in a multithreaded context. (Pull #237)
|
| 436 |
+
|
| 437 |
+
## 0.12.1 (November 7th, 2020)
|
| 438 |
+
|
| 439 |
+
### Added
|
| 440 |
+
|
| 441 |
+
- Add connect retries. (Pull #221)
|
| 442 |
+
|
| 443 |
+
### Fixed
|
| 444 |
+
|
| 445 |
+
- Tweak detection of dropped connections, resolving an issue with open files limits on Linux. (Pull #185)
|
| 446 |
+
- Avoid leaking connections when establishing an HTTP tunnel to a proxy has failed. (Pull #223)
|
| 447 |
+
- Properly wrap OS errors when using `trio`. (Pull #225)
|
| 448 |
+
|
| 449 |
+
## 0.12.0 (October 6th, 2020)
|
| 450 |
+
|
| 451 |
+
### Changed
|
| 452 |
+
|
| 453 |
+
- HTTP header casing is now preserved, rather than always sent in lowercase. (#216 and python-hyper/h11#104)
|
| 454 |
+
|
| 455 |
+
### Added
|
| 456 |
+
|
| 457 |
+
- Add Python 3.9 to officially supported versions.
|
| 458 |
+
|
| 459 |
+
### Fixed
|
| 460 |
+
|
| 461 |
+
- Gracefully handle a stdlib asyncio bug when a connection is closed while it is in a paused-for-reading state. (#201)
|
| 462 |
+
|
| 463 |
+
## 0.11.1 (September 28nd, 2020)
|
| 464 |
+
|
| 465 |
+
### Fixed
|
| 466 |
+
|
| 467 |
+
- Add await to async semaphore release() coroutine (#197)
|
| 468 |
+
- Drop incorrect curio classifier (#192)
|
| 469 |
+
|
| 470 |
+
## 0.11.0 (September 22nd, 2020)
|
| 471 |
+
|
| 472 |
+
The Transport API with 0.11.0 has a couple of significant changes.
|
| 473 |
+
|
| 474 |
+
Firstly we've moved changed the request interface in order to allow extensions, which will later enable us to support features
|
| 475 |
+
such as trailing headers, HTTP/2 server push, and CONNECT/Upgrade connections.
|
| 476 |
+
|
| 477 |
+
The interface changes from:
|
| 478 |
+
|
| 479 |
+
```python
|
| 480 |
+
def request(method, url, headers, stream, timeout):
|
| 481 |
+
return (http_version, status_code, reason, headers, stream)
|
| 482 |
+
```
|
| 483 |
+
|
| 484 |
+
To instead including an optional dictionary of extensions on the request and response:
|
| 485 |
+
|
| 486 |
+
```python
|
| 487 |
+
def request(method, url, headers, stream, ext):
|
| 488 |
+
return (status_code, headers, stream, ext)
|
| 489 |
+
```
|
| 490 |
+
|
| 491 |
+
Having an open-ended extensions point will allow us to add later support for various optional features, that wouldn't otherwise be supported without these API changes.
|
| 492 |
+
|
| 493 |
+
In particular:
|
| 494 |
+
|
| 495 |
+
* Trailing headers support.
|
| 496 |
+
* HTTP/2 Server Push
|
| 497 |
+
* sendfile.
|
| 498 |
+
* Exposing raw connection on CONNECT, Upgrade, HTTP/2 bi-di streaming.
|
| 499 |
+
* Exposing debug information out of the API, including template name, template context.
|
| 500 |
+
|
| 501 |
+
Currently extensions are limited to:
|
| 502 |
+
|
| 503 |
+
* request: `timeout` - Optional. Timeout dictionary.
|
| 504 |
+
* response: `http_version` - Optional. Include the HTTP version used on the response.
|
| 505 |
+
* response: `reason` - Optional. Include the reason phrase used on the response. Only valid with HTTP/1.*.
|
| 506 |
+
|
| 507 |
+
See https://github.com/encode/httpx/issues/1274#issuecomment-694884553 for the history behind this.
|
| 508 |
+
|
| 509 |
+
Secondly, the async version of `request` is now namespaced as `arequest`.
|
| 510 |
+
|
| 511 |
+
This allows concrete transports to support both sync and async implementations on the same class.
|
| 512 |
+
|
| 513 |
+
### Added
|
| 514 |
+
|
| 515 |
+
- Add curio support. (Pull #168)
|
| 516 |
+
- Add anyio support, with `backend="anyio"`. (Pull #169)
|
| 517 |
+
|
| 518 |
+
### Changed
|
| 519 |
+
|
| 520 |
+
- Update the Transport API to use 'ext' for optional extensions. (Pull #190)
|
| 521 |
+
- Update the Transport API to use `.request` and `.arequest` so implementations can support both sync and async. (Pull #189)
|
| 522 |
+
|
| 523 |
+
## 0.10.2 (August 20th, 2020)
|
| 524 |
+
|
| 525 |
+
### Added
|
| 526 |
+
|
| 527 |
+
- Added Unix Domain Socket support. (Pull #139)
|
| 528 |
+
|
| 529 |
+
### Fixed
|
| 530 |
+
|
| 531 |
+
- Always include the port on proxy CONNECT requests. (Pull #154)
|
| 532 |
+
- Fix `max_keepalive_connections` configuration. (Pull #153)
|
| 533 |
+
- Fixes behaviour in HTTP/1.1 where server disconnects can be used to signal the end of the response body. (Pull #164)
|
| 534 |
+
|
| 535 |
+
## 0.10.1 (August 7th, 2020)
|
| 536 |
+
|
| 537 |
+
- Include `max_keepalive_connections` on `AsyncHTTPProxy`/`SyncHTTPProxy` classes.
|
| 538 |
+
|
| 539 |
+
## 0.10.0 (August 7th, 2020)
|
| 540 |
+
|
| 541 |
+
The most notable change in the 0.10.0 release is that HTTP/2 support is now fully optional.
|
| 542 |
+
|
| 543 |
+
Use either `pip install httpcore` for HTTP/1.1 support only, or `pip install httpcore[http2]` for HTTP/1.1 and HTTP/2 support.
|
| 544 |
+
|
| 545 |
+
### Added
|
| 546 |
+
|
| 547 |
+
- HTTP/2 support becomes optional. (Pull #121, #130)
|
| 548 |
+
- Add `local_address=...` support. (Pull #100, #134)
|
| 549 |
+
- Add `PlainByteStream`, `IteratorByteStream`, `AsyncIteratorByteStream`. The `AsyncByteSteam` and `SyncByteStream` classes are now pure interface classes. (#133)
|
| 550 |
+
- Add `LocalProtocolError`, `RemoteProtocolError` exceptions. (Pull #129)
|
| 551 |
+
- Add `UnsupportedProtocol` exception. (Pull #128)
|
| 552 |
+
- Add `.get_connection_info()` method. (Pull #102, #137)
|
| 553 |
+
- Add better TRACE logs. (Pull #101)
|
| 554 |
+
|
| 555 |
+
### Changed
|
| 556 |
+
|
| 557 |
+
- `max_keepalive` is deprecated in favour of `max_keepalive_connections`. (Pull #140)
|
| 558 |
+
|
| 559 |
+
### Fixed
|
| 560 |
+
|
| 561 |
+
- Improve handling of server disconnects. (Pull #112)
|
| 562 |
+
|
| 563 |
+
## 0.9.1 (May 27th, 2020)
|
| 564 |
+
|
| 565 |
+
### Fixed
|
| 566 |
+
|
| 567 |
+
- Proper host resolution for sync case, including IPv6 support. (Pull #97)
|
| 568 |
+
- Close outstanding connections when connection pool is closed. (Pull #98)
|
| 569 |
+
|
| 570 |
+
## 0.9.0 (May 21th, 2020)
|
| 571 |
+
|
| 572 |
+
### Changed
|
| 573 |
+
|
| 574 |
+
- URL port becomes an `Optional[int]` instead of `int`. (Pull #92)
|
| 575 |
+
|
| 576 |
+
### Fixed
|
| 577 |
+
|
| 578 |
+
- Honor HTTP/2 max concurrent streams settings. (Pull #89, #90)
|
| 579 |
+
- Remove incorrect debug log. (Pull #83)
|
| 580 |
+
|
| 581 |
+
## 0.8.4 (May 11th, 2020)
|
| 582 |
+
|
| 583 |
+
### Added
|
| 584 |
+
|
| 585 |
+
- Logging via HTTPCORE_LOG_LEVEL and HTTPX_LOG_LEVEL environment variables
|
| 586 |
+
and TRACE level logging. (Pull #79)
|
| 587 |
+
|
| 588 |
+
### Fixed
|
| 589 |
+
|
| 590 |
+
- Reuse of connections on HTTP/2 in close concurrency situations. (Pull #81)
|
| 591 |
+
|
| 592 |
+
## 0.8.3 (May 6rd, 2020)
|
| 593 |
+
|
| 594 |
+
### Fixed
|
| 595 |
+
|
| 596 |
+
- Include `Host` and `Accept` headers on proxy "CONNECT" requests.
|
| 597 |
+
- De-duplicate any headers also contained in proxy_headers.
|
| 598 |
+
- HTTP/2 flag not being passed down to proxy connections.
|
| 599 |
+
|
| 600 |
+
## 0.8.2 (May 3rd, 2020)
|
| 601 |
+
|
| 602 |
+
### Fixed
|
| 603 |
+
|
| 604 |
+
- Fix connections using proxy forwarding requests not being added to the
|
| 605 |
+
connection pool properly. (Pull #70)
|
| 606 |
+
|
| 607 |
+
## 0.8.1 (April 30th, 2020)
|
| 608 |
+
|
| 609 |
+
### Changed
|
| 610 |
+
|
| 611 |
+
- Allow inherintance of both `httpcore.AsyncByteStream`, `httpcore.SyncByteStream` without type conflicts.
|
| 612 |
+
|
| 613 |
+
## 0.8.0 (April 30th, 2020)
|
| 614 |
+
|
| 615 |
+
### Fixed
|
| 616 |
+
|
| 617 |
+
- Fixed tunnel proxy support.
|
| 618 |
+
|
| 619 |
+
### Added
|
| 620 |
+
|
| 621 |
+
- New `TimeoutException` base class.
|
| 622 |
+
|
| 623 |
+
## 0.7.0 (March 5th, 2020)
|
| 624 |
+
|
| 625 |
+
- First integration with HTTPX.
|
venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/RECORD
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
httpcore-1.0.9.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
httpcore-1.0.9.dist-info/METADATA,sha256=_i1P2mGZEol4d54M8n88BFxTGGP83Zh-rMdPOhjUHCE,21529
|
| 3 |
+
httpcore-1.0.9.dist-info/RECORD,,
|
| 4 |
+
httpcore-1.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
| 5 |
+
httpcore-1.0.9.dist-info/licenses/LICENSE.md,sha256=_ctZFUx0y6uhahEkL3dAvqnyPW_rVUeRfYxflKgDkqU,1518
|
| 6 |
+
httpcore/__init__.py,sha256=9kT_kqChCCJUTHww24ZmR_ezcdbpRYWksD-gYNzkZP8,3445
|
| 7 |
+
httpcore/__pycache__/__init__.cpython-312.pyc,,
|
| 8 |
+
httpcore/__pycache__/_api.cpython-312.pyc,,
|
| 9 |
+
httpcore/__pycache__/_exceptions.cpython-312.pyc,,
|
| 10 |
+
httpcore/__pycache__/_models.cpython-312.pyc,,
|
| 11 |
+
httpcore/__pycache__/_ssl.cpython-312.pyc,,
|
| 12 |
+
httpcore/__pycache__/_synchronization.cpython-312.pyc,,
|
| 13 |
+
httpcore/__pycache__/_trace.cpython-312.pyc,,
|
| 14 |
+
httpcore/__pycache__/_utils.cpython-312.pyc,,
|
| 15 |
+
httpcore/_api.py,sha256=unZmeDschBWCGCPCwkS3Wot9euK6bg_kKxLtGTxw214,3146
|
| 16 |
+
httpcore/_async/__init__.py,sha256=EWdl2v4thnAHzJpqjU4h2a8DUiGAvNiWrkii9pfhTf0,1221
|
| 17 |
+
httpcore/_async/__pycache__/__init__.cpython-312.pyc,,
|
| 18 |
+
httpcore/_async/__pycache__/connection.cpython-312.pyc,,
|
| 19 |
+
httpcore/_async/__pycache__/connection_pool.cpython-312.pyc,,
|
| 20 |
+
httpcore/_async/__pycache__/http11.cpython-312.pyc,,
|
| 21 |
+
httpcore/_async/__pycache__/http2.cpython-312.pyc,,
|
| 22 |
+
httpcore/_async/__pycache__/http_proxy.cpython-312.pyc,,
|
| 23 |
+
httpcore/_async/__pycache__/interfaces.cpython-312.pyc,,
|
| 24 |
+
httpcore/_async/__pycache__/socks_proxy.cpython-312.pyc,,
|
| 25 |
+
httpcore/_async/connection.py,sha256=6OcPXqMEfc0BU38_-iHUNDd1vKSTc2UVT09XqNb_BOk,8449
|
| 26 |
+
httpcore/_async/connection_pool.py,sha256=DOIQ2s2ZCf9qfwxhzMprTPLqCL8OxGXiKF6qRHxvVyY,17307
|
| 27 |
+
httpcore/_async/http11.py,sha256=-qM9bV7PjSQF5vxs37-eUXOIFwbIjPcZbNliuX9TtBw,13880
|
| 28 |
+
httpcore/_async/http2.py,sha256=azX1fcmtXaIwjputFlZ4vd92J8xwjGOa9ax9QIv4394,23936
|
| 29 |
+
httpcore/_async/http_proxy.py,sha256=2zVkrlv-Ds-rWGaqaXlrhEJiAQFPo23BT3Gq_sWoBXU,14701
|
| 30 |
+
httpcore/_async/interfaces.py,sha256=jTiaWL83pgpGC9ziv90ZfwaKNMmHwmOalzaKiuTxATo,4455
|
| 31 |
+
httpcore/_async/socks_proxy.py,sha256=lLKgLlggPfhFlqi0ODeBkOWvt9CghBBUyqsnsU1tx6Q,13841
|
| 32 |
+
httpcore/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 33 |
+
httpcore/_backends/__pycache__/__init__.cpython-312.pyc,,
|
| 34 |
+
httpcore/_backends/__pycache__/anyio.cpython-312.pyc,,
|
| 35 |
+
httpcore/_backends/__pycache__/auto.cpython-312.pyc,,
|
| 36 |
+
httpcore/_backends/__pycache__/base.cpython-312.pyc,,
|
| 37 |
+
httpcore/_backends/__pycache__/mock.cpython-312.pyc,,
|
| 38 |
+
httpcore/_backends/__pycache__/sync.cpython-312.pyc,,
|
| 39 |
+
httpcore/_backends/__pycache__/trio.cpython-312.pyc,,
|
| 40 |
+
httpcore/_backends/anyio.py,sha256=x8PgEhXRC8bVqsdzk_YJx8Y6d9Tub06CuUSwnbmtqoY,5252
|
| 41 |
+
httpcore/_backends/auto.py,sha256=zO136PKZmsaTDK-HRk84eA-MUg8_2wJf4NvmK432Aio,1662
|
| 42 |
+
httpcore/_backends/base.py,sha256=aShgRdZnMmRhFWHetjumlM73f8Kz1YOAyCUP_4kHslA,3042
|
| 43 |
+
httpcore/_backends/mock.py,sha256=er9T436uSe7NLrfiLa4x6Nuqg5ivQ693CxWYCWsgbH4,4077
|
| 44 |
+
httpcore/_backends/sync.py,sha256=bhE4d9iK9Umxdsdsgm2EfKnXaBms2WggGYU-7jmUujU,7977
|
| 45 |
+
httpcore/_backends/trio.py,sha256=LHu4_Mr5MswQmmT3yE4oLgf9b_JJfeVS4BjDxeJc7Ro,5996
|
| 46 |
+
httpcore/_exceptions.py,sha256=looCKga3_YVYu3s-d3L9RMPRJyhsY7fiuuGxvkOD0c0,1184
|
| 47 |
+
httpcore/_models.py,sha256=IO2CcXcdpovRcLTdGFGB6RyBZdEm2h_TOmoCc4rEKho,17623
|
| 48 |
+
httpcore/_ssl.py,sha256=srqmSNU4iOUvWF-SrJvb8G_YEbHFELOXQOwdDIBTS9c,187
|
| 49 |
+
httpcore/_sync/__init__.py,sha256=JBDIgXt5la1LCJ1sLQeKhjKFpLnpNr8Svs6z2ni3fgg,1141
|
| 50 |
+
httpcore/_sync/__pycache__/__init__.cpython-312.pyc,,
|
| 51 |
+
httpcore/_sync/__pycache__/connection.cpython-312.pyc,,
|
| 52 |
+
httpcore/_sync/__pycache__/connection_pool.cpython-312.pyc,,
|
| 53 |
+
httpcore/_sync/__pycache__/http11.cpython-312.pyc,,
|
| 54 |
+
httpcore/_sync/__pycache__/http2.cpython-312.pyc,,
|
| 55 |
+
httpcore/_sync/__pycache__/http_proxy.cpython-312.pyc,,
|
| 56 |
+
httpcore/_sync/__pycache__/interfaces.cpython-312.pyc,,
|
| 57 |
+
httpcore/_sync/__pycache__/socks_proxy.cpython-312.pyc,,
|
| 58 |
+
httpcore/_sync/connection.py,sha256=9exGOb3PB-Mp2T1-sckSeL2t-tJ_9-NXomV8ihmWCgU,8238
|
| 59 |
+
httpcore/_sync/connection_pool.py,sha256=a-T8LTsUxc7r0Ww1atfHSDoWPjQ0fA8Ul7S3-F0Mj70,16955
|
| 60 |
+
httpcore/_sync/http11.py,sha256=IFobD1Md5JFlJGKWnh1_Q3epikUryI8qo09v8MiJIEA,13476
|
| 61 |
+
httpcore/_sync/http2.py,sha256=AxU4yhcq68Bn5vqdJYtiXKYUj7nvhYbxz3v4rT4xnvA,23400
|
| 62 |
+
httpcore/_sync/http_proxy.py,sha256=_al_6crKuEZu2wyvu493RZImJdBJnj5oGKNjLOJL2Zo,14463
|
| 63 |
+
httpcore/_sync/interfaces.py,sha256=snXON42vUDHO5JBJvo8D4VWk2Wat44z2OXXHDrjbl94,4344
|
| 64 |
+
httpcore/_sync/socks_proxy.py,sha256=zegZW9Snqj2_992DFJa8_CppOVBkVL4AgwduRkStakQ,13614
|
| 65 |
+
httpcore/_synchronization.py,sha256=zSi13mAColBnknjZBknUC6hKNDQT4C6ijnezZ-r0T2s,9434
|
| 66 |
+
httpcore/_trace.py,sha256=ck6ZoIzYTkdNAIfq5MGeKqBXDtqjOX-qfYwmZFbrGco,3952
|
| 67 |
+
httpcore/_utils.py,sha256=_RLgXYOAYC350ikALV59GZ68IJrdocRZxPs9PjmzdFY,1537
|
| 68 |
+
httpcore/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: hatchling 1.27.0
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
venv/lib/python3.12/site-packages/httpcore-1.0.9.dist-info/licenses/LICENSE.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright © 2020, [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/python3.12/site-packages/httpcore/_async/http2.py
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import enum
|
| 4 |
+
import logging
|
| 5 |
+
import time
|
| 6 |
+
import types
|
| 7 |
+
import typing
|
| 8 |
+
|
| 9 |
+
import h2.config
|
| 10 |
+
import h2.connection
|
| 11 |
+
import h2.events
|
| 12 |
+
import h2.exceptions
|
| 13 |
+
import h2.settings
|
| 14 |
+
|
| 15 |
+
from .._backends.base import AsyncNetworkStream
|
| 16 |
+
from .._exceptions import (
|
| 17 |
+
ConnectionNotAvailable,
|
| 18 |
+
LocalProtocolError,
|
| 19 |
+
RemoteProtocolError,
|
| 20 |
+
)
|
| 21 |
+
from .._models import Origin, Request, Response
|
| 22 |
+
from .._synchronization import AsyncLock, AsyncSemaphore, AsyncShieldCancellation
|
| 23 |
+
from .._trace import Trace
|
| 24 |
+
from .interfaces import AsyncConnectionInterface
|
| 25 |
+
|
| 26 |
+
logger = logging.getLogger("httpcore.http2")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def has_body_headers(request: Request) -> bool:
|
| 30 |
+
return any(
|
| 31 |
+
k.lower() == b"content-length" or k.lower() == b"transfer-encoding"
|
| 32 |
+
for k, v in request.headers
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class HTTPConnectionState(enum.IntEnum):
|
| 37 |
+
ACTIVE = 1
|
| 38 |
+
IDLE = 2
|
| 39 |
+
CLOSED = 3
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class AsyncHTTP2Connection(AsyncConnectionInterface):
|
| 43 |
+
READ_NUM_BYTES = 64 * 1024
|
| 44 |
+
CONFIG = h2.config.H2Configuration(validate_inbound_headers=False)
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
origin: Origin,
|
| 49 |
+
stream: AsyncNetworkStream,
|
| 50 |
+
keepalive_expiry: float | None = None,
|
| 51 |
+
):
|
| 52 |
+
self._origin = origin
|
| 53 |
+
self._network_stream = stream
|
| 54 |
+
self._keepalive_expiry: float | None = keepalive_expiry
|
| 55 |
+
self._h2_state = h2.connection.H2Connection(config=self.CONFIG)
|
| 56 |
+
self._state = HTTPConnectionState.IDLE
|
| 57 |
+
self._expire_at: float | None = None
|
| 58 |
+
self._request_count = 0
|
| 59 |
+
self._init_lock = AsyncLock()
|
| 60 |
+
self._state_lock = AsyncLock()
|
| 61 |
+
self._read_lock = AsyncLock()
|
| 62 |
+
self._write_lock = AsyncLock()
|
| 63 |
+
self._sent_connection_init = False
|
| 64 |
+
self._used_all_stream_ids = False
|
| 65 |
+
self._connection_error = False
|
| 66 |
+
|
| 67 |
+
# Mapping from stream ID to response stream events.
|
| 68 |
+
self._events: dict[
|
| 69 |
+
int,
|
| 70 |
+
list[
|
| 71 |
+
h2.events.ResponseReceived
|
| 72 |
+
| h2.events.DataReceived
|
| 73 |
+
| h2.events.StreamEnded
|
| 74 |
+
| h2.events.StreamReset,
|
| 75 |
+
],
|
| 76 |
+
] = {}
|
| 77 |
+
|
| 78 |
+
# Connection terminated events are stored as state since
|
| 79 |
+
# we need to handle them for all streams.
|
| 80 |
+
self._connection_terminated: h2.events.ConnectionTerminated | None = None
|
| 81 |
+
|
| 82 |
+
self._read_exception: Exception | None = None
|
| 83 |
+
self._write_exception: Exception | None = None
|
| 84 |
+
|
| 85 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 86 |
+
if not self.can_handle_request(request.url.origin):
|
| 87 |
+
# This cannot occur in normal operation, since the connection pool
|
| 88 |
+
# will only send requests on connections that handle them.
|
| 89 |
+
# It's in place simply for resilience as a guard against incorrect
|
| 90 |
+
# usage, for anyone working directly with httpcore connections.
|
| 91 |
+
raise RuntimeError(
|
| 92 |
+
f"Attempted to send request to {request.url.origin} on connection "
|
| 93 |
+
f"to {self._origin}"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
async with self._state_lock:
|
| 97 |
+
if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE):
|
| 98 |
+
self._request_count += 1
|
| 99 |
+
self._expire_at = None
|
| 100 |
+
self._state = HTTPConnectionState.ACTIVE
|
| 101 |
+
else:
|
| 102 |
+
raise ConnectionNotAvailable()
|
| 103 |
+
|
| 104 |
+
async with self._init_lock:
|
| 105 |
+
if not self._sent_connection_init:
|
| 106 |
+
try:
|
| 107 |
+
sci_kwargs = {"request": request}
|
| 108 |
+
async with Trace(
|
| 109 |
+
"send_connection_init", logger, request, sci_kwargs
|
| 110 |
+
):
|
| 111 |
+
await self._send_connection_init(**sci_kwargs)
|
| 112 |
+
except BaseException as exc:
|
| 113 |
+
with AsyncShieldCancellation():
|
| 114 |
+
await self.aclose()
|
| 115 |
+
raise exc
|
| 116 |
+
|
| 117 |
+
self._sent_connection_init = True
|
| 118 |
+
|
| 119 |
+
# Initially start with just 1 until the remote server provides
|
| 120 |
+
# its max_concurrent_streams value
|
| 121 |
+
self._max_streams = 1
|
| 122 |
+
|
| 123 |
+
local_settings_max_streams = (
|
| 124 |
+
self._h2_state.local_settings.max_concurrent_streams
|
| 125 |
+
)
|
| 126 |
+
self._max_streams_semaphore = AsyncSemaphore(local_settings_max_streams)
|
| 127 |
+
|
| 128 |
+
for _ in range(local_settings_max_streams - self._max_streams):
|
| 129 |
+
await self._max_streams_semaphore.acquire()
|
| 130 |
+
|
| 131 |
+
await self._max_streams_semaphore.acquire()
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
stream_id = self._h2_state.get_next_available_stream_id()
|
| 135 |
+
self._events[stream_id] = []
|
| 136 |
+
except h2.exceptions.NoAvailableStreamIDError: # pragma: nocover
|
| 137 |
+
self._used_all_stream_ids = True
|
| 138 |
+
self._request_count -= 1
|
| 139 |
+
raise ConnectionNotAvailable()
|
| 140 |
+
|
| 141 |
+
try:
|
| 142 |
+
kwargs = {"request": request, "stream_id": stream_id}
|
| 143 |
+
async with Trace("send_request_headers", logger, request, kwargs):
|
| 144 |
+
await self._send_request_headers(request=request, stream_id=stream_id)
|
| 145 |
+
async with Trace("send_request_body", logger, request, kwargs):
|
| 146 |
+
await self._send_request_body(request=request, stream_id=stream_id)
|
| 147 |
+
async with Trace(
|
| 148 |
+
"receive_response_headers", logger, request, kwargs
|
| 149 |
+
) as trace:
|
| 150 |
+
status, headers = await self._receive_response(
|
| 151 |
+
request=request, stream_id=stream_id
|
| 152 |
+
)
|
| 153 |
+
trace.return_value = (status, headers)
|
| 154 |
+
|
| 155 |
+
return Response(
|
| 156 |
+
status=status,
|
| 157 |
+
headers=headers,
|
| 158 |
+
content=HTTP2ConnectionByteStream(self, request, stream_id=stream_id),
|
| 159 |
+
extensions={
|
| 160 |
+
"http_version": b"HTTP/2",
|
| 161 |
+
"network_stream": self._network_stream,
|
| 162 |
+
"stream_id": stream_id,
|
| 163 |
+
},
|
| 164 |
+
)
|
| 165 |
+
except BaseException as exc: # noqa: PIE786
|
| 166 |
+
with AsyncShieldCancellation():
|
| 167 |
+
kwargs = {"stream_id": stream_id}
|
| 168 |
+
async with Trace("response_closed", logger, request, kwargs):
|
| 169 |
+
await self._response_closed(stream_id=stream_id)
|
| 170 |
+
|
| 171 |
+
if isinstance(exc, h2.exceptions.ProtocolError):
|
| 172 |
+
# One case where h2 can raise a protocol error is when a
|
| 173 |
+
# closed frame has been seen by the state machine.
|
| 174 |
+
#
|
| 175 |
+
# This happens when one stream is reading, and encounters
|
| 176 |
+
# a GOAWAY event. Other flows of control may then raise
|
| 177 |
+
# a protocol error at any point they interact with the 'h2_state'.
|
| 178 |
+
#
|
| 179 |
+
# In this case we'll have stored the event, and should raise
|
| 180 |
+
# it as a RemoteProtocolError.
|
| 181 |
+
if self._connection_terminated: # pragma: nocover
|
| 182 |
+
raise RemoteProtocolError(self._connection_terminated)
|
| 183 |
+
# If h2 raises a protocol error in some other state then we
|
| 184 |
+
# must somehow have made a protocol violation.
|
| 185 |
+
raise LocalProtocolError(exc) # pragma: nocover
|
| 186 |
+
|
| 187 |
+
raise exc
|
| 188 |
+
|
| 189 |
+
async def _send_connection_init(self, request: Request) -> None:
|
| 190 |
+
"""
|
| 191 |
+
The HTTP/2 connection requires some initial setup before we can start
|
| 192 |
+
using individual request/response streams on it.
|
| 193 |
+
"""
|
| 194 |
+
# Need to set these manually here instead of manipulating via
|
| 195 |
+
# __setitem__() otherwise the H2Connection will emit SettingsUpdate
|
| 196 |
+
# frames in addition to sending the undesired defaults.
|
| 197 |
+
self._h2_state.local_settings = h2.settings.Settings(
|
| 198 |
+
client=True,
|
| 199 |
+
initial_values={
|
| 200 |
+
# Disable PUSH_PROMISE frames from the server since we don't do anything
|
| 201 |
+
# with them for now. Maybe when we support caching?
|
| 202 |
+
h2.settings.SettingCodes.ENABLE_PUSH: 0,
|
| 203 |
+
# These two are taken from h2 for safe defaults
|
| 204 |
+
h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100,
|
| 205 |
+
h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 65536,
|
| 206 |
+
},
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
# Some websites (*cough* Yahoo *cough*) balk at this setting being
|
| 210 |
+
# present in the initial handshake since it's not defined in the original
|
| 211 |
+
# RFC despite the RFC mandating ignoring settings you don't know about.
|
| 212 |
+
del self._h2_state.local_settings[
|
| 213 |
+
h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL
|
| 214 |
+
]
|
| 215 |
+
|
| 216 |
+
self._h2_state.initiate_connection()
|
| 217 |
+
self._h2_state.increment_flow_control_window(2**24)
|
| 218 |
+
await self._write_outgoing_data(request)
|
| 219 |
+
|
| 220 |
+
# Sending the request...
|
| 221 |
+
|
| 222 |
+
async def _send_request_headers(self, request: Request, stream_id: int) -> None:
|
| 223 |
+
"""
|
| 224 |
+
Send the request headers to a given stream ID.
|
| 225 |
+
"""
|
| 226 |
+
end_stream = not has_body_headers(request)
|
| 227 |
+
|
| 228 |
+
# In HTTP/2 the ':authority' pseudo-header is used instead of 'Host'.
|
| 229 |
+
# In order to gracefully handle HTTP/1.1 and HTTP/2 we always require
|
| 230 |
+
# HTTP/1.1 style headers, and map them appropriately if we end up on
|
| 231 |
+
# an HTTP/2 connection.
|
| 232 |
+
authority = [v for k, v in request.headers if k.lower() == b"host"][0]
|
| 233 |
+
|
| 234 |
+
headers = [
|
| 235 |
+
(b":method", request.method),
|
| 236 |
+
(b":authority", authority),
|
| 237 |
+
(b":scheme", request.url.scheme),
|
| 238 |
+
(b":path", request.url.target),
|
| 239 |
+
] + [
|
| 240 |
+
(k.lower(), v)
|
| 241 |
+
for k, v in request.headers
|
| 242 |
+
if k.lower()
|
| 243 |
+
not in (
|
| 244 |
+
b"host",
|
| 245 |
+
b"transfer-encoding",
|
| 246 |
+
)
|
| 247 |
+
]
|
| 248 |
+
|
| 249 |
+
self._h2_state.send_headers(stream_id, headers, end_stream=end_stream)
|
| 250 |
+
self._h2_state.increment_flow_control_window(2**24, stream_id=stream_id)
|
| 251 |
+
await self._write_outgoing_data(request)
|
| 252 |
+
|
| 253 |
+
async def _send_request_body(self, request: Request, stream_id: int) -> None:
|
| 254 |
+
"""
|
| 255 |
+
Iterate over the request body sending it to a given stream ID.
|
| 256 |
+
"""
|
| 257 |
+
if not has_body_headers(request):
|
| 258 |
+
return
|
| 259 |
+
|
| 260 |
+
assert isinstance(request.stream, typing.AsyncIterable)
|
| 261 |
+
async for data in request.stream:
|
| 262 |
+
await self._send_stream_data(request, stream_id, data)
|
| 263 |
+
await self._send_end_stream(request, stream_id)
|
| 264 |
+
|
| 265 |
+
async def _send_stream_data(
|
| 266 |
+
self, request: Request, stream_id: int, data: bytes
|
| 267 |
+
) -> None:
|
| 268 |
+
"""
|
| 269 |
+
Send a single chunk of data in one or more data frames.
|
| 270 |
+
"""
|
| 271 |
+
while data:
|
| 272 |
+
max_flow = await self._wait_for_outgoing_flow(request, stream_id)
|
| 273 |
+
chunk_size = min(len(data), max_flow)
|
| 274 |
+
chunk, data = data[:chunk_size], data[chunk_size:]
|
| 275 |
+
self._h2_state.send_data(stream_id, chunk)
|
| 276 |
+
await self._write_outgoing_data(request)
|
| 277 |
+
|
| 278 |
+
async def _send_end_stream(self, request: Request, stream_id: int) -> None:
|
| 279 |
+
"""
|
| 280 |
+
Send an empty data frame on on a given stream ID with the END_STREAM flag set.
|
| 281 |
+
"""
|
| 282 |
+
self._h2_state.end_stream(stream_id)
|
| 283 |
+
await self._write_outgoing_data(request)
|
| 284 |
+
|
| 285 |
+
# Receiving the response...
|
| 286 |
+
|
| 287 |
+
async def _receive_response(
|
| 288 |
+
self, request: Request, stream_id: int
|
| 289 |
+
) -> tuple[int, list[tuple[bytes, bytes]]]:
|
| 290 |
+
"""
|
| 291 |
+
Return the response status code and headers for a given stream ID.
|
| 292 |
+
"""
|
| 293 |
+
while True:
|
| 294 |
+
event = await self._receive_stream_event(request, stream_id)
|
| 295 |
+
if isinstance(event, h2.events.ResponseReceived):
|
| 296 |
+
break
|
| 297 |
+
|
| 298 |
+
status_code = 200
|
| 299 |
+
headers = []
|
| 300 |
+
assert event.headers is not None
|
| 301 |
+
for k, v in event.headers:
|
| 302 |
+
if k == b":status":
|
| 303 |
+
status_code = int(v.decode("ascii", errors="ignore"))
|
| 304 |
+
elif not k.startswith(b":"):
|
| 305 |
+
headers.append((k, v))
|
| 306 |
+
|
| 307 |
+
return (status_code, headers)
|
| 308 |
+
|
| 309 |
+
async def _receive_response_body(
|
| 310 |
+
self, request: Request, stream_id: int
|
| 311 |
+
) -> typing.AsyncIterator[bytes]:
|
| 312 |
+
"""
|
| 313 |
+
Iterator that returns the bytes of the response body for a given stream ID.
|
| 314 |
+
"""
|
| 315 |
+
while True:
|
| 316 |
+
event = await self._receive_stream_event(request, stream_id)
|
| 317 |
+
if isinstance(event, h2.events.DataReceived):
|
| 318 |
+
assert event.flow_controlled_length is not None
|
| 319 |
+
assert event.data is not None
|
| 320 |
+
amount = event.flow_controlled_length
|
| 321 |
+
self._h2_state.acknowledge_received_data(amount, stream_id)
|
| 322 |
+
await self._write_outgoing_data(request)
|
| 323 |
+
yield event.data
|
| 324 |
+
elif isinstance(event, h2.events.StreamEnded):
|
| 325 |
+
break
|
| 326 |
+
|
| 327 |
+
async def _receive_stream_event(
|
| 328 |
+
self, request: Request, stream_id: int
|
| 329 |
+
) -> h2.events.ResponseReceived | h2.events.DataReceived | h2.events.StreamEnded:
|
| 330 |
+
"""
|
| 331 |
+
Return the next available event for a given stream ID.
|
| 332 |
+
|
| 333 |
+
Will read more data from the network if required.
|
| 334 |
+
"""
|
| 335 |
+
while not self._events.get(stream_id):
|
| 336 |
+
await self._receive_events(request, stream_id)
|
| 337 |
+
event = self._events[stream_id].pop(0)
|
| 338 |
+
if isinstance(event, h2.events.StreamReset):
|
| 339 |
+
raise RemoteProtocolError(event)
|
| 340 |
+
return event
|
| 341 |
+
|
| 342 |
+
async def _receive_events(
|
| 343 |
+
self, request: Request, stream_id: int | None = None
|
| 344 |
+
) -> None:
|
| 345 |
+
"""
|
| 346 |
+
Read some data from the network until we see one or more events
|
| 347 |
+
for a given stream ID.
|
| 348 |
+
"""
|
| 349 |
+
async with self._read_lock:
|
| 350 |
+
if self._connection_terminated is not None:
|
| 351 |
+
last_stream_id = self._connection_terminated.last_stream_id
|
| 352 |
+
if stream_id and last_stream_id and stream_id > last_stream_id:
|
| 353 |
+
self._request_count -= 1
|
| 354 |
+
raise ConnectionNotAvailable()
|
| 355 |
+
raise RemoteProtocolError(self._connection_terminated)
|
| 356 |
+
|
| 357 |
+
# This conditional is a bit icky. We don't want to block reading if we've
|
| 358 |
+
# actually got an event to return for a given stream. We need to do that
|
| 359 |
+
# check *within* the atomic read lock. Though it also need to be optional,
|
| 360 |
+
# because when we call it from `_wait_for_outgoing_flow` we *do* want to
|
| 361 |
+
# block until we've available flow control, event when we have events
|
| 362 |
+
# pending for the stream ID we're attempting to send on.
|
| 363 |
+
if stream_id is None or not self._events.get(stream_id):
|
| 364 |
+
events = await self._read_incoming_data(request)
|
| 365 |
+
for event in events:
|
| 366 |
+
if isinstance(event, h2.events.RemoteSettingsChanged):
|
| 367 |
+
async with Trace(
|
| 368 |
+
"receive_remote_settings", logger, request
|
| 369 |
+
) as trace:
|
| 370 |
+
await self._receive_remote_settings_change(event)
|
| 371 |
+
trace.return_value = event
|
| 372 |
+
|
| 373 |
+
elif isinstance(
|
| 374 |
+
event,
|
| 375 |
+
(
|
| 376 |
+
h2.events.ResponseReceived,
|
| 377 |
+
h2.events.DataReceived,
|
| 378 |
+
h2.events.StreamEnded,
|
| 379 |
+
h2.events.StreamReset,
|
| 380 |
+
),
|
| 381 |
+
):
|
| 382 |
+
if event.stream_id in self._events:
|
| 383 |
+
self._events[event.stream_id].append(event)
|
| 384 |
+
|
| 385 |
+
elif isinstance(event, h2.events.ConnectionTerminated):
|
| 386 |
+
self._connection_terminated = event
|
| 387 |
+
|
| 388 |
+
await self._write_outgoing_data(request)
|
| 389 |
+
|
| 390 |
+
async def _receive_remote_settings_change(
|
| 391 |
+
self, event: h2.events.RemoteSettingsChanged
|
| 392 |
+
) -> None:
|
| 393 |
+
max_concurrent_streams = event.changed_settings.get(
|
| 394 |
+
h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS
|
| 395 |
+
)
|
| 396 |
+
if max_concurrent_streams:
|
| 397 |
+
new_max_streams = min(
|
| 398 |
+
max_concurrent_streams.new_value,
|
| 399 |
+
self._h2_state.local_settings.max_concurrent_streams,
|
| 400 |
+
)
|
| 401 |
+
if new_max_streams and new_max_streams != self._max_streams:
|
| 402 |
+
while new_max_streams > self._max_streams:
|
| 403 |
+
await self._max_streams_semaphore.release()
|
| 404 |
+
self._max_streams += 1
|
| 405 |
+
while new_max_streams < self._max_streams:
|
| 406 |
+
await self._max_streams_semaphore.acquire()
|
| 407 |
+
self._max_streams -= 1
|
| 408 |
+
|
| 409 |
+
async def _response_closed(self, stream_id: int) -> None:
|
| 410 |
+
await self._max_streams_semaphore.release()
|
| 411 |
+
del self._events[stream_id]
|
| 412 |
+
async with self._state_lock:
|
| 413 |
+
if self._connection_terminated and not self._events:
|
| 414 |
+
await self.aclose()
|
| 415 |
+
|
| 416 |
+
elif self._state == HTTPConnectionState.ACTIVE and not self._events:
|
| 417 |
+
self._state = HTTPConnectionState.IDLE
|
| 418 |
+
if self._keepalive_expiry is not None:
|
| 419 |
+
now = time.monotonic()
|
| 420 |
+
self._expire_at = now + self._keepalive_expiry
|
| 421 |
+
if self._used_all_stream_ids: # pragma: nocover
|
| 422 |
+
await self.aclose()
|
| 423 |
+
|
| 424 |
+
async def aclose(self) -> None:
|
| 425 |
+
# Note that this method unilaterally closes the connection, and does
|
| 426 |
+
# not have any kind of locking in place around it.
|
| 427 |
+
self._h2_state.close_connection()
|
| 428 |
+
self._state = HTTPConnectionState.CLOSED
|
| 429 |
+
await self._network_stream.aclose()
|
| 430 |
+
|
| 431 |
+
# Wrappers around network read/write operations...
|
| 432 |
+
|
| 433 |
+
async def _read_incoming_data(self, request: Request) -> list[h2.events.Event]:
|
| 434 |
+
timeouts = request.extensions.get("timeout", {})
|
| 435 |
+
timeout = timeouts.get("read", None)
|
| 436 |
+
|
| 437 |
+
if self._read_exception is not None:
|
| 438 |
+
raise self._read_exception # pragma: nocover
|
| 439 |
+
|
| 440 |
+
try:
|
| 441 |
+
data = await self._network_stream.read(self.READ_NUM_BYTES, timeout)
|
| 442 |
+
if data == b"":
|
| 443 |
+
raise RemoteProtocolError("Server disconnected")
|
| 444 |
+
except Exception as exc:
|
| 445 |
+
# If we get a network error we should:
|
| 446 |
+
#
|
| 447 |
+
# 1. Save the exception and just raise it immediately on any future reads.
|
| 448 |
+
# (For example, this means that a single read timeout or disconnect will
|
| 449 |
+
# immediately close all pending streams. Without requiring multiple
|
| 450 |
+
# sequential timeouts.)
|
| 451 |
+
# 2. Mark the connection as errored, so that we don't accept any other
|
| 452 |
+
# incoming requests.
|
| 453 |
+
self._read_exception = exc
|
| 454 |
+
self._connection_error = True
|
| 455 |
+
raise exc
|
| 456 |
+
|
| 457 |
+
events: list[h2.events.Event] = self._h2_state.receive_data(data)
|
| 458 |
+
|
| 459 |
+
return events
|
| 460 |
+
|
| 461 |
+
async def _write_outgoing_data(self, request: Request) -> None:
|
| 462 |
+
timeouts = request.extensions.get("timeout", {})
|
| 463 |
+
timeout = timeouts.get("write", None)
|
| 464 |
+
|
| 465 |
+
async with self._write_lock:
|
| 466 |
+
data_to_send = self._h2_state.data_to_send()
|
| 467 |
+
|
| 468 |
+
if self._write_exception is not None:
|
| 469 |
+
raise self._write_exception # pragma: nocover
|
| 470 |
+
|
| 471 |
+
try:
|
| 472 |
+
await self._network_stream.write(data_to_send, timeout)
|
| 473 |
+
except Exception as exc: # pragma: nocover
|
| 474 |
+
# If we get a network error we should:
|
| 475 |
+
#
|
| 476 |
+
# 1. Save the exception and just raise it immediately on any future write.
|
| 477 |
+
# (For example, this means that a single write timeout or disconnect will
|
| 478 |
+
# immediately close all pending streams. Without requiring multiple
|
| 479 |
+
# sequential timeouts.)
|
| 480 |
+
# 2. Mark the connection as errored, so that we don't accept any other
|
| 481 |
+
# incoming requests.
|
| 482 |
+
self._write_exception = exc
|
| 483 |
+
self._connection_error = True
|
| 484 |
+
raise exc
|
| 485 |
+
|
| 486 |
+
# Flow control...
|
| 487 |
+
|
| 488 |
+
async def _wait_for_outgoing_flow(self, request: Request, stream_id: int) -> int:
|
| 489 |
+
"""
|
| 490 |
+
Returns the maximum allowable outgoing flow for a given stream.
|
| 491 |
+
|
| 492 |
+
If the allowable flow is zero, then waits on the network until
|
| 493 |
+
WindowUpdated frames have increased the flow rate.
|
| 494 |
+
https://tools.ietf.org/html/rfc7540#section-6.9
|
| 495 |
+
"""
|
| 496 |
+
local_flow: int = self._h2_state.local_flow_control_window(stream_id)
|
| 497 |
+
max_frame_size: int = self._h2_state.max_outbound_frame_size
|
| 498 |
+
flow = min(local_flow, max_frame_size)
|
| 499 |
+
while flow == 0:
|
| 500 |
+
await self._receive_events(request)
|
| 501 |
+
local_flow = self._h2_state.local_flow_control_window(stream_id)
|
| 502 |
+
max_frame_size = self._h2_state.max_outbound_frame_size
|
| 503 |
+
flow = min(local_flow, max_frame_size)
|
| 504 |
+
return flow
|
| 505 |
+
|
| 506 |
+
# Interface for connection pooling...
|
| 507 |
+
|
| 508 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 509 |
+
return origin == self._origin
|
| 510 |
+
|
| 511 |
+
def is_available(self) -> bool:
|
| 512 |
+
return (
|
| 513 |
+
self._state != HTTPConnectionState.CLOSED
|
| 514 |
+
and not self._connection_error
|
| 515 |
+
and not self._used_all_stream_ids
|
| 516 |
+
and not (
|
| 517 |
+
self._h2_state.state_machine.state
|
| 518 |
+
== h2.connection.ConnectionState.CLOSED
|
| 519 |
+
)
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
def has_expired(self) -> bool:
|
| 523 |
+
now = time.monotonic()
|
| 524 |
+
return self._expire_at is not None and now > self._expire_at
|
| 525 |
+
|
| 526 |
+
def is_idle(self) -> bool:
|
| 527 |
+
return self._state == HTTPConnectionState.IDLE
|
| 528 |
+
|
| 529 |
+
def is_closed(self) -> bool:
|
| 530 |
+
return self._state == HTTPConnectionState.CLOSED
|
| 531 |
+
|
| 532 |
+
def info(self) -> str:
|
| 533 |
+
origin = str(self._origin)
|
| 534 |
+
return (
|
| 535 |
+
f"{origin!r}, HTTP/2, {self._state.name}, "
|
| 536 |
+
f"Request Count: {self._request_count}"
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
def __repr__(self) -> str:
|
| 540 |
+
class_name = self.__class__.__name__
|
| 541 |
+
origin = str(self._origin)
|
| 542 |
+
return (
|
| 543 |
+
f"<{class_name} [{origin!r}, {self._state.name}, "
|
| 544 |
+
f"Request Count: {self._request_count}]>"
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
# These context managers are not used in the standard flow, but are
|
| 548 |
+
# useful for testing or working with connection instances directly.
|
| 549 |
+
|
| 550 |
+
async def __aenter__(self) -> AsyncHTTP2Connection:
|
| 551 |
+
return self
|
| 552 |
+
|
| 553 |
+
async def __aexit__(
|
| 554 |
+
self,
|
| 555 |
+
exc_type: type[BaseException] | None = None,
|
| 556 |
+
exc_value: BaseException | None = None,
|
| 557 |
+
traceback: types.TracebackType | None = None,
|
| 558 |
+
) -> None:
|
| 559 |
+
await self.aclose()
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
class HTTP2ConnectionByteStream:
|
| 563 |
+
def __init__(
|
| 564 |
+
self, connection: AsyncHTTP2Connection, request: Request, stream_id: int
|
| 565 |
+
) -> None:
|
| 566 |
+
self._connection = connection
|
| 567 |
+
self._request = request
|
| 568 |
+
self._stream_id = stream_id
|
| 569 |
+
self._closed = False
|
| 570 |
+
|
| 571 |
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
| 572 |
+
kwargs = {"request": self._request, "stream_id": self._stream_id}
|
| 573 |
+
try:
|
| 574 |
+
async with Trace("receive_response_body", logger, self._request, kwargs):
|
| 575 |
+
async for chunk in self._connection._receive_response_body(
|
| 576 |
+
request=self._request, stream_id=self._stream_id
|
| 577 |
+
):
|
| 578 |
+
yield chunk
|
| 579 |
+
except BaseException as exc:
|
| 580 |
+
# If we get an exception while streaming the response,
|
| 581 |
+
# we want to close the response (and possibly the connection)
|
| 582 |
+
# before raising that exception.
|
| 583 |
+
with AsyncShieldCancellation():
|
| 584 |
+
await self.aclose()
|
| 585 |
+
raise exc
|
| 586 |
+
|
| 587 |
+
async def aclose(self) -> None:
|
| 588 |
+
if not self._closed:
|
| 589 |
+
self._closed = True
|
| 590 |
+
kwargs = {"stream_id": self._stream_id}
|
| 591 |
+
async with Trace("response_closed", logger, self._request, kwargs):
|
| 592 |
+
await self._connection._response_closed(stream_id=self._stream_id)
|
venv/lib/python3.12/site-packages/httpcore/_async/http_proxy.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import logging
|
| 5 |
+
import ssl
|
| 6 |
+
import typing
|
| 7 |
+
|
| 8 |
+
from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend
|
| 9 |
+
from .._exceptions import ProxyError
|
| 10 |
+
from .._models import (
|
| 11 |
+
URL,
|
| 12 |
+
Origin,
|
| 13 |
+
Request,
|
| 14 |
+
Response,
|
| 15 |
+
enforce_bytes,
|
| 16 |
+
enforce_headers,
|
| 17 |
+
enforce_url,
|
| 18 |
+
)
|
| 19 |
+
from .._ssl import default_ssl_context
|
| 20 |
+
from .._synchronization import AsyncLock
|
| 21 |
+
from .._trace import Trace
|
| 22 |
+
from .connection import AsyncHTTPConnection
|
| 23 |
+
from .connection_pool import AsyncConnectionPool
|
| 24 |
+
from .http11 import AsyncHTTP11Connection
|
| 25 |
+
from .interfaces import AsyncConnectionInterface
|
| 26 |
+
|
| 27 |
+
ByteOrStr = typing.Union[bytes, str]
|
| 28 |
+
HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]]
|
| 29 |
+
HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
logger = logging.getLogger("httpcore.proxy")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def merge_headers(
|
| 36 |
+
default_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
|
| 37 |
+
override_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
|
| 38 |
+
) -> list[tuple[bytes, bytes]]:
|
| 39 |
+
"""
|
| 40 |
+
Append default_headers and override_headers, de-duplicating if a key exists
|
| 41 |
+
in both cases.
|
| 42 |
+
"""
|
| 43 |
+
default_headers = [] if default_headers is None else list(default_headers)
|
| 44 |
+
override_headers = [] if override_headers is None else list(override_headers)
|
| 45 |
+
has_override = set(key.lower() for key, value in override_headers)
|
| 46 |
+
default_headers = [
|
| 47 |
+
(key, value)
|
| 48 |
+
for key, value in default_headers
|
| 49 |
+
if key.lower() not in has_override
|
| 50 |
+
]
|
| 51 |
+
return default_headers + override_headers
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class AsyncHTTPProxy(AsyncConnectionPool): # pragma: nocover
|
| 55 |
+
"""
|
| 56 |
+
A connection pool that sends requests via an HTTP proxy.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
def __init__(
|
| 60 |
+
self,
|
| 61 |
+
proxy_url: URL | bytes | str,
|
| 62 |
+
proxy_auth: tuple[bytes | str, bytes | str] | None = None,
|
| 63 |
+
proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None,
|
| 64 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 65 |
+
proxy_ssl_context: ssl.SSLContext | None = None,
|
| 66 |
+
max_connections: int | None = 10,
|
| 67 |
+
max_keepalive_connections: int | None = None,
|
| 68 |
+
keepalive_expiry: float | None = None,
|
| 69 |
+
http1: bool = True,
|
| 70 |
+
http2: bool = False,
|
| 71 |
+
retries: int = 0,
|
| 72 |
+
local_address: str | None = None,
|
| 73 |
+
uds: str | None = None,
|
| 74 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 75 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 76 |
+
) -> None:
|
| 77 |
+
"""
|
| 78 |
+
A connection pool for making HTTP requests.
|
| 79 |
+
|
| 80 |
+
Parameters:
|
| 81 |
+
proxy_url: The URL to use when connecting to the proxy server.
|
| 82 |
+
For example `"http://127.0.0.1:8080/"`.
|
| 83 |
+
proxy_auth: Any proxy authentication as a two-tuple of
|
| 84 |
+
(username, password). May be either bytes or ascii-only str.
|
| 85 |
+
proxy_headers: Any HTTP headers to use for the proxy requests.
|
| 86 |
+
For example `{"Proxy-Authorization": "Basic <username>:<password>"}`.
|
| 87 |
+
ssl_context: An SSL context to use for verifying connections.
|
| 88 |
+
If not specified, the default `httpcore.default_ssl_context()`
|
| 89 |
+
will be used.
|
| 90 |
+
proxy_ssl_context: The same as `ssl_context`, but for a proxy server rather than a remote origin.
|
| 91 |
+
max_connections: The maximum number of concurrent HTTP connections that
|
| 92 |
+
the pool should allow. Any attempt to send a request on a pool that
|
| 93 |
+
would exceed this amount will block until a connection is available.
|
| 94 |
+
max_keepalive_connections: The maximum number of idle HTTP connections
|
| 95 |
+
that will be maintained in the pool.
|
| 96 |
+
keepalive_expiry: The duration in seconds that an idle HTTP connection
|
| 97 |
+
may be maintained for before being expired from the pool.
|
| 98 |
+
http1: A boolean indicating if HTTP/1.1 requests should be supported
|
| 99 |
+
by the connection pool. Defaults to True.
|
| 100 |
+
http2: A boolean indicating if HTTP/2 requests should be supported by
|
| 101 |
+
the connection pool. Defaults to False.
|
| 102 |
+
retries: The maximum number of retries when trying to establish
|
| 103 |
+
a connection.
|
| 104 |
+
local_address: Local address to connect from. Can also be used to
|
| 105 |
+
connect using a particular address family. Using
|
| 106 |
+
`local_address="0.0.0.0"` will connect using an `AF_INET` address
|
| 107 |
+
(IPv4), while using `local_address="::"` will connect using an
|
| 108 |
+
`AF_INET6` address (IPv6).
|
| 109 |
+
uds: Path to a Unix Domain Socket to use instead of TCP sockets.
|
| 110 |
+
network_backend: A backend instance to use for handling network I/O.
|
| 111 |
+
"""
|
| 112 |
+
super().__init__(
|
| 113 |
+
ssl_context=ssl_context,
|
| 114 |
+
max_connections=max_connections,
|
| 115 |
+
max_keepalive_connections=max_keepalive_connections,
|
| 116 |
+
keepalive_expiry=keepalive_expiry,
|
| 117 |
+
http1=http1,
|
| 118 |
+
http2=http2,
|
| 119 |
+
network_backend=network_backend,
|
| 120 |
+
retries=retries,
|
| 121 |
+
local_address=local_address,
|
| 122 |
+
uds=uds,
|
| 123 |
+
socket_options=socket_options,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
|
| 127 |
+
if (
|
| 128 |
+
self._proxy_url.scheme == b"http" and proxy_ssl_context is not None
|
| 129 |
+
): # pragma: no cover
|
| 130 |
+
raise RuntimeError(
|
| 131 |
+
"The `proxy_ssl_context` argument is not allowed for the http scheme"
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
self._ssl_context = ssl_context
|
| 135 |
+
self._proxy_ssl_context = proxy_ssl_context
|
| 136 |
+
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
|
| 137 |
+
if proxy_auth is not None:
|
| 138 |
+
username = enforce_bytes(proxy_auth[0], name="proxy_auth")
|
| 139 |
+
password = enforce_bytes(proxy_auth[1], name="proxy_auth")
|
| 140 |
+
userpass = username + b":" + password
|
| 141 |
+
authorization = b"Basic " + base64.b64encode(userpass)
|
| 142 |
+
self._proxy_headers = [
|
| 143 |
+
(b"Proxy-Authorization", authorization)
|
| 144 |
+
] + self._proxy_headers
|
| 145 |
+
|
| 146 |
+
def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
|
| 147 |
+
if origin.scheme == b"http":
|
| 148 |
+
return AsyncForwardHTTPConnection(
|
| 149 |
+
proxy_origin=self._proxy_url.origin,
|
| 150 |
+
proxy_headers=self._proxy_headers,
|
| 151 |
+
remote_origin=origin,
|
| 152 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 153 |
+
network_backend=self._network_backend,
|
| 154 |
+
proxy_ssl_context=self._proxy_ssl_context,
|
| 155 |
+
)
|
| 156 |
+
return AsyncTunnelHTTPConnection(
|
| 157 |
+
proxy_origin=self._proxy_url.origin,
|
| 158 |
+
proxy_headers=self._proxy_headers,
|
| 159 |
+
remote_origin=origin,
|
| 160 |
+
ssl_context=self._ssl_context,
|
| 161 |
+
proxy_ssl_context=self._proxy_ssl_context,
|
| 162 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 163 |
+
http1=self._http1,
|
| 164 |
+
http2=self._http2,
|
| 165 |
+
network_backend=self._network_backend,
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class AsyncForwardHTTPConnection(AsyncConnectionInterface):
|
| 170 |
+
def __init__(
|
| 171 |
+
self,
|
| 172 |
+
proxy_origin: Origin,
|
| 173 |
+
remote_origin: Origin,
|
| 174 |
+
proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None,
|
| 175 |
+
keepalive_expiry: float | None = None,
|
| 176 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 177 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 178 |
+
proxy_ssl_context: ssl.SSLContext | None = None,
|
| 179 |
+
) -> None:
|
| 180 |
+
self._connection = AsyncHTTPConnection(
|
| 181 |
+
origin=proxy_origin,
|
| 182 |
+
keepalive_expiry=keepalive_expiry,
|
| 183 |
+
network_backend=network_backend,
|
| 184 |
+
socket_options=socket_options,
|
| 185 |
+
ssl_context=proxy_ssl_context,
|
| 186 |
+
)
|
| 187 |
+
self._proxy_origin = proxy_origin
|
| 188 |
+
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
|
| 189 |
+
self._remote_origin = remote_origin
|
| 190 |
+
|
| 191 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 192 |
+
headers = merge_headers(self._proxy_headers, request.headers)
|
| 193 |
+
url = URL(
|
| 194 |
+
scheme=self._proxy_origin.scheme,
|
| 195 |
+
host=self._proxy_origin.host,
|
| 196 |
+
port=self._proxy_origin.port,
|
| 197 |
+
target=bytes(request.url),
|
| 198 |
+
)
|
| 199 |
+
proxy_request = Request(
|
| 200 |
+
method=request.method,
|
| 201 |
+
url=url,
|
| 202 |
+
headers=headers,
|
| 203 |
+
content=request.stream,
|
| 204 |
+
extensions=request.extensions,
|
| 205 |
+
)
|
| 206 |
+
return await self._connection.handle_async_request(proxy_request)
|
| 207 |
+
|
| 208 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 209 |
+
return origin == self._remote_origin
|
| 210 |
+
|
| 211 |
+
async def aclose(self) -> None:
|
| 212 |
+
await self._connection.aclose()
|
| 213 |
+
|
| 214 |
+
def info(self) -> str:
|
| 215 |
+
return self._connection.info()
|
| 216 |
+
|
| 217 |
+
def is_available(self) -> bool:
|
| 218 |
+
return self._connection.is_available()
|
| 219 |
+
|
| 220 |
+
def has_expired(self) -> bool:
|
| 221 |
+
return self._connection.has_expired()
|
| 222 |
+
|
| 223 |
+
def is_idle(self) -> bool:
|
| 224 |
+
return self._connection.is_idle()
|
| 225 |
+
|
| 226 |
+
def is_closed(self) -> bool:
|
| 227 |
+
return self._connection.is_closed()
|
| 228 |
+
|
| 229 |
+
def __repr__(self) -> str:
|
| 230 |
+
return f"<{self.__class__.__name__} [{self.info()}]>"
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class AsyncTunnelHTTPConnection(AsyncConnectionInterface):
|
| 234 |
+
def __init__(
|
| 235 |
+
self,
|
| 236 |
+
proxy_origin: Origin,
|
| 237 |
+
remote_origin: Origin,
|
| 238 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 239 |
+
proxy_ssl_context: ssl.SSLContext | None = None,
|
| 240 |
+
proxy_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
|
| 241 |
+
keepalive_expiry: float | None = None,
|
| 242 |
+
http1: bool = True,
|
| 243 |
+
http2: bool = False,
|
| 244 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 245 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 246 |
+
) -> None:
|
| 247 |
+
self._connection: AsyncConnectionInterface = AsyncHTTPConnection(
|
| 248 |
+
origin=proxy_origin,
|
| 249 |
+
keepalive_expiry=keepalive_expiry,
|
| 250 |
+
network_backend=network_backend,
|
| 251 |
+
socket_options=socket_options,
|
| 252 |
+
ssl_context=proxy_ssl_context,
|
| 253 |
+
)
|
| 254 |
+
self._proxy_origin = proxy_origin
|
| 255 |
+
self._remote_origin = remote_origin
|
| 256 |
+
self._ssl_context = ssl_context
|
| 257 |
+
self._proxy_ssl_context = proxy_ssl_context
|
| 258 |
+
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
|
| 259 |
+
self._keepalive_expiry = keepalive_expiry
|
| 260 |
+
self._http1 = http1
|
| 261 |
+
self._http2 = http2
|
| 262 |
+
self._connect_lock = AsyncLock()
|
| 263 |
+
self._connected = False
|
| 264 |
+
|
| 265 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 266 |
+
timeouts = request.extensions.get("timeout", {})
|
| 267 |
+
timeout = timeouts.get("connect", None)
|
| 268 |
+
|
| 269 |
+
async with self._connect_lock:
|
| 270 |
+
if not self._connected:
|
| 271 |
+
target = b"%b:%d" % (self._remote_origin.host, self._remote_origin.port)
|
| 272 |
+
|
| 273 |
+
connect_url = URL(
|
| 274 |
+
scheme=self._proxy_origin.scheme,
|
| 275 |
+
host=self._proxy_origin.host,
|
| 276 |
+
port=self._proxy_origin.port,
|
| 277 |
+
target=target,
|
| 278 |
+
)
|
| 279 |
+
connect_headers = merge_headers(
|
| 280 |
+
[(b"Host", target), (b"Accept", b"*/*")], self._proxy_headers
|
| 281 |
+
)
|
| 282 |
+
connect_request = Request(
|
| 283 |
+
method=b"CONNECT",
|
| 284 |
+
url=connect_url,
|
| 285 |
+
headers=connect_headers,
|
| 286 |
+
extensions=request.extensions,
|
| 287 |
+
)
|
| 288 |
+
connect_response = await self._connection.handle_async_request(
|
| 289 |
+
connect_request
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
if connect_response.status < 200 or connect_response.status > 299:
|
| 293 |
+
reason_bytes = connect_response.extensions.get("reason_phrase", b"")
|
| 294 |
+
reason_str = reason_bytes.decode("ascii", errors="ignore")
|
| 295 |
+
msg = "%d %s" % (connect_response.status, reason_str)
|
| 296 |
+
await self._connection.aclose()
|
| 297 |
+
raise ProxyError(msg)
|
| 298 |
+
|
| 299 |
+
stream = connect_response.extensions["network_stream"]
|
| 300 |
+
|
| 301 |
+
# Upgrade the stream to SSL
|
| 302 |
+
ssl_context = (
|
| 303 |
+
default_ssl_context()
|
| 304 |
+
if self._ssl_context is None
|
| 305 |
+
else self._ssl_context
|
| 306 |
+
)
|
| 307 |
+
alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"]
|
| 308 |
+
ssl_context.set_alpn_protocols(alpn_protocols)
|
| 309 |
+
|
| 310 |
+
kwargs = {
|
| 311 |
+
"ssl_context": ssl_context,
|
| 312 |
+
"server_hostname": self._remote_origin.host.decode("ascii"),
|
| 313 |
+
"timeout": timeout,
|
| 314 |
+
}
|
| 315 |
+
async with Trace("start_tls", logger, request, kwargs) as trace:
|
| 316 |
+
stream = await stream.start_tls(**kwargs)
|
| 317 |
+
trace.return_value = stream
|
| 318 |
+
|
| 319 |
+
# Determine if we should be using HTTP/1.1 or HTTP/2
|
| 320 |
+
ssl_object = stream.get_extra_info("ssl_object")
|
| 321 |
+
http2_negotiated = (
|
| 322 |
+
ssl_object is not None
|
| 323 |
+
and ssl_object.selected_alpn_protocol() == "h2"
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
# Create the HTTP/1.1 or HTTP/2 connection
|
| 327 |
+
if http2_negotiated or (self._http2 and not self._http1):
|
| 328 |
+
from .http2 import AsyncHTTP2Connection
|
| 329 |
+
|
| 330 |
+
self._connection = AsyncHTTP2Connection(
|
| 331 |
+
origin=self._remote_origin,
|
| 332 |
+
stream=stream,
|
| 333 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 334 |
+
)
|
| 335 |
+
else:
|
| 336 |
+
self._connection = AsyncHTTP11Connection(
|
| 337 |
+
origin=self._remote_origin,
|
| 338 |
+
stream=stream,
|
| 339 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
self._connected = True
|
| 343 |
+
return await self._connection.handle_async_request(request)
|
| 344 |
+
|
| 345 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 346 |
+
return origin == self._remote_origin
|
| 347 |
+
|
| 348 |
+
async def aclose(self) -> None:
|
| 349 |
+
await self._connection.aclose()
|
| 350 |
+
|
| 351 |
+
def info(self) -> str:
|
| 352 |
+
return self._connection.info()
|
| 353 |
+
|
| 354 |
+
def is_available(self) -> bool:
|
| 355 |
+
return self._connection.is_available()
|
| 356 |
+
|
| 357 |
+
def has_expired(self) -> bool:
|
| 358 |
+
return self._connection.has_expired()
|
| 359 |
+
|
| 360 |
+
def is_idle(self) -> bool:
|
| 361 |
+
return self._connection.is_idle()
|
| 362 |
+
|
| 363 |
+
def is_closed(self) -> bool:
|
| 364 |
+
return self._connection.is_closed()
|
| 365 |
+
|
| 366 |
+
def __repr__(self) -> str:
|
| 367 |
+
return f"<{self.__class__.__name__} [{self.info()}]>"
|
venv/lib/python3.12/site-packages/httpcore/_async/interfaces.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import contextlib
|
| 4 |
+
import typing
|
| 5 |
+
|
| 6 |
+
from .._models import (
|
| 7 |
+
URL,
|
| 8 |
+
Extensions,
|
| 9 |
+
HeaderTypes,
|
| 10 |
+
Origin,
|
| 11 |
+
Request,
|
| 12 |
+
Response,
|
| 13 |
+
enforce_bytes,
|
| 14 |
+
enforce_headers,
|
| 15 |
+
enforce_url,
|
| 16 |
+
include_request_headers,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class AsyncRequestInterface:
|
| 21 |
+
async def request(
|
| 22 |
+
self,
|
| 23 |
+
method: bytes | str,
|
| 24 |
+
url: URL | bytes | str,
|
| 25 |
+
*,
|
| 26 |
+
headers: HeaderTypes = None,
|
| 27 |
+
content: bytes | typing.AsyncIterator[bytes] | None = None,
|
| 28 |
+
extensions: Extensions | None = None,
|
| 29 |
+
) -> Response:
|
| 30 |
+
# Strict type checking on our parameters.
|
| 31 |
+
method = enforce_bytes(method, name="method")
|
| 32 |
+
url = enforce_url(url, name="url")
|
| 33 |
+
headers = enforce_headers(headers, name="headers")
|
| 34 |
+
|
| 35 |
+
# Include Host header, and optionally Content-Length or Transfer-Encoding.
|
| 36 |
+
headers = include_request_headers(headers, url=url, content=content)
|
| 37 |
+
|
| 38 |
+
request = Request(
|
| 39 |
+
method=method,
|
| 40 |
+
url=url,
|
| 41 |
+
headers=headers,
|
| 42 |
+
content=content,
|
| 43 |
+
extensions=extensions,
|
| 44 |
+
)
|
| 45 |
+
response = await self.handle_async_request(request)
|
| 46 |
+
try:
|
| 47 |
+
await response.aread()
|
| 48 |
+
finally:
|
| 49 |
+
await response.aclose()
|
| 50 |
+
return response
|
| 51 |
+
|
| 52 |
+
@contextlib.asynccontextmanager
|
| 53 |
+
async def stream(
|
| 54 |
+
self,
|
| 55 |
+
method: bytes | str,
|
| 56 |
+
url: URL | bytes | str,
|
| 57 |
+
*,
|
| 58 |
+
headers: HeaderTypes = None,
|
| 59 |
+
content: bytes | typing.AsyncIterator[bytes] | None = None,
|
| 60 |
+
extensions: Extensions | None = None,
|
| 61 |
+
) -> typing.AsyncIterator[Response]:
|
| 62 |
+
# Strict type checking on our parameters.
|
| 63 |
+
method = enforce_bytes(method, name="method")
|
| 64 |
+
url = enforce_url(url, name="url")
|
| 65 |
+
headers = enforce_headers(headers, name="headers")
|
| 66 |
+
|
| 67 |
+
# Include Host header, and optionally Content-Length or Transfer-Encoding.
|
| 68 |
+
headers = include_request_headers(headers, url=url, content=content)
|
| 69 |
+
|
| 70 |
+
request = Request(
|
| 71 |
+
method=method,
|
| 72 |
+
url=url,
|
| 73 |
+
headers=headers,
|
| 74 |
+
content=content,
|
| 75 |
+
extensions=extensions,
|
| 76 |
+
)
|
| 77 |
+
response = await self.handle_async_request(request)
|
| 78 |
+
try:
|
| 79 |
+
yield response
|
| 80 |
+
finally:
|
| 81 |
+
await response.aclose()
|
| 82 |
+
|
| 83 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 84 |
+
raise NotImplementedError() # pragma: nocover
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class AsyncConnectionInterface(AsyncRequestInterface):
|
| 88 |
+
async def aclose(self) -> None:
|
| 89 |
+
raise NotImplementedError() # pragma: nocover
|
| 90 |
+
|
| 91 |
+
def info(self) -> str:
|
| 92 |
+
raise NotImplementedError() # pragma: nocover
|
| 93 |
+
|
| 94 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 95 |
+
raise NotImplementedError() # pragma: nocover
|
| 96 |
+
|
| 97 |
+
def is_available(self) -> bool:
|
| 98 |
+
"""
|
| 99 |
+
Return `True` if the connection is currently able to accept an
|
| 100 |
+
outgoing request.
|
| 101 |
+
|
| 102 |
+
An HTTP/1.1 connection will only be available if it is currently idle.
|
| 103 |
+
|
| 104 |
+
An HTTP/2 connection will be available so long as the stream ID space is
|
| 105 |
+
not yet exhausted, and the connection is not in an error state.
|
| 106 |
+
|
| 107 |
+
While the connection is being established we may not yet know if it is going
|
| 108 |
+
to result in an HTTP/1.1 or HTTP/2 connection. The connection should be
|
| 109 |
+
treated as being available, but might ultimately raise `NewConnectionRequired`
|
| 110 |
+
required exceptions if multiple requests are attempted over a connection
|
| 111 |
+
that ends up being established as HTTP/1.1.
|
| 112 |
+
"""
|
| 113 |
+
raise NotImplementedError() # pragma: nocover
|
| 114 |
+
|
| 115 |
+
def has_expired(self) -> bool:
|
| 116 |
+
"""
|
| 117 |
+
Return `True` if the connection is in a state where it should be closed.
|
| 118 |
+
|
| 119 |
+
This either means that the connection is idle and it has passed the
|
| 120 |
+
expiry time on its keep-alive, or that server has sent an EOF.
|
| 121 |
+
"""
|
| 122 |
+
raise NotImplementedError() # pragma: nocover
|
| 123 |
+
|
| 124 |
+
def is_idle(self) -> bool:
|
| 125 |
+
"""
|
| 126 |
+
Return `True` if the connection is currently idle.
|
| 127 |
+
"""
|
| 128 |
+
raise NotImplementedError() # pragma: nocover
|
| 129 |
+
|
| 130 |
+
def is_closed(self) -> bool:
|
| 131 |
+
"""
|
| 132 |
+
Return `True` if the connection has been closed.
|
| 133 |
+
|
| 134 |
+
Used when a response is closed to determine if the connection may be
|
| 135 |
+
returned to the connection pool or not.
|
| 136 |
+
"""
|
| 137 |
+
raise NotImplementedError() # pragma: nocover
|
venv/lib/python3.12/site-packages/httpcore/_async/socks_proxy.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import ssl
|
| 5 |
+
|
| 6 |
+
import socksio
|
| 7 |
+
|
| 8 |
+
from .._backends.auto import AutoBackend
|
| 9 |
+
from .._backends.base import AsyncNetworkBackend, AsyncNetworkStream
|
| 10 |
+
from .._exceptions import ConnectionNotAvailable, ProxyError
|
| 11 |
+
from .._models import URL, Origin, Request, Response, enforce_bytes, enforce_url
|
| 12 |
+
from .._ssl import default_ssl_context
|
| 13 |
+
from .._synchronization import AsyncLock
|
| 14 |
+
from .._trace import Trace
|
| 15 |
+
from .connection_pool import AsyncConnectionPool
|
| 16 |
+
from .http11 import AsyncHTTP11Connection
|
| 17 |
+
from .interfaces import AsyncConnectionInterface
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger("httpcore.socks")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
AUTH_METHODS = {
|
| 23 |
+
b"\x00": "NO AUTHENTICATION REQUIRED",
|
| 24 |
+
b"\x01": "GSSAPI",
|
| 25 |
+
b"\x02": "USERNAME/PASSWORD",
|
| 26 |
+
b"\xff": "NO ACCEPTABLE METHODS",
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
REPLY_CODES = {
|
| 30 |
+
b"\x00": "Succeeded",
|
| 31 |
+
b"\x01": "General SOCKS server failure",
|
| 32 |
+
b"\x02": "Connection not allowed by ruleset",
|
| 33 |
+
b"\x03": "Network unreachable",
|
| 34 |
+
b"\x04": "Host unreachable",
|
| 35 |
+
b"\x05": "Connection refused",
|
| 36 |
+
b"\x06": "TTL expired",
|
| 37 |
+
b"\x07": "Command not supported",
|
| 38 |
+
b"\x08": "Address type not supported",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
async def _init_socks5_connection(
|
| 43 |
+
stream: AsyncNetworkStream,
|
| 44 |
+
*,
|
| 45 |
+
host: bytes,
|
| 46 |
+
port: int,
|
| 47 |
+
auth: tuple[bytes, bytes] | None = None,
|
| 48 |
+
) -> None:
|
| 49 |
+
conn = socksio.socks5.SOCKS5Connection()
|
| 50 |
+
|
| 51 |
+
# Auth method request
|
| 52 |
+
auth_method = (
|
| 53 |
+
socksio.socks5.SOCKS5AuthMethod.NO_AUTH_REQUIRED
|
| 54 |
+
if auth is None
|
| 55 |
+
else socksio.socks5.SOCKS5AuthMethod.USERNAME_PASSWORD
|
| 56 |
+
)
|
| 57 |
+
conn.send(socksio.socks5.SOCKS5AuthMethodsRequest([auth_method]))
|
| 58 |
+
outgoing_bytes = conn.data_to_send()
|
| 59 |
+
await stream.write(outgoing_bytes)
|
| 60 |
+
|
| 61 |
+
# Auth method response
|
| 62 |
+
incoming_bytes = await stream.read(max_bytes=4096)
|
| 63 |
+
response = conn.receive_data(incoming_bytes)
|
| 64 |
+
assert isinstance(response, socksio.socks5.SOCKS5AuthReply)
|
| 65 |
+
if response.method != auth_method:
|
| 66 |
+
requested = AUTH_METHODS.get(auth_method, "UNKNOWN")
|
| 67 |
+
responded = AUTH_METHODS.get(response.method, "UNKNOWN")
|
| 68 |
+
raise ProxyError(
|
| 69 |
+
f"Requested {requested} from proxy server, but got {responded}."
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
if response.method == socksio.socks5.SOCKS5AuthMethod.USERNAME_PASSWORD:
|
| 73 |
+
# Username/password request
|
| 74 |
+
assert auth is not None
|
| 75 |
+
username, password = auth
|
| 76 |
+
conn.send(socksio.socks5.SOCKS5UsernamePasswordRequest(username, password))
|
| 77 |
+
outgoing_bytes = conn.data_to_send()
|
| 78 |
+
await stream.write(outgoing_bytes)
|
| 79 |
+
|
| 80 |
+
# Username/password response
|
| 81 |
+
incoming_bytes = await stream.read(max_bytes=4096)
|
| 82 |
+
response = conn.receive_data(incoming_bytes)
|
| 83 |
+
assert isinstance(response, socksio.socks5.SOCKS5UsernamePasswordReply)
|
| 84 |
+
if not response.success:
|
| 85 |
+
raise ProxyError("Invalid username/password")
|
| 86 |
+
|
| 87 |
+
# Connect request
|
| 88 |
+
conn.send(
|
| 89 |
+
socksio.socks5.SOCKS5CommandRequest.from_address(
|
| 90 |
+
socksio.socks5.SOCKS5Command.CONNECT, (host, port)
|
| 91 |
+
)
|
| 92 |
+
)
|
| 93 |
+
outgoing_bytes = conn.data_to_send()
|
| 94 |
+
await stream.write(outgoing_bytes)
|
| 95 |
+
|
| 96 |
+
# Connect response
|
| 97 |
+
incoming_bytes = await stream.read(max_bytes=4096)
|
| 98 |
+
response = conn.receive_data(incoming_bytes)
|
| 99 |
+
assert isinstance(response, socksio.socks5.SOCKS5Reply)
|
| 100 |
+
if response.reply_code != socksio.socks5.SOCKS5ReplyCode.SUCCEEDED:
|
| 101 |
+
reply_code = REPLY_CODES.get(response.reply_code, "UNKOWN")
|
| 102 |
+
raise ProxyError(f"Proxy Server could not connect: {reply_code}.")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class AsyncSOCKSProxy(AsyncConnectionPool): # pragma: nocover
|
| 106 |
+
"""
|
| 107 |
+
A connection pool that sends requests via an HTTP proxy.
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
def __init__(
|
| 111 |
+
self,
|
| 112 |
+
proxy_url: URL | bytes | str,
|
| 113 |
+
proxy_auth: tuple[bytes | str, bytes | str] | None = None,
|
| 114 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 115 |
+
max_connections: int | None = 10,
|
| 116 |
+
max_keepalive_connections: int | None = None,
|
| 117 |
+
keepalive_expiry: float | None = None,
|
| 118 |
+
http1: bool = True,
|
| 119 |
+
http2: bool = False,
|
| 120 |
+
retries: int = 0,
|
| 121 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 122 |
+
) -> None:
|
| 123 |
+
"""
|
| 124 |
+
A connection pool for making HTTP requests.
|
| 125 |
+
|
| 126 |
+
Parameters:
|
| 127 |
+
proxy_url: The URL to use when connecting to the proxy server.
|
| 128 |
+
For example `"http://127.0.0.1:8080/"`.
|
| 129 |
+
ssl_context: An SSL context to use for verifying connections.
|
| 130 |
+
If not specified, the default `httpcore.default_ssl_context()`
|
| 131 |
+
will be used.
|
| 132 |
+
max_connections: The maximum number of concurrent HTTP connections that
|
| 133 |
+
the pool should allow. Any attempt to send a request on a pool that
|
| 134 |
+
would exceed this amount will block until a connection is available.
|
| 135 |
+
max_keepalive_connections: The maximum number of idle HTTP connections
|
| 136 |
+
that will be maintained in the pool.
|
| 137 |
+
keepalive_expiry: The duration in seconds that an idle HTTP connection
|
| 138 |
+
may be maintained for before being expired from the pool.
|
| 139 |
+
http1: A boolean indicating if HTTP/1.1 requests should be supported
|
| 140 |
+
by the connection pool. Defaults to True.
|
| 141 |
+
http2: A boolean indicating if HTTP/2 requests should be supported by
|
| 142 |
+
the connection pool. Defaults to False.
|
| 143 |
+
retries: The maximum number of retries when trying to establish
|
| 144 |
+
a connection.
|
| 145 |
+
local_address: Local address to connect from. Can also be used to
|
| 146 |
+
connect using a particular address family. Using
|
| 147 |
+
`local_address="0.0.0.0"` will connect using an `AF_INET` address
|
| 148 |
+
(IPv4), while using `local_address="::"` will connect using an
|
| 149 |
+
`AF_INET6` address (IPv6).
|
| 150 |
+
uds: Path to a Unix Domain Socket to use instead of TCP sockets.
|
| 151 |
+
network_backend: A backend instance to use for handling network I/O.
|
| 152 |
+
"""
|
| 153 |
+
super().__init__(
|
| 154 |
+
ssl_context=ssl_context,
|
| 155 |
+
max_connections=max_connections,
|
| 156 |
+
max_keepalive_connections=max_keepalive_connections,
|
| 157 |
+
keepalive_expiry=keepalive_expiry,
|
| 158 |
+
http1=http1,
|
| 159 |
+
http2=http2,
|
| 160 |
+
network_backend=network_backend,
|
| 161 |
+
retries=retries,
|
| 162 |
+
)
|
| 163 |
+
self._ssl_context = ssl_context
|
| 164 |
+
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
|
| 165 |
+
if proxy_auth is not None:
|
| 166 |
+
username, password = proxy_auth
|
| 167 |
+
username_bytes = enforce_bytes(username, name="proxy_auth")
|
| 168 |
+
password_bytes = enforce_bytes(password, name="proxy_auth")
|
| 169 |
+
self._proxy_auth: tuple[bytes, bytes] | None = (
|
| 170 |
+
username_bytes,
|
| 171 |
+
password_bytes,
|
| 172 |
+
)
|
| 173 |
+
else:
|
| 174 |
+
self._proxy_auth = None
|
| 175 |
+
|
| 176 |
+
def create_connection(self, origin: Origin) -> AsyncConnectionInterface:
|
| 177 |
+
return AsyncSocks5Connection(
|
| 178 |
+
proxy_origin=self._proxy_url.origin,
|
| 179 |
+
remote_origin=origin,
|
| 180 |
+
proxy_auth=self._proxy_auth,
|
| 181 |
+
ssl_context=self._ssl_context,
|
| 182 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 183 |
+
http1=self._http1,
|
| 184 |
+
http2=self._http2,
|
| 185 |
+
network_backend=self._network_backend,
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class AsyncSocks5Connection(AsyncConnectionInterface):
|
| 190 |
+
def __init__(
|
| 191 |
+
self,
|
| 192 |
+
proxy_origin: Origin,
|
| 193 |
+
remote_origin: Origin,
|
| 194 |
+
proxy_auth: tuple[bytes, bytes] | None = None,
|
| 195 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 196 |
+
keepalive_expiry: float | None = None,
|
| 197 |
+
http1: bool = True,
|
| 198 |
+
http2: bool = False,
|
| 199 |
+
network_backend: AsyncNetworkBackend | None = None,
|
| 200 |
+
) -> None:
|
| 201 |
+
self._proxy_origin = proxy_origin
|
| 202 |
+
self._remote_origin = remote_origin
|
| 203 |
+
self._proxy_auth = proxy_auth
|
| 204 |
+
self._ssl_context = ssl_context
|
| 205 |
+
self._keepalive_expiry = keepalive_expiry
|
| 206 |
+
self._http1 = http1
|
| 207 |
+
self._http2 = http2
|
| 208 |
+
|
| 209 |
+
self._network_backend: AsyncNetworkBackend = (
|
| 210 |
+
AutoBackend() if network_backend is None else network_backend
|
| 211 |
+
)
|
| 212 |
+
self._connect_lock = AsyncLock()
|
| 213 |
+
self._connection: AsyncConnectionInterface | None = None
|
| 214 |
+
self._connect_failed = False
|
| 215 |
+
|
| 216 |
+
async def handle_async_request(self, request: Request) -> Response:
|
| 217 |
+
timeouts = request.extensions.get("timeout", {})
|
| 218 |
+
sni_hostname = request.extensions.get("sni_hostname", None)
|
| 219 |
+
timeout = timeouts.get("connect", None)
|
| 220 |
+
|
| 221 |
+
async with self._connect_lock:
|
| 222 |
+
if self._connection is None:
|
| 223 |
+
try:
|
| 224 |
+
# Connect to the proxy
|
| 225 |
+
kwargs = {
|
| 226 |
+
"host": self._proxy_origin.host.decode("ascii"),
|
| 227 |
+
"port": self._proxy_origin.port,
|
| 228 |
+
"timeout": timeout,
|
| 229 |
+
}
|
| 230 |
+
async with Trace("connect_tcp", logger, request, kwargs) as trace:
|
| 231 |
+
stream = await self._network_backend.connect_tcp(**kwargs)
|
| 232 |
+
trace.return_value = stream
|
| 233 |
+
|
| 234 |
+
# Connect to the remote host using socks5
|
| 235 |
+
kwargs = {
|
| 236 |
+
"stream": stream,
|
| 237 |
+
"host": self._remote_origin.host.decode("ascii"),
|
| 238 |
+
"port": self._remote_origin.port,
|
| 239 |
+
"auth": self._proxy_auth,
|
| 240 |
+
}
|
| 241 |
+
async with Trace(
|
| 242 |
+
"setup_socks5_connection", logger, request, kwargs
|
| 243 |
+
) as trace:
|
| 244 |
+
await _init_socks5_connection(**kwargs)
|
| 245 |
+
trace.return_value = stream
|
| 246 |
+
|
| 247 |
+
# Upgrade the stream to SSL
|
| 248 |
+
if self._remote_origin.scheme == b"https":
|
| 249 |
+
ssl_context = (
|
| 250 |
+
default_ssl_context()
|
| 251 |
+
if self._ssl_context is None
|
| 252 |
+
else self._ssl_context
|
| 253 |
+
)
|
| 254 |
+
alpn_protocols = (
|
| 255 |
+
["http/1.1", "h2"] if self._http2 else ["http/1.1"]
|
| 256 |
+
)
|
| 257 |
+
ssl_context.set_alpn_protocols(alpn_protocols)
|
| 258 |
+
|
| 259 |
+
kwargs = {
|
| 260 |
+
"ssl_context": ssl_context,
|
| 261 |
+
"server_hostname": sni_hostname
|
| 262 |
+
or self._remote_origin.host.decode("ascii"),
|
| 263 |
+
"timeout": timeout,
|
| 264 |
+
}
|
| 265 |
+
async with Trace("start_tls", logger, request, kwargs) as trace:
|
| 266 |
+
stream = await stream.start_tls(**kwargs)
|
| 267 |
+
trace.return_value = stream
|
| 268 |
+
|
| 269 |
+
# Determine if we should be using HTTP/1.1 or HTTP/2
|
| 270 |
+
ssl_object = stream.get_extra_info("ssl_object")
|
| 271 |
+
http2_negotiated = (
|
| 272 |
+
ssl_object is not None
|
| 273 |
+
and ssl_object.selected_alpn_protocol() == "h2"
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# Create the HTTP/1.1 or HTTP/2 connection
|
| 277 |
+
if http2_negotiated or (
|
| 278 |
+
self._http2 and not self._http1
|
| 279 |
+
): # pragma: nocover
|
| 280 |
+
from .http2 import AsyncHTTP2Connection
|
| 281 |
+
|
| 282 |
+
self._connection = AsyncHTTP2Connection(
|
| 283 |
+
origin=self._remote_origin,
|
| 284 |
+
stream=stream,
|
| 285 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 286 |
+
)
|
| 287 |
+
else:
|
| 288 |
+
self._connection = AsyncHTTP11Connection(
|
| 289 |
+
origin=self._remote_origin,
|
| 290 |
+
stream=stream,
|
| 291 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 292 |
+
)
|
| 293 |
+
except Exception as exc:
|
| 294 |
+
self._connect_failed = True
|
| 295 |
+
raise exc
|
| 296 |
+
elif not self._connection.is_available(): # pragma: nocover
|
| 297 |
+
raise ConnectionNotAvailable()
|
| 298 |
+
|
| 299 |
+
return await self._connection.handle_async_request(request)
|
| 300 |
+
|
| 301 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 302 |
+
return origin == self._remote_origin
|
| 303 |
+
|
| 304 |
+
async def aclose(self) -> None:
|
| 305 |
+
if self._connection is not None:
|
| 306 |
+
await self._connection.aclose()
|
| 307 |
+
|
| 308 |
+
def is_available(self) -> bool:
|
| 309 |
+
if self._connection is None: # pragma: nocover
|
| 310 |
+
# If HTTP/2 support is enabled, and the resulting connection could
|
| 311 |
+
# end up as HTTP/2 then we should indicate the connection as being
|
| 312 |
+
# available to service multiple requests.
|
| 313 |
+
return (
|
| 314 |
+
self._http2
|
| 315 |
+
and (self._remote_origin.scheme == b"https" or not self._http1)
|
| 316 |
+
and not self._connect_failed
|
| 317 |
+
)
|
| 318 |
+
return self._connection.is_available()
|
| 319 |
+
|
| 320 |
+
def has_expired(self) -> bool:
|
| 321 |
+
if self._connection is None: # pragma: nocover
|
| 322 |
+
return self._connect_failed
|
| 323 |
+
return self._connection.has_expired()
|
| 324 |
+
|
| 325 |
+
def is_idle(self) -> bool:
|
| 326 |
+
if self._connection is None: # pragma: nocover
|
| 327 |
+
return self._connect_failed
|
| 328 |
+
return self._connection.is_idle()
|
| 329 |
+
|
| 330 |
+
def is_closed(self) -> bool:
|
| 331 |
+
if self._connection is None: # pragma: nocover
|
| 332 |
+
return self._connect_failed
|
| 333 |
+
return self._connection.is_closed()
|
| 334 |
+
|
| 335 |
+
def info(self) -> str:
|
| 336 |
+
if self._connection is None: # pragma: nocover
|
| 337 |
+
return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"
|
| 338 |
+
return self._connection.info()
|
| 339 |
+
|
| 340 |
+
def __repr__(self) -> str:
|
| 341 |
+
return f"<{self.__class__.__name__} [{self.info()}]>"
|
venv/lib/python3.12/site-packages/httpcore/_backends/__init__.py
ADDED
|
File without changes
|
venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (180 Bytes). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/anyio.cpython-312.pyc
ADDED
|
Binary file (8.61 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/auto.cpython-312.pyc
ADDED
|
Binary file (2.67 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/base.cpython-312.pyc
ADDED
|
Binary file (4.87 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/mock.cpython-312.pyc
ADDED
|
Binary file (7.14 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/sync.cpython-312.pyc
ADDED
|
Binary file (11.4 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_backends/__pycache__/trio.cpython-312.pyc
ADDED
|
Binary file (8.95 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_backends/anyio.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import ssl
|
| 4 |
+
import typing
|
| 5 |
+
|
| 6 |
+
import anyio
|
| 7 |
+
|
| 8 |
+
from .._exceptions import (
|
| 9 |
+
ConnectError,
|
| 10 |
+
ConnectTimeout,
|
| 11 |
+
ReadError,
|
| 12 |
+
ReadTimeout,
|
| 13 |
+
WriteError,
|
| 14 |
+
WriteTimeout,
|
| 15 |
+
map_exceptions,
|
| 16 |
+
)
|
| 17 |
+
from .._utils import is_socket_readable
|
| 18 |
+
from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class AnyIOStream(AsyncNetworkStream):
|
| 22 |
+
def __init__(self, stream: anyio.abc.ByteStream) -> None:
|
| 23 |
+
self._stream = stream
|
| 24 |
+
|
| 25 |
+
async def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
|
| 26 |
+
exc_map = {
|
| 27 |
+
TimeoutError: ReadTimeout,
|
| 28 |
+
anyio.BrokenResourceError: ReadError,
|
| 29 |
+
anyio.ClosedResourceError: ReadError,
|
| 30 |
+
anyio.EndOfStream: ReadError,
|
| 31 |
+
}
|
| 32 |
+
with map_exceptions(exc_map):
|
| 33 |
+
with anyio.fail_after(timeout):
|
| 34 |
+
try:
|
| 35 |
+
return await self._stream.receive(max_bytes=max_bytes)
|
| 36 |
+
except anyio.EndOfStream: # pragma: nocover
|
| 37 |
+
return b""
|
| 38 |
+
|
| 39 |
+
async def write(self, buffer: bytes, timeout: float | None = None) -> None:
|
| 40 |
+
if not buffer:
|
| 41 |
+
return
|
| 42 |
+
|
| 43 |
+
exc_map = {
|
| 44 |
+
TimeoutError: WriteTimeout,
|
| 45 |
+
anyio.BrokenResourceError: WriteError,
|
| 46 |
+
anyio.ClosedResourceError: WriteError,
|
| 47 |
+
}
|
| 48 |
+
with map_exceptions(exc_map):
|
| 49 |
+
with anyio.fail_after(timeout):
|
| 50 |
+
await self._stream.send(item=buffer)
|
| 51 |
+
|
| 52 |
+
async def aclose(self) -> None:
|
| 53 |
+
await self._stream.aclose()
|
| 54 |
+
|
| 55 |
+
async def start_tls(
|
| 56 |
+
self,
|
| 57 |
+
ssl_context: ssl.SSLContext,
|
| 58 |
+
server_hostname: str | None = None,
|
| 59 |
+
timeout: float | None = None,
|
| 60 |
+
) -> AsyncNetworkStream:
|
| 61 |
+
exc_map = {
|
| 62 |
+
TimeoutError: ConnectTimeout,
|
| 63 |
+
anyio.BrokenResourceError: ConnectError,
|
| 64 |
+
anyio.EndOfStream: ConnectError,
|
| 65 |
+
ssl.SSLError: ConnectError,
|
| 66 |
+
}
|
| 67 |
+
with map_exceptions(exc_map):
|
| 68 |
+
try:
|
| 69 |
+
with anyio.fail_after(timeout):
|
| 70 |
+
ssl_stream = await anyio.streams.tls.TLSStream.wrap(
|
| 71 |
+
self._stream,
|
| 72 |
+
ssl_context=ssl_context,
|
| 73 |
+
hostname=server_hostname,
|
| 74 |
+
standard_compatible=False,
|
| 75 |
+
server_side=False,
|
| 76 |
+
)
|
| 77 |
+
except Exception as exc: # pragma: nocover
|
| 78 |
+
await self.aclose()
|
| 79 |
+
raise exc
|
| 80 |
+
return AnyIOStream(ssl_stream)
|
| 81 |
+
|
| 82 |
+
def get_extra_info(self, info: str) -> typing.Any:
|
| 83 |
+
if info == "ssl_object":
|
| 84 |
+
return self._stream.extra(anyio.streams.tls.TLSAttribute.ssl_object, None)
|
| 85 |
+
if info == "client_addr":
|
| 86 |
+
return self._stream.extra(anyio.abc.SocketAttribute.local_address, None)
|
| 87 |
+
if info == "server_addr":
|
| 88 |
+
return self._stream.extra(anyio.abc.SocketAttribute.remote_address, None)
|
| 89 |
+
if info == "socket":
|
| 90 |
+
return self._stream.extra(anyio.abc.SocketAttribute.raw_socket, None)
|
| 91 |
+
if info == "is_readable":
|
| 92 |
+
sock = self._stream.extra(anyio.abc.SocketAttribute.raw_socket, None)
|
| 93 |
+
return is_socket_readable(sock)
|
| 94 |
+
return None
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
class AnyIOBackend(AsyncNetworkBackend):
|
| 98 |
+
async def connect_tcp(
|
| 99 |
+
self,
|
| 100 |
+
host: str,
|
| 101 |
+
port: int,
|
| 102 |
+
timeout: float | None = None,
|
| 103 |
+
local_address: str | None = None,
|
| 104 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 105 |
+
) -> AsyncNetworkStream: # pragma: nocover
|
| 106 |
+
if socket_options is None:
|
| 107 |
+
socket_options = []
|
| 108 |
+
exc_map = {
|
| 109 |
+
TimeoutError: ConnectTimeout,
|
| 110 |
+
OSError: ConnectError,
|
| 111 |
+
anyio.BrokenResourceError: ConnectError,
|
| 112 |
+
}
|
| 113 |
+
with map_exceptions(exc_map):
|
| 114 |
+
with anyio.fail_after(timeout):
|
| 115 |
+
stream: anyio.abc.ByteStream = await anyio.connect_tcp(
|
| 116 |
+
remote_host=host,
|
| 117 |
+
remote_port=port,
|
| 118 |
+
local_host=local_address,
|
| 119 |
+
)
|
| 120 |
+
# By default TCP sockets opened in `asyncio` include TCP_NODELAY.
|
| 121 |
+
for option in socket_options:
|
| 122 |
+
stream._raw_socket.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover
|
| 123 |
+
return AnyIOStream(stream)
|
| 124 |
+
|
| 125 |
+
async def connect_unix_socket(
|
| 126 |
+
self,
|
| 127 |
+
path: str,
|
| 128 |
+
timeout: float | None = None,
|
| 129 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 130 |
+
) -> AsyncNetworkStream: # pragma: nocover
|
| 131 |
+
if socket_options is None:
|
| 132 |
+
socket_options = []
|
| 133 |
+
exc_map = {
|
| 134 |
+
TimeoutError: ConnectTimeout,
|
| 135 |
+
OSError: ConnectError,
|
| 136 |
+
anyio.BrokenResourceError: ConnectError,
|
| 137 |
+
}
|
| 138 |
+
with map_exceptions(exc_map):
|
| 139 |
+
with anyio.fail_after(timeout):
|
| 140 |
+
stream: anyio.abc.ByteStream = await anyio.connect_unix(path)
|
| 141 |
+
for option in socket_options:
|
| 142 |
+
stream._raw_socket.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover
|
| 143 |
+
return AnyIOStream(stream)
|
| 144 |
+
|
| 145 |
+
async def sleep(self, seconds: float) -> None:
|
| 146 |
+
await anyio.sleep(seconds) # pragma: nocover
|
venv/lib/python3.12/site-packages/httpcore/_backends/auto.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
|
| 5 |
+
from .._synchronization import current_async_library
|
| 6 |
+
from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class AutoBackend(AsyncNetworkBackend):
|
| 10 |
+
async def _init_backend(self) -> None:
|
| 11 |
+
if not (hasattr(self, "_backend")):
|
| 12 |
+
backend = current_async_library()
|
| 13 |
+
if backend == "trio":
|
| 14 |
+
from .trio import TrioBackend
|
| 15 |
+
|
| 16 |
+
self._backend: AsyncNetworkBackend = TrioBackend()
|
| 17 |
+
else:
|
| 18 |
+
from .anyio import AnyIOBackend
|
| 19 |
+
|
| 20 |
+
self._backend = AnyIOBackend()
|
| 21 |
+
|
| 22 |
+
async def connect_tcp(
|
| 23 |
+
self,
|
| 24 |
+
host: str,
|
| 25 |
+
port: int,
|
| 26 |
+
timeout: float | None = None,
|
| 27 |
+
local_address: str | None = None,
|
| 28 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 29 |
+
) -> AsyncNetworkStream:
|
| 30 |
+
await self._init_backend()
|
| 31 |
+
return await self._backend.connect_tcp(
|
| 32 |
+
host,
|
| 33 |
+
port,
|
| 34 |
+
timeout=timeout,
|
| 35 |
+
local_address=local_address,
|
| 36 |
+
socket_options=socket_options,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
async def connect_unix_socket(
|
| 40 |
+
self,
|
| 41 |
+
path: str,
|
| 42 |
+
timeout: float | None = None,
|
| 43 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 44 |
+
) -> AsyncNetworkStream: # pragma: nocover
|
| 45 |
+
await self._init_backend()
|
| 46 |
+
return await self._backend.connect_unix_socket(
|
| 47 |
+
path, timeout=timeout, socket_options=socket_options
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
async def sleep(self, seconds: float) -> None: # pragma: nocover
|
| 51 |
+
await self._init_backend()
|
| 52 |
+
return await self._backend.sleep(seconds)
|
venv/lib/python3.12/site-packages/httpcore/_backends/base.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import ssl
|
| 4 |
+
import time
|
| 5 |
+
import typing
|
| 6 |
+
|
| 7 |
+
SOCKET_OPTION = typing.Union[
|
| 8 |
+
typing.Tuple[int, int, int],
|
| 9 |
+
typing.Tuple[int, int, typing.Union[bytes, bytearray]],
|
| 10 |
+
typing.Tuple[int, int, None, int],
|
| 11 |
+
]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class NetworkStream:
|
| 15 |
+
def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
|
| 16 |
+
raise NotImplementedError() # pragma: nocover
|
| 17 |
+
|
| 18 |
+
def write(self, buffer: bytes, timeout: float | None = None) -> None:
|
| 19 |
+
raise NotImplementedError() # pragma: nocover
|
| 20 |
+
|
| 21 |
+
def close(self) -> None:
|
| 22 |
+
raise NotImplementedError() # pragma: nocover
|
| 23 |
+
|
| 24 |
+
def start_tls(
|
| 25 |
+
self,
|
| 26 |
+
ssl_context: ssl.SSLContext,
|
| 27 |
+
server_hostname: str | None = None,
|
| 28 |
+
timeout: float | None = None,
|
| 29 |
+
) -> NetworkStream:
|
| 30 |
+
raise NotImplementedError() # pragma: nocover
|
| 31 |
+
|
| 32 |
+
def get_extra_info(self, info: str) -> typing.Any:
|
| 33 |
+
return None # pragma: nocover
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class NetworkBackend:
|
| 37 |
+
def connect_tcp(
|
| 38 |
+
self,
|
| 39 |
+
host: str,
|
| 40 |
+
port: int,
|
| 41 |
+
timeout: float | None = None,
|
| 42 |
+
local_address: str | None = None,
|
| 43 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 44 |
+
) -> NetworkStream:
|
| 45 |
+
raise NotImplementedError() # pragma: nocover
|
| 46 |
+
|
| 47 |
+
def connect_unix_socket(
|
| 48 |
+
self,
|
| 49 |
+
path: str,
|
| 50 |
+
timeout: float | None = None,
|
| 51 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 52 |
+
) -> NetworkStream:
|
| 53 |
+
raise NotImplementedError() # pragma: nocover
|
| 54 |
+
|
| 55 |
+
def sleep(self, seconds: float) -> None:
|
| 56 |
+
time.sleep(seconds) # pragma: nocover
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class AsyncNetworkStream:
|
| 60 |
+
async def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
|
| 61 |
+
raise NotImplementedError() # pragma: nocover
|
| 62 |
+
|
| 63 |
+
async def write(self, buffer: bytes, timeout: float | None = None) -> None:
|
| 64 |
+
raise NotImplementedError() # pragma: nocover
|
| 65 |
+
|
| 66 |
+
async def aclose(self) -> None:
|
| 67 |
+
raise NotImplementedError() # pragma: nocover
|
| 68 |
+
|
| 69 |
+
async def start_tls(
|
| 70 |
+
self,
|
| 71 |
+
ssl_context: ssl.SSLContext,
|
| 72 |
+
server_hostname: str | None = None,
|
| 73 |
+
timeout: float | None = None,
|
| 74 |
+
) -> AsyncNetworkStream:
|
| 75 |
+
raise NotImplementedError() # pragma: nocover
|
| 76 |
+
|
| 77 |
+
def get_extra_info(self, info: str) -> typing.Any:
|
| 78 |
+
return None # pragma: nocover
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class AsyncNetworkBackend:
|
| 82 |
+
async def connect_tcp(
|
| 83 |
+
self,
|
| 84 |
+
host: str,
|
| 85 |
+
port: int,
|
| 86 |
+
timeout: float | None = None,
|
| 87 |
+
local_address: str | None = None,
|
| 88 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 89 |
+
) -> AsyncNetworkStream:
|
| 90 |
+
raise NotImplementedError() # pragma: nocover
|
| 91 |
+
|
| 92 |
+
async def connect_unix_socket(
|
| 93 |
+
self,
|
| 94 |
+
path: str,
|
| 95 |
+
timeout: float | None = None,
|
| 96 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 97 |
+
) -> AsyncNetworkStream:
|
| 98 |
+
raise NotImplementedError() # pragma: nocover
|
| 99 |
+
|
| 100 |
+
async def sleep(self, seconds: float) -> None:
|
| 101 |
+
raise NotImplementedError() # pragma: nocover
|
venv/lib/python3.12/site-packages/httpcore/_backends/mock.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import ssl
|
| 4 |
+
import typing
|
| 5 |
+
|
| 6 |
+
from .._exceptions import ReadError
|
| 7 |
+
from .base import (
|
| 8 |
+
SOCKET_OPTION,
|
| 9 |
+
AsyncNetworkBackend,
|
| 10 |
+
AsyncNetworkStream,
|
| 11 |
+
NetworkBackend,
|
| 12 |
+
NetworkStream,
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class MockSSLObject:
|
| 17 |
+
def __init__(self, http2: bool):
|
| 18 |
+
self._http2 = http2
|
| 19 |
+
|
| 20 |
+
def selected_alpn_protocol(self) -> str:
|
| 21 |
+
return "h2" if self._http2 else "http/1.1"
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class MockStream(NetworkStream):
|
| 25 |
+
def __init__(self, buffer: list[bytes], http2: bool = False) -> None:
|
| 26 |
+
self._buffer = buffer
|
| 27 |
+
self._http2 = http2
|
| 28 |
+
self._closed = False
|
| 29 |
+
|
| 30 |
+
def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
|
| 31 |
+
if self._closed:
|
| 32 |
+
raise ReadError("Connection closed")
|
| 33 |
+
if not self._buffer:
|
| 34 |
+
return b""
|
| 35 |
+
return self._buffer.pop(0)
|
| 36 |
+
|
| 37 |
+
def write(self, buffer: bytes, timeout: float | None = None) -> None:
|
| 38 |
+
pass
|
| 39 |
+
|
| 40 |
+
def close(self) -> None:
|
| 41 |
+
self._closed = True
|
| 42 |
+
|
| 43 |
+
def start_tls(
|
| 44 |
+
self,
|
| 45 |
+
ssl_context: ssl.SSLContext,
|
| 46 |
+
server_hostname: str | None = None,
|
| 47 |
+
timeout: float | None = None,
|
| 48 |
+
) -> NetworkStream:
|
| 49 |
+
return self
|
| 50 |
+
|
| 51 |
+
def get_extra_info(self, info: str) -> typing.Any:
|
| 52 |
+
return MockSSLObject(http2=self._http2) if info == "ssl_object" else None
|
| 53 |
+
|
| 54 |
+
def __repr__(self) -> str:
|
| 55 |
+
return "<httpcore.MockStream>"
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class MockBackend(NetworkBackend):
|
| 59 |
+
def __init__(self, buffer: list[bytes], http2: bool = False) -> None:
|
| 60 |
+
self._buffer = buffer
|
| 61 |
+
self._http2 = http2
|
| 62 |
+
|
| 63 |
+
def connect_tcp(
|
| 64 |
+
self,
|
| 65 |
+
host: str,
|
| 66 |
+
port: int,
|
| 67 |
+
timeout: float | None = None,
|
| 68 |
+
local_address: str | None = None,
|
| 69 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 70 |
+
) -> NetworkStream:
|
| 71 |
+
return MockStream(list(self._buffer), http2=self._http2)
|
| 72 |
+
|
| 73 |
+
def connect_unix_socket(
|
| 74 |
+
self,
|
| 75 |
+
path: str,
|
| 76 |
+
timeout: float | None = None,
|
| 77 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 78 |
+
) -> NetworkStream:
|
| 79 |
+
return MockStream(list(self._buffer), http2=self._http2)
|
| 80 |
+
|
| 81 |
+
def sleep(self, seconds: float) -> None:
|
| 82 |
+
pass
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class AsyncMockStream(AsyncNetworkStream):
|
| 86 |
+
def __init__(self, buffer: list[bytes], http2: bool = False) -> None:
|
| 87 |
+
self._buffer = buffer
|
| 88 |
+
self._http2 = http2
|
| 89 |
+
self._closed = False
|
| 90 |
+
|
| 91 |
+
async def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
|
| 92 |
+
if self._closed:
|
| 93 |
+
raise ReadError("Connection closed")
|
| 94 |
+
if not self._buffer:
|
| 95 |
+
return b""
|
| 96 |
+
return self._buffer.pop(0)
|
| 97 |
+
|
| 98 |
+
async def write(self, buffer: bytes, timeout: float | None = None) -> None:
|
| 99 |
+
pass
|
| 100 |
+
|
| 101 |
+
async def aclose(self) -> None:
|
| 102 |
+
self._closed = True
|
| 103 |
+
|
| 104 |
+
async def start_tls(
|
| 105 |
+
self,
|
| 106 |
+
ssl_context: ssl.SSLContext,
|
| 107 |
+
server_hostname: str | None = None,
|
| 108 |
+
timeout: float | None = None,
|
| 109 |
+
) -> AsyncNetworkStream:
|
| 110 |
+
return self
|
| 111 |
+
|
| 112 |
+
def get_extra_info(self, info: str) -> typing.Any:
|
| 113 |
+
return MockSSLObject(http2=self._http2) if info == "ssl_object" else None
|
| 114 |
+
|
| 115 |
+
def __repr__(self) -> str:
|
| 116 |
+
return "<httpcore.AsyncMockStream>"
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
class AsyncMockBackend(AsyncNetworkBackend):
|
| 120 |
+
def __init__(self, buffer: list[bytes], http2: bool = False) -> None:
|
| 121 |
+
self._buffer = buffer
|
| 122 |
+
self._http2 = http2
|
| 123 |
+
|
| 124 |
+
async def connect_tcp(
|
| 125 |
+
self,
|
| 126 |
+
host: str,
|
| 127 |
+
port: int,
|
| 128 |
+
timeout: float | None = None,
|
| 129 |
+
local_address: str | None = None,
|
| 130 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 131 |
+
) -> AsyncNetworkStream:
|
| 132 |
+
return AsyncMockStream(list(self._buffer), http2=self._http2)
|
| 133 |
+
|
| 134 |
+
async def connect_unix_socket(
|
| 135 |
+
self,
|
| 136 |
+
path: str,
|
| 137 |
+
timeout: float | None = None,
|
| 138 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 139 |
+
) -> AsyncNetworkStream:
|
| 140 |
+
return AsyncMockStream(list(self._buffer), http2=self._http2)
|
| 141 |
+
|
| 142 |
+
async def sleep(self, seconds: float) -> None:
|
| 143 |
+
pass
|
venv/lib/python3.12/site-packages/httpcore/_backends/sync.py
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import functools
|
| 4 |
+
import socket
|
| 5 |
+
import ssl
|
| 6 |
+
import sys
|
| 7 |
+
import typing
|
| 8 |
+
|
| 9 |
+
from .._exceptions import (
|
| 10 |
+
ConnectError,
|
| 11 |
+
ConnectTimeout,
|
| 12 |
+
ExceptionMapping,
|
| 13 |
+
ReadError,
|
| 14 |
+
ReadTimeout,
|
| 15 |
+
WriteError,
|
| 16 |
+
WriteTimeout,
|
| 17 |
+
map_exceptions,
|
| 18 |
+
)
|
| 19 |
+
from .._utils import is_socket_readable
|
| 20 |
+
from .base import SOCKET_OPTION, NetworkBackend, NetworkStream
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class TLSinTLSStream(NetworkStream): # pragma: no cover
|
| 24 |
+
"""
|
| 25 |
+
Because the standard `SSLContext.wrap_socket` method does
|
| 26 |
+
not work for `SSLSocket` objects, we need this class
|
| 27 |
+
to implement TLS stream using an underlying `SSLObject`
|
| 28 |
+
instance in order to support TLS on top of TLS.
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
# Defined in RFC 8449
|
| 32 |
+
TLS_RECORD_SIZE = 16384
|
| 33 |
+
|
| 34 |
+
def __init__(
|
| 35 |
+
self,
|
| 36 |
+
sock: socket.socket,
|
| 37 |
+
ssl_context: ssl.SSLContext,
|
| 38 |
+
server_hostname: str | None = None,
|
| 39 |
+
timeout: float | None = None,
|
| 40 |
+
):
|
| 41 |
+
self._sock = sock
|
| 42 |
+
self._incoming = ssl.MemoryBIO()
|
| 43 |
+
self._outgoing = ssl.MemoryBIO()
|
| 44 |
+
|
| 45 |
+
self.ssl_obj = ssl_context.wrap_bio(
|
| 46 |
+
incoming=self._incoming,
|
| 47 |
+
outgoing=self._outgoing,
|
| 48 |
+
server_hostname=server_hostname,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
self._sock.settimeout(timeout)
|
| 52 |
+
self._perform_io(self.ssl_obj.do_handshake)
|
| 53 |
+
|
| 54 |
+
def _perform_io(
|
| 55 |
+
self,
|
| 56 |
+
func: typing.Callable[..., typing.Any],
|
| 57 |
+
) -> typing.Any:
|
| 58 |
+
ret = None
|
| 59 |
+
|
| 60 |
+
while True:
|
| 61 |
+
errno = None
|
| 62 |
+
try:
|
| 63 |
+
ret = func()
|
| 64 |
+
except (ssl.SSLWantReadError, ssl.SSLWantWriteError) as e:
|
| 65 |
+
errno = e.errno
|
| 66 |
+
|
| 67 |
+
self._sock.sendall(self._outgoing.read())
|
| 68 |
+
|
| 69 |
+
if errno == ssl.SSL_ERROR_WANT_READ:
|
| 70 |
+
buf = self._sock.recv(self.TLS_RECORD_SIZE)
|
| 71 |
+
|
| 72 |
+
if buf:
|
| 73 |
+
self._incoming.write(buf)
|
| 74 |
+
else:
|
| 75 |
+
self._incoming.write_eof()
|
| 76 |
+
if errno is None:
|
| 77 |
+
return ret
|
| 78 |
+
|
| 79 |
+
def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
|
| 80 |
+
exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError}
|
| 81 |
+
with map_exceptions(exc_map):
|
| 82 |
+
self._sock.settimeout(timeout)
|
| 83 |
+
return typing.cast(
|
| 84 |
+
bytes, self._perform_io(functools.partial(self.ssl_obj.read, max_bytes))
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
def write(self, buffer: bytes, timeout: float | None = None) -> None:
|
| 88 |
+
exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError: WriteError}
|
| 89 |
+
with map_exceptions(exc_map):
|
| 90 |
+
self._sock.settimeout(timeout)
|
| 91 |
+
while buffer:
|
| 92 |
+
nsent = self._perform_io(functools.partial(self.ssl_obj.write, buffer))
|
| 93 |
+
buffer = buffer[nsent:]
|
| 94 |
+
|
| 95 |
+
def close(self) -> None:
|
| 96 |
+
self._sock.close()
|
| 97 |
+
|
| 98 |
+
def start_tls(
|
| 99 |
+
self,
|
| 100 |
+
ssl_context: ssl.SSLContext,
|
| 101 |
+
server_hostname: str | None = None,
|
| 102 |
+
timeout: float | None = None,
|
| 103 |
+
) -> NetworkStream:
|
| 104 |
+
raise NotImplementedError()
|
| 105 |
+
|
| 106 |
+
def get_extra_info(self, info: str) -> typing.Any:
|
| 107 |
+
if info == "ssl_object":
|
| 108 |
+
return self.ssl_obj
|
| 109 |
+
if info == "client_addr":
|
| 110 |
+
return self._sock.getsockname()
|
| 111 |
+
if info == "server_addr":
|
| 112 |
+
return self._sock.getpeername()
|
| 113 |
+
if info == "socket":
|
| 114 |
+
return self._sock
|
| 115 |
+
if info == "is_readable":
|
| 116 |
+
return is_socket_readable(self._sock)
|
| 117 |
+
return None
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class SyncStream(NetworkStream):
|
| 121 |
+
def __init__(self, sock: socket.socket) -> None:
|
| 122 |
+
self._sock = sock
|
| 123 |
+
|
| 124 |
+
def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
|
| 125 |
+
exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError}
|
| 126 |
+
with map_exceptions(exc_map):
|
| 127 |
+
self._sock.settimeout(timeout)
|
| 128 |
+
return self._sock.recv(max_bytes)
|
| 129 |
+
|
| 130 |
+
def write(self, buffer: bytes, timeout: float | None = None) -> None:
|
| 131 |
+
if not buffer:
|
| 132 |
+
return
|
| 133 |
+
|
| 134 |
+
exc_map: ExceptionMapping = {socket.timeout: WriteTimeout, OSError: WriteError}
|
| 135 |
+
with map_exceptions(exc_map):
|
| 136 |
+
while buffer:
|
| 137 |
+
self._sock.settimeout(timeout)
|
| 138 |
+
n = self._sock.send(buffer)
|
| 139 |
+
buffer = buffer[n:]
|
| 140 |
+
|
| 141 |
+
def close(self) -> None:
|
| 142 |
+
self._sock.close()
|
| 143 |
+
|
| 144 |
+
def start_tls(
|
| 145 |
+
self,
|
| 146 |
+
ssl_context: ssl.SSLContext,
|
| 147 |
+
server_hostname: str | None = None,
|
| 148 |
+
timeout: float | None = None,
|
| 149 |
+
) -> NetworkStream:
|
| 150 |
+
exc_map: ExceptionMapping = {
|
| 151 |
+
socket.timeout: ConnectTimeout,
|
| 152 |
+
OSError: ConnectError,
|
| 153 |
+
}
|
| 154 |
+
with map_exceptions(exc_map):
|
| 155 |
+
try:
|
| 156 |
+
if isinstance(self._sock, ssl.SSLSocket): # pragma: no cover
|
| 157 |
+
# If the underlying socket has already been upgraded
|
| 158 |
+
# to the TLS layer (i.e. is an instance of SSLSocket),
|
| 159 |
+
# we need some additional smarts to support TLS-in-TLS.
|
| 160 |
+
return TLSinTLSStream(
|
| 161 |
+
self._sock, ssl_context, server_hostname, timeout
|
| 162 |
+
)
|
| 163 |
+
else:
|
| 164 |
+
self._sock.settimeout(timeout)
|
| 165 |
+
sock = ssl_context.wrap_socket(
|
| 166 |
+
self._sock, server_hostname=server_hostname
|
| 167 |
+
)
|
| 168 |
+
except Exception as exc: # pragma: nocover
|
| 169 |
+
self.close()
|
| 170 |
+
raise exc
|
| 171 |
+
return SyncStream(sock)
|
| 172 |
+
|
| 173 |
+
def get_extra_info(self, info: str) -> typing.Any:
|
| 174 |
+
if info == "ssl_object" and isinstance(self._sock, ssl.SSLSocket):
|
| 175 |
+
return self._sock._sslobj # type: ignore
|
| 176 |
+
if info == "client_addr":
|
| 177 |
+
return self._sock.getsockname()
|
| 178 |
+
if info == "server_addr":
|
| 179 |
+
return self._sock.getpeername()
|
| 180 |
+
if info == "socket":
|
| 181 |
+
return self._sock
|
| 182 |
+
if info == "is_readable":
|
| 183 |
+
return is_socket_readable(self._sock)
|
| 184 |
+
return None
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
class SyncBackend(NetworkBackend):
|
| 188 |
+
def connect_tcp(
|
| 189 |
+
self,
|
| 190 |
+
host: str,
|
| 191 |
+
port: int,
|
| 192 |
+
timeout: float | None = None,
|
| 193 |
+
local_address: str | None = None,
|
| 194 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 195 |
+
) -> NetworkStream:
|
| 196 |
+
# Note that we automatically include `TCP_NODELAY`
|
| 197 |
+
# in addition to any other custom socket options.
|
| 198 |
+
if socket_options is None:
|
| 199 |
+
socket_options = [] # pragma: no cover
|
| 200 |
+
address = (host, port)
|
| 201 |
+
source_address = None if local_address is None else (local_address, 0)
|
| 202 |
+
exc_map: ExceptionMapping = {
|
| 203 |
+
socket.timeout: ConnectTimeout,
|
| 204 |
+
OSError: ConnectError,
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
with map_exceptions(exc_map):
|
| 208 |
+
sock = socket.create_connection(
|
| 209 |
+
address,
|
| 210 |
+
timeout,
|
| 211 |
+
source_address=source_address,
|
| 212 |
+
)
|
| 213 |
+
for option in socket_options:
|
| 214 |
+
sock.setsockopt(*option) # pragma: no cover
|
| 215 |
+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
| 216 |
+
return SyncStream(sock)
|
| 217 |
+
|
| 218 |
+
def connect_unix_socket(
|
| 219 |
+
self,
|
| 220 |
+
path: str,
|
| 221 |
+
timeout: float | None = None,
|
| 222 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 223 |
+
) -> NetworkStream: # pragma: nocover
|
| 224 |
+
if sys.platform == "win32":
|
| 225 |
+
raise RuntimeError(
|
| 226 |
+
"Attempted to connect to a UNIX socket on a Windows system."
|
| 227 |
+
)
|
| 228 |
+
if socket_options is None:
|
| 229 |
+
socket_options = []
|
| 230 |
+
|
| 231 |
+
exc_map: ExceptionMapping = {
|
| 232 |
+
socket.timeout: ConnectTimeout,
|
| 233 |
+
OSError: ConnectError,
|
| 234 |
+
}
|
| 235 |
+
with map_exceptions(exc_map):
|
| 236 |
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
| 237 |
+
for option in socket_options:
|
| 238 |
+
sock.setsockopt(*option)
|
| 239 |
+
sock.settimeout(timeout)
|
| 240 |
+
sock.connect(path)
|
| 241 |
+
return SyncStream(sock)
|
venv/lib/python3.12/site-packages/httpcore/_backends/trio.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import ssl
|
| 4 |
+
import typing
|
| 5 |
+
|
| 6 |
+
import trio
|
| 7 |
+
|
| 8 |
+
from .._exceptions import (
|
| 9 |
+
ConnectError,
|
| 10 |
+
ConnectTimeout,
|
| 11 |
+
ExceptionMapping,
|
| 12 |
+
ReadError,
|
| 13 |
+
ReadTimeout,
|
| 14 |
+
WriteError,
|
| 15 |
+
WriteTimeout,
|
| 16 |
+
map_exceptions,
|
| 17 |
+
)
|
| 18 |
+
from .base import SOCKET_OPTION, AsyncNetworkBackend, AsyncNetworkStream
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class TrioStream(AsyncNetworkStream):
|
| 22 |
+
def __init__(self, stream: trio.abc.Stream) -> None:
|
| 23 |
+
self._stream = stream
|
| 24 |
+
|
| 25 |
+
async def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
|
| 26 |
+
timeout_or_inf = float("inf") if timeout is None else timeout
|
| 27 |
+
exc_map: ExceptionMapping = {
|
| 28 |
+
trio.TooSlowError: ReadTimeout,
|
| 29 |
+
trio.BrokenResourceError: ReadError,
|
| 30 |
+
trio.ClosedResourceError: ReadError,
|
| 31 |
+
}
|
| 32 |
+
with map_exceptions(exc_map):
|
| 33 |
+
with trio.fail_after(timeout_or_inf):
|
| 34 |
+
data: bytes = await self._stream.receive_some(max_bytes=max_bytes)
|
| 35 |
+
return data
|
| 36 |
+
|
| 37 |
+
async def write(self, buffer: bytes, timeout: float | None = None) -> None:
|
| 38 |
+
if not buffer:
|
| 39 |
+
return
|
| 40 |
+
|
| 41 |
+
timeout_or_inf = float("inf") if timeout is None else timeout
|
| 42 |
+
exc_map: ExceptionMapping = {
|
| 43 |
+
trio.TooSlowError: WriteTimeout,
|
| 44 |
+
trio.BrokenResourceError: WriteError,
|
| 45 |
+
trio.ClosedResourceError: WriteError,
|
| 46 |
+
}
|
| 47 |
+
with map_exceptions(exc_map):
|
| 48 |
+
with trio.fail_after(timeout_or_inf):
|
| 49 |
+
await self._stream.send_all(data=buffer)
|
| 50 |
+
|
| 51 |
+
async def aclose(self) -> None:
|
| 52 |
+
await self._stream.aclose()
|
| 53 |
+
|
| 54 |
+
async def start_tls(
|
| 55 |
+
self,
|
| 56 |
+
ssl_context: ssl.SSLContext,
|
| 57 |
+
server_hostname: str | None = None,
|
| 58 |
+
timeout: float | None = None,
|
| 59 |
+
) -> AsyncNetworkStream:
|
| 60 |
+
timeout_or_inf = float("inf") if timeout is None else timeout
|
| 61 |
+
exc_map: ExceptionMapping = {
|
| 62 |
+
trio.TooSlowError: ConnectTimeout,
|
| 63 |
+
trio.BrokenResourceError: ConnectError,
|
| 64 |
+
}
|
| 65 |
+
ssl_stream = trio.SSLStream(
|
| 66 |
+
self._stream,
|
| 67 |
+
ssl_context=ssl_context,
|
| 68 |
+
server_hostname=server_hostname,
|
| 69 |
+
https_compatible=True,
|
| 70 |
+
server_side=False,
|
| 71 |
+
)
|
| 72 |
+
with map_exceptions(exc_map):
|
| 73 |
+
try:
|
| 74 |
+
with trio.fail_after(timeout_or_inf):
|
| 75 |
+
await ssl_stream.do_handshake()
|
| 76 |
+
except Exception as exc: # pragma: nocover
|
| 77 |
+
await self.aclose()
|
| 78 |
+
raise exc
|
| 79 |
+
return TrioStream(ssl_stream)
|
| 80 |
+
|
| 81 |
+
def get_extra_info(self, info: str) -> typing.Any:
|
| 82 |
+
if info == "ssl_object" and isinstance(self._stream, trio.SSLStream):
|
| 83 |
+
# Type checkers cannot see `_ssl_object` attribute because trio._ssl.SSLStream uses __getattr__/__setattr__.
|
| 84 |
+
# Tracked at https://github.com/python-trio/trio/issues/542
|
| 85 |
+
return self._stream._ssl_object # type: ignore[attr-defined]
|
| 86 |
+
if info == "client_addr":
|
| 87 |
+
return self._get_socket_stream().socket.getsockname()
|
| 88 |
+
if info == "server_addr":
|
| 89 |
+
return self._get_socket_stream().socket.getpeername()
|
| 90 |
+
if info == "socket":
|
| 91 |
+
stream = self._stream
|
| 92 |
+
while isinstance(stream, trio.SSLStream):
|
| 93 |
+
stream = stream.transport_stream
|
| 94 |
+
assert isinstance(stream, trio.SocketStream)
|
| 95 |
+
return stream.socket
|
| 96 |
+
if info == "is_readable":
|
| 97 |
+
socket = self.get_extra_info("socket")
|
| 98 |
+
return socket.is_readable()
|
| 99 |
+
return None
|
| 100 |
+
|
| 101 |
+
def _get_socket_stream(self) -> trio.SocketStream:
|
| 102 |
+
stream = self._stream
|
| 103 |
+
while isinstance(stream, trio.SSLStream):
|
| 104 |
+
stream = stream.transport_stream
|
| 105 |
+
assert isinstance(stream, trio.SocketStream)
|
| 106 |
+
return stream
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class TrioBackend(AsyncNetworkBackend):
|
| 110 |
+
async def connect_tcp(
|
| 111 |
+
self,
|
| 112 |
+
host: str,
|
| 113 |
+
port: int,
|
| 114 |
+
timeout: float | None = None,
|
| 115 |
+
local_address: str | None = None,
|
| 116 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 117 |
+
) -> AsyncNetworkStream:
|
| 118 |
+
# By default for TCP sockets, trio enables TCP_NODELAY.
|
| 119 |
+
# https://trio.readthedocs.io/en/stable/reference-io.html#trio.SocketStream
|
| 120 |
+
if socket_options is None:
|
| 121 |
+
socket_options = [] # pragma: no cover
|
| 122 |
+
timeout_or_inf = float("inf") if timeout is None else timeout
|
| 123 |
+
exc_map: ExceptionMapping = {
|
| 124 |
+
trio.TooSlowError: ConnectTimeout,
|
| 125 |
+
trio.BrokenResourceError: ConnectError,
|
| 126 |
+
OSError: ConnectError,
|
| 127 |
+
}
|
| 128 |
+
with map_exceptions(exc_map):
|
| 129 |
+
with trio.fail_after(timeout_or_inf):
|
| 130 |
+
stream: trio.abc.Stream = await trio.open_tcp_stream(
|
| 131 |
+
host=host, port=port, local_address=local_address
|
| 132 |
+
)
|
| 133 |
+
for option in socket_options:
|
| 134 |
+
stream.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover
|
| 135 |
+
return TrioStream(stream)
|
| 136 |
+
|
| 137 |
+
async def connect_unix_socket(
|
| 138 |
+
self,
|
| 139 |
+
path: str,
|
| 140 |
+
timeout: float | None = None,
|
| 141 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 142 |
+
) -> AsyncNetworkStream: # pragma: nocover
|
| 143 |
+
if socket_options is None:
|
| 144 |
+
socket_options = []
|
| 145 |
+
timeout_or_inf = float("inf") if timeout is None else timeout
|
| 146 |
+
exc_map: ExceptionMapping = {
|
| 147 |
+
trio.TooSlowError: ConnectTimeout,
|
| 148 |
+
trio.BrokenResourceError: ConnectError,
|
| 149 |
+
OSError: ConnectError,
|
| 150 |
+
}
|
| 151 |
+
with map_exceptions(exc_map):
|
| 152 |
+
with trio.fail_after(timeout_or_inf):
|
| 153 |
+
stream: trio.abc.Stream = await trio.open_unix_socket(path)
|
| 154 |
+
for option in socket_options:
|
| 155 |
+
stream.setsockopt(*option) # type: ignore[attr-defined] # pragma: no cover
|
| 156 |
+
return TrioStream(stream)
|
| 157 |
+
|
| 158 |
+
async def sleep(self, seconds: float) -> None:
|
| 159 |
+
await trio.sleep(seconds) # pragma: nocover
|
venv/lib/python3.12/site-packages/httpcore/_exceptions.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import contextlib
|
| 2 |
+
import typing
|
| 3 |
+
|
| 4 |
+
ExceptionMapping = typing.Mapping[typing.Type[Exception], typing.Type[Exception]]
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@contextlib.contextmanager
|
| 8 |
+
def map_exceptions(map: ExceptionMapping) -> typing.Iterator[None]:
|
| 9 |
+
try:
|
| 10 |
+
yield
|
| 11 |
+
except Exception as exc: # noqa: PIE786
|
| 12 |
+
for from_exc, to_exc in map.items():
|
| 13 |
+
if isinstance(exc, from_exc):
|
| 14 |
+
raise to_exc(exc) from exc
|
| 15 |
+
raise # pragma: nocover
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class ConnectionNotAvailable(Exception):
|
| 19 |
+
pass
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class ProxyError(Exception):
|
| 23 |
+
pass
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class UnsupportedProtocol(Exception):
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ProtocolError(Exception):
|
| 31 |
+
pass
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class RemoteProtocolError(ProtocolError):
|
| 35 |
+
pass
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class LocalProtocolError(ProtocolError):
|
| 39 |
+
pass
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# Timeout errors
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class TimeoutException(Exception):
|
| 46 |
+
pass
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class PoolTimeout(TimeoutException):
|
| 50 |
+
pass
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class ConnectTimeout(TimeoutException):
|
| 54 |
+
pass
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class ReadTimeout(TimeoutException):
|
| 58 |
+
pass
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class WriteTimeout(TimeoutException):
|
| 62 |
+
pass
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# Network errors
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class NetworkError(Exception):
|
| 69 |
+
pass
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class ConnectError(NetworkError):
|
| 73 |
+
pass
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class ReadError(NetworkError):
|
| 77 |
+
pass
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class WriteError(NetworkError):
|
| 81 |
+
pass
|
venv/lib/python3.12/site-packages/httpcore/_models.py
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import ssl
|
| 5 |
+
import typing
|
| 6 |
+
import urllib.parse
|
| 7 |
+
|
| 8 |
+
# Functions for typechecking...
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
ByteOrStr = typing.Union[bytes, str]
|
| 12 |
+
HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]]
|
| 13 |
+
HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr]
|
| 14 |
+
HeaderTypes = typing.Union[HeadersAsSequence, HeadersAsMapping, None]
|
| 15 |
+
|
| 16 |
+
Extensions = typing.MutableMapping[str, typing.Any]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def enforce_bytes(value: bytes | str, *, name: str) -> bytes:
|
| 20 |
+
"""
|
| 21 |
+
Any arguments that are ultimately represented as bytes can be specified
|
| 22 |
+
either as bytes or as strings.
|
| 23 |
+
|
| 24 |
+
However we enforce that any string arguments must only contain characters in
|
| 25 |
+
the plain ASCII range. chr(0)...chr(127). If you need to use characters
|
| 26 |
+
outside that range then be precise, and use a byte-wise argument.
|
| 27 |
+
"""
|
| 28 |
+
if isinstance(value, str):
|
| 29 |
+
try:
|
| 30 |
+
return value.encode("ascii")
|
| 31 |
+
except UnicodeEncodeError:
|
| 32 |
+
raise TypeError(f"{name} strings may not include unicode characters.")
|
| 33 |
+
elif isinstance(value, bytes):
|
| 34 |
+
return value
|
| 35 |
+
|
| 36 |
+
seen_type = type(value).__name__
|
| 37 |
+
raise TypeError(f"{name} must be bytes or str, but got {seen_type}.")
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def enforce_url(value: URL | bytes | str, *, name: str) -> URL:
|
| 41 |
+
"""
|
| 42 |
+
Type check for URL parameters.
|
| 43 |
+
"""
|
| 44 |
+
if isinstance(value, (bytes, str)):
|
| 45 |
+
return URL(value)
|
| 46 |
+
elif isinstance(value, URL):
|
| 47 |
+
return value
|
| 48 |
+
|
| 49 |
+
seen_type = type(value).__name__
|
| 50 |
+
raise TypeError(f"{name} must be a URL, bytes, or str, but got {seen_type}.")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def enforce_headers(
|
| 54 |
+
value: HeadersAsMapping | HeadersAsSequence | None = None, *, name: str
|
| 55 |
+
) -> list[tuple[bytes, bytes]]:
|
| 56 |
+
"""
|
| 57 |
+
Convienence function that ensure all items in request or response headers
|
| 58 |
+
are either bytes or strings in the plain ASCII range.
|
| 59 |
+
"""
|
| 60 |
+
if value is None:
|
| 61 |
+
return []
|
| 62 |
+
elif isinstance(value, typing.Mapping):
|
| 63 |
+
return [
|
| 64 |
+
(
|
| 65 |
+
enforce_bytes(k, name="header name"),
|
| 66 |
+
enforce_bytes(v, name="header value"),
|
| 67 |
+
)
|
| 68 |
+
for k, v in value.items()
|
| 69 |
+
]
|
| 70 |
+
elif isinstance(value, typing.Sequence):
|
| 71 |
+
return [
|
| 72 |
+
(
|
| 73 |
+
enforce_bytes(k, name="header name"),
|
| 74 |
+
enforce_bytes(v, name="header value"),
|
| 75 |
+
)
|
| 76 |
+
for k, v in value
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
seen_type = type(value).__name__
|
| 80 |
+
raise TypeError(
|
| 81 |
+
f"{name} must be a mapping or sequence of two-tuples, but got {seen_type}."
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def enforce_stream(
|
| 86 |
+
value: bytes | typing.Iterable[bytes] | typing.AsyncIterable[bytes] | None,
|
| 87 |
+
*,
|
| 88 |
+
name: str,
|
| 89 |
+
) -> typing.Iterable[bytes] | typing.AsyncIterable[bytes]:
|
| 90 |
+
if value is None:
|
| 91 |
+
return ByteStream(b"")
|
| 92 |
+
elif isinstance(value, bytes):
|
| 93 |
+
return ByteStream(value)
|
| 94 |
+
return value
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# * https://tools.ietf.org/html/rfc3986#section-3.2.3
|
| 98 |
+
# * https://url.spec.whatwg.org/#url-miscellaneous
|
| 99 |
+
# * https://url.spec.whatwg.org/#scheme-state
|
| 100 |
+
DEFAULT_PORTS = {
|
| 101 |
+
b"ftp": 21,
|
| 102 |
+
b"http": 80,
|
| 103 |
+
b"https": 443,
|
| 104 |
+
b"ws": 80,
|
| 105 |
+
b"wss": 443,
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def include_request_headers(
|
| 110 |
+
headers: list[tuple[bytes, bytes]],
|
| 111 |
+
*,
|
| 112 |
+
url: "URL",
|
| 113 |
+
content: None | bytes | typing.Iterable[bytes] | typing.AsyncIterable[bytes],
|
| 114 |
+
) -> list[tuple[bytes, bytes]]:
|
| 115 |
+
headers_set = set(k.lower() for k, v in headers)
|
| 116 |
+
|
| 117 |
+
if b"host" not in headers_set:
|
| 118 |
+
default_port = DEFAULT_PORTS.get(url.scheme)
|
| 119 |
+
if url.port is None or url.port == default_port:
|
| 120 |
+
header_value = url.host
|
| 121 |
+
else:
|
| 122 |
+
header_value = b"%b:%d" % (url.host, url.port)
|
| 123 |
+
headers = [(b"Host", header_value)] + headers
|
| 124 |
+
|
| 125 |
+
if (
|
| 126 |
+
content is not None
|
| 127 |
+
and b"content-length" not in headers_set
|
| 128 |
+
and b"transfer-encoding" not in headers_set
|
| 129 |
+
):
|
| 130 |
+
if isinstance(content, bytes):
|
| 131 |
+
content_length = str(len(content)).encode("ascii")
|
| 132 |
+
headers += [(b"Content-Length", content_length)]
|
| 133 |
+
else:
|
| 134 |
+
headers += [(b"Transfer-Encoding", b"chunked")] # pragma: nocover
|
| 135 |
+
|
| 136 |
+
return headers
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
# Interfaces for byte streams...
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class ByteStream:
|
| 143 |
+
"""
|
| 144 |
+
A container for non-streaming content, and that supports both sync and async
|
| 145 |
+
stream iteration.
|
| 146 |
+
"""
|
| 147 |
+
|
| 148 |
+
def __init__(self, content: bytes) -> None:
|
| 149 |
+
self._content = content
|
| 150 |
+
|
| 151 |
+
def __iter__(self) -> typing.Iterator[bytes]:
|
| 152 |
+
yield self._content
|
| 153 |
+
|
| 154 |
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
| 155 |
+
yield self._content
|
| 156 |
+
|
| 157 |
+
def __repr__(self) -> str:
|
| 158 |
+
return f"<{self.__class__.__name__} [{len(self._content)} bytes]>"
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
class Origin:
|
| 162 |
+
def __init__(self, scheme: bytes, host: bytes, port: int) -> None:
|
| 163 |
+
self.scheme = scheme
|
| 164 |
+
self.host = host
|
| 165 |
+
self.port = port
|
| 166 |
+
|
| 167 |
+
def __eq__(self, other: typing.Any) -> bool:
|
| 168 |
+
return (
|
| 169 |
+
isinstance(other, Origin)
|
| 170 |
+
and self.scheme == other.scheme
|
| 171 |
+
and self.host == other.host
|
| 172 |
+
and self.port == other.port
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
def __str__(self) -> str:
|
| 176 |
+
scheme = self.scheme.decode("ascii")
|
| 177 |
+
host = self.host.decode("ascii")
|
| 178 |
+
port = str(self.port)
|
| 179 |
+
return f"{scheme}://{host}:{port}"
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
class URL:
|
| 183 |
+
"""
|
| 184 |
+
Represents the URL against which an HTTP request may be made.
|
| 185 |
+
|
| 186 |
+
The URL may either be specified as a plain string, for convienence:
|
| 187 |
+
|
| 188 |
+
```python
|
| 189 |
+
url = httpcore.URL("https://www.example.com/")
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
Or be constructed with explicitily pre-parsed components:
|
| 193 |
+
|
| 194 |
+
```python
|
| 195 |
+
url = httpcore.URL(scheme=b'https', host=b'www.example.com', port=None, target=b'/')
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
Using this second more explicit style allows integrations that are using
|
| 199 |
+
`httpcore` to pass through URLs that have already been parsed in order to use
|
| 200 |
+
libraries such as `rfc-3986` rather than relying on the stdlib. It also ensures
|
| 201 |
+
that URL parsing is treated identically at both the networking level and at any
|
| 202 |
+
higher layers of abstraction.
|
| 203 |
+
|
| 204 |
+
The four components are important here, as they allow the URL to be precisely
|
| 205 |
+
specified in a pre-parsed format. They also allow certain types of request to
|
| 206 |
+
be created that could not otherwise be expressed.
|
| 207 |
+
|
| 208 |
+
For example, an HTTP request to `http://www.example.com/` forwarded via a proxy
|
| 209 |
+
at `http://localhost:8080`...
|
| 210 |
+
|
| 211 |
+
```python
|
| 212 |
+
# Constructs an HTTP request with a complete URL as the target:
|
| 213 |
+
# GET https://www.example.com/ HTTP/1.1
|
| 214 |
+
url = httpcore.URL(
|
| 215 |
+
scheme=b'http',
|
| 216 |
+
host=b'localhost',
|
| 217 |
+
port=8080,
|
| 218 |
+
target=b'https://www.example.com/'
|
| 219 |
+
)
|
| 220 |
+
request = httpcore.Request(
|
| 221 |
+
method="GET",
|
| 222 |
+
url=url
|
| 223 |
+
)
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
Another example is constructing an `OPTIONS *` request...
|
| 227 |
+
|
| 228 |
+
```python
|
| 229 |
+
# Constructs an 'OPTIONS *' HTTP request:
|
| 230 |
+
# OPTIONS * HTTP/1.1
|
| 231 |
+
url = httpcore.URL(scheme=b'https', host=b'www.example.com', target=b'*')
|
| 232 |
+
request = httpcore.Request(method="OPTIONS", url=url)
|
| 233 |
+
```
|
| 234 |
+
|
| 235 |
+
This kind of request is not possible to formulate with a URL string,
|
| 236 |
+
because the `/` delimiter is always used to demark the target from the
|
| 237 |
+
host/port portion of the URL.
|
| 238 |
+
|
| 239 |
+
For convenience, string-like arguments may be specified either as strings or
|
| 240 |
+
as bytes. However, once a request is being issue over-the-wire, the URL
|
| 241 |
+
components are always ultimately required to be a bytewise representation.
|
| 242 |
+
|
| 243 |
+
In order to avoid any ambiguity over character encodings, when strings are used
|
| 244 |
+
as arguments, they must be strictly limited to the ASCII range `chr(0)`-`chr(127)`.
|
| 245 |
+
If you require a bytewise representation that is outside this range you must
|
| 246 |
+
handle the character encoding directly, and pass a bytes instance.
|
| 247 |
+
"""
|
| 248 |
+
|
| 249 |
+
def __init__(
|
| 250 |
+
self,
|
| 251 |
+
url: bytes | str = "",
|
| 252 |
+
*,
|
| 253 |
+
scheme: bytes | str = b"",
|
| 254 |
+
host: bytes | str = b"",
|
| 255 |
+
port: int | None = None,
|
| 256 |
+
target: bytes | str = b"",
|
| 257 |
+
) -> None:
|
| 258 |
+
"""
|
| 259 |
+
Parameters:
|
| 260 |
+
url: The complete URL as a string or bytes.
|
| 261 |
+
scheme: The URL scheme as a string or bytes.
|
| 262 |
+
Typically either `"http"` or `"https"`.
|
| 263 |
+
host: The URL host as a string or bytes. Such as `"www.example.com"`.
|
| 264 |
+
port: The port to connect to. Either an integer or `None`.
|
| 265 |
+
target: The target of the HTTP request. Such as `"/items?search=red"`.
|
| 266 |
+
"""
|
| 267 |
+
if url:
|
| 268 |
+
parsed = urllib.parse.urlparse(enforce_bytes(url, name="url"))
|
| 269 |
+
self.scheme = parsed.scheme
|
| 270 |
+
self.host = parsed.hostname or b""
|
| 271 |
+
self.port = parsed.port
|
| 272 |
+
self.target = (parsed.path or b"/") + (
|
| 273 |
+
b"?" + parsed.query if parsed.query else b""
|
| 274 |
+
)
|
| 275 |
+
else:
|
| 276 |
+
self.scheme = enforce_bytes(scheme, name="scheme")
|
| 277 |
+
self.host = enforce_bytes(host, name="host")
|
| 278 |
+
self.port = port
|
| 279 |
+
self.target = enforce_bytes(target, name="target")
|
| 280 |
+
|
| 281 |
+
@property
|
| 282 |
+
def origin(self) -> Origin:
|
| 283 |
+
default_port = {
|
| 284 |
+
b"http": 80,
|
| 285 |
+
b"https": 443,
|
| 286 |
+
b"ws": 80,
|
| 287 |
+
b"wss": 443,
|
| 288 |
+
b"socks5": 1080,
|
| 289 |
+
b"socks5h": 1080,
|
| 290 |
+
}[self.scheme]
|
| 291 |
+
return Origin(
|
| 292 |
+
scheme=self.scheme, host=self.host, port=self.port or default_port
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
def __eq__(self, other: typing.Any) -> bool:
|
| 296 |
+
return (
|
| 297 |
+
isinstance(other, URL)
|
| 298 |
+
and other.scheme == self.scheme
|
| 299 |
+
and other.host == self.host
|
| 300 |
+
and other.port == self.port
|
| 301 |
+
and other.target == self.target
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
def __bytes__(self) -> bytes:
|
| 305 |
+
if self.port is None:
|
| 306 |
+
return b"%b://%b%b" % (self.scheme, self.host, self.target)
|
| 307 |
+
return b"%b://%b:%d%b" % (self.scheme, self.host, self.port, self.target)
|
| 308 |
+
|
| 309 |
+
def __repr__(self) -> str:
|
| 310 |
+
return (
|
| 311 |
+
f"{self.__class__.__name__}(scheme={self.scheme!r}, "
|
| 312 |
+
f"host={self.host!r}, port={self.port!r}, target={self.target!r})"
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
|
| 316 |
+
class Request:
|
| 317 |
+
"""
|
| 318 |
+
An HTTP request.
|
| 319 |
+
"""
|
| 320 |
+
|
| 321 |
+
def __init__(
|
| 322 |
+
self,
|
| 323 |
+
method: bytes | str,
|
| 324 |
+
url: URL | bytes | str,
|
| 325 |
+
*,
|
| 326 |
+
headers: HeaderTypes = None,
|
| 327 |
+
content: bytes
|
| 328 |
+
| typing.Iterable[bytes]
|
| 329 |
+
| typing.AsyncIterable[bytes]
|
| 330 |
+
| None = None,
|
| 331 |
+
extensions: Extensions | None = None,
|
| 332 |
+
) -> None:
|
| 333 |
+
"""
|
| 334 |
+
Parameters:
|
| 335 |
+
method: The HTTP request method, either as a string or bytes.
|
| 336 |
+
For example: `GET`.
|
| 337 |
+
url: The request URL, either as a `URL` instance, or as a string or bytes.
|
| 338 |
+
For example: `"https://www.example.com".`
|
| 339 |
+
headers: The HTTP request headers.
|
| 340 |
+
content: The content of the request body.
|
| 341 |
+
extensions: A dictionary of optional extra information included on
|
| 342 |
+
the request. Possible keys include `"timeout"`, and `"trace"`.
|
| 343 |
+
"""
|
| 344 |
+
self.method: bytes = enforce_bytes(method, name="method")
|
| 345 |
+
self.url: URL = enforce_url(url, name="url")
|
| 346 |
+
self.headers: list[tuple[bytes, bytes]] = enforce_headers(
|
| 347 |
+
headers, name="headers"
|
| 348 |
+
)
|
| 349 |
+
self.stream: typing.Iterable[bytes] | typing.AsyncIterable[bytes] = (
|
| 350 |
+
enforce_stream(content, name="content")
|
| 351 |
+
)
|
| 352 |
+
self.extensions = {} if extensions is None else extensions
|
| 353 |
+
|
| 354 |
+
if "target" in self.extensions:
|
| 355 |
+
self.url = URL(
|
| 356 |
+
scheme=self.url.scheme,
|
| 357 |
+
host=self.url.host,
|
| 358 |
+
port=self.url.port,
|
| 359 |
+
target=self.extensions["target"],
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
def __repr__(self) -> str:
|
| 363 |
+
return f"<{self.__class__.__name__} [{self.method!r}]>"
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
class Response:
|
| 367 |
+
"""
|
| 368 |
+
An HTTP response.
|
| 369 |
+
"""
|
| 370 |
+
|
| 371 |
+
def __init__(
|
| 372 |
+
self,
|
| 373 |
+
status: int,
|
| 374 |
+
*,
|
| 375 |
+
headers: HeaderTypes = None,
|
| 376 |
+
content: bytes
|
| 377 |
+
| typing.Iterable[bytes]
|
| 378 |
+
| typing.AsyncIterable[bytes]
|
| 379 |
+
| None = None,
|
| 380 |
+
extensions: Extensions | None = None,
|
| 381 |
+
) -> None:
|
| 382 |
+
"""
|
| 383 |
+
Parameters:
|
| 384 |
+
status: The HTTP status code of the response. For example `200`.
|
| 385 |
+
headers: The HTTP response headers.
|
| 386 |
+
content: The content of the response body.
|
| 387 |
+
extensions: A dictionary of optional extra information included on
|
| 388 |
+
the responseself.Possible keys include `"http_version"`,
|
| 389 |
+
`"reason_phrase"`, and `"network_stream"`.
|
| 390 |
+
"""
|
| 391 |
+
self.status: int = status
|
| 392 |
+
self.headers: list[tuple[bytes, bytes]] = enforce_headers(
|
| 393 |
+
headers, name="headers"
|
| 394 |
+
)
|
| 395 |
+
self.stream: typing.Iterable[bytes] | typing.AsyncIterable[bytes] = (
|
| 396 |
+
enforce_stream(content, name="content")
|
| 397 |
+
)
|
| 398 |
+
self.extensions = {} if extensions is None else extensions
|
| 399 |
+
|
| 400 |
+
self._stream_consumed = False
|
| 401 |
+
|
| 402 |
+
@property
|
| 403 |
+
def content(self) -> bytes:
|
| 404 |
+
if not hasattr(self, "_content"):
|
| 405 |
+
if isinstance(self.stream, typing.Iterable):
|
| 406 |
+
raise RuntimeError(
|
| 407 |
+
"Attempted to access 'response.content' on a streaming response. "
|
| 408 |
+
"Call 'response.read()' first."
|
| 409 |
+
)
|
| 410 |
+
else:
|
| 411 |
+
raise RuntimeError(
|
| 412 |
+
"Attempted to access 'response.content' on a streaming response. "
|
| 413 |
+
"Call 'await response.aread()' first."
|
| 414 |
+
)
|
| 415 |
+
return self._content
|
| 416 |
+
|
| 417 |
+
def __repr__(self) -> str:
|
| 418 |
+
return f"<{self.__class__.__name__} [{self.status}]>"
|
| 419 |
+
|
| 420 |
+
# Sync interface...
|
| 421 |
+
|
| 422 |
+
def read(self) -> bytes:
|
| 423 |
+
if not isinstance(self.stream, typing.Iterable): # pragma: nocover
|
| 424 |
+
raise RuntimeError(
|
| 425 |
+
"Attempted to read an asynchronous response using 'response.read()'. "
|
| 426 |
+
"You should use 'await response.aread()' instead."
|
| 427 |
+
)
|
| 428 |
+
if not hasattr(self, "_content"):
|
| 429 |
+
self._content = b"".join([part for part in self.iter_stream()])
|
| 430 |
+
return self._content
|
| 431 |
+
|
| 432 |
+
def iter_stream(self) -> typing.Iterator[bytes]:
|
| 433 |
+
if not isinstance(self.stream, typing.Iterable): # pragma: nocover
|
| 434 |
+
raise RuntimeError(
|
| 435 |
+
"Attempted to stream an asynchronous response using 'for ... in "
|
| 436 |
+
"response.iter_stream()'. "
|
| 437 |
+
"You should use 'async for ... in response.aiter_stream()' instead."
|
| 438 |
+
)
|
| 439 |
+
if self._stream_consumed:
|
| 440 |
+
raise RuntimeError(
|
| 441 |
+
"Attempted to call 'for ... in response.iter_stream()' more than once."
|
| 442 |
+
)
|
| 443 |
+
self._stream_consumed = True
|
| 444 |
+
for chunk in self.stream:
|
| 445 |
+
yield chunk
|
| 446 |
+
|
| 447 |
+
def close(self) -> None:
|
| 448 |
+
if not isinstance(self.stream, typing.Iterable): # pragma: nocover
|
| 449 |
+
raise RuntimeError(
|
| 450 |
+
"Attempted to close an asynchronous response using 'response.close()'. "
|
| 451 |
+
"You should use 'await response.aclose()' instead."
|
| 452 |
+
)
|
| 453 |
+
if hasattr(self.stream, "close"):
|
| 454 |
+
self.stream.close()
|
| 455 |
+
|
| 456 |
+
# Async interface...
|
| 457 |
+
|
| 458 |
+
async def aread(self) -> bytes:
|
| 459 |
+
if not isinstance(self.stream, typing.AsyncIterable): # pragma: nocover
|
| 460 |
+
raise RuntimeError(
|
| 461 |
+
"Attempted to read an synchronous response using "
|
| 462 |
+
"'await response.aread()'. "
|
| 463 |
+
"You should use 'response.read()' instead."
|
| 464 |
+
)
|
| 465 |
+
if not hasattr(self, "_content"):
|
| 466 |
+
self._content = b"".join([part async for part in self.aiter_stream()])
|
| 467 |
+
return self._content
|
| 468 |
+
|
| 469 |
+
async def aiter_stream(self) -> typing.AsyncIterator[bytes]:
|
| 470 |
+
if not isinstance(self.stream, typing.AsyncIterable): # pragma: nocover
|
| 471 |
+
raise RuntimeError(
|
| 472 |
+
"Attempted to stream an synchronous response using 'async for ... in "
|
| 473 |
+
"response.aiter_stream()'. "
|
| 474 |
+
"You should use 'for ... in response.iter_stream()' instead."
|
| 475 |
+
)
|
| 476 |
+
if self._stream_consumed:
|
| 477 |
+
raise RuntimeError(
|
| 478 |
+
"Attempted to call 'async for ... in response.aiter_stream()' "
|
| 479 |
+
"more than once."
|
| 480 |
+
)
|
| 481 |
+
self._stream_consumed = True
|
| 482 |
+
async for chunk in self.stream:
|
| 483 |
+
yield chunk
|
| 484 |
+
|
| 485 |
+
async def aclose(self) -> None:
|
| 486 |
+
if not isinstance(self.stream, typing.AsyncIterable): # pragma: nocover
|
| 487 |
+
raise RuntimeError(
|
| 488 |
+
"Attempted to close a synchronous response using "
|
| 489 |
+
"'await response.aclose()'. "
|
| 490 |
+
"You should use 'response.close()' instead."
|
| 491 |
+
)
|
| 492 |
+
if hasattr(self.stream, "aclose"):
|
| 493 |
+
await self.stream.aclose()
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
class Proxy:
|
| 497 |
+
def __init__(
|
| 498 |
+
self,
|
| 499 |
+
url: URL | bytes | str,
|
| 500 |
+
auth: tuple[bytes | str, bytes | str] | None = None,
|
| 501 |
+
headers: HeadersAsMapping | HeadersAsSequence | None = None,
|
| 502 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 503 |
+
):
|
| 504 |
+
self.url = enforce_url(url, name="url")
|
| 505 |
+
self.headers = enforce_headers(headers, name="headers")
|
| 506 |
+
self.ssl_context = ssl_context
|
| 507 |
+
|
| 508 |
+
if auth is not None:
|
| 509 |
+
username = enforce_bytes(auth[0], name="auth")
|
| 510 |
+
password = enforce_bytes(auth[1], name="auth")
|
| 511 |
+
userpass = username + b":" + password
|
| 512 |
+
authorization = b"Basic " + base64.b64encode(userpass)
|
| 513 |
+
self.auth: tuple[bytes, bytes] | None = (username, password)
|
| 514 |
+
self.headers = [(b"Proxy-Authorization", authorization)] + self.headers
|
| 515 |
+
else:
|
| 516 |
+
self.auth = None
|
venv/lib/python3.12/site-packages/httpcore/_ssl.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ssl
|
| 2 |
+
|
| 3 |
+
import certifi
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def default_ssl_context() -> ssl.SSLContext:
|
| 7 |
+
context = ssl.create_default_context()
|
| 8 |
+
context.load_verify_locations(certifi.where())
|
| 9 |
+
return context
|
venv/lib/python3.12/site-packages/httpcore/_sync/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .connection import HTTPConnection
|
| 2 |
+
from .connection_pool import ConnectionPool
|
| 3 |
+
from .http11 import HTTP11Connection
|
| 4 |
+
from .http_proxy import HTTPProxy
|
| 5 |
+
from .interfaces import ConnectionInterface
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
from .http2 import HTTP2Connection
|
| 9 |
+
except ImportError: # pragma: nocover
|
| 10 |
+
|
| 11 |
+
class HTTP2Connection: # type: ignore
|
| 12 |
+
def __init__(self, *args, **kwargs) -> None: # type: ignore
|
| 13 |
+
raise RuntimeError(
|
| 14 |
+
"Attempted to use http2 support, but the `h2` package is not "
|
| 15 |
+
"installed. Use 'pip install httpcore[http2]'."
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from .socks_proxy import SOCKSProxy
|
| 21 |
+
except ImportError: # pragma: nocover
|
| 22 |
+
|
| 23 |
+
class SOCKSProxy: # type: ignore
|
| 24 |
+
def __init__(self, *args, **kwargs) -> None: # type: ignore
|
| 25 |
+
raise RuntimeError(
|
| 26 |
+
"Attempted to use SOCKS support, but the `socksio` package is not "
|
| 27 |
+
"installed. Use 'pip install httpcore[socks]'."
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
__all__ = [
|
| 32 |
+
"HTTPConnection",
|
| 33 |
+
"ConnectionPool",
|
| 34 |
+
"HTTPProxy",
|
| 35 |
+
"HTTP11Connection",
|
| 36 |
+
"HTTP2Connection",
|
| 37 |
+
"ConnectionInterface",
|
| 38 |
+
"SOCKSProxy",
|
| 39 |
+
]
|
venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.56 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/connection.cpython-312.pyc
ADDED
|
Binary file (10.4 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/connection_pool.cpython-312.pyc
ADDED
|
Binary file (18.7 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http11.cpython-312.pyc
ADDED
|
Binary file (17.8 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http2.cpython-312.pyc
ADDED
|
Binary file (27.4 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/http_proxy.cpython-312.pyc
ADDED
|
Binary file (17.2 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/interfaces.cpython-312.pyc
ADDED
|
Binary file (5.28 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_sync/__pycache__/socks_proxy.cpython-312.pyc
ADDED
|
Binary file (15.6 kB). View file
|
|
|
venv/lib/python3.12/site-packages/httpcore/_sync/connection.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import itertools
|
| 4 |
+
import logging
|
| 5 |
+
import ssl
|
| 6 |
+
import types
|
| 7 |
+
import typing
|
| 8 |
+
|
| 9 |
+
from .._backends.sync import SyncBackend
|
| 10 |
+
from .._backends.base import SOCKET_OPTION, NetworkBackend, NetworkStream
|
| 11 |
+
from .._exceptions import ConnectError, ConnectTimeout
|
| 12 |
+
from .._models import Origin, Request, Response
|
| 13 |
+
from .._ssl import default_ssl_context
|
| 14 |
+
from .._synchronization import Lock
|
| 15 |
+
from .._trace import Trace
|
| 16 |
+
from .http11 import HTTP11Connection
|
| 17 |
+
from .interfaces import ConnectionInterface
|
| 18 |
+
|
| 19 |
+
RETRIES_BACKOFF_FACTOR = 0.5 # 0s, 0.5s, 1s, 2s, 4s, etc.
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger("httpcore.connection")
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def exponential_backoff(factor: float) -> typing.Iterator[float]:
|
| 26 |
+
"""
|
| 27 |
+
Generate a geometric sequence that has a ratio of 2 and starts with 0.
|
| 28 |
+
|
| 29 |
+
For example:
|
| 30 |
+
- `factor = 2`: `0, 2, 4, 8, 16, 32, 64, ...`
|
| 31 |
+
- `factor = 3`: `0, 3, 6, 12, 24, 48, 96, ...`
|
| 32 |
+
"""
|
| 33 |
+
yield 0
|
| 34 |
+
for n in itertools.count():
|
| 35 |
+
yield factor * 2**n
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class HTTPConnection(ConnectionInterface):
|
| 39 |
+
def __init__(
|
| 40 |
+
self,
|
| 41 |
+
origin: Origin,
|
| 42 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 43 |
+
keepalive_expiry: float | None = None,
|
| 44 |
+
http1: bool = True,
|
| 45 |
+
http2: bool = False,
|
| 46 |
+
retries: int = 0,
|
| 47 |
+
local_address: str | None = None,
|
| 48 |
+
uds: str | None = None,
|
| 49 |
+
network_backend: NetworkBackend | None = None,
|
| 50 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 51 |
+
) -> None:
|
| 52 |
+
self._origin = origin
|
| 53 |
+
self._ssl_context = ssl_context
|
| 54 |
+
self._keepalive_expiry = keepalive_expiry
|
| 55 |
+
self._http1 = http1
|
| 56 |
+
self._http2 = http2
|
| 57 |
+
self._retries = retries
|
| 58 |
+
self._local_address = local_address
|
| 59 |
+
self._uds = uds
|
| 60 |
+
|
| 61 |
+
self._network_backend: NetworkBackend = (
|
| 62 |
+
SyncBackend() if network_backend is None else network_backend
|
| 63 |
+
)
|
| 64 |
+
self._connection: ConnectionInterface | None = None
|
| 65 |
+
self._connect_failed: bool = False
|
| 66 |
+
self._request_lock = Lock()
|
| 67 |
+
self._socket_options = socket_options
|
| 68 |
+
|
| 69 |
+
def handle_request(self, request: Request) -> Response:
|
| 70 |
+
if not self.can_handle_request(request.url.origin):
|
| 71 |
+
raise RuntimeError(
|
| 72 |
+
f"Attempted to send request to {request.url.origin} on connection to {self._origin}"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
with self._request_lock:
|
| 77 |
+
if self._connection is None:
|
| 78 |
+
stream = self._connect(request)
|
| 79 |
+
|
| 80 |
+
ssl_object = stream.get_extra_info("ssl_object")
|
| 81 |
+
http2_negotiated = (
|
| 82 |
+
ssl_object is not None
|
| 83 |
+
and ssl_object.selected_alpn_protocol() == "h2"
|
| 84 |
+
)
|
| 85 |
+
if http2_negotiated or (self._http2 and not self._http1):
|
| 86 |
+
from .http2 import HTTP2Connection
|
| 87 |
+
|
| 88 |
+
self._connection = HTTP2Connection(
|
| 89 |
+
origin=self._origin,
|
| 90 |
+
stream=stream,
|
| 91 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 92 |
+
)
|
| 93 |
+
else:
|
| 94 |
+
self._connection = HTTP11Connection(
|
| 95 |
+
origin=self._origin,
|
| 96 |
+
stream=stream,
|
| 97 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 98 |
+
)
|
| 99 |
+
except BaseException as exc:
|
| 100 |
+
self._connect_failed = True
|
| 101 |
+
raise exc
|
| 102 |
+
|
| 103 |
+
return self._connection.handle_request(request)
|
| 104 |
+
|
| 105 |
+
def _connect(self, request: Request) -> NetworkStream:
|
| 106 |
+
timeouts = request.extensions.get("timeout", {})
|
| 107 |
+
sni_hostname = request.extensions.get("sni_hostname", None)
|
| 108 |
+
timeout = timeouts.get("connect", None)
|
| 109 |
+
|
| 110 |
+
retries_left = self._retries
|
| 111 |
+
delays = exponential_backoff(factor=RETRIES_BACKOFF_FACTOR)
|
| 112 |
+
|
| 113 |
+
while True:
|
| 114 |
+
try:
|
| 115 |
+
if self._uds is None:
|
| 116 |
+
kwargs = {
|
| 117 |
+
"host": self._origin.host.decode("ascii"),
|
| 118 |
+
"port": self._origin.port,
|
| 119 |
+
"local_address": self._local_address,
|
| 120 |
+
"timeout": timeout,
|
| 121 |
+
"socket_options": self._socket_options,
|
| 122 |
+
}
|
| 123 |
+
with Trace("connect_tcp", logger, request, kwargs) as trace:
|
| 124 |
+
stream = self._network_backend.connect_tcp(**kwargs)
|
| 125 |
+
trace.return_value = stream
|
| 126 |
+
else:
|
| 127 |
+
kwargs = {
|
| 128 |
+
"path": self._uds,
|
| 129 |
+
"timeout": timeout,
|
| 130 |
+
"socket_options": self._socket_options,
|
| 131 |
+
}
|
| 132 |
+
with Trace(
|
| 133 |
+
"connect_unix_socket", logger, request, kwargs
|
| 134 |
+
) as trace:
|
| 135 |
+
stream = self._network_backend.connect_unix_socket(
|
| 136 |
+
**kwargs
|
| 137 |
+
)
|
| 138 |
+
trace.return_value = stream
|
| 139 |
+
|
| 140 |
+
if self._origin.scheme in (b"https", b"wss"):
|
| 141 |
+
ssl_context = (
|
| 142 |
+
default_ssl_context()
|
| 143 |
+
if self._ssl_context is None
|
| 144 |
+
else self._ssl_context
|
| 145 |
+
)
|
| 146 |
+
alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"]
|
| 147 |
+
ssl_context.set_alpn_protocols(alpn_protocols)
|
| 148 |
+
|
| 149 |
+
kwargs = {
|
| 150 |
+
"ssl_context": ssl_context,
|
| 151 |
+
"server_hostname": sni_hostname
|
| 152 |
+
or self._origin.host.decode("ascii"),
|
| 153 |
+
"timeout": timeout,
|
| 154 |
+
}
|
| 155 |
+
with Trace("start_tls", logger, request, kwargs) as trace:
|
| 156 |
+
stream = stream.start_tls(**kwargs)
|
| 157 |
+
trace.return_value = stream
|
| 158 |
+
return stream
|
| 159 |
+
except (ConnectError, ConnectTimeout):
|
| 160 |
+
if retries_left <= 0:
|
| 161 |
+
raise
|
| 162 |
+
retries_left -= 1
|
| 163 |
+
delay = next(delays)
|
| 164 |
+
with Trace("retry", logger, request, kwargs) as trace:
|
| 165 |
+
self._network_backend.sleep(delay)
|
| 166 |
+
|
| 167 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 168 |
+
return origin == self._origin
|
| 169 |
+
|
| 170 |
+
def close(self) -> None:
|
| 171 |
+
if self._connection is not None:
|
| 172 |
+
with Trace("close", logger, None, {}):
|
| 173 |
+
self._connection.close()
|
| 174 |
+
|
| 175 |
+
def is_available(self) -> bool:
|
| 176 |
+
if self._connection is None:
|
| 177 |
+
# If HTTP/2 support is enabled, and the resulting connection could
|
| 178 |
+
# end up as HTTP/2 then we should indicate the connection as being
|
| 179 |
+
# available to service multiple requests.
|
| 180 |
+
return (
|
| 181 |
+
self._http2
|
| 182 |
+
and (self._origin.scheme == b"https" or not self._http1)
|
| 183 |
+
and not self._connect_failed
|
| 184 |
+
)
|
| 185 |
+
return self._connection.is_available()
|
| 186 |
+
|
| 187 |
+
def has_expired(self) -> bool:
|
| 188 |
+
if self._connection is None:
|
| 189 |
+
return self._connect_failed
|
| 190 |
+
return self._connection.has_expired()
|
| 191 |
+
|
| 192 |
+
def is_idle(self) -> bool:
|
| 193 |
+
if self._connection is None:
|
| 194 |
+
return self._connect_failed
|
| 195 |
+
return self._connection.is_idle()
|
| 196 |
+
|
| 197 |
+
def is_closed(self) -> bool:
|
| 198 |
+
if self._connection is None:
|
| 199 |
+
return self._connect_failed
|
| 200 |
+
return self._connection.is_closed()
|
| 201 |
+
|
| 202 |
+
def info(self) -> str:
|
| 203 |
+
if self._connection is None:
|
| 204 |
+
return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"
|
| 205 |
+
return self._connection.info()
|
| 206 |
+
|
| 207 |
+
def __repr__(self) -> str:
|
| 208 |
+
return f"<{self.__class__.__name__} [{self.info()}]>"
|
| 209 |
+
|
| 210 |
+
# These context managers are not used in the standard flow, but are
|
| 211 |
+
# useful for testing or working with connection instances directly.
|
| 212 |
+
|
| 213 |
+
def __enter__(self) -> HTTPConnection:
|
| 214 |
+
return self
|
| 215 |
+
|
| 216 |
+
def __exit__(
|
| 217 |
+
self,
|
| 218 |
+
exc_type: type[BaseException] | None = None,
|
| 219 |
+
exc_value: BaseException | None = None,
|
| 220 |
+
traceback: types.TracebackType | None = None,
|
| 221 |
+
) -> None:
|
| 222 |
+
self.close()
|
venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import ssl
|
| 4 |
+
import sys
|
| 5 |
+
import types
|
| 6 |
+
import typing
|
| 7 |
+
|
| 8 |
+
from .._backends.sync import SyncBackend
|
| 9 |
+
from .._backends.base import SOCKET_OPTION, NetworkBackend
|
| 10 |
+
from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol
|
| 11 |
+
from .._models import Origin, Proxy, Request, Response
|
| 12 |
+
from .._synchronization import Event, ShieldCancellation, ThreadLock
|
| 13 |
+
from .connection import HTTPConnection
|
| 14 |
+
from .interfaces import ConnectionInterface, RequestInterface
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class PoolRequest:
|
| 18 |
+
def __init__(self, request: Request) -> None:
|
| 19 |
+
self.request = request
|
| 20 |
+
self.connection: ConnectionInterface | None = None
|
| 21 |
+
self._connection_acquired = Event()
|
| 22 |
+
|
| 23 |
+
def assign_to_connection(self, connection: ConnectionInterface | None) -> None:
|
| 24 |
+
self.connection = connection
|
| 25 |
+
self._connection_acquired.set()
|
| 26 |
+
|
| 27 |
+
def clear_connection(self) -> None:
|
| 28 |
+
self.connection = None
|
| 29 |
+
self._connection_acquired = Event()
|
| 30 |
+
|
| 31 |
+
def wait_for_connection(
|
| 32 |
+
self, timeout: float | None = None
|
| 33 |
+
) -> ConnectionInterface:
|
| 34 |
+
if self.connection is None:
|
| 35 |
+
self._connection_acquired.wait(timeout=timeout)
|
| 36 |
+
assert self.connection is not None
|
| 37 |
+
return self.connection
|
| 38 |
+
|
| 39 |
+
def is_queued(self) -> bool:
|
| 40 |
+
return self.connection is None
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class ConnectionPool(RequestInterface):
|
| 44 |
+
"""
|
| 45 |
+
A connection pool for making HTTP requests.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(
|
| 49 |
+
self,
|
| 50 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 51 |
+
proxy: Proxy | None = None,
|
| 52 |
+
max_connections: int | None = 10,
|
| 53 |
+
max_keepalive_connections: int | None = None,
|
| 54 |
+
keepalive_expiry: float | None = None,
|
| 55 |
+
http1: bool = True,
|
| 56 |
+
http2: bool = False,
|
| 57 |
+
retries: int = 0,
|
| 58 |
+
local_address: str | None = None,
|
| 59 |
+
uds: str | None = None,
|
| 60 |
+
network_backend: NetworkBackend | None = None,
|
| 61 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 62 |
+
) -> None:
|
| 63 |
+
"""
|
| 64 |
+
A connection pool for making HTTP requests.
|
| 65 |
+
|
| 66 |
+
Parameters:
|
| 67 |
+
ssl_context: An SSL context to use for verifying connections.
|
| 68 |
+
If not specified, the default `httpcore.default_ssl_context()`
|
| 69 |
+
will be used.
|
| 70 |
+
max_connections: The maximum number of concurrent HTTP connections that
|
| 71 |
+
the pool should allow. Any attempt to send a request on a pool that
|
| 72 |
+
would exceed this amount will block until a connection is available.
|
| 73 |
+
max_keepalive_connections: The maximum number of idle HTTP connections
|
| 74 |
+
that will be maintained in the pool.
|
| 75 |
+
keepalive_expiry: The duration in seconds that an idle HTTP connection
|
| 76 |
+
may be maintained for before being expired from the pool.
|
| 77 |
+
http1: A boolean indicating if HTTP/1.1 requests should be supported
|
| 78 |
+
by the connection pool. Defaults to True.
|
| 79 |
+
http2: A boolean indicating if HTTP/2 requests should be supported by
|
| 80 |
+
the connection pool. Defaults to False.
|
| 81 |
+
retries: The maximum number of retries when trying to establish a
|
| 82 |
+
connection.
|
| 83 |
+
local_address: Local address to connect from. Can also be used to connect
|
| 84 |
+
using a particular address family. Using `local_address="0.0.0.0"`
|
| 85 |
+
will connect using an `AF_INET` address (IPv4), while using
|
| 86 |
+
`local_address="::"` will connect using an `AF_INET6` address (IPv6).
|
| 87 |
+
uds: Path to a Unix Domain Socket to use instead of TCP sockets.
|
| 88 |
+
network_backend: A backend instance to use for handling network I/O.
|
| 89 |
+
socket_options: Socket options that have to be included
|
| 90 |
+
in the TCP socket when the connection was established.
|
| 91 |
+
"""
|
| 92 |
+
self._ssl_context = ssl_context
|
| 93 |
+
self._proxy = proxy
|
| 94 |
+
self._max_connections = (
|
| 95 |
+
sys.maxsize if max_connections is None else max_connections
|
| 96 |
+
)
|
| 97 |
+
self._max_keepalive_connections = (
|
| 98 |
+
sys.maxsize
|
| 99 |
+
if max_keepalive_connections is None
|
| 100 |
+
else max_keepalive_connections
|
| 101 |
+
)
|
| 102 |
+
self._max_keepalive_connections = min(
|
| 103 |
+
self._max_connections, self._max_keepalive_connections
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
self._keepalive_expiry = keepalive_expiry
|
| 107 |
+
self._http1 = http1
|
| 108 |
+
self._http2 = http2
|
| 109 |
+
self._retries = retries
|
| 110 |
+
self._local_address = local_address
|
| 111 |
+
self._uds = uds
|
| 112 |
+
|
| 113 |
+
self._network_backend = (
|
| 114 |
+
SyncBackend() if network_backend is None else network_backend
|
| 115 |
+
)
|
| 116 |
+
self._socket_options = socket_options
|
| 117 |
+
|
| 118 |
+
# The mutable state on a connection pool is the queue of incoming requests,
|
| 119 |
+
# and the set of connections that are servicing those requests.
|
| 120 |
+
self._connections: list[ConnectionInterface] = []
|
| 121 |
+
self._requests: list[PoolRequest] = []
|
| 122 |
+
|
| 123 |
+
# We only mutate the state of the connection pool within an 'optional_thread_lock'
|
| 124 |
+
# context. This holds a threading lock unless we're running in async mode,
|
| 125 |
+
# in which case it is a no-op.
|
| 126 |
+
self._optional_thread_lock = ThreadLock()
|
| 127 |
+
|
| 128 |
+
def create_connection(self, origin: Origin) -> ConnectionInterface:
|
| 129 |
+
if self._proxy is not None:
|
| 130 |
+
if self._proxy.url.scheme in (b"socks5", b"socks5h"):
|
| 131 |
+
from .socks_proxy import Socks5Connection
|
| 132 |
+
|
| 133 |
+
return Socks5Connection(
|
| 134 |
+
proxy_origin=self._proxy.url.origin,
|
| 135 |
+
proxy_auth=self._proxy.auth,
|
| 136 |
+
remote_origin=origin,
|
| 137 |
+
ssl_context=self._ssl_context,
|
| 138 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 139 |
+
http1=self._http1,
|
| 140 |
+
http2=self._http2,
|
| 141 |
+
network_backend=self._network_backend,
|
| 142 |
+
)
|
| 143 |
+
elif origin.scheme == b"http":
|
| 144 |
+
from .http_proxy import ForwardHTTPConnection
|
| 145 |
+
|
| 146 |
+
return ForwardHTTPConnection(
|
| 147 |
+
proxy_origin=self._proxy.url.origin,
|
| 148 |
+
proxy_headers=self._proxy.headers,
|
| 149 |
+
proxy_ssl_context=self._proxy.ssl_context,
|
| 150 |
+
remote_origin=origin,
|
| 151 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 152 |
+
network_backend=self._network_backend,
|
| 153 |
+
)
|
| 154 |
+
from .http_proxy import TunnelHTTPConnection
|
| 155 |
+
|
| 156 |
+
return TunnelHTTPConnection(
|
| 157 |
+
proxy_origin=self._proxy.url.origin,
|
| 158 |
+
proxy_headers=self._proxy.headers,
|
| 159 |
+
proxy_ssl_context=self._proxy.ssl_context,
|
| 160 |
+
remote_origin=origin,
|
| 161 |
+
ssl_context=self._ssl_context,
|
| 162 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 163 |
+
http1=self._http1,
|
| 164 |
+
http2=self._http2,
|
| 165 |
+
network_backend=self._network_backend,
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
return HTTPConnection(
|
| 169 |
+
origin=origin,
|
| 170 |
+
ssl_context=self._ssl_context,
|
| 171 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 172 |
+
http1=self._http1,
|
| 173 |
+
http2=self._http2,
|
| 174 |
+
retries=self._retries,
|
| 175 |
+
local_address=self._local_address,
|
| 176 |
+
uds=self._uds,
|
| 177 |
+
network_backend=self._network_backend,
|
| 178 |
+
socket_options=self._socket_options,
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
@property
|
| 182 |
+
def connections(self) -> list[ConnectionInterface]:
|
| 183 |
+
"""
|
| 184 |
+
Return a list of the connections currently in the pool.
|
| 185 |
+
|
| 186 |
+
For example:
|
| 187 |
+
|
| 188 |
+
```python
|
| 189 |
+
>>> pool.connections
|
| 190 |
+
[
|
| 191 |
+
<HTTPConnection ['https://example.com:443', HTTP/1.1, ACTIVE, Request Count: 6]>,
|
| 192 |
+
<HTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 9]> ,
|
| 193 |
+
<HTTPConnection ['http://example.com:80', HTTP/1.1, IDLE, Request Count: 1]>,
|
| 194 |
+
]
|
| 195 |
+
```
|
| 196 |
+
"""
|
| 197 |
+
return list(self._connections)
|
| 198 |
+
|
| 199 |
+
def handle_request(self, request: Request) -> Response:
|
| 200 |
+
"""
|
| 201 |
+
Send an HTTP request, and return an HTTP response.
|
| 202 |
+
|
| 203 |
+
This is the core implementation that is called into by `.request()` or `.stream()`.
|
| 204 |
+
"""
|
| 205 |
+
scheme = request.url.scheme.decode()
|
| 206 |
+
if scheme == "":
|
| 207 |
+
raise UnsupportedProtocol(
|
| 208 |
+
"Request URL is missing an 'http://' or 'https://' protocol."
|
| 209 |
+
)
|
| 210 |
+
if scheme not in ("http", "https", "ws", "wss"):
|
| 211 |
+
raise UnsupportedProtocol(
|
| 212 |
+
f"Request URL has an unsupported protocol '{scheme}://'."
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
timeouts = request.extensions.get("timeout", {})
|
| 216 |
+
timeout = timeouts.get("pool", None)
|
| 217 |
+
|
| 218 |
+
with self._optional_thread_lock:
|
| 219 |
+
# Add the incoming request to our request queue.
|
| 220 |
+
pool_request = PoolRequest(request)
|
| 221 |
+
self._requests.append(pool_request)
|
| 222 |
+
|
| 223 |
+
try:
|
| 224 |
+
while True:
|
| 225 |
+
with self._optional_thread_lock:
|
| 226 |
+
# Assign incoming requests to available connections,
|
| 227 |
+
# closing or creating new connections as required.
|
| 228 |
+
closing = self._assign_requests_to_connections()
|
| 229 |
+
self._close_connections(closing)
|
| 230 |
+
|
| 231 |
+
# Wait until this request has an assigned connection.
|
| 232 |
+
connection = pool_request.wait_for_connection(timeout=timeout)
|
| 233 |
+
|
| 234 |
+
try:
|
| 235 |
+
# Send the request on the assigned connection.
|
| 236 |
+
response = connection.handle_request(
|
| 237 |
+
pool_request.request
|
| 238 |
+
)
|
| 239 |
+
except ConnectionNotAvailable:
|
| 240 |
+
# In some cases a connection may initially be available to
|
| 241 |
+
# handle a request, but then become unavailable.
|
| 242 |
+
#
|
| 243 |
+
# In this case we clear the connection and try again.
|
| 244 |
+
pool_request.clear_connection()
|
| 245 |
+
else:
|
| 246 |
+
break # pragma: nocover
|
| 247 |
+
|
| 248 |
+
except BaseException as exc:
|
| 249 |
+
with self._optional_thread_lock:
|
| 250 |
+
# For any exception or cancellation we remove the request from
|
| 251 |
+
# the queue, and then re-assign requests to connections.
|
| 252 |
+
self._requests.remove(pool_request)
|
| 253 |
+
closing = self._assign_requests_to_connections()
|
| 254 |
+
|
| 255 |
+
self._close_connections(closing)
|
| 256 |
+
raise exc from None
|
| 257 |
+
|
| 258 |
+
# Return the response. Note that in this case we still have to manage
|
| 259 |
+
# the point at which the response is closed.
|
| 260 |
+
assert isinstance(response.stream, typing.Iterable)
|
| 261 |
+
return Response(
|
| 262 |
+
status=response.status,
|
| 263 |
+
headers=response.headers,
|
| 264 |
+
content=PoolByteStream(
|
| 265 |
+
stream=response.stream, pool_request=pool_request, pool=self
|
| 266 |
+
),
|
| 267 |
+
extensions=response.extensions,
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
def _assign_requests_to_connections(self) -> list[ConnectionInterface]:
|
| 271 |
+
"""
|
| 272 |
+
Manage the state of the connection pool, assigning incoming
|
| 273 |
+
requests to connections as available.
|
| 274 |
+
|
| 275 |
+
Called whenever a new request is added or removed from the pool.
|
| 276 |
+
|
| 277 |
+
Any closing connections are returned, allowing the I/O for closing
|
| 278 |
+
those connections to be handled seperately.
|
| 279 |
+
"""
|
| 280 |
+
closing_connections = []
|
| 281 |
+
|
| 282 |
+
# First we handle cleaning up any connections that are closed,
|
| 283 |
+
# have expired their keep-alive, or surplus idle connections.
|
| 284 |
+
for connection in list(self._connections):
|
| 285 |
+
if connection.is_closed():
|
| 286 |
+
# log: "removing closed connection"
|
| 287 |
+
self._connections.remove(connection)
|
| 288 |
+
elif connection.has_expired():
|
| 289 |
+
# log: "closing expired connection"
|
| 290 |
+
self._connections.remove(connection)
|
| 291 |
+
closing_connections.append(connection)
|
| 292 |
+
elif (
|
| 293 |
+
connection.is_idle()
|
| 294 |
+
and len([connection.is_idle() for connection in self._connections])
|
| 295 |
+
> self._max_keepalive_connections
|
| 296 |
+
):
|
| 297 |
+
# log: "closing idle connection"
|
| 298 |
+
self._connections.remove(connection)
|
| 299 |
+
closing_connections.append(connection)
|
| 300 |
+
|
| 301 |
+
# Assign queued requests to connections.
|
| 302 |
+
queued_requests = [request for request in self._requests if request.is_queued()]
|
| 303 |
+
for pool_request in queued_requests:
|
| 304 |
+
origin = pool_request.request.url.origin
|
| 305 |
+
available_connections = [
|
| 306 |
+
connection
|
| 307 |
+
for connection in self._connections
|
| 308 |
+
if connection.can_handle_request(origin) and connection.is_available()
|
| 309 |
+
]
|
| 310 |
+
idle_connections = [
|
| 311 |
+
connection for connection in self._connections if connection.is_idle()
|
| 312 |
+
]
|
| 313 |
+
|
| 314 |
+
# There are three cases for how we may be able to handle the request:
|
| 315 |
+
#
|
| 316 |
+
# 1. There is an existing connection that can handle the request.
|
| 317 |
+
# 2. We can create a new connection to handle the request.
|
| 318 |
+
# 3. We can close an idle connection and then create a new connection
|
| 319 |
+
# to handle the request.
|
| 320 |
+
if available_connections:
|
| 321 |
+
# log: "reusing existing connection"
|
| 322 |
+
connection = available_connections[0]
|
| 323 |
+
pool_request.assign_to_connection(connection)
|
| 324 |
+
elif len(self._connections) < self._max_connections:
|
| 325 |
+
# log: "creating new connection"
|
| 326 |
+
connection = self.create_connection(origin)
|
| 327 |
+
self._connections.append(connection)
|
| 328 |
+
pool_request.assign_to_connection(connection)
|
| 329 |
+
elif idle_connections:
|
| 330 |
+
# log: "closing idle connection"
|
| 331 |
+
connection = idle_connections[0]
|
| 332 |
+
self._connections.remove(connection)
|
| 333 |
+
closing_connections.append(connection)
|
| 334 |
+
# log: "creating new connection"
|
| 335 |
+
connection = self.create_connection(origin)
|
| 336 |
+
self._connections.append(connection)
|
| 337 |
+
pool_request.assign_to_connection(connection)
|
| 338 |
+
|
| 339 |
+
return closing_connections
|
| 340 |
+
|
| 341 |
+
def _close_connections(self, closing: list[ConnectionInterface]) -> None:
|
| 342 |
+
# Close connections which have been removed from the pool.
|
| 343 |
+
with ShieldCancellation():
|
| 344 |
+
for connection in closing:
|
| 345 |
+
connection.close()
|
| 346 |
+
|
| 347 |
+
def close(self) -> None:
|
| 348 |
+
# Explicitly close the connection pool.
|
| 349 |
+
# Clears all existing requests and connections.
|
| 350 |
+
with self._optional_thread_lock:
|
| 351 |
+
closing_connections = list(self._connections)
|
| 352 |
+
self._connections = []
|
| 353 |
+
self._close_connections(closing_connections)
|
| 354 |
+
|
| 355 |
+
def __enter__(self) -> ConnectionPool:
|
| 356 |
+
return self
|
| 357 |
+
|
| 358 |
+
def __exit__(
|
| 359 |
+
self,
|
| 360 |
+
exc_type: type[BaseException] | None = None,
|
| 361 |
+
exc_value: BaseException | None = None,
|
| 362 |
+
traceback: types.TracebackType | None = None,
|
| 363 |
+
) -> None:
|
| 364 |
+
self.close()
|
| 365 |
+
|
| 366 |
+
def __repr__(self) -> str:
|
| 367 |
+
class_name = self.__class__.__name__
|
| 368 |
+
with self._optional_thread_lock:
|
| 369 |
+
request_is_queued = [request.is_queued() for request in self._requests]
|
| 370 |
+
connection_is_idle = [
|
| 371 |
+
connection.is_idle() for connection in self._connections
|
| 372 |
+
]
|
| 373 |
+
|
| 374 |
+
num_active_requests = request_is_queued.count(False)
|
| 375 |
+
num_queued_requests = request_is_queued.count(True)
|
| 376 |
+
num_active_connections = connection_is_idle.count(False)
|
| 377 |
+
num_idle_connections = connection_is_idle.count(True)
|
| 378 |
+
|
| 379 |
+
requests_info = (
|
| 380 |
+
f"Requests: {num_active_requests} active, {num_queued_requests} queued"
|
| 381 |
+
)
|
| 382 |
+
connection_info = (
|
| 383 |
+
f"Connections: {num_active_connections} active, {num_idle_connections} idle"
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
return f"<{class_name} [{requests_info} | {connection_info}]>"
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
class PoolByteStream:
|
| 390 |
+
def __init__(
|
| 391 |
+
self,
|
| 392 |
+
stream: typing.Iterable[bytes],
|
| 393 |
+
pool_request: PoolRequest,
|
| 394 |
+
pool: ConnectionPool,
|
| 395 |
+
) -> None:
|
| 396 |
+
self._stream = stream
|
| 397 |
+
self._pool_request = pool_request
|
| 398 |
+
self._pool = pool
|
| 399 |
+
self._closed = False
|
| 400 |
+
|
| 401 |
+
def __iter__(self) -> typing.Iterator[bytes]:
|
| 402 |
+
try:
|
| 403 |
+
for part in self._stream:
|
| 404 |
+
yield part
|
| 405 |
+
except BaseException as exc:
|
| 406 |
+
self.close()
|
| 407 |
+
raise exc from None
|
| 408 |
+
|
| 409 |
+
def close(self) -> None:
|
| 410 |
+
if not self._closed:
|
| 411 |
+
self._closed = True
|
| 412 |
+
with ShieldCancellation():
|
| 413 |
+
if hasattr(self._stream, "close"):
|
| 414 |
+
self._stream.close()
|
| 415 |
+
|
| 416 |
+
with self._pool._optional_thread_lock:
|
| 417 |
+
self._pool._requests.remove(self._pool_request)
|
| 418 |
+
closing = self._pool._assign_requests_to_connections()
|
| 419 |
+
|
| 420 |
+
self._pool._close_connections(closing)
|
venv/lib/python3.12/site-packages/httpcore/_sync/http11.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import enum
|
| 4 |
+
import logging
|
| 5 |
+
import ssl
|
| 6 |
+
import time
|
| 7 |
+
import types
|
| 8 |
+
import typing
|
| 9 |
+
|
| 10 |
+
import h11
|
| 11 |
+
|
| 12 |
+
from .._backends.base import NetworkStream
|
| 13 |
+
from .._exceptions import (
|
| 14 |
+
ConnectionNotAvailable,
|
| 15 |
+
LocalProtocolError,
|
| 16 |
+
RemoteProtocolError,
|
| 17 |
+
WriteError,
|
| 18 |
+
map_exceptions,
|
| 19 |
+
)
|
| 20 |
+
from .._models import Origin, Request, Response
|
| 21 |
+
from .._synchronization import Lock, ShieldCancellation
|
| 22 |
+
from .._trace import Trace
|
| 23 |
+
from .interfaces import ConnectionInterface
|
| 24 |
+
|
| 25 |
+
logger = logging.getLogger("httpcore.http11")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
# A subset of `h11.Event` types supported by `_send_event`
|
| 29 |
+
H11SendEvent = typing.Union[
|
| 30 |
+
h11.Request,
|
| 31 |
+
h11.Data,
|
| 32 |
+
h11.EndOfMessage,
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class HTTPConnectionState(enum.IntEnum):
|
| 37 |
+
NEW = 0
|
| 38 |
+
ACTIVE = 1
|
| 39 |
+
IDLE = 2
|
| 40 |
+
CLOSED = 3
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class HTTP11Connection(ConnectionInterface):
|
| 44 |
+
READ_NUM_BYTES = 64 * 1024
|
| 45 |
+
MAX_INCOMPLETE_EVENT_SIZE = 100 * 1024
|
| 46 |
+
|
| 47 |
+
def __init__(
|
| 48 |
+
self,
|
| 49 |
+
origin: Origin,
|
| 50 |
+
stream: NetworkStream,
|
| 51 |
+
keepalive_expiry: float | None = None,
|
| 52 |
+
) -> None:
|
| 53 |
+
self._origin = origin
|
| 54 |
+
self._network_stream = stream
|
| 55 |
+
self._keepalive_expiry: float | None = keepalive_expiry
|
| 56 |
+
self._expire_at: float | None = None
|
| 57 |
+
self._state = HTTPConnectionState.NEW
|
| 58 |
+
self._state_lock = Lock()
|
| 59 |
+
self._request_count = 0
|
| 60 |
+
self._h11_state = h11.Connection(
|
| 61 |
+
our_role=h11.CLIENT,
|
| 62 |
+
max_incomplete_event_size=self.MAX_INCOMPLETE_EVENT_SIZE,
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
def handle_request(self, request: Request) -> Response:
|
| 66 |
+
if not self.can_handle_request(request.url.origin):
|
| 67 |
+
raise RuntimeError(
|
| 68 |
+
f"Attempted to send request to {request.url.origin} on connection "
|
| 69 |
+
f"to {self._origin}"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
with self._state_lock:
|
| 73 |
+
if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE):
|
| 74 |
+
self._request_count += 1
|
| 75 |
+
self._state = HTTPConnectionState.ACTIVE
|
| 76 |
+
self._expire_at = None
|
| 77 |
+
else:
|
| 78 |
+
raise ConnectionNotAvailable()
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
kwargs = {"request": request}
|
| 82 |
+
try:
|
| 83 |
+
with Trace(
|
| 84 |
+
"send_request_headers", logger, request, kwargs
|
| 85 |
+
) as trace:
|
| 86 |
+
self._send_request_headers(**kwargs)
|
| 87 |
+
with Trace("send_request_body", logger, request, kwargs) as trace:
|
| 88 |
+
self._send_request_body(**kwargs)
|
| 89 |
+
except WriteError:
|
| 90 |
+
# If we get a write error while we're writing the request,
|
| 91 |
+
# then we supress this error and move on to attempting to
|
| 92 |
+
# read the response. Servers can sometimes close the request
|
| 93 |
+
# pre-emptively and then respond with a well formed HTTP
|
| 94 |
+
# error response.
|
| 95 |
+
pass
|
| 96 |
+
|
| 97 |
+
with Trace(
|
| 98 |
+
"receive_response_headers", logger, request, kwargs
|
| 99 |
+
) as trace:
|
| 100 |
+
(
|
| 101 |
+
http_version,
|
| 102 |
+
status,
|
| 103 |
+
reason_phrase,
|
| 104 |
+
headers,
|
| 105 |
+
trailing_data,
|
| 106 |
+
) = self._receive_response_headers(**kwargs)
|
| 107 |
+
trace.return_value = (
|
| 108 |
+
http_version,
|
| 109 |
+
status,
|
| 110 |
+
reason_phrase,
|
| 111 |
+
headers,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
network_stream = self._network_stream
|
| 115 |
+
|
| 116 |
+
# CONNECT or Upgrade request
|
| 117 |
+
if (status == 101) or (
|
| 118 |
+
(request.method == b"CONNECT") and (200 <= status < 300)
|
| 119 |
+
):
|
| 120 |
+
network_stream = HTTP11UpgradeStream(network_stream, trailing_data)
|
| 121 |
+
|
| 122 |
+
return Response(
|
| 123 |
+
status=status,
|
| 124 |
+
headers=headers,
|
| 125 |
+
content=HTTP11ConnectionByteStream(self, request),
|
| 126 |
+
extensions={
|
| 127 |
+
"http_version": http_version,
|
| 128 |
+
"reason_phrase": reason_phrase,
|
| 129 |
+
"network_stream": network_stream,
|
| 130 |
+
},
|
| 131 |
+
)
|
| 132 |
+
except BaseException as exc:
|
| 133 |
+
with ShieldCancellation():
|
| 134 |
+
with Trace("response_closed", logger, request) as trace:
|
| 135 |
+
self._response_closed()
|
| 136 |
+
raise exc
|
| 137 |
+
|
| 138 |
+
# Sending the request...
|
| 139 |
+
|
| 140 |
+
def _send_request_headers(self, request: Request) -> None:
|
| 141 |
+
timeouts = request.extensions.get("timeout", {})
|
| 142 |
+
timeout = timeouts.get("write", None)
|
| 143 |
+
|
| 144 |
+
with map_exceptions({h11.LocalProtocolError: LocalProtocolError}):
|
| 145 |
+
event = h11.Request(
|
| 146 |
+
method=request.method,
|
| 147 |
+
target=request.url.target,
|
| 148 |
+
headers=request.headers,
|
| 149 |
+
)
|
| 150 |
+
self._send_event(event, timeout=timeout)
|
| 151 |
+
|
| 152 |
+
def _send_request_body(self, request: Request) -> None:
|
| 153 |
+
timeouts = request.extensions.get("timeout", {})
|
| 154 |
+
timeout = timeouts.get("write", None)
|
| 155 |
+
|
| 156 |
+
assert isinstance(request.stream, typing.Iterable)
|
| 157 |
+
for chunk in request.stream:
|
| 158 |
+
event = h11.Data(data=chunk)
|
| 159 |
+
self._send_event(event, timeout=timeout)
|
| 160 |
+
|
| 161 |
+
self._send_event(h11.EndOfMessage(), timeout=timeout)
|
| 162 |
+
|
| 163 |
+
def _send_event(self, event: h11.Event, timeout: float | None = None) -> None:
|
| 164 |
+
bytes_to_send = self._h11_state.send(event)
|
| 165 |
+
if bytes_to_send is not None:
|
| 166 |
+
self._network_stream.write(bytes_to_send, timeout=timeout)
|
| 167 |
+
|
| 168 |
+
# Receiving the response...
|
| 169 |
+
|
| 170 |
+
def _receive_response_headers(
|
| 171 |
+
self, request: Request
|
| 172 |
+
) -> tuple[bytes, int, bytes, list[tuple[bytes, bytes]], bytes]:
|
| 173 |
+
timeouts = request.extensions.get("timeout", {})
|
| 174 |
+
timeout = timeouts.get("read", None)
|
| 175 |
+
|
| 176 |
+
while True:
|
| 177 |
+
event = self._receive_event(timeout=timeout)
|
| 178 |
+
if isinstance(event, h11.Response):
|
| 179 |
+
break
|
| 180 |
+
if (
|
| 181 |
+
isinstance(event, h11.InformationalResponse)
|
| 182 |
+
and event.status_code == 101
|
| 183 |
+
):
|
| 184 |
+
break
|
| 185 |
+
|
| 186 |
+
http_version = b"HTTP/" + event.http_version
|
| 187 |
+
|
| 188 |
+
# h11 version 0.11+ supports a `raw_items` interface to get the
|
| 189 |
+
# raw header casing, rather than the enforced lowercase headers.
|
| 190 |
+
headers = event.headers.raw_items()
|
| 191 |
+
|
| 192 |
+
trailing_data, _ = self._h11_state.trailing_data
|
| 193 |
+
|
| 194 |
+
return http_version, event.status_code, event.reason, headers, trailing_data
|
| 195 |
+
|
| 196 |
+
def _receive_response_body(
|
| 197 |
+
self, request: Request
|
| 198 |
+
) -> typing.Iterator[bytes]:
|
| 199 |
+
timeouts = request.extensions.get("timeout", {})
|
| 200 |
+
timeout = timeouts.get("read", None)
|
| 201 |
+
|
| 202 |
+
while True:
|
| 203 |
+
event = self._receive_event(timeout=timeout)
|
| 204 |
+
if isinstance(event, h11.Data):
|
| 205 |
+
yield bytes(event.data)
|
| 206 |
+
elif isinstance(event, (h11.EndOfMessage, h11.PAUSED)):
|
| 207 |
+
break
|
| 208 |
+
|
| 209 |
+
def _receive_event(
|
| 210 |
+
self, timeout: float | None = None
|
| 211 |
+
) -> h11.Event | type[h11.PAUSED]:
|
| 212 |
+
while True:
|
| 213 |
+
with map_exceptions({h11.RemoteProtocolError: RemoteProtocolError}):
|
| 214 |
+
event = self._h11_state.next_event()
|
| 215 |
+
|
| 216 |
+
if event is h11.NEED_DATA:
|
| 217 |
+
data = self._network_stream.read(
|
| 218 |
+
self.READ_NUM_BYTES, timeout=timeout
|
| 219 |
+
)
|
| 220 |
+
|
| 221 |
+
# If we feed this case through h11 we'll raise an exception like:
|
| 222 |
+
#
|
| 223 |
+
# httpcore.RemoteProtocolError: can't handle event type
|
| 224 |
+
# ConnectionClosed when role=SERVER and state=SEND_RESPONSE
|
| 225 |
+
#
|
| 226 |
+
# Which is accurate, but not very informative from an end-user
|
| 227 |
+
# perspective. Instead we handle this case distinctly and treat
|
| 228 |
+
# it as a ConnectError.
|
| 229 |
+
if data == b"" and self._h11_state.their_state == h11.SEND_RESPONSE:
|
| 230 |
+
msg = "Server disconnected without sending a response."
|
| 231 |
+
raise RemoteProtocolError(msg)
|
| 232 |
+
|
| 233 |
+
self._h11_state.receive_data(data)
|
| 234 |
+
else:
|
| 235 |
+
# mypy fails to narrow the type in the above if statement above
|
| 236 |
+
return event # type: ignore[return-value]
|
| 237 |
+
|
| 238 |
+
def _response_closed(self) -> None:
|
| 239 |
+
with self._state_lock:
|
| 240 |
+
if (
|
| 241 |
+
self._h11_state.our_state is h11.DONE
|
| 242 |
+
and self._h11_state.their_state is h11.DONE
|
| 243 |
+
):
|
| 244 |
+
self._state = HTTPConnectionState.IDLE
|
| 245 |
+
self._h11_state.start_next_cycle()
|
| 246 |
+
if self._keepalive_expiry is not None:
|
| 247 |
+
now = time.monotonic()
|
| 248 |
+
self._expire_at = now + self._keepalive_expiry
|
| 249 |
+
else:
|
| 250 |
+
self.close()
|
| 251 |
+
|
| 252 |
+
# Once the connection is no longer required...
|
| 253 |
+
|
| 254 |
+
def close(self) -> None:
|
| 255 |
+
# Note that this method unilaterally closes the connection, and does
|
| 256 |
+
# not have any kind of locking in place around it.
|
| 257 |
+
self._state = HTTPConnectionState.CLOSED
|
| 258 |
+
self._network_stream.close()
|
| 259 |
+
|
| 260 |
+
# The ConnectionInterface methods provide information about the state of
|
| 261 |
+
# the connection, allowing for a connection pooling implementation to
|
| 262 |
+
# determine when to reuse and when to close the connection...
|
| 263 |
+
|
| 264 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 265 |
+
return origin == self._origin
|
| 266 |
+
|
| 267 |
+
def is_available(self) -> bool:
|
| 268 |
+
# Note that HTTP/1.1 connections in the "NEW" state are not treated as
|
| 269 |
+
# being "available". The control flow which created the connection will
|
| 270 |
+
# be able to send an outgoing request, but the connection will not be
|
| 271 |
+
# acquired from the connection pool for any other request.
|
| 272 |
+
return self._state == HTTPConnectionState.IDLE
|
| 273 |
+
|
| 274 |
+
def has_expired(self) -> bool:
|
| 275 |
+
now = time.monotonic()
|
| 276 |
+
keepalive_expired = self._expire_at is not None and now > self._expire_at
|
| 277 |
+
|
| 278 |
+
# If the HTTP connection is idle but the socket is readable, then the
|
| 279 |
+
# only valid state is that the socket is about to return b"", indicating
|
| 280 |
+
# a server-initiated disconnect.
|
| 281 |
+
server_disconnected = (
|
| 282 |
+
self._state == HTTPConnectionState.IDLE
|
| 283 |
+
and self._network_stream.get_extra_info("is_readable")
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
return keepalive_expired or server_disconnected
|
| 287 |
+
|
| 288 |
+
def is_idle(self) -> bool:
|
| 289 |
+
return self._state == HTTPConnectionState.IDLE
|
| 290 |
+
|
| 291 |
+
def is_closed(self) -> bool:
|
| 292 |
+
return self._state == HTTPConnectionState.CLOSED
|
| 293 |
+
|
| 294 |
+
def info(self) -> str:
|
| 295 |
+
origin = str(self._origin)
|
| 296 |
+
return (
|
| 297 |
+
f"{origin!r}, HTTP/1.1, {self._state.name}, "
|
| 298 |
+
f"Request Count: {self._request_count}"
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
def __repr__(self) -> str:
|
| 302 |
+
class_name = self.__class__.__name__
|
| 303 |
+
origin = str(self._origin)
|
| 304 |
+
return (
|
| 305 |
+
f"<{class_name} [{origin!r}, {self._state.name}, "
|
| 306 |
+
f"Request Count: {self._request_count}]>"
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
# These context managers are not used in the standard flow, but are
|
| 310 |
+
# useful for testing or working with connection instances directly.
|
| 311 |
+
|
| 312 |
+
def __enter__(self) -> HTTP11Connection:
|
| 313 |
+
return self
|
| 314 |
+
|
| 315 |
+
def __exit__(
|
| 316 |
+
self,
|
| 317 |
+
exc_type: type[BaseException] | None = None,
|
| 318 |
+
exc_value: BaseException | None = None,
|
| 319 |
+
traceback: types.TracebackType | None = None,
|
| 320 |
+
) -> None:
|
| 321 |
+
self.close()
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
class HTTP11ConnectionByteStream:
|
| 325 |
+
def __init__(self, connection: HTTP11Connection, request: Request) -> None:
|
| 326 |
+
self._connection = connection
|
| 327 |
+
self._request = request
|
| 328 |
+
self._closed = False
|
| 329 |
+
|
| 330 |
+
def __iter__(self) -> typing.Iterator[bytes]:
|
| 331 |
+
kwargs = {"request": self._request}
|
| 332 |
+
try:
|
| 333 |
+
with Trace("receive_response_body", logger, self._request, kwargs):
|
| 334 |
+
for chunk in self._connection._receive_response_body(**kwargs):
|
| 335 |
+
yield chunk
|
| 336 |
+
except BaseException as exc:
|
| 337 |
+
# If we get an exception while streaming the response,
|
| 338 |
+
# we want to close the response (and possibly the connection)
|
| 339 |
+
# before raising that exception.
|
| 340 |
+
with ShieldCancellation():
|
| 341 |
+
self.close()
|
| 342 |
+
raise exc
|
| 343 |
+
|
| 344 |
+
def close(self) -> None:
|
| 345 |
+
if not self._closed:
|
| 346 |
+
self._closed = True
|
| 347 |
+
with Trace("response_closed", logger, self._request):
|
| 348 |
+
self._connection._response_closed()
|
| 349 |
+
|
| 350 |
+
|
| 351 |
+
class HTTP11UpgradeStream(NetworkStream):
|
| 352 |
+
def __init__(self, stream: NetworkStream, leading_data: bytes) -> None:
|
| 353 |
+
self._stream = stream
|
| 354 |
+
self._leading_data = leading_data
|
| 355 |
+
|
| 356 |
+
def read(self, max_bytes: int, timeout: float | None = None) -> bytes:
|
| 357 |
+
if self._leading_data:
|
| 358 |
+
buffer = self._leading_data[:max_bytes]
|
| 359 |
+
self._leading_data = self._leading_data[max_bytes:]
|
| 360 |
+
return buffer
|
| 361 |
+
else:
|
| 362 |
+
return self._stream.read(max_bytes, timeout)
|
| 363 |
+
|
| 364 |
+
def write(self, buffer: bytes, timeout: float | None = None) -> None:
|
| 365 |
+
self._stream.write(buffer, timeout)
|
| 366 |
+
|
| 367 |
+
def close(self) -> None:
|
| 368 |
+
self._stream.close()
|
| 369 |
+
|
| 370 |
+
def start_tls(
|
| 371 |
+
self,
|
| 372 |
+
ssl_context: ssl.SSLContext,
|
| 373 |
+
server_hostname: str | None = None,
|
| 374 |
+
timeout: float | None = None,
|
| 375 |
+
) -> NetworkStream:
|
| 376 |
+
return self._stream.start_tls(ssl_context, server_hostname, timeout)
|
| 377 |
+
|
| 378 |
+
def get_extra_info(self, info: str) -> typing.Any:
|
| 379 |
+
return self._stream.get_extra_info(info)
|
venv/lib/python3.12/site-packages/httpcore/_sync/http2.py
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import enum
|
| 4 |
+
import logging
|
| 5 |
+
import time
|
| 6 |
+
import types
|
| 7 |
+
import typing
|
| 8 |
+
|
| 9 |
+
import h2.config
|
| 10 |
+
import h2.connection
|
| 11 |
+
import h2.events
|
| 12 |
+
import h2.exceptions
|
| 13 |
+
import h2.settings
|
| 14 |
+
|
| 15 |
+
from .._backends.base import NetworkStream
|
| 16 |
+
from .._exceptions import (
|
| 17 |
+
ConnectionNotAvailable,
|
| 18 |
+
LocalProtocolError,
|
| 19 |
+
RemoteProtocolError,
|
| 20 |
+
)
|
| 21 |
+
from .._models import Origin, Request, Response
|
| 22 |
+
from .._synchronization import Lock, Semaphore, ShieldCancellation
|
| 23 |
+
from .._trace import Trace
|
| 24 |
+
from .interfaces import ConnectionInterface
|
| 25 |
+
|
| 26 |
+
logger = logging.getLogger("httpcore.http2")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def has_body_headers(request: Request) -> bool:
|
| 30 |
+
return any(
|
| 31 |
+
k.lower() == b"content-length" or k.lower() == b"transfer-encoding"
|
| 32 |
+
for k, v in request.headers
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class HTTPConnectionState(enum.IntEnum):
|
| 37 |
+
ACTIVE = 1
|
| 38 |
+
IDLE = 2
|
| 39 |
+
CLOSED = 3
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class HTTP2Connection(ConnectionInterface):
|
| 43 |
+
READ_NUM_BYTES = 64 * 1024
|
| 44 |
+
CONFIG = h2.config.H2Configuration(validate_inbound_headers=False)
|
| 45 |
+
|
| 46 |
+
def __init__(
|
| 47 |
+
self,
|
| 48 |
+
origin: Origin,
|
| 49 |
+
stream: NetworkStream,
|
| 50 |
+
keepalive_expiry: float | None = None,
|
| 51 |
+
):
|
| 52 |
+
self._origin = origin
|
| 53 |
+
self._network_stream = stream
|
| 54 |
+
self._keepalive_expiry: float | None = keepalive_expiry
|
| 55 |
+
self._h2_state = h2.connection.H2Connection(config=self.CONFIG)
|
| 56 |
+
self._state = HTTPConnectionState.IDLE
|
| 57 |
+
self._expire_at: float | None = None
|
| 58 |
+
self._request_count = 0
|
| 59 |
+
self._init_lock = Lock()
|
| 60 |
+
self._state_lock = Lock()
|
| 61 |
+
self._read_lock = Lock()
|
| 62 |
+
self._write_lock = Lock()
|
| 63 |
+
self._sent_connection_init = False
|
| 64 |
+
self._used_all_stream_ids = False
|
| 65 |
+
self._connection_error = False
|
| 66 |
+
|
| 67 |
+
# Mapping from stream ID to response stream events.
|
| 68 |
+
self._events: dict[
|
| 69 |
+
int,
|
| 70 |
+
list[
|
| 71 |
+
h2.events.ResponseReceived
|
| 72 |
+
| h2.events.DataReceived
|
| 73 |
+
| h2.events.StreamEnded
|
| 74 |
+
| h2.events.StreamReset,
|
| 75 |
+
],
|
| 76 |
+
] = {}
|
| 77 |
+
|
| 78 |
+
# Connection terminated events are stored as state since
|
| 79 |
+
# we need to handle them for all streams.
|
| 80 |
+
self._connection_terminated: h2.events.ConnectionTerminated | None = None
|
| 81 |
+
|
| 82 |
+
self._read_exception: Exception | None = None
|
| 83 |
+
self._write_exception: Exception | None = None
|
| 84 |
+
|
| 85 |
+
def handle_request(self, request: Request) -> Response:
|
| 86 |
+
if not self.can_handle_request(request.url.origin):
|
| 87 |
+
# This cannot occur in normal operation, since the connection pool
|
| 88 |
+
# will only send requests on connections that handle them.
|
| 89 |
+
# It's in place simply for resilience as a guard against incorrect
|
| 90 |
+
# usage, for anyone working directly with httpcore connections.
|
| 91 |
+
raise RuntimeError(
|
| 92 |
+
f"Attempted to send request to {request.url.origin} on connection "
|
| 93 |
+
f"to {self._origin}"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
with self._state_lock:
|
| 97 |
+
if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE):
|
| 98 |
+
self._request_count += 1
|
| 99 |
+
self._expire_at = None
|
| 100 |
+
self._state = HTTPConnectionState.ACTIVE
|
| 101 |
+
else:
|
| 102 |
+
raise ConnectionNotAvailable()
|
| 103 |
+
|
| 104 |
+
with self._init_lock:
|
| 105 |
+
if not self._sent_connection_init:
|
| 106 |
+
try:
|
| 107 |
+
sci_kwargs = {"request": request}
|
| 108 |
+
with Trace(
|
| 109 |
+
"send_connection_init", logger, request, sci_kwargs
|
| 110 |
+
):
|
| 111 |
+
self._send_connection_init(**sci_kwargs)
|
| 112 |
+
except BaseException as exc:
|
| 113 |
+
with ShieldCancellation():
|
| 114 |
+
self.close()
|
| 115 |
+
raise exc
|
| 116 |
+
|
| 117 |
+
self._sent_connection_init = True
|
| 118 |
+
|
| 119 |
+
# Initially start with just 1 until the remote server provides
|
| 120 |
+
# its max_concurrent_streams value
|
| 121 |
+
self._max_streams = 1
|
| 122 |
+
|
| 123 |
+
local_settings_max_streams = (
|
| 124 |
+
self._h2_state.local_settings.max_concurrent_streams
|
| 125 |
+
)
|
| 126 |
+
self._max_streams_semaphore = Semaphore(local_settings_max_streams)
|
| 127 |
+
|
| 128 |
+
for _ in range(local_settings_max_streams - self._max_streams):
|
| 129 |
+
self._max_streams_semaphore.acquire()
|
| 130 |
+
|
| 131 |
+
self._max_streams_semaphore.acquire()
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
stream_id = self._h2_state.get_next_available_stream_id()
|
| 135 |
+
self._events[stream_id] = []
|
| 136 |
+
except h2.exceptions.NoAvailableStreamIDError: # pragma: nocover
|
| 137 |
+
self._used_all_stream_ids = True
|
| 138 |
+
self._request_count -= 1
|
| 139 |
+
raise ConnectionNotAvailable()
|
| 140 |
+
|
| 141 |
+
try:
|
| 142 |
+
kwargs = {"request": request, "stream_id": stream_id}
|
| 143 |
+
with Trace("send_request_headers", logger, request, kwargs):
|
| 144 |
+
self._send_request_headers(request=request, stream_id=stream_id)
|
| 145 |
+
with Trace("send_request_body", logger, request, kwargs):
|
| 146 |
+
self._send_request_body(request=request, stream_id=stream_id)
|
| 147 |
+
with Trace(
|
| 148 |
+
"receive_response_headers", logger, request, kwargs
|
| 149 |
+
) as trace:
|
| 150 |
+
status, headers = self._receive_response(
|
| 151 |
+
request=request, stream_id=stream_id
|
| 152 |
+
)
|
| 153 |
+
trace.return_value = (status, headers)
|
| 154 |
+
|
| 155 |
+
return Response(
|
| 156 |
+
status=status,
|
| 157 |
+
headers=headers,
|
| 158 |
+
content=HTTP2ConnectionByteStream(self, request, stream_id=stream_id),
|
| 159 |
+
extensions={
|
| 160 |
+
"http_version": b"HTTP/2",
|
| 161 |
+
"network_stream": self._network_stream,
|
| 162 |
+
"stream_id": stream_id,
|
| 163 |
+
},
|
| 164 |
+
)
|
| 165 |
+
except BaseException as exc: # noqa: PIE786
|
| 166 |
+
with ShieldCancellation():
|
| 167 |
+
kwargs = {"stream_id": stream_id}
|
| 168 |
+
with Trace("response_closed", logger, request, kwargs):
|
| 169 |
+
self._response_closed(stream_id=stream_id)
|
| 170 |
+
|
| 171 |
+
if isinstance(exc, h2.exceptions.ProtocolError):
|
| 172 |
+
# One case where h2 can raise a protocol error is when a
|
| 173 |
+
# closed frame has been seen by the state machine.
|
| 174 |
+
#
|
| 175 |
+
# This happens when one stream is reading, and encounters
|
| 176 |
+
# a GOAWAY event. Other flows of control may then raise
|
| 177 |
+
# a protocol error at any point they interact with the 'h2_state'.
|
| 178 |
+
#
|
| 179 |
+
# In this case we'll have stored the event, and should raise
|
| 180 |
+
# it as a RemoteProtocolError.
|
| 181 |
+
if self._connection_terminated: # pragma: nocover
|
| 182 |
+
raise RemoteProtocolError(self._connection_terminated)
|
| 183 |
+
# If h2 raises a protocol error in some other state then we
|
| 184 |
+
# must somehow have made a protocol violation.
|
| 185 |
+
raise LocalProtocolError(exc) # pragma: nocover
|
| 186 |
+
|
| 187 |
+
raise exc
|
| 188 |
+
|
| 189 |
+
def _send_connection_init(self, request: Request) -> None:
|
| 190 |
+
"""
|
| 191 |
+
The HTTP/2 connection requires some initial setup before we can start
|
| 192 |
+
using individual request/response streams on it.
|
| 193 |
+
"""
|
| 194 |
+
# Need to set these manually here instead of manipulating via
|
| 195 |
+
# __setitem__() otherwise the H2Connection will emit SettingsUpdate
|
| 196 |
+
# frames in addition to sending the undesired defaults.
|
| 197 |
+
self._h2_state.local_settings = h2.settings.Settings(
|
| 198 |
+
client=True,
|
| 199 |
+
initial_values={
|
| 200 |
+
# Disable PUSH_PROMISE frames from the server since we don't do anything
|
| 201 |
+
# with them for now. Maybe when we support caching?
|
| 202 |
+
h2.settings.SettingCodes.ENABLE_PUSH: 0,
|
| 203 |
+
# These two are taken from h2 for safe defaults
|
| 204 |
+
h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 100,
|
| 205 |
+
h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 65536,
|
| 206 |
+
},
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
# Some websites (*cough* Yahoo *cough*) balk at this setting being
|
| 210 |
+
# present in the initial handshake since it's not defined in the original
|
| 211 |
+
# RFC despite the RFC mandating ignoring settings you don't know about.
|
| 212 |
+
del self._h2_state.local_settings[
|
| 213 |
+
h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL
|
| 214 |
+
]
|
| 215 |
+
|
| 216 |
+
self._h2_state.initiate_connection()
|
| 217 |
+
self._h2_state.increment_flow_control_window(2**24)
|
| 218 |
+
self._write_outgoing_data(request)
|
| 219 |
+
|
| 220 |
+
# Sending the request...
|
| 221 |
+
|
| 222 |
+
def _send_request_headers(self, request: Request, stream_id: int) -> None:
|
| 223 |
+
"""
|
| 224 |
+
Send the request headers to a given stream ID.
|
| 225 |
+
"""
|
| 226 |
+
end_stream = not has_body_headers(request)
|
| 227 |
+
|
| 228 |
+
# In HTTP/2 the ':authority' pseudo-header is used instead of 'Host'.
|
| 229 |
+
# In order to gracefully handle HTTP/1.1 and HTTP/2 we always require
|
| 230 |
+
# HTTP/1.1 style headers, and map them appropriately if we end up on
|
| 231 |
+
# an HTTP/2 connection.
|
| 232 |
+
authority = [v for k, v in request.headers if k.lower() == b"host"][0]
|
| 233 |
+
|
| 234 |
+
headers = [
|
| 235 |
+
(b":method", request.method),
|
| 236 |
+
(b":authority", authority),
|
| 237 |
+
(b":scheme", request.url.scheme),
|
| 238 |
+
(b":path", request.url.target),
|
| 239 |
+
] + [
|
| 240 |
+
(k.lower(), v)
|
| 241 |
+
for k, v in request.headers
|
| 242 |
+
if k.lower()
|
| 243 |
+
not in (
|
| 244 |
+
b"host",
|
| 245 |
+
b"transfer-encoding",
|
| 246 |
+
)
|
| 247 |
+
]
|
| 248 |
+
|
| 249 |
+
self._h2_state.send_headers(stream_id, headers, end_stream=end_stream)
|
| 250 |
+
self._h2_state.increment_flow_control_window(2**24, stream_id=stream_id)
|
| 251 |
+
self._write_outgoing_data(request)
|
| 252 |
+
|
| 253 |
+
def _send_request_body(self, request: Request, stream_id: int) -> None:
|
| 254 |
+
"""
|
| 255 |
+
Iterate over the request body sending it to a given stream ID.
|
| 256 |
+
"""
|
| 257 |
+
if not has_body_headers(request):
|
| 258 |
+
return
|
| 259 |
+
|
| 260 |
+
assert isinstance(request.stream, typing.Iterable)
|
| 261 |
+
for data in request.stream:
|
| 262 |
+
self._send_stream_data(request, stream_id, data)
|
| 263 |
+
self._send_end_stream(request, stream_id)
|
| 264 |
+
|
| 265 |
+
def _send_stream_data(
|
| 266 |
+
self, request: Request, stream_id: int, data: bytes
|
| 267 |
+
) -> None:
|
| 268 |
+
"""
|
| 269 |
+
Send a single chunk of data in one or more data frames.
|
| 270 |
+
"""
|
| 271 |
+
while data:
|
| 272 |
+
max_flow = self._wait_for_outgoing_flow(request, stream_id)
|
| 273 |
+
chunk_size = min(len(data), max_flow)
|
| 274 |
+
chunk, data = data[:chunk_size], data[chunk_size:]
|
| 275 |
+
self._h2_state.send_data(stream_id, chunk)
|
| 276 |
+
self._write_outgoing_data(request)
|
| 277 |
+
|
| 278 |
+
def _send_end_stream(self, request: Request, stream_id: int) -> None:
|
| 279 |
+
"""
|
| 280 |
+
Send an empty data frame on on a given stream ID with the END_STREAM flag set.
|
| 281 |
+
"""
|
| 282 |
+
self._h2_state.end_stream(stream_id)
|
| 283 |
+
self._write_outgoing_data(request)
|
| 284 |
+
|
| 285 |
+
# Receiving the response...
|
| 286 |
+
|
| 287 |
+
def _receive_response(
|
| 288 |
+
self, request: Request, stream_id: int
|
| 289 |
+
) -> tuple[int, list[tuple[bytes, bytes]]]:
|
| 290 |
+
"""
|
| 291 |
+
Return the response status code and headers for a given stream ID.
|
| 292 |
+
"""
|
| 293 |
+
while True:
|
| 294 |
+
event = self._receive_stream_event(request, stream_id)
|
| 295 |
+
if isinstance(event, h2.events.ResponseReceived):
|
| 296 |
+
break
|
| 297 |
+
|
| 298 |
+
status_code = 200
|
| 299 |
+
headers = []
|
| 300 |
+
assert event.headers is not None
|
| 301 |
+
for k, v in event.headers:
|
| 302 |
+
if k == b":status":
|
| 303 |
+
status_code = int(v.decode("ascii", errors="ignore"))
|
| 304 |
+
elif not k.startswith(b":"):
|
| 305 |
+
headers.append((k, v))
|
| 306 |
+
|
| 307 |
+
return (status_code, headers)
|
| 308 |
+
|
| 309 |
+
def _receive_response_body(
|
| 310 |
+
self, request: Request, stream_id: int
|
| 311 |
+
) -> typing.Iterator[bytes]:
|
| 312 |
+
"""
|
| 313 |
+
Iterator that returns the bytes of the response body for a given stream ID.
|
| 314 |
+
"""
|
| 315 |
+
while True:
|
| 316 |
+
event = self._receive_stream_event(request, stream_id)
|
| 317 |
+
if isinstance(event, h2.events.DataReceived):
|
| 318 |
+
assert event.flow_controlled_length is not None
|
| 319 |
+
assert event.data is not None
|
| 320 |
+
amount = event.flow_controlled_length
|
| 321 |
+
self._h2_state.acknowledge_received_data(amount, stream_id)
|
| 322 |
+
self._write_outgoing_data(request)
|
| 323 |
+
yield event.data
|
| 324 |
+
elif isinstance(event, h2.events.StreamEnded):
|
| 325 |
+
break
|
| 326 |
+
|
| 327 |
+
def _receive_stream_event(
|
| 328 |
+
self, request: Request, stream_id: int
|
| 329 |
+
) -> h2.events.ResponseReceived | h2.events.DataReceived | h2.events.StreamEnded:
|
| 330 |
+
"""
|
| 331 |
+
Return the next available event for a given stream ID.
|
| 332 |
+
|
| 333 |
+
Will read more data from the network if required.
|
| 334 |
+
"""
|
| 335 |
+
while not self._events.get(stream_id):
|
| 336 |
+
self._receive_events(request, stream_id)
|
| 337 |
+
event = self._events[stream_id].pop(0)
|
| 338 |
+
if isinstance(event, h2.events.StreamReset):
|
| 339 |
+
raise RemoteProtocolError(event)
|
| 340 |
+
return event
|
| 341 |
+
|
| 342 |
+
def _receive_events(
|
| 343 |
+
self, request: Request, stream_id: int | None = None
|
| 344 |
+
) -> None:
|
| 345 |
+
"""
|
| 346 |
+
Read some data from the network until we see one or more events
|
| 347 |
+
for a given stream ID.
|
| 348 |
+
"""
|
| 349 |
+
with self._read_lock:
|
| 350 |
+
if self._connection_terminated is not None:
|
| 351 |
+
last_stream_id = self._connection_terminated.last_stream_id
|
| 352 |
+
if stream_id and last_stream_id and stream_id > last_stream_id:
|
| 353 |
+
self._request_count -= 1
|
| 354 |
+
raise ConnectionNotAvailable()
|
| 355 |
+
raise RemoteProtocolError(self._connection_terminated)
|
| 356 |
+
|
| 357 |
+
# This conditional is a bit icky. We don't want to block reading if we've
|
| 358 |
+
# actually got an event to return for a given stream. We need to do that
|
| 359 |
+
# check *within* the atomic read lock. Though it also need to be optional,
|
| 360 |
+
# because when we call it from `_wait_for_outgoing_flow` we *do* want to
|
| 361 |
+
# block until we've available flow control, event when we have events
|
| 362 |
+
# pending for the stream ID we're attempting to send on.
|
| 363 |
+
if stream_id is None or not self._events.get(stream_id):
|
| 364 |
+
events = self._read_incoming_data(request)
|
| 365 |
+
for event in events:
|
| 366 |
+
if isinstance(event, h2.events.RemoteSettingsChanged):
|
| 367 |
+
with Trace(
|
| 368 |
+
"receive_remote_settings", logger, request
|
| 369 |
+
) as trace:
|
| 370 |
+
self._receive_remote_settings_change(event)
|
| 371 |
+
trace.return_value = event
|
| 372 |
+
|
| 373 |
+
elif isinstance(
|
| 374 |
+
event,
|
| 375 |
+
(
|
| 376 |
+
h2.events.ResponseReceived,
|
| 377 |
+
h2.events.DataReceived,
|
| 378 |
+
h2.events.StreamEnded,
|
| 379 |
+
h2.events.StreamReset,
|
| 380 |
+
),
|
| 381 |
+
):
|
| 382 |
+
if event.stream_id in self._events:
|
| 383 |
+
self._events[event.stream_id].append(event)
|
| 384 |
+
|
| 385 |
+
elif isinstance(event, h2.events.ConnectionTerminated):
|
| 386 |
+
self._connection_terminated = event
|
| 387 |
+
|
| 388 |
+
self._write_outgoing_data(request)
|
| 389 |
+
|
| 390 |
+
def _receive_remote_settings_change(
|
| 391 |
+
self, event: h2.events.RemoteSettingsChanged
|
| 392 |
+
) -> None:
|
| 393 |
+
max_concurrent_streams = event.changed_settings.get(
|
| 394 |
+
h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS
|
| 395 |
+
)
|
| 396 |
+
if max_concurrent_streams:
|
| 397 |
+
new_max_streams = min(
|
| 398 |
+
max_concurrent_streams.new_value,
|
| 399 |
+
self._h2_state.local_settings.max_concurrent_streams,
|
| 400 |
+
)
|
| 401 |
+
if new_max_streams and new_max_streams != self._max_streams:
|
| 402 |
+
while new_max_streams > self._max_streams:
|
| 403 |
+
self._max_streams_semaphore.release()
|
| 404 |
+
self._max_streams += 1
|
| 405 |
+
while new_max_streams < self._max_streams:
|
| 406 |
+
self._max_streams_semaphore.acquire()
|
| 407 |
+
self._max_streams -= 1
|
| 408 |
+
|
| 409 |
+
def _response_closed(self, stream_id: int) -> None:
|
| 410 |
+
self._max_streams_semaphore.release()
|
| 411 |
+
del self._events[stream_id]
|
| 412 |
+
with self._state_lock:
|
| 413 |
+
if self._connection_terminated and not self._events:
|
| 414 |
+
self.close()
|
| 415 |
+
|
| 416 |
+
elif self._state == HTTPConnectionState.ACTIVE and not self._events:
|
| 417 |
+
self._state = HTTPConnectionState.IDLE
|
| 418 |
+
if self._keepalive_expiry is not None:
|
| 419 |
+
now = time.monotonic()
|
| 420 |
+
self._expire_at = now + self._keepalive_expiry
|
| 421 |
+
if self._used_all_stream_ids: # pragma: nocover
|
| 422 |
+
self.close()
|
| 423 |
+
|
| 424 |
+
def close(self) -> None:
|
| 425 |
+
# Note that this method unilaterally closes the connection, and does
|
| 426 |
+
# not have any kind of locking in place around it.
|
| 427 |
+
self._h2_state.close_connection()
|
| 428 |
+
self._state = HTTPConnectionState.CLOSED
|
| 429 |
+
self._network_stream.close()
|
| 430 |
+
|
| 431 |
+
# Wrappers around network read/write operations...
|
| 432 |
+
|
| 433 |
+
def _read_incoming_data(self, request: Request) -> list[h2.events.Event]:
|
| 434 |
+
timeouts = request.extensions.get("timeout", {})
|
| 435 |
+
timeout = timeouts.get("read", None)
|
| 436 |
+
|
| 437 |
+
if self._read_exception is not None:
|
| 438 |
+
raise self._read_exception # pragma: nocover
|
| 439 |
+
|
| 440 |
+
try:
|
| 441 |
+
data = self._network_stream.read(self.READ_NUM_BYTES, timeout)
|
| 442 |
+
if data == b"":
|
| 443 |
+
raise RemoteProtocolError("Server disconnected")
|
| 444 |
+
except Exception as exc:
|
| 445 |
+
# If we get a network error we should:
|
| 446 |
+
#
|
| 447 |
+
# 1. Save the exception and just raise it immediately on any future reads.
|
| 448 |
+
# (For example, this means that a single read timeout or disconnect will
|
| 449 |
+
# immediately close all pending streams. Without requiring multiple
|
| 450 |
+
# sequential timeouts.)
|
| 451 |
+
# 2. Mark the connection as errored, so that we don't accept any other
|
| 452 |
+
# incoming requests.
|
| 453 |
+
self._read_exception = exc
|
| 454 |
+
self._connection_error = True
|
| 455 |
+
raise exc
|
| 456 |
+
|
| 457 |
+
events: list[h2.events.Event] = self._h2_state.receive_data(data)
|
| 458 |
+
|
| 459 |
+
return events
|
| 460 |
+
|
| 461 |
+
def _write_outgoing_data(self, request: Request) -> None:
|
| 462 |
+
timeouts = request.extensions.get("timeout", {})
|
| 463 |
+
timeout = timeouts.get("write", None)
|
| 464 |
+
|
| 465 |
+
with self._write_lock:
|
| 466 |
+
data_to_send = self._h2_state.data_to_send()
|
| 467 |
+
|
| 468 |
+
if self._write_exception is not None:
|
| 469 |
+
raise self._write_exception # pragma: nocover
|
| 470 |
+
|
| 471 |
+
try:
|
| 472 |
+
self._network_stream.write(data_to_send, timeout)
|
| 473 |
+
except Exception as exc: # pragma: nocover
|
| 474 |
+
# If we get a network error we should:
|
| 475 |
+
#
|
| 476 |
+
# 1. Save the exception and just raise it immediately on any future write.
|
| 477 |
+
# (For example, this means that a single write timeout or disconnect will
|
| 478 |
+
# immediately close all pending streams. Without requiring multiple
|
| 479 |
+
# sequential timeouts.)
|
| 480 |
+
# 2. Mark the connection as errored, so that we don't accept any other
|
| 481 |
+
# incoming requests.
|
| 482 |
+
self._write_exception = exc
|
| 483 |
+
self._connection_error = True
|
| 484 |
+
raise exc
|
| 485 |
+
|
| 486 |
+
# Flow control...
|
| 487 |
+
|
| 488 |
+
def _wait_for_outgoing_flow(self, request: Request, stream_id: int) -> int:
|
| 489 |
+
"""
|
| 490 |
+
Returns the maximum allowable outgoing flow for a given stream.
|
| 491 |
+
|
| 492 |
+
If the allowable flow is zero, then waits on the network until
|
| 493 |
+
WindowUpdated frames have increased the flow rate.
|
| 494 |
+
https://tools.ietf.org/html/rfc7540#section-6.9
|
| 495 |
+
"""
|
| 496 |
+
local_flow: int = self._h2_state.local_flow_control_window(stream_id)
|
| 497 |
+
max_frame_size: int = self._h2_state.max_outbound_frame_size
|
| 498 |
+
flow = min(local_flow, max_frame_size)
|
| 499 |
+
while flow == 0:
|
| 500 |
+
self._receive_events(request)
|
| 501 |
+
local_flow = self._h2_state.local_flow_control_window(stream_id)
|
| 502 |
+
max_frame_size = self._h2_state.max_outbound_frame_size
|
| 503 |
+
flow = min(local_flow, max_frame_size)
|
| 504 |
+
return flow
|
| 505 |
+
|
| 506 |
+
# Interface for connection pooling...
|
| 507 |
+
|
| 508 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 509 |
+
return origin == self._origin
|
| 510 |
+
|
| 511 |
+
def is_available(self) -> bool:
|
| 512 |
+
return (
|
| 513 |
+
self._state != HTTPConnectionState.CLOSED
|
| 514 |
+
and not self._connection_error
|
| 515 |
+
and not self._used_all_stream_ids
|
| 516 |
+
and not (
|
| 517 |
+
self._h2_state.state_machine.state
|
| 518 |
+
== h2.connection.ConnectionState.CLOSED
|
| 519 |
+
)
|
| 520 |
+
)
|
| 521 |
+
|
| 522 |
+
def has_expired(self) -> bool:
|
| 523 |
+
now = time.monotonic()
|
| 524 |
+
return self._expire_at is not None and now > self._expire_at
|
| 525 |
+
|
| 526 |
+
def is_idle(self) -> bool:
|
| 527 |
+
return self._state == HTTPConnectionState.IDLE
|
| 528 |
+
|
| 529 |
+
def is_closed(self) -> bool:
|
| 530 |
+
return self._state == HTTPConnectionState.CLOSED
|
| 531 |
+
|
| 532 |
+
def info(self) -> str:
|
| 533 |
+
origin = str(self._origin)
|
| 534 |
+
return (
|
| 535 |
+
f"{origin!r}, HTTP/2, {self._state.name}, "
|
| 536 |
+
f"Request Count: {self._request_count}"
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
def __repr__(self) -> str:
|
| 540 |
+
class_name = self.__class__.__name__
|
| 541 |
+
origin = str(self._origin)
|
| 542 |
+
return (
|
| 543 |
+
f"<{class_name} [{origin!r}, {self._state.name}, "
|
| 544 |
+
f"Request Count: {self._request_count}]>"
|
| 545 |
+
)
|
| 546 |
+
|
| 547 |
+
# These context managers are not used in the standard flow, but are
|
| 548 |
+
# useful for testing or working with connection instances directly.
|
| 549 |
+
|
| 550 |
+
def __enter__(self) -> HTTP2Connection:
|
| 551 |
+
return self
|
| 552 |
+
|
| 553 |
+
def __exit__(
|
| 554 |
+
self,
|
| 555 |
+
exc_type: type[BaseException] | None = None,
|
| 556 |
+
exc_value: BaseException | None = None,
|
| 557 |
+
traceback: types.TracebackType | None = None,
|
| 558 |
+
) -> None:
|
| 559 |
+
self.close()
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
class HTTP2ConnectionByteStream:
|
| 563 |
+
def __init__(
|
| 564 |
+
self, connection: HTTP2Connection, request: Request, stream_id: int
|
| 565 |
+
) -> None:
|
| 566 |
+
self._connection = connection
|
| 567 |
+
self._request = request
|
| 568 |
+
self._stream_id = stream_id
|
| 569 |
+
self._closed = False
|
| 570 |
+
|
| 571 |
+
def __iter__(self) -> typing.Iterator[bytes]:
|
| 572 |
+
kwargs = {"request": self._request, "stream_id": self._stream_id}
|
| 573 |
+
try:
|
| 574 |
+
with Trace("receive_response_body", logger, self._request, kwargs):
|
| 575 |
+
for chunk in self._connection._receive_response_body(
|
| 576 |
+
request=self._request, stream_id=self._stream_id
|
| 577 |
+
):
|
| 578 |
+
yield chunk
|
| 579 |
+
except BaseException as exc:
|
| 580 |
+
# If we get an exception while streaming the response,
|
| 581 |
+
# we want to close the response (and possibly the connection)
|
| 582 |
+
# before raising that exception.
|
| 583 |
+
with ShieldCancellation():
|
| 584 |
+
self.close()
|
| 585 |
+
raise exc
|
| 586 |
+
|
| 587 |
+
def close(self) -> None:
|
| 588 |
+
if not self._closed:
|
| 589 |
+
self._closed = True
|
| 590 |
+
kwargs = {"stream_id": self._stream_id}
|
| 591 |
+
with Trace("response_closed", logger, self._request, kwargs):
|
| 592 |
+
self._connection._response_closed(stream_id=self._stream_id)
|
venv/lib/python3.12/site-packages/httpcore/_sync/http_proxy.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import logging
|
| 5 |
+
import ssl
|
| 6 |
+
import typing
|
| 7 |
+
|
| 8 |
+
from .._backends.base import SOCKET_OPTION, NetworkBackend
|
| 9 |
+
from .._exceptions import ProxyError
|
| 10 |
+
from .._models import (
|
| 11 |
+
URL,
|
| 12 |
+
Origin,
|
| 13 |
+
Request,
|
| 14 |
+
Response,
|
| 15 |
+
enforce_bytes,
|
| 16 |
+
enforce_headers,
|
| 17 |
+
enforce_url,
|
| 18 |
+
)
|
| 19 |
+
from .._ssl import default_ssl_context
|
| 20 |
+
from .._synchronization import Lock
|
| 21 |
+
from .._trace import Trace
|
| 22 |
+
from .connection import HTTPConnection
|
| 23 |
+
from .connection_pool import ConnectionPool
|
| 24 |
+
from .http11 import HTTP11Connection
|
| 25 |
+
from .interfaces import ConnectionInterface
|
| 26 |
+
|
| 27 |
+
ByteOrStr = typing.Union[bytes, str]
|
| 28 |
+
HeadersAsSequence = typing.Sequence[typing.Tuple[ByteOrStr, ByteOrStr]]
|
| 29 |
+
HeadersAsMapping = typing.Mapping[ByteOrStr, ByteOrStr]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
logger = logging.getLogger("httpcore.proxy")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def merge_headers(
|
| 36 |
+
default_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
|
| 37 |
+
override_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
|
| 38 |
+
) -> list[tuple[bytes, bytes]]:
|
| 39 |
+
"""
|
| 40 |
+
Append default_headers and override_headers, de-duplicating if a key exists
|
| 41 |
+
in both cases.
|
| 42 |
+
"""
|
| 43 |
+
default_headers = [] if default_headers is None else list(default_headers)
|
| 44 |
+
override_headers = [] if override_headers is None else list(override_headers)
|
| 45 |
+
has_override = set(key.lower() for key, value in override_headers)
|
| 46 |
+
default_headers = [
|
| 47 |
+
(key, value)
|
| 48 |
+
for key, value in default_headers
|
| 49 |
+
if key.lower() not in has_override
|
| 50 |
+
]
|
| 51 |
+
return default_headers + override_headers
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class HTTPProxy(ConnectionPool): # pragma: nocover
|
| 55 |
+
"""
|
| 56 |
+
A connection pool that sends requests via an HTTP proxy.
|
| 57 |
+
"""
|
| 58 |
+
|
| 59 |
+
def __init__(
|
| 60 |
+
self,
|
| 61 |
+
proxy_url: URL | bytes | str,
|
| 62 |
+
proxy_auth: tuple[bytes | str, bytes | str] | None = None,
|
| 63 |
+
proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None,
|
| 64 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 65 |
+
proxy_ssl_context: ssl.SSLContext | None = None,
|
| 66 |
+
max_connections: int | None = 10,
|
| 67 |
+
max_keepalive_connections: int | None = None,
|
| 68 |
+
keepalive_expiry: float | None = None,
|
| 69 |
+
http1: bool = True,
|
| 70 |
+
http2: bool = False,
|
| 71 |
+
retries: int = 0,
|
| 72 |
+
local_address: str | None = None,
|
| 73 |
+
uds: str | None = None,
|
| 74 |
+
network_backend: NetworkBackend | None = None,
|
| 75 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 76 |
+
) -> None:
|
| 77 |
+
"""
|
| 78 |
+
A connection pool for making HTTP requests.
|
| 79 |
+
|
| 80 |
+
Parameters:
|
| 81 |
+
proxy_url: The URL to use when connecting to the proxy server.
|
| 82 |
+
For example `"http://127.0.0.1:8080/"`.
|
| 83 |
+
proxy_auth: Any proxy authentication as a two-tuple of
|
| 84 |
+
(username, password). May be either bytes or ascii-only str.
|
| 85 |
+
proxy_headers: Any HTTP headers to use for the proxy requests.
|
| 86 |
+
For example `{"Proxy-Authorization": "Basic <username>:<password>"}`.
|
| 87 |
+
ssl_context: An SSL context to use for verifying connections.
|
| 88 |
+
If not specified, the default `httpcore.default_ssl_context()`
|
| 89 |
+
will be used.
|
| 90 |
+
proxy_ssl_context: The same as `ssl_context`, but for a proxy server rather than a remote origin.
|
| 91 |
+
max_connections: The maximum number of concurrent HTTP connections that
|
| 92 |
+
the pool should allow. Any attempt to send a request on a pool that
|
| 93 |
+
would exceed this amount will block until a connection is available.
|
| 94 |
+
max_keepalive_connections: The maximum number of idle HTTP connections
|
| 95 |
+
that will be maintained in the pool.
|
| 96 |
+
keepalive_expiry: The duration in seconds that an idle HTTP connection
|
| 97 |
+
may be maintained for before being expired from the pool.
|
| 98 |
+
http1: A boolean indicating if HTTP/1.1 requests should be supported
|
| 99 |
+
by the connection pool. Defaults to True.
|
| 100 |
+
http2: A boolean indicating if HTTP/2 requests should be supported by
|
| 101 |
+
the connection pool. Defaults to False.
|
| 102 |
+
retries: The maximum number of retries when trying to establish
|
| 103 |
+
a connection.
|
| 104 |
+
local_address: Local address to connect from. Can also be used to
|
| 105 |
+
connect using a particular address family. Using
|
| 106 |
+
`local_address="0.0.0.0"` will connect using an `AF_INET` address
|
| 107 |
+
(IPv4), while using `local_address="::"` will connect using an
|
| 108 |
+
`AF_INET6` address (IPv6).
|
| 109 |
+
uds: Path to a Unix Domain Socket to use instead of TCP sockets.
|
| 110 |
+
network_backend: A backend instance to use for handling network I/O.
|
| 111 |
+
"""
|
| 112 |
+
super().__init__(
|
| 113 |
+
ssl_context=ssl_context,
|
| 114 |
+
max_connections=max_connections,
|
| 115 |
+
max_keepalive_connections=max_keepalive_connections,
|
| 116 |
+
keepalive_expiry=keepalive_expiry,
|
| 117 |
+
http1=http1,
|
| 118 |
+
http2=http2,
|
| 119 |
+
network_backend=network_backend,
|
| 120 |
+
retries=retries,
|
| 121 |
+
local_address=local_address,
|
| 122 |
+
uds=uds,
|
| 123 |
+
socket_options=socket_options,
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
|
| 127 |
+
if (
|
| 128 |
+
self._proxy_url.scheme == b"http" and proxy_ssl_context is not None
|
| 129 |
+
): # pragma: no cover
|
| 130 |
+
raise RuntimeError(
|
| 131 |
+
"The `proxy_ssl_context` argument is not allowed for the http scheme"
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
self._ssl_context = ssl_context
|
| 135 |
+
self._proxy_ssl_context = proxy_ssl_context
|
| 136 |
+
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
|
| 137 |
+
if proxy_auth is not None:
|
| 138 |
+
username = enforce_bytes(proxy_auth[0], name="proxy_auth")
|
| 139 |
+
password = enforce_bytes(proxy_auth[1], name="proxy_auth")
|
| 140 |
+
userpass = username + b":" + password
|
| 141 |
+
authorization = b"Basic " + base64.b64encode(userpass)
|
| 142 |
+
self._proxy_headers = [
|
| 143 |
+
(b"Proxy-Authorization", authorization)
|
| 144 |
+
] + self._proxy_headers
|
| 145 |
+
|
| 146 |
+
def create_connection(self, origin: Origin) -> ConnectionInterface:
|
| 147 |
+
if origin.scheme == b"http":
|
| 148 |
+
return ForwardHTTPConnection(
|
| 149 |
+
proxy_origin=self._proxy_url.origin,
|
| 150 |
+
proxy_headers=self._proxy_headers,
|
| 151 |
+
remote_origin=origin,
|
| 152 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 153 |
+
network_backend=self._network_backend,
|
| 154 |
+
proxy_ssl_context=self._proxy_ssl_context,
|
| 155 |
+
)
|
| 156 |
+
return TunnelHTTPConnection(
|
| 157 |
+
proxy_origin=self._proxy_url.origin,
|
| 158 |
+
proxy_headers=self._proxy_headers,
|
| 159 |
+
remote_origin=origin,
|
| 160 |
+
ssl_context=self._ssl_context,
|
| 161 |
+
proxy_ssl_context=self._proxy_ssl_context,
|
| 162 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 163 |
+
http1=self._http1,
|
| 164 |
+
http2=self._http2,
|
| 165 |
+
network_backend=self._network_backend,
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class ForwardHTTPConnection(ConnectionInterface):
|
| 170 |
+
def __init__(
|
| 171 |
+
self,
|
| 172 |
+
proxy_origin: Origin,
|
| 173 |
+
remote_origin: Origin,
|
| 174 |
+
proxy_headers: HeadersAsMapping | HeadersAsSequence | None = None,
|
| 175 |
+
keepalive_expiry: float | None = None,
|
| 176 |
+
network_backend: NetworkBackend | None = None,
|
| 177 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 178 |
+
proxy_ssl_context: ssl.SSLContext | None = None,
|
| 179 |
+
) -> None:
|
| 180 |
+
self._connection = HTTPConnection(
|
| 181 |
+
origin=proxy_origin,
|
| 182 |
+
keepalive_expiry=keepalive_expiry,
|
| 183 |
+
network_backend=network_backend,
|
| 184 |
+
socket_options=socket_options,
|
| 185 |
+
ssl_context=proxy_ssl_context,
|
| 186 |
+
)
|
| 187 |
+
self._proxy_origin = proxy_origin
|
| 188 |
+
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
|
| 189 |
+
self._remote_origin = remote_origin
|
| 190 |
+
|
| 191 |
+
def handle_request(self, request: Request) -> Response:
|
| 192 |
+
headers = merge_headers(self._proxy_headers, request.headers)
|
| 193 |
+
url = URL(
|
| 194 |
+
scheme=self._proxy_origin.scheme,
|
| 195 |
+
host=self._proxy_origin.host,
|
| 196 |
+
port=self._proxy_origin.port,
|
| 197 |
+
target=bytes(request.url),
|
| 198 |
+
)
|
| 199 |
+
proxy_request = Request(
|
| 200 |
+
method=request.method,
|
| 201 |
+
url=url,
|
| 202 |
+
headers=headers,
|
| 203 |
+
content=request.stream,
|
| 204 |
+
extensions=request.extensions,
|
| 205 |
+
)
|
| 206 |
+
return self._connection.handle_request(proxy_request)
|
| 207 |
+
|
| 208 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 209 |
+
return origin == self._remote_origin
|
| 210 |
+
|
| 211 |
+
def close(self) -> None:
|
| 212 |
+
self._connection.close()
|
| 213 |
+
|
| 214 |
+
def info(self) -> str:
|
| 215 |
+
return self._connection.info()
|
| 216 |
+
|
| 217 |
+
def is_available(self) -> bool:
|
| 218 |
+
return self._connection.is_available()
|
| 219 |
+
|
| 220 |
+
def has_expired(self) -> bool:
|
| 221 |
+
return self._connection.has_expired()
|
| 222 |
+
|
| 223 |
+
def is_idle(self) -> bool:
|
| 224 |
+
return self._connection.is_idle()
|
| 225 |
+
|
| 226 |
+
def is_closed(self) -> bool:
|
| 227 |
+
return self._connection.is_closed()
|
| 228 |
+
|
| 229 |
+
def __repr__(self) -> str:
|
| 230 |
+
return f"<{self.__class__.__name__} [{self.info()}]>"
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class TunnelHTTPConnection(ConnectionInterface):
|
| 234 |
+
def __init__(
|
| 235 |
+
self,
|
| 236 |
+
proxy_origin: Origin,
|
| 237 |
+
remote_origin: Origin,
|
| 238 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 239 |
+
proxy_ssl_context: ssl.SSLContext | None = None,
|
| 240 |
+
proxy_headers: typing.Sequence[tuple[bytes, bytes]] | None = None,
|
| 241 |
+
keepalive_expiry: float | None = None,
|
| 242 |
+
http1: bool = True,
|
| 243 |
+
http2: bool = False,
|
| 244 |
+
network_backend: NetworkBackend | None = None,
|
| 245 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 246 |
+
) -> None:
|
| 247 |
+
self._connection: ConnectionInterface = HTTPConnection(
|
| 248 |
+
origin=proxy_origin,
|
| 249 |
+
keepalive_expiry=keepalive_expiry,
|
| 250 |
+
network_backend=network_backend,
|
| 251 |
+
socket_options=socket_options,
|
| 252 |
+
ssl_context=proxy_ssl_context,
|
| 253 |
+
)
|
| 254 |
+
self._proxy_origin = proxy_origin
|
| 255 |
+
self._remote_origin = remote_origin
|
| 256 |
+
self._ssl_context = ssl_context
|
| 257 |
+
self._proxy_ssl_context = proxy_ssl_context
|
| 258 |
+
self._proxy_headers = enforce_headers(proxy_headers, name="proxy_headers")
|
| 259 |
+
self._keepalive_expiry = keepalive_expiry
|
| 260 |
+
self._http1 = http1
|
| 261 |
+
self._http2 = http2
|
| 262 |
+
self._connect_lock = Lock()
|
| 263 |
+
self._connected = False
|
| 264 |
+
|
| 265 |
+
def handle_request(self, request: Request) -> Response:
|
| 266 |
+
timeouts = request.extensions.get("timeout", {})
|
| 267 |
+
timeout = timeouts.get("connect", None)
|
| 268 |
+
|
| 269 |
+
with self._connect_lock:
|
| 270 |
+
if not self._connected:
|
| 271 |
+
target = b"%b:%d" % (self._remote_origin.host, self._remote_origin.port)
|
| 272 |
+
|
| 273 |
+
connect_url = URL(
|
| 274 |
+
scheme=self._proxy_origin.scheme,
|
| 275 |
+
host=self._proxy_origin.host,
|
| 276 |
+
port=self._proxy_origin.port,
|
| 277 |
+
target=target,
|
| 278 |
+
)
|
| 279 |
+
connect_headers = merge_headers(
|
| 280 |
+
[(b"Host", target), (b"Accept", b"*/*")], self._proxy_headers
|
| 281 |
+
)
|
| 282 |
+
connect_request = Request(
|
| 283 |
+
method=b"CONNECT",
|
| 284 |
+
url=connect_url,
|
| 285 |
+
headers=connect_headers,
|
| 286 |
+
extensions=request.extensions,
|
| 287 |
+
)
|
| 288 |
+
connect_response = self._connection.handle_request(
|
| 289 |
+
connect_request
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
if connect_response.status < 200 or connect_response.status > 299:
|
| 293 |
+
reason_bytes = connect_response.extensions.get("reason_phrase", b"")
|
| 294 |
+
reason_str = reason_bytes.decode("ascii", errors="ignore")
|
| 295 |
+
msg = "%d %s" % (connect_response.status, reason_str)
|
| 296 |
+
self._connection.close()
|
| 297 |
+
raise ProxyError(msg)
|
| 298 |
+
|
| 299 |
+
stream = connect_response.extensions["network_stream"]
|
| 300 |
+
|
| 301 |
+
# Upgrade the stream to SSL
|
| 302 |
+
ssl_context = (
|
| 303 |
+
default_ssl_context()
|
| 304 |
+
if self._ssl_context is None
|
| 305 |
+
else self._ssl_context
|
| 306 |
+
)
|
| 307 |
+
alpn_protocols = ["http/1.1", "h2"] if self._http2 else ["http/1.1"]
|
| 308 |
+
ssl_context.set_alpn_protocols(alpn_protocols)
|
| 309 |
+
|
| 310 |
+
kwargs = {
|
| 311 |
+
"ssl_context": ssl_context,
|
| 312 |
+
"server_hostname": self._remote_origin.host.decode("ascii"),
|
| 313 |
+
"timeout": timeout,
|
| 314 |
+
}
|
| 315 |
+
with Trace("start_tls", logger, request, kwargs) as trace:
|
| 316 |
+
stream = stream.start_tls(**kwargs)
|
| 317 |
+
trace.return_value = stream
|
| 318 |
+
|
| 319 |
+
# Determine if we should be using HTTP/1.1 or HTTP/2
|
| 320 |
+
ssl_object = stream.get_extra_info("ssl_object")
|
| 321 |
+
http2_negotiated = (
|
| 322 |
+
ssl_object is not None
|
| 323 |
+
and ssl_object.selected_alpn_protocol() == "h2"
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
# Create the HTTP/1.1 or HTTP/2 connection
|
| 327 |
+
if http2_negotiated or (self._http2 and not self._http1):
|
| 328 |
+
from .http2 import HTTP2Connection
|
| 329 |
+
|
| 330 |
+
self._connection = HTTP2Connection(
|
| 331 |
+
origin=self._remote_origin,
|
| 332 |
+
stream=stream,
|
| 333 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 334 |
+
)
|
| 335 |
+
else:
|
| 336 |
+
self._connection = HTTP11Connection(
|
| 337 |
+
origin=self._remote_origin,
|
| 338 |
+
stream=stream,
|
| 339 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
self._connected = True
|
| 343 |
+
return self._connection.handle_request(request)
|
| 344 |
+
|
| 345 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 346 |
+
return origin == self._remote_origin
|
| 347 |
+
|
| 348 |
+
def close(self) -> None:
|
| 349 |
+
self._connection.close()
|
| 350 |
+
|
| 351 |
+
def info(self) -> str:
|
| 352 |
+
return self._connection.info()
|
| 353 |
+
|
| 354 |
+
def is_available(self) -> bool:
|
| 355 |
+
return self._connection.is_available()
|
| 356 |
+
|
| 357 |
+
def has_expired(self) -> bool:
|
| 358 |
+
return self._connection.has_expired()
|
| 359 |
+
|
| 360 |
+
def is_idle(self) -> bool:
|
| 361 |
+
return self._connection.is_idle()
|
| 362 |
+
|
| 363 |
+
def is_closed(self) -> bool:
|
| 364 |
+
return self._connection.is_closed()
|
| 365 |
+
|
| 366 |
+
def __repr__(self) -> str:
|
| 367 |
+
return f"<{self.__class__.__name__} [{self.info()}]>"
|
venv/lib/python3.12/site-packages/httpcore/_sync/interfaces.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import contextlib
|
| 4 |
+
import typing
|
| 5 |
+
|
| 6 |
+
from .._models import (
|
| 7 |
+
URL,
|
| 8 |
+
Extensions,
|
| 9 |
+
HeaderTypes,
|
| 10 |
+
Origin,
|
| 11 |
+
Request,
|
| 12 |
+
Response,
|
| 13 |
+
enforce_bytes,
|
| 14 |
+
enforce_headers,
|
| 15 |
+
enforce_url,
|
| 16 |
+
include_request_headers,
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class RequestInterface:
|
| 21 |
+
def request(
|
| 22 |
+
self,
|
| 23 |
+
method: bytes | str,
|
| 24 |
+
url: URL | bytes | str,
|
| 25 |
+
*,
|
| 26 |
+
headers: HeaderTypes = None,
|
| 27 |
+
content: bytes | typing.Iterator[bytes] | None = None,
|
| 28 |
+
extensions: Extensions | None = None,
|
| 29 |
+
) -> Response:
|
| 30 |
+
# Strict type checking on our parameters.
|
| 31 |
+
method = enforce_bytes(method, name="method")
|
| 32 |
+
url = enforce_url(url, name="url")
|
| 33 |
+
headers = enforce_headers(headers, name="headers")
|
| 34 |
+
|
| 35 |
+
# Include Host header, and optionally Content-Length or Transfer-Encoding.
|
| 36 |
+
headers = include_request_headers(headers, url=url, content=content)
|
| 37 |
+
|
| 38 |
+
request = Request(
|
| 39 |
+
method=method,
|
| 40 |
+
url=url,
|
| 41 |
+
headers=headers,
|
| 42 |
+
content=content,
|
| 43 |
+
extensions=extensions,
|
| 44 |
+
)
|
| 45 |
+
response = self.handle_request(request)
|
| 46 |
+
try:
|
| 47 |
+
response.read()
|
| 48 |
+
finally:
|
| 49 |
+
response.close()
|
| 50 |
+
return response
|
| 51 |
+
|
| 52 |
+
@contextlib.contextmanager
|
| 53 |
+
def stream(
|
| 54 |
+
self,
|
| 55 |
+
method: bytes | str,
|
| 56 |
+
url: URL | bytes | str,
|
| 57 |
+
*,
|
| 58 |
+
headers: HeaderTypes = None,
|
| 59 |
+
content: bytes | typing.Iterator[bytes] | None = None,
|
| 60 |
+
extensions: Extensions | None = None,
|
| 61 |
+
) -> typing.Iterator[Response]:
|
| 62 |
+
# Strict type checking on our parameters.
|
| 63 |
+
method = enforce_bytes(method, name="method")
|
| 64 |
+
url = enforce_url(url, name="url")
|
| 65 |
+
headers = enforce_headers(headers, name="headers")
|
| 66 |
+
|
| 67 |
+
# Include Host header, and optionally Content-Length or Transfer-Encoding.
|
| 68 |
+
headers = include_request_headers(headers, url=url, content=content)
|
| 69 |
+
|
| 70 |
+
request = Request(
|
| 71 |
+
method=method,
|
| 72 |
+
url=url,
|
| 73 |
+
headers=headers,
|
| 74 |
+
content=content,
|
| 75 |
+
extensions=extensions,
|
| 76 |
+
)
|
| 77 |
+
response = self.handle_request(request)
|
| 78 |
+
try:
|
| 79 |
+
yield response
|
| 80 |
+
finally:
|
| 81 |
+
response.close()
|
| 82 |
+
|
| 83 |
+
def handle_request(self, request: Request) -> Response:
|
| 84 |
+
raise NotImplementedError() # pragma: nocover
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class ConnectionInterface(RequestInterface):
|
| 88 |
+
def close(self) -> None:
|
| 89 |
+
raise NotImplementedError() # pragma: nocover
|
| 90 |
+
|
| 91 |
+
def info(self) -> str:
|
| 92 |
+
raise NotImplementedError() # pragma: nocover
|
| 93 |
+
|
| 94 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 95 |
+
raise NotImplementedError() # pragma: nocover
|
| 96 |
+
|
| 97 |
+
def is_available(self) -> bool:
|
| 98 |
+
"""
|
| 99 |
+
Return `True` if the connection is currently able to accept an
|
| 100 |
+
outgoing request.
|
| 101 |
+
|
| 102 |
+
An HTTP/1.1 connection will only be available if it is currently idle.
|
| 103 |
+
|
| 104 |
+
An HTTP/2 connection will be available so long as the stream ID space is
|
| 105 |
+
not yet exhausted, and the connection is not in an error state.
|
| 106 |
+
|
| 107 |
+
While the connection is being established we may not yet know if it is going
|
| 108 |
+
to result in an HTTP/1.1 or HTTP/2 connection. The connection should be
|
| 109 |
+
treated as being available, but might ultimately raise `NewConnectionRequired`
|
| 110 |
+
required exceptions if multiple requests are attempted over a connection
|
| 111 |
+
that ends up being established as HTTP/1.1.
|
| 112 |
+
"""
|
| 113 |
+
raise NotImplementedError() # pragma: nocover
|
| 114 |
+
|
| 115 |
+
def has_expired(self) -> bool:
|
| 116 |
+
"""
|
| 117 |
+
Return `True` if the connection is in a state where it should be closed.
|
| 118 |
+
|
| 119 |
+
This either means that the connection is idle and it has passed the
|
| 120 |
+
expiry time on its keep-alive, or that server has sent an EOF.
|
| 121 |
+
"""
|
| 122 |
+
raise NotImplementedError() # pragma: nocover
|
| 123 |
+
|
| 124 |
+
def is_idle(self) -> bool:
|
| 125 |
+
"""
|
| 126 |
+
Return `True` if the connection is currently idle.
|
| 127 |
+
"""
|
| 128 |
+
raise NotImplementedError() # pragma: nocover
|
| 129 |
+
|
| 130 |
+
def is_closed(self) -> bool:
|
| 131 |
+
"""
|
| 132 |
+
Return `True` if the connection has been closed.
|
| 133 |
+
|
| 134 |
+
Used when a response is closed to determine if the connection may be
|
| 135 |
+
returned to the connection pool or not.
|
| 136 |
+
"""
|
| 137 |
+
raise NotImplementedError() # pragma: nocover
|
venv/lib/python3.12/site-packages/httpcore/_sync/socks_proxy.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
import ssl
|
| 5 |
+
|
| 6 |
+
import socksio
|
| 7 |
+
|
| 8 |
+
from .._backends.sync import SyncBackend
|
| 9 |
+
from .._backends.base import NetworkBackend, NetworkStream
|
| 10 |
+
from .._exceptions import ConnectionNotAvailable, ProxyError
|
| 11 |
+
from .._models import URL, Origin, Request, Response, enforce_bytes, enforce_url
|
| 12 |
+
from .._ssl import default_ssl_context
|
| 13 |
+
from .._synchronization import Lock
|
| 14 |
+
from .._trace import Trace
|
| 15 |
+
from .connection_pool import ConnectionPool
|
| 16 |
+
from .http11 import HTTP11Connection
|
| 17 |
+
from .interfaces import ConnectionInterface
|
| 18 |
+
|
| 19 |
+
logger = logging.getLogger("httpcore.socks")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
AUTH_METHODS = {
|
| 23 |
+
b"\x00": "NO AUTHENTICATION REQUIRED",
|
| 24 |
+
b"\x01": "GSSAPI",
|
| 25 |
+
b"\x02": "USERNAME/PASSWORD",
|
| 26 |
+
b"\xff": "NO ACCEPTABLE METHODS",
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
REPLY_CODES = {
|
| 30 |
+
b"\x00": "Succeeded",
|
| 31 |
+
b"\x01": "General SOCKS server failure",
|
| 32 |
+
b"\x02": "Connection not allowed by ruleset",
|
| 33 |
+
b"\x03": "Network unreachable",
|
| 34 |
+
b"\x04": "Host unreachable",
|
| 35 |
+
b"\x05": "Connection refused",
|
| 36 |
+
b"\x06": "TTL expired",
|
| 37 |
+
b"\x07": "Command not supported",
|
| 38 |
+
b"\x08": "Address type not supported",
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _init_socks5_connection(
|
| 43 |
+
stream: NetworkStream,
|
| 44 |
+
*,
|
| 45 |
+
host: bytes,
|
| 46 |
+
port: int,
|
| 47 |
+
auth: tuple[bytes, bytes] | None = None,
|
| 48 |
+
) -> None:
|
| 49 |
+
conn = socksio.socks5.SOCKS5Connection()
|
| 50 |
+
|
| 51 |
+
# Auth method request
|
| 52 |
+
auth_method = (
|
| 53 |
+
socksio.socks5.SOCKS5AuthMethod.NO_AUTH_REQUIRED
|
| 54 |
+
if auth is None
|
| 55 |
+
else socksio.socks5.SOCKS5AuthMethod.USERNAME_PASSWORD
|
| 56 |
+
)
|
| 57 |
+
conn.send(socksio.socks5.SOCKS5AuthMethodsRequest([auth_method]))
|
| 58 |
+
outgoing_bytes = conn.data_to_send()
|
| 59 |
+
stream.write(outgoing_bytes)
|
| 60 |
+
|
| 61 |
+
# Auth method response
|
| 62 |
+
incoming_bytes = stream.read(max_bytes=4096)
|
| 63 |
+
response = conn.receive_data(incoming_bytes)
|
| 64 |
+
assert isinstance(response, socksio.socks5.SOCKS5AuthReply)
|
| 65 |
+
if response.method != auth_method:
|
| 66 |
+
requested = AUTH_METHODS.get(auth_method, "UNKNOWN")
|
| 67 |
+
responded = AUTH_METHODS.get(response.method, "UNKNOWN")
|
| 68 |
+
raise ProxyError(
|
| 69 |
+
f"Requested {requested} from proxy server, but got {responded}."
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
if response.method == socksio.socks5.SOCKS5AuthMethod.USERNAME_PASSWORD:
|
| 73 |
+
# Username/password request
|
| 74 |
+
assert auth is not None
|
| 75 |
+
username, password = auth
|
| 76 |
+
conn.send(socksio.socks5.SOCKS5UsernamePasswordRequest(username, password))
|
| 77 |
+
outgoing_bytes = conn.data_to_send()
|
| 78 |
+
stream.write(outgoing_bytes)
|
| 79 |
+
|
| 80 |
+
# Username/password response
|
| 81 |
+
incoming_bytes = stream.read(max_bytes=4096)
|
| 82 |
+
response = conn.receive_data(incoming_bytes)
|
| 83 |
+
assert isinstance(response, socksio.socks5.SOCKS5UsernamePasswordReply)
|
| 84 |
+
if not response.success:
|
| 85 |
+
raise ProxyError("Invalid username/password")
|
| 86 |
+
|
| 87 |
+
# Connect request
|
| 88 |
+
conn.send(
|
| 89 |
+
socksio.socks5.SOCKS5CommandRequest.from_address(
|
| 90 |
+
socksio.socks5.SOCKS5Command.CONNECT, (host, port)
|
| 91 |
+
)
|
| 92 |
+
)
|
| 93 |
+
outgoing_bytes = conn.data_to_send()
|
| 94 |
+
stream.write(outgoing_bytes)
|
| 95 |
+
|
| 96 |
+
# Connect response
|
| 97 |
+
incoming_bytes = stream.read(max_bytes=4096)
|
| 98 |
+
response = conn.receive_data(incoming_bytes)
|
| 99 |
+
assert isinstance(response, socksio.socks5.SOCKS5Reply)
|
| 100 |
+
if response.reply_code != socksio.socks5.SOCKS5ReplyCode.SUCCEEDED:
|
| 101 |
+
reply_code = REPLY_CODES.get(response.reply_code, "UNKOWN")
|
| 102 |
+
raise ProxyError(f"Proxy Server could not connect: {reply_code}.")
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class SOCKSProxy(ConnectionPool): # pragma: nocover
|
| 106 |
+
"""
|
| 107 |
+
A connection pool that sends requests via an HTTP proxy.
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
def __init__(
|
| 111 |
+
self,
|
| 112 |
+
proxy_url: URL | bytes | str,
|
| 113 |
+
proxy_auth: tuple[bytes | str, bytes | str] | None = None,
|
| 114 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 115 |
+
max_connections: int | None = 10,
|
| 116 |
+
max_keepalive_connections: int | None = None,
|
| 117 |
+
keepalive_expiry: float | None = None,
|
| 118 |
+
http1: bool = True,
|
| 119 |
+
http2: bool = False,
|
| 120 |
+
retries: int = 0,
|
| 121 |
+
network_backend: NetworkBackend | None = None,
|
| 122 |
+
) -> None:
|
| 123 |
+
"""
|
| 124 |
+
A connection pool for making HTTP requests.
|
| 125 |
+
|
| 126 |
+
Parameters:
|
| 127 |
+
proxy_url: The URL to use when connecting to the proxy server.
|
| 128 |
+
For example `"http://127.0.0.1:8080/"`.
|
| 129 |
+
ssl_context: An SSL context to use for verifying connections.
|
| 130 |
+
If not specified, the default `httpcore.default_ssl_context()`
|
| 131 |
+
will be used.
|
| 132 |
+
max_connections: The maximum number of concurrent HTTP connections that
|
| 133 |
+
the pool should allow. Any attempt to send a request on a pool that
|
| 134 |
+
would exceed this amount will block until a connection is available.
|
| 135 |
+
max_keepalive_connections: The maximum number of idle HTTP connections
|
| 136 |
+
that will be maintained in the pool.
|
| 137 |
+
keepalive_expiry: The duration in seconds that an idle HTTP connection
|
| 138 |
+
may be maintained for before being expired from the pool.
|
| 139 |
+
http1: A boolean indicating if HTTP/1.1 requests should be supported
|
| 140 |
+
by the connection pool. Defaults to True.
|
| 141 |
+
http2: A boolean indicating if HTTP/2 requests should be supported by
|
| 142 |
+
the connection pool. Defaults to False.
|
| 143 |
+
retries: The maximum number of retries when trying to establish
|
| 144 |
+
a connection.
|
| 145 |
+
local_address: Local address to connect from. Can also be used to
|
| 146 |
+
connect using a particular address family. Using
|
| 147 |
+
`local_address="0.0.0.0"` will connect using an `AF_INET` address
|
| 148 |
+
(IPv4), while using `local_address="::"` will connect using an
|
| 149 |
+
`AF_INET6` address (IPv6).
|
| 150 |
+
uds: Path to a Unix Domain Socket to use instead of TCP sockets.
|
| 151 |
+
network_backend: A backend instance to use for handling network I/O.
|
| 152 |
+
"""
|
| 153 |
+
super().__init__(
|
| 154 |
+
ssl_context=ssl_context,
|
| 155 |
+
max_connections=max_connections,
|
| 156 |
+
max_keepalive_connections=max_keepalive_connections,
|
| 157 |
+
keepalive_expiry=keepalive_expiry,
|
| 158 |
+
http1=http1,
|
| 159 |
+
http2=http2,
|
| 160 |
+
network_backend=network_backend,
|
| 161 |
+
retries=retries,
|
| 162 |
+
)
|
| 163 |
+
self._ssl_context = ssl_context
|
| 164 |
+
self._proxy_url = enforce_url(proxy_url, name="proxy_url")
|
| 165 |
+
if proxy_auth is not None:
|
| 166 |
+
username, password = proxy_auth
|
| 167 |
+
username_bytes = enforce_bytes(username, name="proxy_auth")
|
| 168 |
+
password_bytes = enforce_bytes(password, name="proxy_auth")
|
| 169 |
+
self._proxy_auth: tuple[bytes, bytes] | None = (
|
| 170 |
+
username_bytes,
|
| 171 |
+
password_bytes,
|
| 172 |
+
)
|
| 173 |
+
else:
|
| 174 |
+
self._proxy_auth = None
|
| 175 |
+
|
| 176 |
+
def create_connection(self, origin: Origin) -> ConnectionInterface:
|
| 177 |
+
return Socks5Connection(
|
| 178 |
+
proxy_origin=self._proxy_url.origin,
|
| 179 |
+
remote_origin=origin,
|
| 180 |
+
proxy_auth=self._proxy_auth,
|
| 181 |
+
ssl_context=self._ssl_context,
|
| 182 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 183 |
+
http1=self._http1,
|
| 184 |
+
http2=self._http2,
|
| 185 |
+
network_backend=self._network_backend,
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class Socks5Connection(ConnectionInterface):
|
| 190 |
+
def __init__(
|
| 191 |
+
self,
|
| 192 |
+
proxy_origin: Origin,
|
| 193 |
+
remote_origin: Origin,
|
| 194 |
+
proxy_auth: tuple[bytes, bytes] | None = None,
|
| 195 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 196 |
+
keepalive_expiry: float | None = None,
|
| 197 |
+
http1: bool = True,
|
| 198 |
+
http2: bool = False,
|
| 199 |
+
network_backend: NetworkBackend | None = None,
|
| 200 |
+
) -> None:
|
| 201 |
+
self._proxy_origin = proxy_origin
|
| 202 |
+
self._remote_origin = remote_origin
|
| 203 |
+
self._proxy_auth = proxy_auth
|
| 204 |
+
self._ssl_context = ssl_context
|
| 205 |
+
self._keepalive_expiry = keepalive_expiry
|
| 206 |
+
self._http1 = http1
|
| 207 |
+
self._http2 = http2
|
| 208 |
+
|
| 209 |
+
self._network_backend: NetworkBackend = (
|
| 210 |
+
SyncBackend() if network_backend is None else network_backend
|
| 211 |
+
)
|
| 212 |
+
self._connect_lock = Lock()
|
| 213 |
+
self._connection: ConnectionInterface | None = None
|
| 214 |
+
self._connect_failed = False
|
| 215 |
+
|
| 216 |
+
def handle_request(self, request: Request) -> Response:
|
| 217 |
+
timeouts = request.extensions.get("timeout", {})
|
| 218 |
+
sni_hostname = request.extensions.get("sni_hostname", None)
|
| 219 |
+
timeout = timeouts.get("connect", None)
|
| 220 |
+
|
| 221 |
+
with self._connect_lock:
|
| 222 |
+
if self._connection is None:
|
| 223 |
+
try:
|
| 224 |
+
# Connect to the proxy
|
| 225 |
+
kwargs = {
|
| 226 |
+
"host": self._proxy_origin.host.decode("ascii"),
|
| 227 |
+
"port": self._proxy_origin.port,
|
| 228 |
+
"timeout": timeout,
|
| 229 |
+
}
|
| 230 |
+
with Trace("connect_tcp", logger, request, kwargs) as trace:
|
| 231 |
+
stream = self._network_backend.connect_tcp(**kwargs)
|
| 232 |
+
trace.return_value = stream
|
| 233 |
+
|
| 234 |
+
# Connect to the remote host using socks5
|
| 235 |
+
kwargs = {
|
| 236 |
+
"stream": stream,
|
| 237 |
+
"host": self._remote_origin.host.decode("ascii"),
|
| 238 |
+
"port": self._remote_origin.port,
|
| 239 |
+
"auth": self._proxy_auth,
|
| 240 |
+
}
|
| 241 |
+
with Trace(
|
| 242 |
+
"setup_socks5_connection", logger, request, kwargs
|
| 243 |
+
) as trace:
|
| 244 |
+
_init_socks5_connection(**kwargs)
|
| 245 |
+
trace.return_value = stream
|
| 246 |
+
|
| 247 |
+
# Upgrade the stream to SSL
|
| 248 |
+
if self._remote_origin.scheme == b"https":
|
| 249 |
+
ssl_context = (
|
| 250 |
+
default_ssl_context()
|
| 251 |
+
if self._ssl_context is None
|
| 252 |
+
else self._ssl_context
|
| 253 |
+
)
|
| 254 |
+
alpn_protocols = (
|
| 255 |
+
["http/1.1", "h2"] if self._http2 else ["http/1.1"]
|
| 256 |
+
)
|
| 257 |
+
ssl_context.set_alpn_protocols(alpn_protocols)
|
| 258 |
+
|
| 259 |
+
kwargs = {
|
| 260 |
+
"ssl_context": ssl_context,
|
| 261 |
+
"server_hostname": sni_hostname
|
| 262 |
+
or self._remote_origin.host.decode("ascii"),
|
| 263 |
+
"timeout": timeout,
|
| 264 |
+
}
|
| 265 |
+
with Trace("start_tls", logger, request, kwargs) as trace:
|
| 266 |
+
stream = stream.start_tls(**kwargs)
|
| 267 |
+
trace.return_value = stream
|
| 268 |
+
|
| 269 |
+
# Determine if we should be using HTTP/1.1 or HTTP/2
|
| 270 |
+
ssl_object = stream.get_extra_info("ssl_object")
|
| 271 |
+
http2_negotiated = (
|
| 272 |
+
ssl_object is not None
|
| 273 |
+
and ssl_object.selected_alpn_protocol() == "h2"
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# Create the HTTP/1.1 or HTTP/2 connection
|
| 277 |
+
if http2_negotiated or (
|
| 278 |
+
self._http2 and not self._http1
|
| 279 |
+
): # pragma: nocover
|
| 280 |
+
from .http2 import HTTP2Connection
|
| 281 |
+
|
| 282 |
+
self._connection = HTTP2Connection(
|
| 283 |
+
origin=self._remote_origin,
|
| 284 |
+
stream=stream,
|
| 285 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 286 |
+
)
|
| 287 |
+
else:
|
| 288 |
+
self._connection = HTTP11Connection(
|
| 289 |
+
origin=self._remote_origin,
|
| 290 |
+
stream=stream,
|
| 291 |
+
keepalive_expiry=self._keepalive_expiry,
|
| 292 |
+
)
|
| 293 |
+
except Exception as exc:
|
| 294 |
+
self._connect_failed = True
|
| 295 |
+
raise exc
|
| 296 |
+
elif not self._connection.is_available(): # pragma: nocover
|
| 297 |
+
raise ConnectionNotAvailable()
|
| 298 |
+
|
| 299 |
+
return self._connection.handle_request(request)
|
| 300 |
+
|
| 301 |
+
def can_handle_request(self, origin: Origin) -> bool:
|
| 302 |
+
return origin == self._remote_origin
|
| 303 |
+
|
| 304 |
+
def close(self) -> None:
|
| 305 |
+
if self._connection is not None:
|
| 306 |
+
self._connection.close()
|
| 307 |
+
|
| 308 |
+
def is_available(self) -> bool:
|
| 309 |
+
if self._connection is None: # pragma: nocover
|
| 310 |
+
# If HTTP/2 support is enabled, and the resulting connection could
|
| 311 |
+
# end up as HTTP/2 then we should indicate the connection as being
|
| 312 |
+
# available to service multiple requests.
|
| 313 |
+
return (
|
| 314 |
+
self._http2
|
| 315 |
+
and (self._remote_origin.scheme == b"https" or not self._http1)
|
| 316 |
+
and not self._connect_failed
|
| 317 |
+
)
|
| 318 |
+
return self._connection.is_available()
|
| 319 |
+
|
| 320 |
+
def has_expired(self) -> bool:
|
| 321 |
+
if self._connection is None: # pragma: nocover
|
| 322 |
+
return self._connect_failed
|
| 323 |
+
return self._connection.has_expired()
|
| 324 |
+
|
| 325 |
+
def is_idle(self) -> bool:
|
| 326 |
+
if self._connection is None: # pragma: nocover
|
| 327 |
+
return self._connect_failed
|
| 328 |
+
return self._connection.is_idle()
|
| 329 |
+
|
| 330 |
+
def is_closed(self) -> bool:
|
| 331 |
+
if self._connection is None: # pragma: nocover
|
| 332 |
+
return self._connect_failed
|
| 333 |
+
return self._connection.is_closed()
|
| 334 |
+
|
| 335 |
+
def info(self) -> str:
|
| 336 |
+
if self._connection is None: # pragma: nocover
|
| 337 |
+
return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"
|
| 338 |
+
return self._connection.info()
|
| 339 |
+
|
| 340 |
+
def __repr__(self) -> str:
|
| 341 |
+
return f"<{self.__class__.__name__} [{self.info()}]>"
|
venv/lib/python3.12/site-packages/httpcore/_synchronization.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import threading
|
| 4 |
+
import types
|
| 5 |
+
|
| 6 |
+
from ._exceptions import ExceptionMapping, PoolTimeout, map_exceptions
|
| 7 |
+
|
| 8 |
+
# Our async synchronization primatives use either 'anyio' or 'trio' depending
|
| 9 |
+
# on if they're running under asyncio or trio.
|
| 10 |
+
|
| 11 |
+
try:
|
| 12 |
+
import trio
|
| 13 |
+
except (ImportError, NotImplementedError): # pragma: nocover
|
| 14 |
+
trio = None # type: ignore
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
import anyio
|
| 18 |
+
except ImportError: # pragma: nocover
|
| 19 |
+
anyio = None # type: ignore
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def current_async_library() -> str:
|
| 23 |
+
# Determine if we're running under trio or asyncio.
|
| 24 |
+
# See https://sniffio.readthedocs.io/en/latest/
|
| 25 |
+
try:
|
| 26 |
+
import sniffio
|
| 27 |
+
except ImportError: # pragma: nocover
|
| 28 |
+
environment = "asyncio"
|
| 29 |
+
else:
|
| 30 |
+
environment = sniffio.current_async_library()
|
| 31 |
+
|
| 32 |
+
if environment not in ("asyncio", "trio"): # pragma: nocover
|
| 33 |
+
raise RuntimeError("Running under an unsupported async environment.")
|
| 34 |
+
|
| 35 |
+
if environment == "asyncio" and anyio is None: # pragma: nocover
|
| 36 |
+
raise RuntimeError(
|
| 37 |
+
"Running with asyncio requires installation of 'httpcore[asyncio]'."
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
if environment == "trio" and trio is None: # pragma: nocover
|
| 41 |
+
raise RuntimeError(
|
| 42 |
+
"Running with trio requires installation of 'httpcore[trio]'."
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
return environment
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class AsyncLock:
|
| 49 |
+
"""
|
| 50 |
+
This is a standard lock.
|
| 51 |
+
|
| 52 |
+
In the sync case `Lock` provides thread locking.
|
| 53 |
+
In the async case `AsyncLock` provides async locking.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
def __init__(self) -> None:
|
| 57 |
+
self._backend = ""
|
| 58 |
+
|
| 59 |
+
def setup(self) -> None:
|
| 60 |
+
"""
|
| 61 |
+
Detect if we're running under 'asyncio' or 'trio' and create
|
| 62 |
+
a lock with the correct implementation.
|
| 63 |
+
"""
|
| 64 |
+
self._backend = current_async_library()
|
| 65 |
+
if self._backend == "trio":
|
| 66 |
+
self._trio_lock = trio.Lock()
|
| 67 |
+
elif self._backend == "asyncio":
|
| 68 |
+
self._anyio_lock = anyio.Lock()
|
| 69 |
+
|
| 70 |
+
async def __aenter__(self) -> AsyncLock:
|
| 71 |
+
if not self._backend:
|
| 72 |
+
self.setup()
|
| 73 |
+
|
| 74 |
+
if self._backend == "trio":
|
| 75 |
+
await self._trio_lock.acquire()
|
| 76 |
+
elif self._backend == "asyncio":
|
| 77 |
+
await self._anyio_lock.acquire()
|
| 78 |
+
|
| 79 |
+
return self
|
| 80 |
+
|
| 81 |
+
async def __aexit__(
|
| 82 |
+
self,
|
| 83 |
+
exc_type: type[BaseException] | None = None,
|
| 84 |
+
exc_value: BaseException | None = None,
|
| 85 |
+
traceback: types.TracebackType | None = None,
|
| 86 |
+
) -> None:
|
| 87 |
+
if self._backend == "trio":
|
| 88 |
+
self._trio_lock.release()
|
| 89 |
+
elif self._backend == "asyncio":
|
| 90 |
+
self._anyio_lock.release()
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class AsyncThreadLock:
|
| 94 |
+
"""
|
| 95 |
+
This is a threading-only lock for no-I/O contexts.
|
| 96 |
+
|
| 97 |
+
In the sync case `ThreadLock` provides thread locking.
|
| 98 |
+
In the async case `AsyncThreadLock` is a no-op.
|
| 99 |
+
"""
|
| 100 |
+
|
| 101 |
+
def __enter__(self) -> AsyncThreadLock:
|
| 102 |
+
return self
|
| 103 |
+
|
| 104 |
+
def __exit__(
|
| 105 |
+
self,
|
| 106 |
+
exc_type: type[BaseException] | None = None,
|
| 107 |
+
exc_value: BaseException | None = None,
|
| 108 |
+
traceback: types.TracebackType | None = None,
|
| 109 |
+
) -> None:
|
| 110 |
+
pass
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class AsyncEvent:
|
| 114 |
+
def __init__(self) -> None:
|
| 115 |
+
self._backend = ""
|
| 116 |
+
|
| 117 |
+
def setup(self) -> None:
|
| 118 |
+
"""
|
| 119 |
+
Detect if we're running under 'asyncio' or 'trio' and create
|
| 120 |
+
a lock with the correct implementation.
|
| 121 |
+
"""
|
| 122 |
+
self._backend = current_async_library()
|
| 123 |
+
if self._backend == "trio":
|
| 124 |
+
self._trio_event = trio.Event()
|
| 125 |
+
elif self._backend == "asyncio":
|
| 126 |
+
self._anyio_event = anyio.Event()
|
| 127 |
+
|
| 128 |
+
def set(self) -> None:
|
| 129 |
+
if not self._backend:
|
| 130 |
+
self.setup()
|
| 131 |
+
|
| 132 |
+
if self._backend == "trio":
|
| 133 |
+
self._trio_event.set()
|
| 134 |
+
elif self._backend == "asyncio":
|
| 135 |
+
self._anyio_event.set()
|
| 136 |
+
|
| 137 |
+
async def wait(self, timeout: float | None = None) -> None:
|
| 138 |
+
if not self._backend:
|
| 139 |
+
self.setup()
|
| 140 |
+
|
| 141 |
+
if self._backend == "trio":
|
| 142 |
+
trio_exc_map: ExceptionMapping = {trio.TooSlowError: PoolTimeout}
|
| 143 |
+
timeout_or_inf = float("inf") if timeout is None else timeout
|
| 144 |
+
with map_exceptions(trio_exc_map):
|
| 145 |
+
with trio.fail_after(timeout_or_inf):
|
| 146 |
+
await self._trio_event.wait()
|
| 147 |
+
elif self._backend == "asyncio":
|
| 148 |
+
anyio_exc_map: ExceptionMapping = {TimeoutError: PoolTimeout}
|
| 149 |
+
with map_exceptions(anyio_exc_map):
|
| 150 |
+
with anyio.fail_after(timeout):
|
| 151 |
+
await self._anyio_event.wait()
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
class AsyncSemaphore:
|
| 155 |
+
def __init__(self, bound: int) -> None:
|
| 156 |
+
self._bound = bound
|
| 157 |
+
self._backend = ""
|
| 158 |
+
|
| 159 |
+
def setup(self) -> None:
|
| 160 |
+
"""
|
| 161 |
+
Detect if we're running under 'asyncio' or 'trio' and create
|
| 162 |
+
a semaphore with the correct implementation.
|
| 163 |
+
"""
|
| 164 |
+
self._backend = current_async_library()
|
| 165 |
+
if self._backend == "trio":
|
| 166 |
+
self._trio_semaphore = trio.Semaphore(
|
| 167 |
+
initial_value=self._bound, max_value=self._bound
|
| 168 |
+
)
|
| 169 |
+
elif self._backend == "asyncio":
|
| 170 |
+
self._anyio_semaphore = anyio.Semaphore(
|
| 171 |
+
initial_value=self._bound, max_value=self._bound
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
async def acquire(self) -> None:
|
| 175 |
+
if not self._backend:
|
| 176 |
+
self.setup()
|
| 177 |
+
|
| 178 |
+
if self._backend == "trio":
|
| 179 |
+
await self._trio_semaphore.acquire()
|
| 180 |
+
elif self._backend == "asyncio":
|
| 181 |
+
await self._anyio_semaphore.acquire()
|
| 182 |
+
|
| 183 |
+
async def release(self) -> None:
|
| 184 |
+
if self._backend == "trio":
|
| 185 |
+
self._trio_semaphore.release()
|
| 186 |
+
elif self._backend == "asyncio":
|
| 187 |
+
self._anyio_semaphore.release()
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
class AsyncShieldCancellation:
|
| 191 |
+
# For certain portions of our codebase where we're dealing with
|
| 192 |
+
# closing connections during exception handling we want to shield
|
| 193 |
+
# the operation from being cancelled.
|
| 194 |
+
#
|
| 195 |
+
# with AsyncShieldCancellation():
|
| 196 |
+
# ... # clean-up operations, shielded from cancellation.
|
| 197 |
+
|
| 198 |
+
def __init__(self) -> None:
|
| 199 |
+
"""
|
| 200 |
+
Detect if we're running under 'asyncio' or 'trio' and create
|
| 201 |
+
a shielded scope with the correct implementation.
|
| 202 |
+
"""
|
| 203 |
+
self._backend = current_async_library()
|
| 204 |
+
|
| 205 |
+
if self._backend == "trio":
|
| 206 |
+
self._trio_shield = trio.CancelScope(shield=True)
|
| 207 |
+
elif self._backend == "asyncio":
|
| 208 |
+
self._anyio_shield = anyio.CancelScope(shield=True)
|
| 209 |
+
|
| 210 |
+
def __enter__(self) -> AsyncShieldCancellation:
|
| 211 |
+
if self._backend == "trio":
|
| 212 |
+
self._trio_shield.__enter__()
|
| 213 |
+
elif self._backend == "asyncio":
|
| 214 |
+
self._anyio_shield.__enter__()
|
| 215 |
+
return self
|
| 216 |
+
|
| 217 |
+
def __exit__(
|
| 218 |
+
self,
|
| 219 |
+
exc_type: type[BaseException] | None = None,
|
| 220 |
+
exc_value: BaseException | None = None,
|
| 221 |
+
traceback: types.TracebackType | None = None,
|
| 222 |
+
) -> None:
|
| 223 |
+
if self._backend == "trio":
|
| 224 |
+
self._trio_shield.__exit__(exc_type, exc_value, traceback)
|
| 225 |
+
elif self._backend == "asyncio":
|
| 226 |
+
self._anyio_shield.__exit__(exc_type, exc_value, traceback)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
# Our thread-based synchronization primitives...
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class Lock:
|
| 233 |
+
"""
|
| 234 |
+
This is a standard lock.
|
| 235 |
+
|
| 236 |
+
In the sync case `Lock` provides thread locking.
|
| 237 |
+
In the async case `AsyncLock` provides async locking.
|
| 238 |
+
"""
|
| 239 |
+
|
| 240 |
+
def __init__(self) -> None:
|
| 241 |
+
self._lock = threading.Lock()
|
| 242 |
+
|
| 243 |
+
def __enter__(self) -> Lock:
|
| 244 |
+
self._lock.acquire()
|
| 245 |
+
return self
|
| 246 |
+
|
| 247 |
+
def __exit__(
|
| 248 |
+
self,
|
| 249 |
+
exc_type: type[BaseException] | None = None,
|
| 250 |
+
exc_value: BaseException | None = None,
|
| 251 |
+
traceback: types.TracebackType | None = None,
|
| 252 |
+
) -> None:
|
| 253 |
+
self._lock.release()
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
class ThreadLock:
|
| 257 |
+
"""
|
| 258 |
+
This is a threading-only lock for no-I/O contexts.
|
| 259 |
+
|
| 260 |
+
In the sync case `ThreadLock` provides thread locking.
|
| 261 |
+
In the async case `AsyncThreadLock` is a no-op.
|
| 262 |
+
"""
|
| 263 |
+
|
| 264 |
+
def __init__(self) -> None:
|
| 265 |
+
self._lock = threading.Lock()
|
| 266 |
+
|
| 267 |
+
def __enter__(self) -> ThreadLock:
|
| 268 |
+
self._lock.acquire()
|
| 269 |
+
return self
|
| 270 |
+
|
| 271 |
+
def __exit__(
|
| 272 |
+
self,
|
| 273 |
+
exc_type: type[BaseException] | None = None,
|
| 274 |
+
exc_value: BaseException | None = None,
|
| 275 |
+
traceback: types.TracebackType | None = None,
|
| 276 |
+
) -> None:
|
| 277 |
+
self._lock.release()
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
class Event:
|
| 281 |
+
def __init__(self) -> None:
|
| 282 |
+
self._event = threading.Event()
|
| 283 |
+
|
| 284 |
+
def set(self) -> None:
|
| 285 |
+
self._event.set()
|
| 286 |
+
|
| 287 |
+
def wait(self, timeout: float | None = None) -> None:
|
| 288 |
+
if timeout == float("inf"): # pragma: no cover
|
| 289 |
+
timeout = None
|
| 290 |
+
if not self._event.wait(timeout=timeout):
|
| 291 |
+
raise PoolTimeout() # pragma: nocover
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
class Semaphore:
|
| 295 |
+
def __init__(self, bound: int) -> None:
|
| 296 |
+
self._semaphore = threading.Semaphore(value=bound)
|
| 297 |
+
|
| 298 |
+
def acquire(self) -> None:
|
| 299 |
+
self._semaphore.acquire()
|
| 300 |
+
|
| 301 |
+
def release(self) -> None:
|
| 302 |
+
self._semaphore.release()
|
| 303 |
+
|
| 304 |
+
|
| 305 |
+
class ShieldCancellation:
|
| 306 |
+
# Thread-synchronous codebases don't support cancellation semantics.
|
| 307 |
+
# We have this class because we need to mirror the async and sync
|
| 308 |
+
# cases within our package, but it's just a no-op.
|
| 309 |
+
def __enter__(self) -> ShieldCancellation:
|
| 310 |
+
return self
|
| 311 |
+
|
| 312 |
+
def __exit__(
|
| 313 |
+
self,
|
| 314 |
+
exc_type: type[BaseException] | None = None,
|
| 315 |
+
exc_value: BaseException | None = None,
|
| 316 |
+
traceback: types.TracebackType | None = None,
|
| 317 |
+
) -> None:
|
| 318 |
+
pass
|
venv/lib/python3.12/site-packages/httpcore/_trace.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import inspect
|
| 4 |
+
import logging
|
| 5 |
+
import types
|
| 6 |
+
import typing
|
| 7 |
+
|
| 8 |
+
from ._models import Request
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Trace:
|
| 12 |
+
def __init__(
|
| 13 |
+
self,
|
| 14 |
+
name: str,
|
| 15 |
+
logger: logging.Logger,
|
| 16 |
+
request: Request | None = None,
|
| 17 |
+
kwargs: dict[str, typing.Any] | None = None,
|
| 18 |
+
) -> None:
|
| 19 |
+
self.name = name
|
| 20 |
+
self.logger = logger
|
| 21 |
+
self.trace_extension = (
|
| 22 |
+
None if request is None else request.extensions.get("trace")
|
| 23 |
+
)
|
| 24 |
+
self.debug = self.logger.isEnabledFor(logging.DEBUG)
|
| 25 |
+
self.kwargs = kwargs or {}
|
| 26 |
+
self.return_value: typing.Any = None
|
| 27 |
+
self.should_trace = self.debug or self.trace_extension is not None
|
| 28 |
+
self.prefix = self.logger.name.split(".")[-1]
|
| 29 |
+
|
| 30 |
+
def trace(self, name: str, info: dict[str, typing.Any]) -> None:
|
| 31 |
+
if self.trace_extension is not None:
|
| 32 |
+
prefix_and_name = f"{self.prefix}.{name}"
|
| 33 |
+
ret = self.trace_extension(prefix_and_name, info)
|
| 34 |
+
if inspect.iscoroutine(ret): # pragma: no cover
|
| 35 |
+
raise TypeError(
|
| 36 |
+
"If you are using a synchronous interface, "
|
| 37 |
+
"the callback of the `trace` extension should "
|
| 38 |
+
"be a normal function instead of an asynchronous function."
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
if self.debug:
|
| 42 |
+
if not info or "return_value" in info and info["return_value"] is None:
|
| 43 |
+
message = name
|
| 44 |
+
else:
|
| 45 |
+
args = " ".join([f"{key}={value!r}" for key, value in info.items()])
|
| 46 |
+
message = f"{name} {args}"
|
| 47 |
+
self.logger.debug(message)
|
| 48 |
+
|
| 49 |
+
def __enter__(self) -> Trace:
|
| 50 |
+
if self.should_trace:
|
| 51 |
+
info = self.kwargs
|
| 52 |
+
self.trace(f"{self.name}.started", info)
|
| 53 |
+
return self
|
| 54 |
+
|
| 55 |
+
def __exit__(
|
| 56 |
+
self,
|
| 57 |
+
exc_type: type[BaseException] | None = None,
|
| 58 |
+
exc_value: BaseException | None = None,
|
| 59 |
+
traceback: types.TracebackType | None = None,
|
| 60 |
+
) -> None:
|
| 61 |
+
if self.should_trace:
|
| 62 |
+
if exc_value is None:
|
| 63 |
+
info = {"return_value": self.return_value}
|
| 64 |
+
self.trace(f"{self.name}.complete", info)
|
| 65 |
+
else:
|
| 66 |
+
info = {"exception": exc_value}
|
| 67 |
+
self.trace(f"{self.name}.failed", info)
|
| 68 |
+
|
| 69 |
+
async def atrace(self, name: str, info: dict[str, typing.Any]) -> None:
|
| 70 |
+
if self.trace_extension is not None:
|
| 71 |
+
prefix_and_name = f"{self.prefix}.{name}"
|
| 72 |
+
coro = self.trace_extension(prefix_and_name, info)
|
| 73 |
+
if not inspect.iscoroutine(coro): # pragma: no cover
|
| 74 |
+
raise TypeError(
|
| 75 |
+
"If you're using an asynchronous interface, "
|
| 76 |
+
"the callback of the `trace` extension should "
|
| 77 |
+
"be an asynchronous function rather than a normal function."
|
| 78 |
+
)
|
| 79 |
+
await coro
|
| 80 |
+
|
| 81 |
+
if self.debug:
|
| 82 |
+
if not info or "return_value" in info and info["return_value"] is None:
|
| 83 |
+
message = name
|
| 84 |
+
else:
|
| 85 |
+
args = " ".join([f"{key}={value!r}" for key, value in info.items()])
|
| 86 |
+
message = f"{name} {args}"
|
| 87 |
+
self.logger.debug(message)
|
| 88 |
+
|
| 89 |
+
async def __aenter__(self) -> Trace:
|
| 90 |
+
if self.should_trace:
|
| 91 |
+
info = self.kwargs
|
| 92 |
+
await self.atrace(f"{self.name}.started", info)
|
| 93 |
+
return self
|
| 94 |
+
|
| 95 |
+
async def __aexit__(
|
| 96 |
+
self,
|
| 97 |
+
exc_type: type[BaseException] | None = None,
|
| 98 |
+
exc_value: BaseException | None = None,
|
| 99 |
+
traceback: types.TracebackType | None = None,
|
| 100 |
+
) -> None:
|
| 101 |
+
if self.should_trace:
|
| 102 |
+
if exc_value is None:
|
| 103 |
+
info = {"return_value": self.return_value}
|
| 104 |
+
await self.atrace(f"{self.name}.complete", info)
|
| 105 |
+
else:
|
| 106 |
+
info = {"exception": exc_value}
|
| 107 |
+
await self.atrace(f"{self.name}.failed", info)
|
venv/lib/python3.12/site-packages/httpcore/_utils.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import select
|
| 4 |
+
import socket
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def is_socket_readable(sock: socket.socket | None) -> bool:
|
| 9 |
+
"""
|
| 10 |
+
Return whether a socket, as identifed by its file descriptor, is readable.
|
| 11 |
+
"A socket is readable" means that the read buffer isn't empty, i.e. that calling
|
| 12 |
+
.recv() on it would immediately return some data.
|
| 13 |
+
"""
|
| 14 |
+
# NOTE: we want check for readability without actually attempting to read, because
|
| 15 |
+
# we don't want to block forever if it's not readable.
|
| 16 |
+
|
| 17 |
+
# In the case that the socket no longer exists, or cannot return a file
|
| 18 |
+
# descriptor, we treat it as being readable, as if it the next read operation
|
| 19 |
+
# on it is ready to return the terminating `b""`.
|
| 20 |
+
sock_fd = None if sock is None else sock.fileno()
|
| 21 |
+
if sock_fd is None or sock_fd < 0: # pragma: nocover
|
| 22 |
+
return True
|
| 23 |
+
|
| 24 |
+
# The implementation below was stolen from:
|
| 25 |
+
# https://github.com/python-trio/trio/blob/20ee2b1b7376db637435d80e266212a35837ddcc/trio/_socket.py#L471-L478
|
| 26 |
+
# See also: https://github.com/encode/httpcore/pull/193#issuecomment-703129316
|
| 27 |
+
|
| 28 |
+
# Use select.select on Windows, and when poll is unavailable and select.poll
|
| 29 |
+
# everywhere else. (E.g. When eventlet is in use. See #327)
|
| 30 |
+
if (
|
| 31 |
+
sys.platform == "win32" or getattr(select, "poll", None) is None
|
| 32 |
+
): # pragma: nocover
|
| 33 |
+
rready, _, _ = select.select([sock_fd], [], [], 0)
|
| 34 |
+
return bool(rready)
|
| 35 |
+
p = select.poll()
|
| 36 |
+
p.register(sock_fd, select.POLLIN)
|
| 37 |
+
return bool(p.poll(0))
|
venv/lib/python3.12/site-packages/httpcore/py.typed
ADDED
|
File without changes
|
venv/lib/python3.12/site-packages/httpx-0.28.1.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
venv/lib/python3.12/site-packages/httpx-0.28.1.dist-info/METADATA
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.3
|
| 2 |
+
Name: httpx
|
| 3 |
+
Version: 0.28.1
|
| 4 |
+
Summary: The next generation HTTP client.
|
| 5 |
+
Project-URL: Changelog, https://github.com/encode/httpx/blob/master/CHANGELOG.md
|
| 6 |
+
Project-URL: Documentation, https://www.python-httpx.org
|
| 7 |
+
Project-URL: Homepage, https://github.com/encode/httpx
|
| 8 |
+
Project-URL: Source, https://github.com/encode/httpx
|
| 9 |
+
Author-email: Tom Christie <tom@tomchristie.com>
|
| 10 |
+
License: BSD-3-Clause
|
| 11 |
+
Classifier: Development Status :: 4 - Beta
|
| 12 |
+
Classifier: Environment :: Web Environment
|
| 13 |
+
Classifier: Framework :: AsyncIO
|
| 14 |
+
Classifier: Framework :: Trio
|
| 15 |
+
Classifier: Intended Audience :: Developers
|
| 16 |
+
Classifier: License :: OSI Approved :: BSD License
|
| 17 |
+
Classifier: Operating System :: OS Independent
|
| 18 |
+
Classifier: Programming Language :: Python :: 3
|
| 19 |
+
Classifier: Programming Language :: Python :: 3 :: Only
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.8
|
| 21 |
+
Classifier: Programming Language :: Python :: 3.9
|
| 22 |
+
Classifier: Programming Language :: Python :: 3.10
|
| 23 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 24 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 25 |
+
Classifier: Topic :: Internet :: WWW/HTTP
|
| 26 |
+
Requires-Python: >=3.8
|
| 27 |
+
Requires-Dist: anyio
|
| 28 |
+
Requires-Dist: certifi
|
| 29 |
+
Requires-Dist: httpcore==1.*
|
| 30 |
+
Requires-Dist: idna
|
| 31 |
+
Provides-Extra: brotli
|
| 32 |
+
Requires-Dist: brotli; (platform_python_implementation == 'CPython') and extra == 'brotli'
|
| 33 |
+
Requires-Dist: brotlicffi; (platform_python_implementation != 'CPython') and extra == 'brotli'
|
| 34 |
+
Provides-Extra: cli
|
| 35 |
+
Requires-Dist: click==8.*; extra == 'cli'
|
| 36 |
+
Requires-Dist: pygments==2.*; extra == 'cli'
|
| 37 |
+
Requires-Dist: rich<14,>=10; extra == 'cli'
|
| 38 |
+
Provides-Extra: http2
|
| 39 |
+
Requires-Dist: h2<5,>=3; extra == 'http2'
|
| 40 |
+
Provides-Extra: socks
|
| 41 |
+
Requires-Dist: socksio==1.*; extra == 'socks'
|
| 42 |
+
Provides-Extra: zstd
|
| 43 |
+
Requires-Dist: zstandard>=0.18.0; extra == 'zstd'
|
| 44 |
+
Description-Content-Type: text/markdown
|
| 45 |
+
|
| 46 |
+
<p align="center">
|
| 47 |
+
<a href="https://www.python-httpx.org/"><img width="350" height="208" src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/butterfly.png" alt='HTTPX'></a>
|
| 48 |
+
</p>
|
| 49 |
+
|
| 50 |
+
<p align="center"><strong>HTTPX</strong> <em>- A next-generation HTTP client for Python.</em></p>
|
| 51 |
+
|
| 52 |
+
<p align="center">
|
| 53 |
+
<a href="https://github.com/encode/httpx/actions">
|
| 54 |
+
<img src="https://github.com/encode/httpx/workflows/Test%20Suite/badge.svg" alt="Test Suite">
|
| 55 |
+
</a>
|
| 56 |
+
<a href="https://pypi.org/project/httpx/">
|
| 57 |
+
<img src="https://badge.fury.io/py/httpx.svg" alt="Package version">
|
| 58 |
+
</a>
|
| 59 |
+
</p>
|
| 60 |
+
|
| 61 |
+
HTTPX is a fully featured HTTP client library for Python 3. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**.
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
Install HTTPX using pip:
|
| 66 |
+
|
| 67 |
+
```shell
|
| 68 |
+
$ pip install httpx
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
Now, let's get started:
|
| 72 |
+
|
| 73 |
+
```pycon
|
| 74 |
+
>>> import httpx
|
| 75 |
+
>>> r = httpx.get('https://www.example.org/')
|
| 76 |
+
>>> r
|
| 77 |
+
<Response [200 OK]>
|
| 78 |
+
>>> r.status_code
|
| 79 |
+
200
|
| 80 |
+
>>> r.headers['content-type']
|
| 81 |
+
'text/html; charset=UTF-8'
|
| 82 |
+
>>> r.text
|
| 83 |
+
'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>...'
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
Or, using the command-line client.
|
| 87 |
+
|
| 88 |
+
```shell
|
| 89 |
+
$ pip install 'httpx[cli]' # The command line client is an optional dependency.
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
Which now allows us to use HTTPX directly from the command-line...
|
| 93 |
+
|
| 94 |
+
<p align="center">
|
| 95 |
+
<img width="700" src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/httpx-help.png" alt='httpx --help'>
|
| 96 |
+
</p>
|
| 97 |
+
|
| 98 |
+
Sending a request...
|
| 99 |
+
|
| 100 |
+
<p align="center">
|
| 101 |
+
<img width="700" src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/httpx-request.png" alt='httpx http://httpbin.org/json'>
|
| 102 |
+
</p>
|
| 103 |
+
|
| 104 |
+
## Features
|
| 105 |
+
|
| 106 |
+
HTTPX builds on the well-established usability of `requests`, and gives you:
|
| 107 |
+
|
| 108 |
+
* A broadly [requests-compatible API](https://www.python-httpx.org/compatibility/).
|
| 109 |
+
* An integrated command-line client.
|
| 110 |
+
* HTTP/1.1 [and HTTP/2 support](https://www.python-httpx.org/http2/).
|
| 111 |
+
* Standard synchronous interface, but with [async support if you need it](https://www.python-httpx.org/async/).
|
| 112 |
+
* Ability to make requests directly to [WSGI applications](https://www.python-httpx.org/advanced/transports/#wsgi-transport) or [ASGI applications](https://www.python-httpx.org/advanced/transports/#asgi-transport).
|
| 113 |
+
* Strict timeouts everywhere.
|
| 114 |
+
* Fully type annotated.
|
| 115 |
+
* 100% test coverage.
|
| 116 |
+
|
| 117 |
+
Plus all the standard features of `requests`...
|
| 118 |
+
|
| 119 |
+
* International Domains and URLs
|
| 120 |
+
* Keep-Alive & Connection Pooling
|
| 121 |
+
* Sessions with Cookie Persistence
|
| 122 |
+
* Browser-style SSL Verification
|
| 123 |
+
* Basic/Digest Authentication
|
| 124 |
+
* Elegant Key/Value Cookies
|
| 125 |
+
* Automatic Decompression
|
| 126 |
+
* Automatic Content Decoding
|
| 127 |
+
* Unicode Response Bodies
|
| 128 |
+
* Multipart File Uploads
|
| 129 |
+
* HTTP(S) Proxy Support
|
| 130 |
+
* Connection Timeouts
|
| 131 |
+
* Streaming Downloads
|
| 132 |
+
* .netrc Support
|
| 133 |
+
* Chunked Requests
|
| 134 |
+
|
| 135 |
+
## Installation
|
| 136 |
+
|
| 137 |
+
Install with pip:
|
| 138 |
+
|
| 139 |
+
```shell
|
| 140 |
+
$ pip install httpx
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
Or, to include the optional HTTP/2 support, use:
|
| 144 |
+
|
| 145 |
+
```shell
|
| 146 |
+
$ pip install httpx[http2]
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
HTTPX requires Python 3.8+.
|
| 150 |
+
|
| 151 |
+
## Documentation
|
| 152 |
+
|
| 153 |
+
Project documentation is available at [https://www.python-httpx.org/](https://www.python-httpx.org/).
|
| 154 |
+
|
| 155 |
+
For a run-through of all the basics, head over to the [QuickStart](https://www.python-httpx.org/quickstart/).
|
| 156 |
+
|
| 157 |
+
For more advanced topics, see the [Advanced Usage](https://www.python-httpx.org/advanced/) section, the [async support](https://www.python-httpx.org/async/) section, or the [HTTP/2](https://www.python-httpx.org/http2/) section.
|
| 158 |
+
|
| 159 |
+
The [Developer Interface](https://www.python-httpx.org/api/) provides a comprehensive API reference.
|
| 160 |
+
|
| 161 |
+
To find out about tools that integrate with HTTPX, see [Third Party Packages](https://www.python-httpx.org/third_party_packages/).
|
| 162 |
+
|
| 163 |
+
## Contribute
|
| 164 |
+
|
| 165 |
+
If you want to contribute with HTTPX check out the [Contributing Guide](https://www.python-httpx.org/contributing/) to learn how to start.
|
| 166 |
+
|
| 167 |
+
## Dependencies
|
| 168 |
+
|
| 169 |
+
The HTTPX project relies on these excellent libraries:
|
| 170 |
+
|
| 171 |
+
* `httpcore` - The underlying transport implementation for `httpx`.
|
| 172 |
+
* `h11` - HTTP/1.1 support.
|
| 173 |
+
* `certifi` - SSL certificates.
|
| 174 |
+
* `idna` - Internationalized domain name support.
|
| 175 |
+
* `sniffio` - Async library autodetection.
|
| 176 |
+
|
| 177 |
+
As well as these optional installs:
|
| 178 |
+
|
| 179 |
+
* `h2` - HTTP/2 support. *(Optional, with `httpx[http2]`)*
|
| 180 |
+
* `socksio` - SOCKS proxy support. *(Optional, with `httpx[socks]`)*
|
| 181 |
+
* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)*
|
| 182 |
+
* `click` - Command line client support. *(Optional, with `httpx[cli]`)*
|
| 183 |
+
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)*
|
| 184 |
+
* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)*
|
| 185 |
+
|
| 186 |
+
A huge amount of credit is due to `requests` for the API layout that
|
| 187 |
+
much of this work follows, as well as to `urllib3` for plenty of design
|
| 188 |
+
inspiration around the lower-level networking details.
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
<p align="center"><i>HTTPX is <a href="https://github.com/encode/httpx/blob/master/LICENSE.md">BSD licensed</a> code.<br/>Designed & crafted with care.</i><br/>— 🦋 —</p>
|
| 193 |
+
|
| 194 |
+
## Release Information
|
| 195 |
+
|
| 196 |
+
### Fixed
|
| 197 |
+
|
| 198 |
+
* Reintroduced supposedly-private `URLTypes` shortcut. (#2673)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
---
|
| 202 |
+
|
| 203 |
+
[Full changelog](https://github.com/encode/httpx/blob/master/CHANGELOG.md)
|
venv/lib/python3.12/site-packages/httpx-0.28.1.dist-info/RECORD
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
../../../bin/httpx,sha256=MnZC8HUjBL-ubtCy37H8jXXP7LM_acAi9t3VeBqtRgM,223
|
| 2 |
+
httpx-0.28.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 3 |
+
httpx-0.28.1.dist-info/METADATA,sha256=_rubD48-gNV8gZnDBPNcQzboWB0dGNeYPJJ2a4J5OyU,7052
|
| 4 |
+
httpx-0.28.1.dist-info/RECORD,,
|
| 5 |
+
httpx-0.28.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
| 6 |
+
httpx-0.28.1.dist-info/entry_points.txt,sha256=2lVkdQmxLA1pNMgSN2eV89o90HCZezhmNwsy6ryKDSA,37
|
| 7 |
+
httpx-0.28.1.dist-info/licenses/LICENSE.md,sha256=TsWdVE8StfU5o6cW_TIaxYzNgDC0ZSIfLIgCAM3yjY0,1508
|
| 8 |
+
httpx/__init__.py,sha256=CsaZe6yZj0rHg6322AWKWHGTMVr9txgEfD5P3_Rrz60,2171
|
| 9 |
+
httpx/__pycache__/__init__.cpython-312.pyc,,
|
| 10 |
+
httpx/__pycache__/__version__.cpython-312.pyc,,
|
| 11 |
+
httpx/__pycache__/_api.cpython-312.pyc,,
|
| 12 |
+
httpx/__pycache__/_auth.cpython-312.pyc,,
|
| 13 |
+
httpx/__pycache__/_client.cpython-312.pyc,,
|
| 14 |
+
httpx/__pycache__/_config.cpython-312.pyc,,
|
| 15 |
+
httpx/__pycache__/_content.cpython-312.pyc,,
|
| 16 |
+
httpx/__pycache__/_decoders.cpython-312.pyc,,
|
| 17 |
+
httpx/__pycache__/_exceptions.cpython-312.pyc,,
|
| 18 |
+
httpx/__pycache__/_main.cpython-312.pyc,,
|
| 19 |
+
httpx/__pycache__/_models.cpython-312.pyc,,
|
| 20 |
+
httpx/__pycache__/_multipart.cpython-312.pyc,,
|
| 21 |
+
httpx/__pycache__/_status_codes.cpython-312.pyc,,
|
| 22 |
+
httpx/__pycache__/_types.cpython-312.pyc,,
|
| 23 |
+
httpx/__pycache__/_urlparse.cpython-312.pyc,,
|
| 24 |
+
httpx/__pycache__/_urls.cpython-312.pyc,,
|
| 25 |
+
httpx/__pycache__/_utils.cpython-312.pyc,,
|
| 26 |
+
httpx/__version__.py,sha256=LoUyYeOXTieGzuP_64UL0wxdtxjuu_QbOvE7NOg-IqU,108
|
| 27 |
+
httpx/_api.py,sha256=r_Zgs4jIpcPJLqK5dbbSayqo_iVMKFaxZCd-oOHxLEs,11743
|
| 28 |
+
httpx/_auth.py,sha256=Yr3QwaUSK17rGYx-7j-FdicFIzz4Y9FFV-1F4-7RXX4,11891
|
| 29 |
+
httpx/_client.py,sha256=xD-UG67-WMkeltAAOeGGj-cZ2RRTAm19sWRxlFY7_40,65714
|
| 30 |
+
httpx/_config.py,sha256=pPp2U-wicfcKsF-KYRE1LYdt3e6ERGeIoXZ8Gjo3LWc,8547
|
| 31 |
+
httpx/_content.py,sha256=LGGzrJTR3OvN4Mb1GVVNLXkXJH-6oKlwAttO9p5w_yg,8161
|
| 32 |
+
httpx/_decoders.py,sha256=p0dX8I0NEHexs3UGp4SsZutiMhsXrrWl6-GnqVb0iKM,12041
|
| 33 |
+
httpx/_exceptions.py,sha256=bxW7fxzgVMAdNTbwT0Vnq04gJDW1_gI_GFiQPuMyjL0,8527
|
| 34 |
+
httpx/_main.py,sha256=Cg9GMabiTT_swaDfUgIRitSwxLRMSwUDOm7LdSGqlA4,15626
|
| 35 |
+
httpx/_models.py,sha256=4__Guyv1gLxuZChwim8kfQNiIOcJ9acreFOSurvZfms,44700
|
| 36 |
+
httpx/_multipart.py,sha256=KOHEZZl6oohg9mPaKyyu345qq1rJLg35TUG3YAzXB3Y,9843
|
| 37 |
+
httpx/_status_codes.py,sha256=DYn-2ufBgMeXy5s8x3_TB7wjAuAAMewTakPrm5rXEsc,5639
|
| 38 |
+
httpx/_transports/__init__.py,sha256=GbUoBSAOp7z-l-9j5YhMhR3DMIcn6FVLhj072O3Nnno,275
|
| 39 |
+
httpx/_transports/__pycache__/__init__.cpython-312.pyc,,
|
| 40 |
+
httpx/_transports/__pycache__/asgi.cpython-312.pyc,,
|
| 41 |
+
httpx/_transports/__pycache__/base.cpython-312.pyc,,
|
| 42 |
+
httpx/_transports/__pycache__/default.cpython-312.pyc,,
|
| 43 |
+
httpx/_transports/__pycache__/mock.cpython-312.pyc,,
|
| 44 |
+
httpx/_transports/__pycache__/wsgi.cpython-312.pyc,,
|
| 45 |
+
httpx/_transports/asgi.py,sha256=HRfiDYMPt4wQH2gFgHZg4c-i3sblo6bL5GTqcET-xz8,5501
|
| 46 |
+
httpx/_transports/base.py,sha256=kZS_VMbViYfF570pogUCJ1bulz-ybfL51Pqs9yktebU,2523
|
| 47 |
+
httpx/_transports/default.py,sha256=AzeaRUyVwCccTyyNJexDf0n1dFfzzydpdIQgvw7PLnk,13983
|
| 48 |
+
httpx/_transports/mock.py,sha256=PTo0d567RITXxGrki6kN7_67wwAxfwiMDcuXJiZCjEo,1232
|
| 49 |
+
httpx/_transports/wsgi.py,sha256=NcPX3Xap_EwCFZWO_OaSyQNuInCYx1QMNbO8GAei6jY,4825
|
| 50 |
+
httpx/_types.py,sha256=Jyh41GQq7AOev8IOWKDAg7zCbvHAfufmW5g_PiTtErY,2965
|
| 51 |
+
httpx/_urlparse.py,sha256=ZAmH47ONfkxrrj-PPYhGeiHjb6AjKCS-ANWIN4OL_KY,18546
|
| 52 |
+
httpx/_urls.py,sha256=dX99VR1DSOHpgo9Aq7PzYO4FKdxqKjwyNp8grf8dHN0,21550
|
| 53 |
+
httpx/_utils.py,sha256=_TVeqAKvxJkKHdz7dFeb4s0LZqQXgeFkXSgfiHBK_1o,8285
|
| 54 |
+
httpx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|