Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- .venv/lib/python3.11/site-packages/httpx/__init__.py +105 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/__version__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_api.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_auth.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_client.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_config.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_content.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_decoders.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_exceptions.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_main.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_models.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_multipart.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_status_codes.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_types.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_urlparse.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_urls.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__pycache__/_utils.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/__version__.py +3 -0
- .venv/lib/python3.11/site-packages/httpx/_api.py +438 -0
- .venv/lib/python3.11/site-packages/httpx/_auth.py +348 -0
- .venv/lib/python3.11/site-packages/httpx/_client.py +2019 -0
- .venv/lib/python3.11/site-packages/httpx/_config.py +248 -0
- .venv/lib/python3.11/site-packages/httpx/_content.py +240 -0
- .venv/lib/python3.11/site-packages/httpx/_decoders.py +393 -0
- .venv/lib/python3.11/site-packages/httpx/_exceptions.py +379 -0
- .venv/lib/python3.11/site-packages/httpx/_main.py +506 -0
- .venv/lib/python3.11/site-packages/httpx/_models.py +1277 -0
- .venv/lib/python3.11/site-packages/httpx/_multipart.py +300 -0
- .venv/lib/python3.11/site-packages/httpx/_status_codes.py +162 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/__init__.py +15 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/__init__.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/asgi.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/base.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/default.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/mock.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/wsgi.cpython-311.pyc +0 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/asgi.py +187 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/base.py +86 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/default.py +406 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/mock.py +43 -0
- .venv/lib/python3.11/site-packages/httpx/_transports/wsgi.py +149 -0
- .venv/lib/python3.11/site-packages/httpx/_types.py +114 -0
- .venv/lib/python3.11/site-packages/httpx/_urlparse.py +527 -0
- .venv/lib/python3.11/site-packages/httpx/_urls.py +641 -0
- .venv/lib/python3.11/site-packages/httpx/_utils.py +242 -0
- .venv/lib/python3.11/site-packages/httpx/py.typed +0 -0
- .venv/lib/python3.11/site-packages/propcache-0.2.1.dist-info/INSTALLER +1 -0
- .venv/lib/python3.11/site-packages/propcache-0.2.1.dist-info/LICENSE +202 -0
.gitattributes
CHANGED
|
@@ -346,3 +346,4 @@ tuning-competition-baseline/.venv/lib/python3.11/site-packages/nvidia/cudnn/lib/
|
|
| 346 |
.venv/lib/python3.11/site-packages/multidict/_multidict.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 347 |
.venv/lib/python3.11/site-packages/torchvision/image.so filter=lfs diff=lfs merge=lfs -text
|
| 348 |
.venv/lib/python3.11/site-packages/torchvision/_C.so filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 346 |
.venv/lib/python3.11/site-packages/multidict/_multidict.cpython-311-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 347 |
.venv/lib/python3.11/site-packages/torchvision/image.so filter=lfs diff=lfs merge=lfs -text
|
| 348 |
.venv/lib/python3.11/site-packages/torchvision/_C.so filter=lfs diff=lfs merge=lfs -text
|
| 349 |
+
.venv/lib/python3.11/site-packages/torchvision/transforms/__pycache__/transforms.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
.venv/lib/python3.11/site-packages/httpx/__init__.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .__version__ import __description__, __title__, __version__
|
| 2 |
+
from ._api import *
|
| 3 |
+
from ._auth import *
|
| 4 |
+
from ._client import *
|
| 5 |
+
from ._config import *
|
| 6 |
+
from ._content import *
|
| 7 |
+
from ._exceptions import *
|
| 8 |
+
from ._models import *
|
| 9 |
+
from ._status_codes import *
|
| 10 |
+
from ._transports import *
|
| 11 |
+
from ._types import *
|
| 12 |
+
from ._urls import *
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
from ._main import main
|
| 16 |
+
except ImportError: # pragma: no cover
|
| 17 |
+
|
| 18 |
+
def main() -> None: # type: ignore
|
| 19 |
+
import sys
|
| 20 |
+
|
| 21 |
+
print(
|
| 22 |
+
"The httpx command line client could not run because the required "
|
| 23 |
+
"dependencies were not installed.\nMake sure you've installed "
|
| 24 |
+
"everything with: pip install 'httpx[cli]'"
|
| 25 |
+
)
|
| 26 |
+
sys.exit(1)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
__all__ = [
|
| 30 |
+
"__description__",
|
| 31 |
+
"__title__",
|
| 32 |
+
"__version__",
|
| 33 |
+
"ASGITransport",
|
| 34 |
+
"AsyncBaseTransport",
|
| 35 |
+
"AsyncByteStream",
|
| 36 |
+
"AsyncClient",
|
| 37 |
+
"AsyncHTTPTransport",
|
| 38 |
+
"Auth",
|
| 39 |
+
"BaseTransport",
|
| 40 |
+
"BasicAuth",
|
| 41 |
+
"ByteStream",
|
| 42 |
+
"Client",
|
| 43 |
+
"CloseError",
|
| 44 |
+
"codes",
|
| 45 |
+
"ConnectError",
|
| 46 |
+
"ConnectTimeout",
|
| 47 |
+
"CookieConflict",
|
| 48 |
+
"Cookies",
|
| 49 |
+
"create_ssl_context",
|
| 50 |
+
"DecodingError",
|
| 51 |
+
"delete",
|
| 52 |
+
"DigestAuth",
|
| 53 |
+
"get",
|
| 54 |
+
"head",
|
| 55 |
+
"Headers",
|
| 56 |
+
"HTTPError",
|
| 57 |
+
"HTTPStatusError",
|
| 58 |
+
"HTTPTransport",
|
| 59 |
+
"InvalidURL",
|
| 60 |
+
"Limits",
|
| 61 |
+
"LocalProtocolError",
|
| 62 |
+
"main",
|
| 63 |
+
"MockTransport",
|
| 64 |
+
"NetRCAuth",
|
| 65 |
+
"NetworkError",
|
| 66 |
+
"options",
|
| 67 |
+
"patch",
|
| 68 |
+
"PoolTimeout",
|
| 69 |
+
"post",
|
| 70 |
+
"ProtocolError",
|
| 71 |
+
"Proxy",
|
| 72 |
+
"ProxyError",
|
| 73 |
+
"put",
|
| 74 |
+
"QueryParams",
|
| 75 |
+
"ReadError",
|
| 76 |
+
"ReadTimeout",
|
| 77 |
+
"RemoteProtocolError",
|
| 78 |
+
"request",
|
| 79 |
+
"Request",
|
| 80 |
+
"RequestError",
|
| 81 |
+
"RequestNotRead",
|
| 82 |
+
"Response",
|
| 83 |
+
"ResponseNotRead",
|
| 84 |
+
"stream",
|
| 85 |
+
"StreamClosed",
|
| 86 |
+
"StreamConsumed",
|
| 87 |
+
"StreamError",
|
| 88 |
+
"SyncByteStream",
|
| 89 |
+
"Timeout",
|
| 90 |
+
"TimeoutException",
|
| 91 |
+
"TooManyRedirects",
|
| 92 |
+
"TransportError",
|
| 93 |
+
"UnsupportedProtocol",
|
| 94 |
+
"URL",
|
| 95 |
+
"USE_CLIENT_DEFAULT",
|
| 96 |
+
"WriteError",
|
| 97 |
+
"WriteTimeout",
|
| 98 |
+
"WSGITransport",
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
__locals = locals()
|
| 103 |
+
for __name in __all__:
|
| 104 |
+
if not __name.startswith("__"):
|
| 105 |
+
setattr(__locals[__name], "__module__", "httpx") # noqa
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (2.37 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/__version__.cpython-311.pyc
ADDED
|
Binary file (301 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_api.cpython-311.pyc
ADDED
|
Binary file (9.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_auth.cpython-311.pyc
ADDED
|
Binary file (17 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_client.cpython-311.pyc
ADDED
|
Binary file (65.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_config.cpython-311.pyc
ADDED
|
Binary file (11.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_content.cpython-311.pyc
ADDED
|
Binary file (11.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_decoders.cpython-311.pyc
ADDED
|
Binary file (18.9 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_exceptions.cpython-311.pyc
ADDED
|
Binary file (14.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_main.cpython-311.pyc
ADDED
|
Binary file (23 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_models.cpython-311.pyc
ADDED
|
Binary file (66.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_multipart.cpython-311.pyc
ADDED
|
Binary file (15.6 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_status_codes.cpython-311.pyc
ADDED
|
Binary file (7.54 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_types.cpython-311.pyc
ADDED
|
Binary file (4.65 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_urlparse.cpython-311.pyc
ADDED
|
Binary file (20.3 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_urls.cpython-311.pyc
ADDED
|
Binary file (30.7 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__pycache__/_utils.cpython-311.pyc
ADDED
|
Binary file (10.4 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/__version__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__title__ = "httpx"
|
| 2 |
+
__description__ = "A next generation HTTP client, for Python 3."
|
| 3 |
+
__version__ = "0.28.1"
|
.venv/lib/python3.11/site-packages/httpx/_api.py
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
from contextlib import contextmanager
|
| 5 |
+
|
| 6 |
+
from ._client import Client
|
| 7 |
+
from ._config import DEFAULT_TIMEOUT_CONFIG
|
| 8 |
+
from ._models import Response
|
| 9 |
+
from ._types import (
|
| 10 |
+
AuthTypes,
|
| 11 |
+
CookieTypes,
|
| 12 |
+
HeaderTypes,
|
| 13 |
+
ProxyTypes,
|
| 14 |
+
QueryParamTypes,
|
| 15 |
+
RequestContent,
|
| 16 |
+
RequestData,
|
| 17 |
+
RequestFiles,
|
| 18 |
+
TimeoutTypes,
|
| 19 |
+
)
|
| 20 |
+
from ._urls import URL
|
| 21 |
+
|
| 22 |
+
if typing.TYPE_CHECKING:
|
| 23 |
+
import ssl # pragma: no cover
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
__all__ = [
|
| 27 |
+
"delete",
|
| 28 |
+
"get",
|
| 29 |
+
"head",
|
| 30 |
+
"options",
|
| 31 |
+
"patch",
|
| 32 |
+
"post",
|
| 33 |
+
"put",
|
| 34 |
+
"request",
|
| 35 |
+
"stream",
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def request(
|
| 40 |
+
method: str,
|
| 41 |
+
url: URL | str,
|
| 42 |
+
*,
|
| 43 |
+
params: QueryParamTypes | None = None,
|
| 44 |
+
content: RequestContent | None = None,
|
| 45 |
+
data: RequestData | None = None,
|
| 46 |
+
files: RequestFiles | None = None,
|
| 47 |
+
json: typing.Any | None = None,
|
| 48 |
+
headers: HeaderTypes | None = None,
|
| 49 |
+
cookies: CookieTypes | None = None,
|
| 50 |
+
auth: AuthTypes | None = None,
|
| 51 |
+
proxy: ProxyTypes | None = None,
|
| 52 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 53 |
+
follow_redirects: bool = False,
|
| 54 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 55 |
+
trust_env: bool = True,
|
| 56 |
+
) -> Response:
|
| 57 |
+
"""
|
| 58 |
+
Sends an HTTP request.
|
| 59 |
+
|
| 60 |
+
**Parameters:**
|
| 61 |
+
|
| 62 |
+
* **method** - HTTP method for the new `Request` object: `GET`, `OPTIONS`,
|
| 63 |
+
`HEAD`, `POST`, `PUT`, `PATCH`, or `DELETE`.
|
| 64 |
+
* **url** - URL for the new `Request` object.
|
| 65 |
+
* **params** - *(optional)* Query parameters to include in the URL, as a
|
| 66 |
+
string, dictionary, or sequence of two-tuples.
|
| 67 |
+
* **content** - *(optional)* Binary content to include in the body of the
|
| 68 |
+
request, as bytes or a byte iterator.
|
| 69 |
+
* **data** - *(optional)* Form data to include in the body of the request,
|
| 70 |
+
as a dictionary.
|
| 71 |
+
* **files** - *(optional)* A dictionary of upload files to include in the
|
| 72 |
+
body of the request.
|
| 73 |
+
* **json** - *(optional)* A JSON serializable object to include in the body
|
| 74 |
+
of the request.
|
| 75 |
+
* **headers** - *(optional)* Dictionary of HTTP headers to include in the
|
| 76 |
+
request.
|
| 77 |
+
* **cookies** - *(optional)* Dictionary of Cookie items to include in the
|
| 78 |
+
request.
|
| 79 |
+
* **auth** - *(optional)* An authentication class to use when sending the
|
| 80 |
+
request.
|
| 81 |
+
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
|
| 82 |
+
* **timeout** - *(optional)* The timeout configuration to use when sending
|
| 83 |
+
the request.
|
| 84 |
+
* **follow_redirects** - *(optional)* Enables or disables HTTP redirects.
|
| 85 |
+
* **verify** - *(optional)* Either `True` to use an SSL context with the
|
| 86 |
+
default CA bundle, `False` to disable verification, or an instance of
|
| 87 |
+
`ssl.SSLContext` to use a custom context.
|
| 88 |
+
* **trust_env** - *(optional)* Enables or disables usage of environment
|
| 89 |
+
variables for configuration.
|
| 90 |
+
|
| 91 |
+
**Returns:** `Response`
|
| 92 |
+
|
| 93 |
+
Usage:
|
| 94 |
+
|
| 95 |
+
```
|
| 96 |
+
>>> import httpx
|
| 97 |
+
>>> response = httpx.request('GET', 'https://httpbin.org/get')
|
| 98 |
+
>>> response
|
| 99 |
+
<Response [200 OK]>
|
| 100 |
+
```
|
| 101 |
+
"""
|
| 102 |
+
with Client(
|
| 103 |
+
cookies=cookies,
|
| 104 |
+
proxy=proxy,
|
| 105 |
+
verify=verify,
|
| 106 |
+
timeout=timeout,
|
| 107 |
+
trust_env=trust_env,
|
| 108 |
+
) as client:
|
| 109 |
+
return client.request(
|
| 110 |
+
method=method,
|
| 111 |
+
url=url,
|
| 112 |
+
content=content,
|
| 113 |
+
data=data,
|
| 114 |
+
files=files,
|
| 115 |
+
json=json,
|
| 116 |
+
params=params,
|
| 117 |
+
headers=headers,
|
| 118 |
+
auth=auth,
|
| 119 |
+
follow_redirects=follow_redirects,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
@contextmanager
|
| 124 |
+
def stream(
|
| 125 |
+
method: str,
|
| 126 |
+
url: URL | str,
|
| 127 |
+
*,
|
| 128 |
+
params: QueryParamTypes | None = None,
|
| 129 |
+
content: RequestContent | None = None,
|
| 130 |
+
data: RequestData | None = None,
|
| 131 |
+
files: RequestFiles | None = None,
|
| 132 |
+
json: typing.Any | None = None,
|
| 133 |
+
headers: HeaderTypes | None = None,
|
| 134 |
+
cookies: CookieTypes | None = None,
|
| 135 |
+
auth: AuthTypes | None = None,
|
| 136 |
+
proxy: ProxyTypes | None = None,
|
| 137 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 138 |
+
follow_redirects: bool = False,
|
| 139 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 140 |
+
trust_env: bool = True,
|
| 141 |
+
) -> typing.Iterator[Response]:
|
| 142 |
+
"""
|
| 143 |
+
Alternative to `httpx.request()` that streams the response body
|
| 144 |
+
instead of loading it into memory at once.
|
| 145 |
+
|
| 146 |
+
**Parameters**: See `httpx.request`.
|
| 147 |
+
|
| 148 |
+
See also: [Streaming Responses][0]
|
| 149 |
+
|
| 150 |
+
[0]: /quickstart#streaming-responses
|
| 151 |
+
"""
|
| 152 |
+
with Client(
|
| 153 |
+
cookies=cookies,
|
| 154 |
+
proxy=proxy,
|
| 155 |
+
verify=verify,
|
| 156 |
+
timeout=timeout,
|
| 157 |
+
trust_env=trust_env,
|
| 158 |
+
) as client:
|
| 159 |
+
with client.stream(
|
| 160 |
+
method=method,
|
| 161 |
+
url=url,
|
| 162 |
+
content=content,
|
| 163 |
+
data=data,
|
| 164 |
+
files=files,
|
| 165 |
+
json=json,
|
| 166 |
+
params=params,
|
| 167 |
+
headers=headers,
|
| 168 |
+
auth=auth,
|
| 169 |
+
follow_redirects=follow_redirects,
|
| 170 |
+
) as response:
|
| 171 |
+
yield response
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def get(
|
| 175 |
+
url: URL | str,
|
| 176 |
+
*,
|
| 177 |
+
params: QueryParamTypes | None = None,
|
| 178 |
+
headers: HeaderTypes | None = None,
|
| 179 |
+
cookies: CookieTypes | None = None,
|
| 180 |
+
auth: AuthTypes | None = None,
|
| 181 |
+
proxy: ProxyTypes | None = None,
|
| 182 |
+
follow_redirects: bool = False,
|
| 183 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 184 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 185 |
+
trust_env: bool = True,
|
| 186 |
+
) -> Response:
|
| 187 |
+
"""
|
| 188 |
+
Sends a `GET` request.
|
| 189 |
+
|
| 190 |
+
**Parameters**: See `httpx.request`.
|
| 191 |
+
|
| 192 |
+
Note that the `data`, `files`, `json` and `content` parameters are not available
|
| 193 |
+
on this function, as `GET` requests should not include a request body.
|
| 194 |
+
"""
|
| 195 |
+
return request(
|
| 196 |
+
"GET",
|
| 197 |
+
url,
|
| 198 |
+
params=params,
|
| 199 |
+
headers=headers,
|
| 200 |
+
cookies=cookies,
|
| 201 |
+
auth=auth,
|
| 202 |
+
proxy=proxy,
|
| 203 |
+
follow_redirects=follow_redirects,
|
| 204 |
+
verify=verify,
|
| 205 |
+
timeout=timeout,
|
| 206 |
+
trust_env=trust_env,
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def options(
|
| 211 |
+
url: URL | str,
|
| 212 |
+
*,
|
| 213 |
+
params: QueryParamTypes | None = None,
|
| 214 |
+
headers: HeaderTypes | None = None,
|
| 215 |
+
cookies: CookieTypes | None = None,
|
| 216 |
+
auth: AuthTypes | None = None,
|
| 217 |
+
proxy: ProxyTypes | None = None,
|
| 218 |
+
follow_redirects: bool = False,
|
| 219 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 220 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 221 |
+
trust_env: bool = True,
|
| 222 |
+
) -> Response:
|
| 223 |
+
"""
|
| 224 |
+
Sends an `OPTIONS` request.
|
| 225 |
+
|
| 226 |
+
**Parameters**: See `httpx.request`.
|
| 227 |
+
|
| 228 |
+
Note that the `data`, `files`, `json` and `content` parameters are not available
|
| 229 |
+
on this function, as `OPTIONS` requests should not include a request body.
|
| 230 |
+
"""
|
| 231 |
+
return request(
|
| 232 |
+
"OPTIONS",
|
| 233 |
+
url,
|
| 234 |
+
params=params,
|
| 235 |
+
headers=headers,
|
| 236 |
+
cookies=cookies,
|
| 237 |
+
auth=auth,
|
| 238 |
+
proxy=proxy,
|
| 239 |
+
follow_redirects=follow_redirects,
|
| 240 |
+
verify=verify,
|
| 241 |
+
timeout=timeout,
|
| 242 |
+
trust_env=trust_env,
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def head(
|
| 247 |
+
url: URL | str,
|
| 248 |
+
*,
|
| 249 |
+
params: QueryParamTypes | None = None,
|
| 250 |
+
headers: HeaderTypes | None = None,
|
| 251 |
+
cookies: CookieTypes | None = None,
|
| 252 |
+
auth: AuthTypes | None = None,
|
| 253 |
+
proxy: ProxyTypes | None = None,
|
| 254 |
+
follow_redirects: bool = False,
|
| 255 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 256 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 257 |
+
trust_env: bool = True,
|
| 258 |
+
) -> Response:
|
| 259 |
+
"""
|
| 260 |
+
Sends a `HEAD` request.
|
| 261 |
+
|
| 262 |
+
**Parameters**: See `httpx.request`.
|
| 263 |
+
|
| 264 |
+
Note that the `data`, `files`, `json` and `content` parameters are not available
|
| 265 |
+
on this function, as `HEAD` requests should not include a request body.
|
| 266 |
+
"""
|
| 267 |
+
return request(
|
| 268 |
+
"HEAD",
|
| 269 |
+
url,
|
| 270 |
+
params=params,
|
| 271 |
+
headers=headers,
|
| 272 |
+
cookies=cookies,
|
| 273 |
+
auth=auth,
|
| 274 |
+
proxy=proxy,
|
| 275 |
+
follow_redirects=follow_redirects,
|
| 276 |
+
verify=verify,
|
| 277 |
+
timeout=timeout,
|
| 278 |
+
trust_env=trust_env,
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def post(
|
| 283 |
+
url: URL | str,
|
| 284 |
+
*,
|
| 285 |
+
content: RequestContent | None = None,
|
| 286 |
+
data: RequestData | None = None,
|
| 287 |
+
files: RequestFiles | None = None,
|
| 288 |
+
json: typing.Any | None = None,
|
| 289 |
+
params: QueryParamTypes | None = None,
|
| 290 |
+
headers: HeaderTypes | None = None,
|
| 291 |
+
cookies: CookieTypes | None = None,
|
| 292 |
+
auth: AuthTypes | None = None,
|
| 293 |
+
proxy: ProxyTypes | None = None,
|
| 294 |
+
follow_redirects: bool = False,
|
| 295 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 296 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 297 |
+
trust_env: bool = True,
|
| 298 |
+
) -> Response:
|
| 299 |
+
"""
|
| 300 |
+
Sends a `POST` request.
|
| 301 |
+
|
| 302 |
+
**Parameters**: See `httpx.request`.
|
| 303 |
+
"""
|
| 304 |
+
return request(
|
| 305 |
+
"POST",
|
| 306 |
+
url,
|
| 307 |
+
content=content,
|
| 308 |
+
data=data,
|
| 309 |
+
files=files,
|
| 310 |
+
json=json,
|
| 311 |
+
params=params,
|
| 312 |
+
headers=headers,
|
| 313 |
+
cookies=cookies,
|
| 314 |
+
auth=auth,
|
| 315 |
+
proxy=proxy,
|
| 316 |
+
follow_redirects=follow_redirects,
|
| 317 |
+
verify=verify,
|
| 318 |
+
timeout=timeout,
|
| 319 |
+
trust_env=trust_env,
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def put(
|
| 324 |
+
url: URL | str,
|
| 325 |
+
*,
|
| 326 |
+
content: RequestContent | None = None,
|
| 327 |
+
data: RequestData | None = None,
|
| 328 |
+
files: RequestFiles | None = None,
|
| 329 |
+
json: typing.Any | None = None,
|
| 330 |
+
params: QueryParamTypes | None = None,
|
| 331 |
+
headers: HeaderTypes | None = None,
|
| 332 |
+
cookies: CookieTypes | None = None,
|
| 333 |
+
auth: AuthTypes | None = None,
|
| 334 |
+
proxy: ProxyTypes | None = None,
|
| 335 |
+
follow_redirects: bool = False,
|
| 336 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 337 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 338 |
+
trust_env: bool = True,
|
| 339 |
+
) -> Response:
|
| 340 |
+
"""
|
| 341 |
+
Sends a `PUT` request.
|
| 342 |
+
|
| 343 |
+
**Parameters**: See `httpx.request`.
|
| 344 |
+
"""
|
| 345 |
+
return request(
|
| 346 |
+
"PUT",
|
| 347 |
+
url,
|
| 348 |
+
content=content,
|
| 349 |
+
data=data,
|
| 350 |
+
files=files,
|
| 351 |
+
json=json,
|
| 352 |
+
params=params,
|
| 353 |
+
headers=headers,
|
| 354 |
+
cookies=cookies,
|
| 355 |
+
auth=auth,
|
| 356 |
+
proxy=proxy,
|
| 357 |
+
follow_redirects=follow_redirects,
|
| 358 |
+
verify=verify,
|
| 359 |
+
timeout=timeout,
|
| 360 |
+
trust_env=trust_env,
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
def patch(
|
| 365 |
+
url: URL | str,
|
| 366 |
+
*,
|
| 367 |
+
content: RequestContent | None = None,
|
| 368 |
+
data: RequestData | None = None,
|
| 369 |
+
files: RequestFiles | None = None,
|
| 370 |
+
json: typing.Any | None = None,
|
| 371 |
+
params: QueryParamTypes | None = None,
|
| 372 |
+
headers: HeaderTypes | None = None,
|
| 373 |
+
cookies: CookieTypes | None = None,
|
| 374 |
+
auth: AuthTypes | None = None,
|
| 375 |
+
proxy: ProxyTypes | None = None,
|
| 376 |
+
follow_redirects: bool = False,
|
| 377 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 378 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 379 |
+
trust_env: bool = True,
|
| 380 |
+
) -> Response:
|
| 381 |
+
"""
|
| 382 |
+
Sends a `PATCH` request.
|
| 383 |
+
|
| 384 |
+
**Parameters**: See `httpx.request`.
|
| 385 |
+
"""
|
| 386 |
+
return request(
|
| 387 |
+
"PATCH",
|
| 388 |
+
url,
|
| 389 |
+
content=content,
|
| 390 |
+
data=data,
|
| 391 |
+
files=files,
|
| 392 |
+
json=json,
|
| 393 |
+
params=params,
|
| 394 |
+
headers=headers,
|
| 395 |
+
cookies=cookies,
|
| 396 |
+
auth=auth,
|
| 397 |
+
proxy=proxy,
|
| 398 |
+
follow_redirects=follow_redirects,
|
| 399 |
+
verify=verify,
|
| 400 |
+
timeout=timeout,
|
| 401 |
+
trust_env=trust_env,
|
| 402 |
+
)
|
| 403 |
+
|
| 404 |
+
|
| 405 |
+
def delete(
|
| 406 |
+
url: URL | str,
|
| 407 |
+
*,
|
| 408 |
+
params: QueryParamTypes | None = None,
|
| 409 |
+
headers: HeaderTypes | None = None,
|
| 410 |
+
cookies: CookieTypes | None = None,
|
| 411 |
+
auth: AuthTypes | None = None,
|
| 412 |
+
proxy: ProxyTypes | None = None,
|
| 413 |
+
follow_redirects: bool = False,
|
| 414 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 415 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 416 |
+
trust_env: bool = True,
|
| 417 |
+
) -> Response:
|
| 418 |
+
"""
|
| 419 |
+
Sends a `DELETE` request.
|
| 420 |
+
|
| 421 |
+
**Parameters**: See `httpx.request`.
|
| 422 |
+
|
| 423 |
+
Note that the `data`, `files`, `json` and `content` parameters are not available
|
| 424 |
+
on this function, as `DELETE` requests should not include a request body.
|
| 425 |
+
"""
|
| 426 |
+
return request(
|
| 427 |
+
"DELETE",
|
| 428 |
+
url,
|
| 429 |
+
params=params,
|
| 430 |
+
headers=headers,
|
| 431 |
+
cookies=cookies,
|
| 432 |
+
auth=auth,
|
| 433 |
+
proxy=proxy,
|
| 434 |
+
follow_redirects=follow_redirects,
|
| 435 |
+
verify=verify,
|
| 436 |
+
timeout=timeout,
|
| 437 |
+
trust_env=trust_env,
|
| 438 |
+
)
|
.venv/lib/python3.11/site-packages/httpx/_auth.py
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
import time
|
| 7 |
+
import typing
|
| 8 |
+
from base64 import b64encode
|
| 9 |
+
from urllib.request import parse_http_list
|
| 10 |
+
|
| 11 |
+
from ._exceptions import ProtocolError
|
| 12 |
+
from ._models import Cookies, Request, Response
|
| 13 |
+
from ._utils import to_bytes, to_str, unquote
|
| 14 |
+
|
| 15 |
+
if typing.TYPE_CHECKING: # pragma: no cover
|
| 16 |
+
from hashlib import _Hash
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
__all__ = ["Auth", "BasicAuth", "DigestAuth", "NetRCAuth"]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class Auth:
|
| 23 |
+
"""
|
| 24 |
+
Base class for all authentication schemes.
|
| 25 |
+
|
| 26 |
+
To implement a custom authentication scheme, subclass `Auth` and override
|
| 27 |
+
the `.auth_flow()` method.
|
| 28 |
+
|
| 29 |
+
If the authentication scheme does I/O such as disk access or network calls, or uses
|
| 30 |
+
synchronization primitives such as locks, you should override `.sync_auth_flow()`
|
| 31 |
+
and/or `.async_auth_flow()` instead of `.auth_flow()` to provide specialized
|
| 32 |
+
implementations that will be used by `Client` and `AsyncClient` respectively.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
requires_request_body = False
|
| 36 |
+
requires_response_body = False
|
| 37 |
+
|
| 38 |
+
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
|
| 39 |
+
"""
|
| 40 |
+
Execute the authentication flow.
|
| 41 |
+
|
| 42 |
+
To dispatch a request, `yield` it:
|
| 43 |
+
|
| 44 |
+
```
|
| 45 |
+
yield request
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
The client will `.send()` the response back into the flow generator. You can
|
| 49 |
+
access it like so:
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
response = yield request
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
A `return` (or reaching the end of the generator) will result in the
|
| 56 |
+
client returning the last response obtained from the server.
|
| 57 |
+
|
| 58 |
+
You can dispatch as many requests as is necessary.
|
| 59 |
+
"""
|
| 60 |
+
yield request
|
| 61 |
+
|
| 62 |
+
def sync_auth_flow(
|
| 63 |
+
self, request: Request
|
| 64 |
+
) -> typing.Generator[Request, Response, None]:
|
| 65 |
+
"""
|
| 66 |
+
Execute the authentication flow synchronously.
|
| 67 |
+
|
| 68 |
+
By default, this defers to `.auth_flow()`. You should override this method
|
| 69 |
+
when the authentication scheme does I/O and/or uses concurrency primitives.
|
| 70 |
+
"""
|
| 71 |
+
if self.requires_request_body:
|
| 72 |
+
request.read()
|
| 73 |
+
|
| 74 |
+
flow = self.auth_flow(request)
|
| 75 |
+
request = next(flow)
|
| 76 |
+
|
| 77 |
+
while True:
|
| 78 |
+
response = yield request
|
| 79 |
+
if self.requires_response_body:
|
| 80 |
+
response.read()
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
request = flow.send(response)
|
| 84 |
+
except StopIteration:
|
| 85 |
+
break
|
| 86 |
+
|
| 87 |
+
async def async_auth_flow(
|
| 88 |
+
self, request: Request
|
| 89 |
+
) -> typing.AsyncGenerator[Request, Response]:
|
| 90 |
+
"""
|
| 91 |
+
Execute the authentication flow asynchronously.
|
| 92 |
+
|
| 93 |
+
By default, this defers to `.auth_flow()`. You should override this method
|
| 94 |
+
when the authentication scheme does I/O and/or uses concurrency primitives.
|
| 95 |
+
"""
|
| 96 |
+
if self.requires_request_body:
|
| 97 |
+
await request.aread()
|
| 98 |
+
|
| 99 |
+
flow = self.auth_flow(request)
|
| 100 |
+
request = next(flow)
|
| 101 |
+
|
| 102 |
+
while True:
|
| 103 |
+
response = yield request
|
| 104 |
+
if self.requires_response_body:
|
| 105 |
+
await response.aread()
|
| 106 |
+
|
| 107 |
+
try:
|
| 108 |
+
request = flow.send(response)
|
| 109 |
+
except StopIteration:
|
| 110 |
+
break
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class FunctionAuth(Auth):
|
| 114 |
+
"""
|
| 115 |
+
Allows the 'auth' argument to be passed as a simple callable function,
|
| 116 |
+
that takes the request, and returns a new, modified request.
|
| 117 |
+
"""
|
| 118 |
+
|
| 119 |
+
def __init__(self, func: typing.Callable[[Request], Request]) -> None:
|
| 120 |
+
self._func = func
|
| 121 |
+
|
| 122 |
+
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
|
| 123 |
+
yield self._func(request)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
class BasicAuth(Auth):
|
| 127 |
+
"""
|
| 128 |
+
Allows the 'auth' argument to be passed as a (username, password) pair,
|
| 129 |
+
and uses HTTP Basic authentication.
|
| 130 |
+
"""
|
| 131 |
+
|
| 132 |
+
def __init__(self, username: str | bytes, password: str | bytes) -> None:
|
| 133 |
+
self._auth_header = self._build_auth_header(username, password)
|
| 134 |
+
|
| 135 |
+
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
|
| 136 |
+
request.headers["Authorization"] = self._auth_header
|
| 137 |
+
yield request
|
| 138 |
+
|
| 139 |
+
def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str:
|
| 140 |
+
userpass = b":".join((to_bytes(username), to_bytes(password)))
|
| 141 |
+
token = b64encode(userpass).decode()
|
| 142 |
+
return f"Basic {token}"
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
class NetRCAuth(Auth):
|
| 146 |
+
"""
|
| 147 |
+
Use a 'netrc' file to lookup basic auth credentials based on the url host.
|
| 148 |
+
"""
|
| 149 |
+
|
| 150 |
+
def __init__(self, file: str | None = None) -> None:
|
| 151 |
+
# Lazily import 'netrc'.
|
| 152 |
+
# There's no need for us to load this module unless 'NetRCAuth' is being used.
|
| 153 |
+
import netrc
|
| 154 |
+
|
| 155 |
+
self._netrc_info = netrc.netrc(file)
|
| 156 |
+
|
| 157 |
+
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
|
| 158 |
+
auth_info = self._netrc_info.authenticators(request.url.host)
|
| 159 |
+
if auth_info is None or not auth_info[2]:
|
| 160 |
+
# The netrc file did not have authentication credentials for this host.
|
| 161 |
+
yield request
|
| 162 |
+
else:
|
| 163 |
+
# Build a basic auth header with credentials from the netrc file.
|
| 164 |
+
request.headers["Authorization"] = self._build_auth_header(
|
| 165 |
+
username=auth_info[0], password=auth_info[2]
|
| 166 |
+
)
|
| 167 |
+
yield request
|
| 168 |
+
|
| 169 |
+
def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str:
|
| 170 |
+
userpass = b":".join((to_bytes(username), to_bytes(password)))
|
| 171 |
+
token = b64encode(userpass).decode()
|
| 172 |
+
return f"Basic {token}"
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
class DigestAuth(Auth):
|
| 176 |
+
_ALGORITHM_TO_HASH_FUNCTION: dict[str, typing.Callable[[bytes], _Hash]] = {
|
| 177 |
+
"MD5": hashlib.md5,
|
| 178 |
+
"MD5-SESS": hashlib.md5,
|
| 179 |
+
"SHA": hashlib.sha1,
|
| 180 |
+
"SHA-SESS": hashlib.sha1,
|
| 181 |
+
"SHA-256": hashlib.sha256,
|
| 182 |
+
"SHA-256-SESS": hashlib.sha256,
|
| 183 |
+
"SHA-512": hashlib.sha512,
|
| 184 |
+
"SHA-512-SESS": hashlib.sha512,
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
def __init__(self, username: str | bytes, password: str | bytes) -> None:
|
| 188 |
+
self._username = to_bytes(username)
|
| 189 |
+
self._password = to_bytes(password)
|
| 190 |
+
self._last_challenge: _DigestAuthChallenge | None = None
|
| 191 |
+
self._nonce_count = 1
|
| 192 |
+
|
| 193 |
+
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
|
| 194 |
+
if self._last_challenge:
|
| 195 |
+
request.headers["Authorization"] = self._build_auth_header(
|
| 196 |
+
request, self._last_challenge
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
response = yield request
|
| 200 |
+
|
| 201 |
+
if response.status_code != 401 or "www-authenticate" not in response.headers:
|
| 202 |
+
# If the response is not a 401 then we don't
|
| 203 |
+
# need to build an authenticated request.
|
| 204 |
+
return
|
| 205 |
+
|
| 206 |
+
for auth_header in response.headers.get_list("www-authenticate"):
|
| 207 |
+
if auth_header.lower().startswith("digest "):
|
| 208 |
+
break
|
| 209 |
+
else:
|
| 210 |
+
# If the response does not include a 'WWW-Authenticate: Digest ...'
|
| 211 |
+
# header, then we don't need to build an authenticated request.
|
| 212 |
+
return
|
| 213 |
+
|
| 214 |
+
self._last_challenge = self._parse_challenge(request, response, auth_header)
|
| 215 |
+
self._nonce_count = 1
|
| 216 |
+
|
| 217 |
+
request.headers["Authorization"] = self._build_auth_header(
|
| 218 |
+
request, self._last_challenge
|
| 219 |
+
)
|
| 220 |
+
if response.cookies:
|
| 221 |
+
Cookies(response.cookies).set_cookie_header(request=request)
|
| 222 |
+
yield request
|
| 223 |
+
|
| 224 |
+
def _parse_challenge(
|
| 225 |
+
self, request: Request, response: Response, auth_header: str
|
| 226 |
+
) -> _DigestAuthChallenge:
|
| 227 |
+
"""
|
| 228 |
+
Returns a challenge from a Digest WWW-Authenticate header.
|
| 229 |
+
These take the form of:
|
| 230 |
+
`Digest realm="realm@host.com",qop="auth,auth-int",nonce="abc",opaque="xyz"`
|
| 231 |
+
"""
|
| 232 |
+
scheme, _, fields = auth_header.partition(" ")
|
| 233 |
+
|
| 234 |
+
# This method should only ever have been called with a Digest auth header.
|
| 235 |
+
assert scheme.lower() == "digest"
|
| 236 |
+
|
| 237 |
+
header_dict: dict[str, str] = {}
|
| 238 |
+
for field in parse_http_list(fields):
|
| 239 |
+
key, value = field.strip().split("=", 1)
|
| 240 |
+
header_dict[key] = unquote(value)
|
| 241 |
+
|
| 242 |
+
try:
|
| 243 |
+
realm = header_dict["realm"].encode()
|
| 244 |
+
nonce = header_dict["nonce"].encode()
|
| 245 |
+
algorithm = header_dict.get("algorithm", "MD5")
|
| 246 |
+
opaque = header_dict["opaque"].encode() if "opaque" in header_dict else None
|
| 247 |
+
qop = header_dict["qop"].encode() if "qop" in header_dict else None
|
| 248 |
+
return _DigestAuthChallenge(
|
| 249 |
+
realm=realm, nonce=nonce, algorithm=algorithm, opaque=opaque, qop=qop
|
| 250 |
+
)
|
| 251 |
+
except KeyError as exc:
|
| 252 |
+
message = "Malformed Digest WWW-Authenticate header"
|
| 253 |
+
raise ProtocolError(message, request=request) from exc
|
| 254 |
+
|
| 255 |
+
def _build_auth_header(
|
| 256 |
+
self, request: Request, challenge: _DigestAuthChallenge
|
| 257 |
+
) -> str:
|
| 258 |
+
hash_func = self._ALGORITHM_TO_HASH_FUNCTION[challenge.algorithm.upper()]
|
| 259 |
+
|
| 260 |
+
def digest(data: bytes) -> bytes:
|
| 261 |
+
return hash_func(data).hexdigest().encode()
|
| 262 |
+
|
| 263 |
+
A1 = b":".join((self._username, challenge.realm, self._password))
|
| 264 |
+
|
| 265 |
+
path = request.url.raw_path
|
| 266 |
+
A2 = b":".join((request.method.encode(), path))
|
| 267 |
+
# TODO: implement auth-int
|
| 268 |
+
HA2 = digest(A2)
|
| 269 |
+
|
| 270 |
+
nc_value = b"%08x" % self._nonce_count
|
| 271 |
+
cnonce = self._get_client_nonce(self._nonce_count, challenge.nonce)
|
| 272 |
+
self._nonce_count += 1
|
| 273 |
+
|
| 274 |
+
HA1 = digest(A1)
|
| 275 |
+
if challenge.algorithm.lower().endswith("-sess"):
|
| 276 |
+
HA1 = digest(b":".join((HA1, challenge.nonce, cnonce)))
|
| 277 |
+
|
| 278 |
+
qop = self._resolve_qop(challenge.qop, request=request)
|
| 279 |
+
if qop is None:
|
| 280 |
+
# Following RFC 2069
|
| 281 |
+
digest_data = [HA1, challenge.nonce, HA2]
|
| 282 |
+
else:
|
| 283 |
+
# Following RFC 2617/7616
|
| 284 |
+
digest_data = [HA1, challenge.nonce, nc_value, cnonce, qop, HA2]
|
| 285 |
+
|
| 286 |
+
format_args = {
|
| 287 |
+
"username": self._username,
|
| 288 |
+
"realm": challenge.realm,
|
| 289 |
+
"nonce": challenge.nonce,
|
| 290 |
+
"uri": path,
|
| 291 |
+
"response": digest(b":".join(digest_data)),
|
| 292 |
+
"algorithm": challenge.algorithm.encode(),
|
| 293 |
+
}
|
| 294 |
+
if challenge.opaque:
|
| 295 |
+
format_args["opaque"] = challenge.opaque
|
| 296 |
+
if qop:
|
| 297 |
+
format_args["qop"] = b"auth"
|
| 298 |
+
format_args["nc"] = nc_value
|
| 299 |
+
format_args["cnonce"] = cnonce
|
| 300 |
+
|
| 301 |
+
return "Digest " + self._get_header_value(format_args)
|
| 302 |
+
|
| 303 |
+
def _get_client_nonce(self, nonce_count: int, nonce: bytes) -> bytes:
|
| 304 |
+
s = str(nonce_count).encode()
|
| 305 |
+
s += nonce
|
| 306 |
+
s += time.ctime().encode()
|
| 307 |
+
s += os.urandom(8)
|
| 308 |
+
|
| 309 |
+
return hashlib.sha1(s).hexdigest()[:16].encode()
|
| 310 |
+
|
| 311 |
+
def _get_header_value(self, header_fields: dict[str, bytes]) -> str:
|
| 312 |
+
NON_QUOTED_FIELDS = ("algorithm", "qop", "nc")
|
| 313 |
+
QUOTED_TEMPLATE = '{}="{}"'
|
| 314 |
+
NON_QUOTED_TEMPLATE = "{}={}"
|
| 315 |
+
|
| 316 |
+
header_value = ""
|
| 317 |
+
for i, (field, value) in enumerate(header_fields.items()):
|
| 318 |
+
if i > 0:
|
| 319 |
+
header_value += ", "
|
| 320 |
+
template = (
|
| 321 |
+
QUOTED_TEMPLATE
|
| 322 |
+
if field not in NON_QUOTED_FIELDS
|
| 323 |
+
else NON_QUOTED_TEMPLATE
|
| 324 |
+
)
|
| 325 |
+
header_value += template.format(field, to_str(value))
|
| 326 |
+
|
| 327 |
+
return header_value
|
| 328 |
+
|
| 329 |
+
def _resolve_qop(self, qop: bytes | None, request: Request) -> bytes | None:
|
| 330 |
+
if qop is None:
|
| 331 |
+
return None
|
| 332 |
+
qops = re.split(b", ?", qop)
|
| 333 |
+
if b"auth" in qops:
|
| 334 |
+
return b"auth"
|
| 335 |
+
|
| 336 |
+
if qops == [b"auth-int"]:
|
| 337 |
+
raise NotImplementedError("Digest auth-int support is not yet implemented")
|
| 338 |
+
|
| 339 |
+
message = f'Unexpected qop value "{qop!r}" in digest auth'
|
| 340 |
+
raise ProtocolError(message, request=request)
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
class _DigestAuthChallenge(typing.NamedTuple):
|
| 344 |
+
realm: bytes
|
| 345 |
+
nonce: bytes
|
| 346 |
+
algorithm: str
|
| 347 |
+
opaque: bytes | None
|
| 348 |
+
qop: bytes | None
|
.venv/lib/python3.11/site-packages/httpx/_client.py
ADDED
|
@@ -0,0 +1,2019 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import datetime
|
| 4 |
+
import enum
|
| 5 |
+
import logging
|
| 6 |
+
import time
|
| 7 |
+
import typing
|
| 8 |
+
import warnings
|
| 9 |
+
from contextlib import asynccontextmanager, contextmanager
|
| 10 |
+
from types import TracebackType
|
| 11 |
+
|
| 12 |
+
from .__version__ import __version__
|
| 13 |
+
from ._auth import Auth, BasicAuth, FunctionAuth
|
| 14 |
+
from ._config import (
|
| 15 |
+
DEFAULT_LIMITS,
|
| 16 |
+
DEFAULT_MAX_REDIRECTS,
|
| 17 |
+
DEFAULT_TIMEOUT_CONFIG,
|
| 18 |
+
Limits,
|
| 19 |
+
Proxy,
|
| 20 |
+
Timeout,
|
| 21 |
+
)
|
| 22 |
+
from ._decoders import SUPPORTED_DECODERS
|
| 23 |
+
from ._exceptions import (
|
| 24 |
+
InvalidURL,
|
| 25 |
+
RemoteProtocolError,
|
| 26 |
+
TooManyRedirects,
|
| 27 |
+
request_context,
|
| 28 |
+
)
|
| 29 |
+
from ._models import Cookies, Headers, Request, Response
|
| 30 |
+
from ._status_codes import codes
|
| 31 |
+
from ._transports.base import AsyncBaseTransport, BaseTransport
|
| 32 |
+
from ._transports.default import AsyncHTTPTransport, HTTPTransport
|
| 33 |
+
from ._types import (
|
| 34 |
+
AsyncByteStream,
|
| 35 |
+
AuthTypes,
|
| 36 |
+
CertTypes,
|
| 37 |
+
CookieTypes,
|
| 38 |
+
HeaderTypes,
|
| 39 |
+
ProxyTypes,
|
| 40 |
+
QueryParamTypes,
|
| 41 |
+
RequestContent,
|
| 42 |
+
RequestData,
|
| 43 |
+
RequestExtensions,
|
| 44 |
+
RequestFiles,
|
| 45 |
+
SyncByteStream,
|
| 46 |
+
TimeoutTypes,
|
| 47 |
+
)
|
| 48 |
+
from ._urls import URL, QueryParams
|
| 49 |
+
from ._utils import URLPattern, get_environment_proxies
|
| 50 |
+
|
| 51 |
+
if typing.TYPE_CHECKING:
|
| 52 |
+
import ssl # pragma: no cover
|
| 53 |
+
|
| 54 |
+
__all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"]
|
| 55 |
+
|
| 56 |
+
# The type annotation for @classmethod and context managers here follows PEP 484
|
| 57 |
+
# https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods
|
| 58 |
+
T = typing.TypeVar("T", bound="Client")
|
| 59 |
+
U = typing.TypeVar("U", bound="AsyncClient")
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _is_https_redirect(url: URL, location: URL) -> bool:
|
| 63 |
+
"""
|
| 64 |
+
Return 'True' if 'location' is a HTTPS upgrade of 'url'
|
| 65 |
+
"""
|
| 66 |
+
if url.host != location.host:
|
| 67 |
+
return False
|
| 68 |
+
|
| 69 |
+
return (
|
| 70 |
+
url.scheme == "http"
|
| 71 |
+
and _port_or_default(url) == 80
|
| 72 |
+
and location.scheme == "https"
|
| 73 |
+
and _port_or_default(location) == 443
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _port_or_default(url: URL) -> int | None:
|
| 78 |
+
if url.port is not None:
|
| 79 |
+
return url.port
|
| 80 |
+
return {"http": 80, "https": 443}.get(url.scheme)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _same_origin(url: URL, other: URL) -> bool:
|
| 84 |
+
"""
|
| 85 |
+
Return 'True' if the given URLs share the same origin.
|
| 86 |
+
"""
|
| 87 |
+
return (
|
| 88 |
+
url.scheme == other.scheme
|
| 89 |
+
and url.host == other.host
|
| 90 |
+
and _port_or_default(url) == _port_or_default(other)
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class UseClientDefault:
|
| 95 |
+
"""
|
| 96 |
+
For some parameters such as `auth=...` and `timeout=...` we need to be able
|
| 97 |
+
to indicate the default "unset" state, in a way that is distinctly different
|
| 98 |
+
to using `None`.
|
| 99 |
+
|
| 100 |
+
The default "unset" state indicates that whatever default is set on the
|
| 101 |
+
client should be used. This is different to setting `None`, which
|
| 102 |
+
explicitly disables the parameter, possibly overriding a client default.
|
| 103 |
+
|
| 104 |
+
For example we use `timeout=USE_CLIENT_DEFAULT` in the `request()` signature.
|
| 105 |
+
Omitting the `timeout` parameter will send a request using whatever default
|
| 106 |
+
timeout has been configured on the client. Including `timeout=None` will
|
| 107 |
+
ensure no timeout is used.
|
| 108 |
+
|
| 109 |
+
Note that user code shouldn't need to use the `USE_CLIENT_DEFAULT` constant,
|
| 110 |
+
but it is used internally when a parameter is not included.
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
USE_CLIENT_DEFAULT = UseClientDefault()
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
logger = logging.getLogger("httpx")
|
| 118 |
+
|
| 119 |
+
USER_AGENT = f"python-httpx/{__version__}"
|
| 120 |
+
ACCEPT_ENCODING = ", ".join(
|
| 121 |
+
[key for key in SUPPORTED_DECODERS.keys() if key != "identity"]
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class ClientState(enum.Enum):
|
| 126 |
+
# UNOPENED:
|
| 127 |
+
# The client has been instantiated, but has not been used to send a request,
|
| 128 |
+
# or been opened by entering the context of a `with` block.
|
| 129 |
+
UNOPENED = 1
|
| 130 |
+
# OPENED:
|
| 131 |
+
# The client has either sent a request, or is within a `with` block.
|
| 132 |
+
OPENED = 2
|
| 133 |
+
# CLOSED:
|
| 134 |
+
# The client has either exited the `with` block, or `close()` has
|
| 135 |
+
# been called explicitly.
|
| 136 |
+
CLOSED = 3
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class BoundSyncStream(SyncByteStream):
|
| 140 |
+
"""
|
| 141 |
+
A byte stream that is bound to a given response instance, and that
|
| 142 |
+
ensures the `response.elapsed` is set once the response is closed.
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
def __init__(
|
| 146 |
+
self, stream: SyncByteStream, response: Response, start: float
|
| 147 |
+
) -> None:
|
| 148 |
+
self._stream = stream
|
| 149 |
+
self._response = response
|
| 150 |
+
self._start = start
|
| 151 |
+
|
| 152 |
+
def __iter__(self) -> typing.Iterator[bytes]:
|
| 153 |
+
for chunk in self._stream:
|
| 154 |
+
yield chunk
|
| 155 |
+
|
| 156 |
+
def close(self) -> None:
|
| 157 |
+
elapsed = time.perf_counter() - self._start
|
| 158 |
+
self._response.elapsed = datetime.timedelta(seconds=elapsed)
|
| 159 |
+
self._stream.close()
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
class BoundAsyncStream(AsyncByteStream):
|
| 163 |
+
"""
|
| 164 |
+
An async byte stream that is bound to a given response instance, and that
|
| 165 |
+
ensures the `response.elapsed` is set once the response is closed.
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
def __init__(
|
| 169 |
+
self, stream: AsyncByteStream, response: Response, start: float
|
| 170 |
+
) -> None:
|
| 171 |
+
self._stream = stream
|
| 172 |
+
self._response = response
|
| 173 |
+
self._start = start
|
| 174 |
+
|
| 175 |
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
| 176 |
+
async for chunk in self._stream:
|
| 177 |
+
yield chunk
|
| 178 |
+
|
| 179 |
+
async def aclose(self) -> None:
|
| 180 |
+
elapsed = time.perf_counter() - self._start
|
| 181 |
+
self._response.elapsed = datetime.timedelta(seconds=elapsed)
|
| 182 |
+
await self._stream.aclose()
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
EventHook = typing.Callable[..., typing.Any]
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
class BaseClient:
|
| 189 |
+
def __init__(
|
| 190 |
+
self,
|
| 191 |
+
*,
|
| 192 |
+
auth: AuthTypes | None = None,
|
| 193 |
+
params: QueryParamTypes | None = None,
|
| 194 |
+
headers: HeaderTypes | None = None,
|
| 195 |
+
cookies: CookieTypes | None = None,
|
| 196 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 197 |
+
follow_redirects: bool = False,
|
| 198 |
+
max_redirects: int = DEFAULT_MAX_REDIRECTS,
|
| 199 |
+
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
|
| 200 |
+
base_url: URL | str = "",
|
| 201 |
+
trust_env: bool = True,
|
| 202 |
+
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
|
| 203 |
+
) -> None:
|
| 204 |
+
event_hooks = {} if event_hooks is None else event_hooks
|
| 205 |
+
|
| 206 |
+
self._base_url = self._enforce_trailing_slash(URL(base_url))
|
| 207 |
+
|
| 208 |
+
self._auth = self._build_auth(auth)
|
| 209 |
+
self._params = QueryParams(params)
|
| 210 |
+
self.headers = Headers(headers)
|
| 211 |
+
self._cookies = Cookies(cookies)
|
| 212 |
+
self._timeout = Timeout(timeout)
|
| 213 |
+
self.follow_redirects = follow_redirects
|
| 214 |
+
self.max_redirects = max_redirects
|
| 215 |
+
self._event_hooks = {
|
| 216 |
+
"request": list(event_hooks.get("request", [])),
|
| 217 |
+
"response": list(event_hooks.get("response", [])),
|
| 218 |
+
}
|
| 219 |
+
self._trust_env = trust_env
|
| 220 |
+
self._default_encoding = default_encoding
|
| 221 |
+
self._state = ClientState.UNOPENED
|
| 222 |
+
|
| 223 |
+
@property
|
| 224 |
+
def is_closed(self) -> bool:
|
| 225 |
+
"""
|
| 226 |
+
Check if the client being closed
|
| 227 |
+
"""
|
| 228 |
+
return self._state == ClientState.CLOSED
|
| 229 |
+
|
| 230 |
+
@property
|
| 231 |
+
def trust_env(self) -> bool:
|
| 232 |
+
return self._trust_env
|
| 233 |
+
|
| 234 |
+
def _enforce_trailing_slash(self, url: URL) -> URL:
|
| 235 |
+
if url.raw_path.endswith(b"/"):
|
| 236 |
+
return url
|
| 237 |
+
return url.copy_with(raw_path=url.raw_path + b"/")
|
| 238 |
+
|
| 239 |
+
def _get_proxy_map(
|
| 240 |
+
self, proxy: ProxyTypes | None, allow_env_proxies: bool
|
| 241 |
+
) -> dict[str, Proxy | None]:
|
| 242 |
+
if proxy is None:
|
| 243 |
+
if allow_env_proxies:
|
| 244 |
+
return {
|
| 245 |
+
key: None if url is None else Proxy(url=url)
|
| 246 |
+
for key, url in get_environment_proxies().items()
|
| 247 |
+
}
|
| 248 |
+
return {}
|
| 249 |
+
else:
|
| 250 |
+
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
|
| 251 |
+
return {"all://": proxy}
|
| 252 |
+
|
| 253 |
+
@property
|
| 254 |
+
def timeout(self) -> Timeout:
|
| 255 |
+
return self._timeout
|
| 256 |
+
|
| 257 |
+
@timeout.setter
|
| 258 |
+
def timeout(self, timeout: TimeoutTypes) -> None:
|
| 259 |
+
self._timeout = Timeout(timeout)
|
| 260 |
+
|
| 261 |
+
@property
|
| 262 |
+
def event_hooks(self) -> dict[str, list[EventHook]]:
|
| 263 |
+
return self._event_hooks
|
| 264 |
+
|
| 265 |
+
@event_hooks.setter
|
| 266 |
+
def event_hooks(self, event_hooks: dict[str, list[EventHook]]) -> None:
|
| 267 |
+
self._event_hooks = {
|
| 268 |
+
"request": list(event_hooks.get("request", [])),
|
| 269 |
+
"response": list(event_hooks.get("response", [])),
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
@property
|
| 273 |
+
def auth(self) -> Auth | None:
|
| 274 |
+
"""
|
| 275 |
+
Authentication class used when none is passed at the request-level.
|
| 276 |
+
|
| 277 |
+
See also [Authentication][0].
|
| 278 |
+
|
| 279 |
+
[0]: /quickstart/#authentication
|
| 280 |
+
"""
|
| 281 |
+
return self._auth
|
| 282 |
+
|
| 283 |
+
@auth.setter
|
| 284 |
+
def auth(self, auth: AuthTypes) -> None:
|
| 285 |
+
self._auth = self._build_auth(auth)
|
| 286 |
+
|
| 287 |
+
@property
|
| 288 |
+
def base_url(self) -> URL:
|
| 289 |
+
"""
|
| 290 |
+
Base URL to use when sending requests with relative URLs.
|
| 291 |
+
"""
|
| 292 |
+
return self._base_url
|
| 293 |
+
|
| 294 |
+
@base_url.setter
|
| 295 |
+
def base_url(self, url: URL | str) -> None:
|
| 296 |
+
self._base_url = self._enforce_trailing_slash(URL(url))
|
| 297 |
+
|
| 298 |
+
@property
|
| 299 |
+
def headers(self) -> Headers:
|
| 300 |
+
"""
|
| 301 |
+
HTTP headers to include when sending requests.
|
| 302 |
+
"""
|
| 303 |
+
return self._headers
|
| 304 |
+
|
| 305 |
+
@headers.setter
|
| 306 |
+
def headers(self, headers: HeaderTypes) -> None:
|
| 307 |
+
client_headers = Headers(
|
| 308 |
+
{
|
| 309 |
+
b"Accept": b"*/*",
|
| 310 |
+
b"Accept-Encoding": ACCEPT_ENCODING.encode("ascii"),
|
| 311 |
+
b"Connection": b"keep-alive",
|
| 312 |
+
b"User-Agent": USER_AGENT.encode("ascii"),
|
| 313 |
+
}
|
| 314 |
+
)
|
| 315 |
+
client_headers.update(headers)
|
| 316 |
+
self._headers = client_headers
|
| 317 |
+
|
| 318 |
+
@property
|
| 319 |
+
def cookies(self) -> Cookies:
|
| 320 |
+
"""
|
| 321 |
+
Cookie values to include when sending requests.
|
| 322 |
+
"""
|
| 323 |
+
return self._cookies
|
| 324 |
+
|
| 325 |
+
@cookies.setter
|
| 326 |
+
def cookies(self, cookies: CookieTypes) -> None:
|
| 327 |
+
self._cookies = Cookies(cookies)
|
| 328 |
+
|
| 329 |
+
@property
|
| 330 |
+
def params(self) -> QueryParams:
|
| 331 |
+
"""
|
| 332 |
+
Query parameters to include in the URL when sending requests.
|
| 333 |
+
"""
|
| 334 |
+
return self._params
|
| 335 |
+
|
| 336 |
+
@params.setter
|
| 337 |
+
def params(self, params: QueryParamTypes) -> None:
|
| 338 |
+
self._params = QueryParams(params)
|
| 339 |
+
|
| 340 |
+
def build_request(
|
| 341 |
+
self,
|
| 342 |
+
method: str,
|
| 343 |
+
url: URL | str,
|
| 344 |
+
*,
|
| 345 |
+
content: RequestContent | None = None,
|
| 346 |
+
data: RequestData | None = None,
|
| 347 |
+
files: RequestFiles | None = None,
|
| 348 |
+
json: typing.Any | None = None,
|
| 349 |
+
params: QueryParamTypes | None = None,
|
| 350 |
+
headers: HeaderTypes | None = None,
|
| 351 |
+
cookies: CookieTypes | None = None,
|
| 352 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 353 |
+
extensions: RequestExtensions | None = None,
|
| 354 |
+
) -> Request:
|
| 355 |
+
"""
|
| 356 |
+
Build and return a request instance.
|
| 357 |
+
|
| 358 |
+
* The `params`, `headers` and `cookies` arguments
|
| 359 |
+
are merged with any values set on the client.
|
| 360 |
+
* The `url` argument is merged with any `base_url` set on the client.
|
| 361 |
+
|
| 362 |
+
See also: [Request instances][0]
|
| 363 |
+
|
| 364 |
+
[0]: /advanced/clients/#request-instances
|
| 365 |
+
"""
|
| 366 |
+
url = self._merge_url(url)
|
| 367 |
+
headers = self._merge_headers(headers)
|
| 368 |
+
cookies = self._merge_cookies(cookies)
|
| 369 |
+
params = self._merge_queryparams(params)
|
| 370 |
+
extensions = {} if extensions is None else extensions
|
| 371 |
+
if "timeout" not in extensions:
|
| 372 |
+
timeout = (
|
| 373 |
+
self.timeout
|
| 374 |
+
if isinstance(timeout, UseClientDefault)
|
| 375 |
+
else Timeout(timeout)
|
| 376 |
+
)
|
| 377 |
+
extensions = dict(**extensions, timeout=timeout.as_dict())
|
| 378 |
+
return Request(
|
| 379 |
+
method,
|
| 380 |
+
url,
|
| 381 |
+
content=content,
|
| 382 |
+
data=data,
|
| 383 |
+
files=files,
|
| 384 |
+
json=json,
|
| 385 |
+
params=params,
|
| 386 |
+
headers=headers,
|
| 387 |
+
cookies=cookies,
|
| 388 |
+
extensions=extensions,
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
def _merge_url(self, url: URL | str) -> URL:
|
| 392 |
+
"""
|
| 393 |
+
Merge a URL argument together with any 'base_url' on the client,
|
| 394 |
+
to create the URL used for the outgoing request.
|
| 395 |
+
"""
|
| 396 |
+
merge_url = URL(url)
|
| 397 |
+
if merge_url.is_relative_url:
|
| 398 |
+
# To merge URLs we always append to the base URL. To get this
|
| 399 |
+
# behaviour correct we always ensure the base URL ends in a '/'
|
| 400 |
+
# separator, and strip any leading '/' from the merge URL.
|
| 401 |
+
#
|
| 402 |
+
# So, eg...
|
| 403 |
+
#
|
| 404 |
+
# >>> client = Client(base_url="https://www.example.com/subpath")
|
| 405 |
+
# >>> client.base_url
|
| 406 |
+
# URL('https://www.example.com/subpath/')
|
| 407 |
+
# >>> client.build_request("GET", "/path").url
|
| 408 |
+
# URL('https://www.example.com/subpath/path')
|
| 409 |
+
merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/")
|
| 410 |
+
return self.base_url.copy_with(raw_path=merge_raw_path)
|
| 411 |
+
return merge_url
|
| 412 |
+
|
| 413 |
+
def _merge_cookies(self, cookies: CookieTypes | None = None) -> CookieTypes | None:
|
| 414 |
+
"""
|
| 415 |
+
Merge a cookies argument together with any cookies on the client,
|
| 416 |
+
to create the cookies used for the outgoing request.
|
| 417 |
+
"""
|
| 418 |
+
if cookies or self.cookies:
|
| 419 |
+
merged_cookies = Cookies(self.cookies)
|
| 420 |
+
merged_cookies.update(cookies)
|
| 421 |
+
return merged_cookies
|
| 422 |
+
return cookies
|
| 423 |
+
|
| 424 |
+
def _merge_headers(self, headers: HeaderTypes | None = None) -> HeaderTypes | None:
|
| 425 |
+
"""
|
| 426 |
+
Merge a headers argument together with any headers on the client,
|
| 427 |
+
to create the headers used for the outgoing request.
|
| 428 |
+
"""
|
| 429 |
+
merged_headers = Headers(self.headers)
|
| 430 |
+
merged_headers.update(headers)
|
| 431 |
+
return merged_headers
|
| 432 |
+
|
| 433 |
+
def _merge_queryparams(
|
| 434 |
+
self, params: QueryParamTypes | None = None
|
| 435 |
+
) -> QueryParamTypes | None:
|
| 436 |
+
"""
|
| 437 |
+
Merge a queryparams argument together with any queryparams on the client,
|
| 438 |
+
to create the queryparams used for the outgoing request.
|
| 439 |
+
"""
|
| 440 |
+
if params or self.params:
|
| 441 |
+
merged_queryparams = QueryParams(self.params)
|
| 442 |
+
return merged_queryparams.merge(params)
|
| 443 |
+
return params
|
| 444 |
+
|
| 445 |
+
def _build_auth(self, auth: AuthTypes | None) -> Auth | None:
|
| 446 |
+
if auth is None:
|
| 447 |
+
return None
|
| 448 |
+
elif isinstance(auth, tuple):
|
| 449 |
+
return BasicAuth(username=auth[0], password=auth[1])
|
| 450 |
+
elif isinstance(auth, Auth):
|
| 451 |
+
return auth
|
| 452 |
+
elif callable(auth):
|
| 453 |
+
return FunctionAuth(func=auth)
|
| 454 |
+
else:
|
| 455 |
+
raise TypeError(f'Invalid "auth" argument: {auth!r}')
|
| 456 |
+
|
| 457 |
+
def _build_request_auth(
|
| 458 |
+
self,
|
| 459 |
+
request: Request,
|
| 460 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 461 |
+
) -> Auth:
|
| 462 |
+
auth = (
|
| 463 |
+
self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth)
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
+
if auth is not None:
|
| 467 |
+
return auth
|
| 468 |
+
|
| 469 |
+
username, password = request.url.username, request.url.password
|
| 470 |
+
if username or password:
|
| 471 |
+
return BasicAuth(username=username, password=password)
|
| 472 |
+
|
| 473 |
+
return Auth()
|
| 474 |
+
|
| 475 |
+
def _build_redirect_request(self, request: Request, response: Response) -> Request:
|
| 476 |
+
"""
|
| 477 |
+
Given a request and a redirect response, return a new request that
|
| 478 |
+
should be used to effect the redirect.
|
| 479 |
+
"""
|
| 480 |
+
method = self._redirect_method(request, response)
|
| 481 |
+
url = self._redirect_url(request, response)
|
| 482 |
+
headers = self._redirect_headers(request, url, method)
|
| 483 |
+
stream = self._redirect_stream(request, method)
|
| 484 |
+
cookies = Cookies(self.cookies)
|
| 485 |
+
return Request(
|
| 486 |
+
method=method,
|
| 487 |
+
url=url,
|
| 488 |
+
headers=headers,
|
| 489 |
+
cookies=cookies,
|
| 490 |
+
stream=stream,
|
| 491 |
+
extensions=request.extensions,
|
| 492 |
+
)
|
| 493 |
+
|
| 494 |
+
def _redirect_method(self, request: Request, response: Response) -> str:
|
| 495 |
+
"""
|
| 496 |
+
When being redirected we may want to change the method of the request
|
| 497 |
+
based on certain specs or browser behavior.
|
| 498 |
+
"""
|
| 499 |
+
method = request.method
|
| 500 |
+
|
| 501 |
+
# https://tools.ietf.org/html/rfc7231#section-6.4.4
|
| 502 |
+
if response.status_code == codes.SEE_OTHER and method != "HEAD":
|
| 503 |
+
method = "GET"
|
| 504 |
+
|
| 505 |
+
# Do what the browsers do, despite standards...
|
| 506 |
+
# Turn 302s into GETs.
|
| 507 |
+
if response.status_code == codes.FOUND and method != "HEAD":
|
| 508 |
+
method = "GET"
|
| 509 |
+
|
| 510 |
+
# If a POST is responded to with a 301, turn it into a GET.
|
| 511 |
+
# This bizarre behaviour is explained in 'requests' issue 1704.
|
| 512 |
+
if response.status_code == codes.MOVED_PERMANENTLY and method == "POST":
|
| 513 |
+
method = "GET"
|
| 514 |
+
|
| 515 |
+
return method
|
| 516 |
+
|
| 517 |
+
def _redirect_url(self, request: Request, response: Response) -> URL:
|
| 518 |
+
"""
|
| 519 |
+
Return the URL for the redirect to follow.
|
| 520 |
+
"""
|
| 521 |
+
location = response.headers["Location"]
|
| 522 |
+
|
| 523 |
+
try:
|
| 524 |
+
url = URL(location)
|
| 525 |
+
except InvalidURL as exc:
|
| 526 |
+
raise RemoteProtocolError(
|
| 527 |
+
f"Invalid URL in location header: {exc}.", request=request
|
| 528 |
+
) from None
|
| 529 |
+
|
| 530 |
+
# Handle malformed 'Location' headers that are "absolute" form, have no host.
|
| 531 |
+
# See: https://github.com/encode/httpx/issues/771
|
| 532 |
+
if url.scheme and not url.host:
|
| 533 |
+
url = url.copy_with(host=request.url.host)
|
| 534 |
+
|
| 535 |
+
# Facilitate relative 'Location' headers, as allowed by RFC 7231.
|
| 536 |
+
# (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
|
| 537 |
+
if url.is_relative_url:
|
| 538 |
+
url = request.url.join(url)
|
| 539 |
+
|
| 540 |
+
# Attach previous fragment if needed (RFC 7231 7.1.2)
|
| 541 |
+
if request.url.fragment and not url.fragment:
|
| 542 |
+
url = url.copy_with(fragment=request.url.fragment)
|
| 543 |
+
|
| 544 |
+
return url
|
| 545 |
+
|
| 546 |
+
def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers:
|
| 547 |
+
"""
|
| 548 |
+
Return the headers that should be used for the redirect request.
|
| 549 |
+
"""
|
| 550 |
+
headers = Headers(request.headers)
|
| 551 |
+
|
| 552 |
+
if not _same_origin(url, request.url):
|
| 553 |
+
if not _is_https_redirect(request.url, url):
|
| 554 |
+
# Strip Authorization headers when responses are redirected
|
| 555 |
+
# away from the origin. (Except for direct HTTP to HTTPS redirects.)
|
| 556 |
+
headers.pop("Authorization", None)
|
| 557 |
+
|
| 558 |
+
# Update the Host header.
|
| 559 |
+
headers["Host"] = url.netloc.decode("ascii")
|
| 560 |
+
|
| 561 |
+
if method != request.method and method == "GET":
|
| 562 |
+
# If we've switch to a 'GET' request, then strip any headers which
|
| 563 |
+
# are only relevant to the request body.
|
| 564 |
+
headers.pop("Content-Length", None)
|
| 565 |
+
headers.pop("Transfer-Encoding", None)
|
| 566 |
+
|
| 567 |
+
# We should use the client cookie store to determine any cookie header,
|
| 568 |
+
# rather than whatever was on the original outgoing request.
|
| 569 |
+
headers.pop("Cookie", None)
|
| 570 |
+
|
| 571 |
+
return headers
|
| 572 |
+
|
| 573 |
+
def _redirect_stream(
|
| 574 |
+
self, request: Request, method: str
|
| 575 |
+
) -> SyncByteStream | AsyncByteStream | None:
|
| 576 |
+
"""
|
| 577 |
+
Return the body that should be used for the redirect request.
|
| 578 |
+
"""
|
| 579 |
+
if method != request.method and method == "GET":
|
| 580 |
+
return None
|
| 581 |
+
|
| 582 |
+
return request.stream
|
| 583 |
+
|
| 584 |
+
def _set_timeout(self, request: Request) -> None:
|
| 585 |
+
if "timeout" not in request.extensions:
|
| 586 |
+
timeout = (
|
| 587 |
+
self.timeout
|
| 588 |
+
if isinstance(self.timeout, UseClientDefault)
|
| 589 |
+
else Timeout(self.timeout)
|
| 590 |
+
)
|
| 591 |
+
request.extensions = dict(**request.extensions, timeout=timeout.as_dict())
|
| 592 |
+
|
| 593 |
+
|
| 594 |
+
class Client(BaseClient):
|
| 595 |
+
"""
|
| 596 |
+
An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc.
|
| 597 |
+
|
| 598 |
+
It can be shared between threads.
|
| 599 |
+
|
| 600 |
+
Usage:
|
| 601 |
+
|
| 602 |
+
```python
|
| 603 |
+
>>> client = httpx.Client()
|
| 604 |
+
>>> response = client.get('https://example.org')
|
| 605 |
+
```
|
| 606 |
+
|
| 607 |
+
**Parameters:**
|
| 608 |
+
|
| 609 |
+
* **auth** - *(optional)* An authentication class to use when sending
|
| 610 |
+
requests.
|
| 611 |
+
* **params** - *(optional)* Query parameters to include in request URLs, as
|
| 612 |
+
a string, dictionary, or sequence of two-tuples.
|
| 613 |
+
* **headers** - *(optional)* Dictionary of HTTP headers to include when
|
| 614 |
+
sending requests.
|
| 615 |
+
* **cookies** - *(optional)* Dictionary of Cookie items to include when
|
| 616 |
+
sending requests.
|
| 617 |
+
* **verify** - *(optional)* Either `True` to use an SSL context with the
|
| 618 |
+
default CA bundle, `False` to disable verification, or an instance of
|
| 619 |
+
`ssl.SSLContext` to use a custom context.
|
| 620 |
+
* **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
|
| 621 |
+
enabled. Defaults to `False`.
|
| 622 |
+
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
|
| 623 |
+
* **timeout** - *(optional)* The timeout configuration to use when sending
|
| 624 |
+
requests.
|
| 625 |
+
* **limits** - *(optional)* The limits configuration to use.
|
| 626 |
+
* **max_redirects** - *(optional)* The maximum number of redirect responses
|
| 627 |
+
that should be followed.
|
| 628 |
+
* **base_url** - *(optional)* A URL to use as the base when building
|
| 629 |
+
request URLs.
|
| 630 |
+
* **transport** - *(optional)* A transport class to use for sending requests
|
| 631 |
+
over the network.
|
| 632 |
+
* **trust_env** - *(optional)* Enables or disables usage of environment
|
| 633 |
+
variables for configuration.
|
| 634 |
+
* **default_encoding** - *(optional)* The default encoding to use for decoding
|
| 635 |
+
response text, if no charset information is included in a response Content-Type
|
| 636 |
+
header. Set to a callable for automatic character set detection. Default: "utf-8".
|
| 637 |
+
"""
|
| 638 |
+
|
| 639 |
+
def __init__(
|
| 640 |
+
self,
|
| 641 |
+
*,
|
| 642 |
+
auth: AuthTypes | None = None,
|
| 643 |
+
params: QueryParamTypes | None = None,
|
| 644 |
+
headers: HeaderTypes | None = None,
|
| 645 |
+
cookies: CookieTypes | None = None,
|
| 646 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 647 |
+
cert: CertTypes | None = None,
|
| 648 |
+
trust_env: bool = True,
|
| 649 |
+
http1: bool = True,
|
| 650 |
+
http2: bool = False,
|
| 651 |
+
proxy: ProxyTypes | None = None,
|
| 652 |
+
mounts: None | (typing.Mapping[str, BaseTransport | None]) = None,
|
| 653 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 654 |
+
follow_redirects: bool = False,
|
| 655 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 656 |
+
max_redirects: int = DEFAULT_MAX_REDIRECTS,
|
| 657 |
+
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
|
| 658 |
+
base_url: URL | str = "",
|
| 659 |
+
transport: BaseTransport | None = None,
|
| 660 |
+
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
|
| 661 |
+
) -> None:
|
| 662 |
+
super().__init__(
|
| 663 |
+
auth=auth,
|
| 664 |
+
params=params,
|
| 665 |
+
headers=headers,
|
| 666 |
+
cookies=cookies,
|
| 667 |
+
timeout=timeout,
|
| 668 |
+
follow_redirects=follow_redirects,
|
| 669 |
+
max_redirects=max_redirects,
|
| 670 |
+
event_hooks=event_hooks,
|
| 671 |
+
base_url=base_url,
|
| 672 |
+
trust_env=trust_env,
|
| 673 |
+
default_encoding=default_encoding,
|
| 674 |
+
)
|
| 675 |
+
|
| 676 |
+
if http2:
|
| 677 |
+
try:
|
| 678 |
+
import h2 # noqa
|
| 679 |
+
except ImportError: # pragma: no cover
|
| 680 |
+
raise ImportError(
|
| 681 |
+
"Using http2=True, but the 'h2' package is not installed. "
|
| 682 |
+
"Make sure to install httpx using `pip install httpx[http2]`."
|
| 683 |
+
) from None
|
| 684 |
+
|
| 685 |
+
allow_env_proxies = trust_env and transport is None
|
| 686 |
+
proxy_map = self._get_proxy_map(proxy, allow_env_proxies)
|
| 687 |
+
|
| 688 |
+
self._transport = self._init_transport(
|
| 689 |
+
verify=verify,
|
| 690 |
+
cert=cert,
|
| 691 |
+
trust_env=trust_env,
|
| 692 |
+
http1=http1,
|
| 693 |
+
http2=http2,
|
| 694 |
+
limits=limits,
|
| 695 |
+
transport=transport,
|
| 696 |
+
)
|
| 697 |
+
self._mounts: dict[URLPattern, BaseTransport | None] = {
|
| 698 |
+
URLPattern(key): None
|
| 699 |
+
if proxy is None
|
| 700 |
+
else self._init_proxy_transport(
|
| 701 |
+
proxy,
|
| 702 |
+
verify=verify,
|
| 703 |
+
cert=cert,
|
| 704 |
+
trust_env=trust_env,
|
| 705 |
+
http1=http1,
|
| 706 |
+
http2=http2,
|
| 707 |
+
limits=limits,
|
| 708 |
+
)
|
| 709 |
+
for key, proxy in proxy_map.items()
|
| 710 |
+
}
|
| 711 |
+
if mounts is not None:
|
| 712 |
+
self._mounts.update(
|
| 713 |
+
{URLPattern(key): transport for key, transport in mounts.items()}
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
self._mounts = dict(sorted(self._mounts.items()))
|
| 717 |
+
|
| 718 |
+
def _init_transport(
|
| 719 |
+
self,
|
| 720 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 721 |
+
cert: CertTypes | None = None,
|
| 722 |
+
trust_env: bool = True,
|
| 723 |
+
http1: bool = True,
|
| 724 |
+
http2: bool = False,
|
| 725 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 726 |
+
transport: BaseTransport | None = None,
|
| 727 |
+
) -> BaseTransport:
|
| 728 |
+
if transport is not None:
|
| 729 |
+
return transport
|
| 730 |
+
|
| 731 |
+
return HTTPTransport(
|
| 732 |
+
verify=verify,
|
| 733 |
+
cert=cert,
|
| 734 |
+
trust_env=trust_env,
|
| 735 |
+
http1=http1,
|
| 736 |
+
http2=http2,
|
| 737 |
+
limits=limits,
|
| 738 |
+
)
|
| 739 |
+
|
| 740 |
+
def _init_proxy_transport(
|
| 741 |
+
self,
|
| 742 |
+
proxy: Proxy,
|
| 743 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 744 |
+
cert: CertTypes | None = None,
|
| 745 |
+
trust_env: bool = True,
|
| 746 |
+
http1: bool = True,
|
| 747 |
+
http2: bool = False,
|
| 748 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 749 |
+
) -> BaseTransport:
|
| 750 |
+
return HTTPTransport(
|
| 751 |
+
verify=verify,
|
| 752 |
+
cert=cert,
|
| 753 |
+
trust_env=trust_env,
|
| 754 |
+
http1=http1,
|
| 755 |
+
http2=http2,
|
| 756 |
+
limits=limits,
|
| 757 |
+
proxy=proxy,
|
| 758 |
+
)
|
| 759 |
+
|
| 760 |
+
def _transport_for_url(self, url: URL) -> BaseTransport:
|
| 761 |
+
"""
|
| 762 |
+
Returns the transport instance that should be used for a given URL.
|
| 763 |
+
This will either be the standard connection pool, or a proxy.
|
| 764 |
+
"""
|
| 765 |
+
for pattern, transport in self._mounts.items():
|
| 766 |
+
if pattern.matches(url):
|
| 767 |
+
return self._transport if transport is None else transport
|
| 768 |
+
|
| 769 |
+
return self._transport
|
| 770 |
+
|
| 771 |
+
def request(
|
| 772 |
+
self,
|
| 773 |
+
method: str,
|
| 774 |
+
url: URL | str,
|
| 775 |
+
*,
|
| 776 |
+
content: RequestContent | None = None,
|
| 777 |
+
data: RequestData | None = None,
|
| 778 |
+
files: RequestFiles | None = None,
|
| 779 |
+
json: typing.Any | None = None,
|
| 780 |
+
params: QueryParamTypes | None = None,
|
| 781 |
+
headers: HeaderTypes | None = None,
|
| 782 |
+
cookies: CookieTypes | None = None,
|
| 783 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 784 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 785 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 786 |
+
extensions: RequestExtensions | None = None,
|
| 787 |
+
) -> Response:
|
| 788 |
+
"""
|
| 789 |
+
Build and send a request.
|
| 790 |
+
|
| 791 |
+
Equivalent to:
|
| 792 |
+
|
| 793 |
+
```python
|
| 794 |
+
request = client.build_request(...)
|
| 795 |
+
response = client.send(request, ...)
|
| 796 |
+
```
|
| 797 |
+
|
| 798 |
+
See `Client.build_request()`, `Client.send()` and
|
| 799 |
+
[Merging of configuration][0] for how the various parameters
|
| 800 |
+
are merged with client-level configuration.
|
| 801 |
+
|
| 802 |
+
[0]: /advanced/clients/#merging-of-configuration
|
| 803 |
+
"""
|
| 804 |
+
if cookies is not None:
|
| 805 |
+
message = (
|
| 806 |
+
"Setting per-request cookies=<...> is being deprecated, because "
|
| 807 |
+
"the expected behaviour on cookie persistence is ambiguous. Set "
|
| 808 |
+
"cookies directly on the client instance instead."
|
| 809 |
+
)
|
| 810 |
+
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
| 811 |
+
|
| 812 |
+
request = self.build_request(
|
| 813 |
+
method=method,
|
| 814 |
+
url=url,
|
| 815 |
+
content=content,
|
| 816 |
+
data=data,
|
| 817 |
+
files=files,
|
| 818 |
+
json=json,
|
| 819 |
+
params=params,
|
| 820 |
+
headers=headers,
|
| 821 |
+
cookies=cookies,
|
| 822 |
+
timeout=timeout,
|
| 823 |
+
extensions=extensions,
|
| 824 |
+
)
|
| 825 |
+
return self.send(request, auth=auth, follow_redirects=follow_redirects)
|
| 826 |
+
|
| 827 |
+
@contextmanager
|
| 828 |
+
def stream(
|
| 829 |
+
self,
|
| 830 |
+
method: str,
|
| 831 |
+
url: URL | str,
|
| 832 |
+
*,
|
| 833 |
+
content: RequestContent | None = None,
|
| 834 |
+
data: RequestData | None = None,
|
| 835 |
+
files: RequestFiles | None = None,
|
| 836 |
+
json: typing.Any | None = None,
|
| 837 |
+
params: QueryParamTypes | None = None,
|
| 838 |
+
headers: HeaderTypes | None = None,
|
| 839 |
+
cookies: CookieTypes | None = None,
|
| 840 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 841 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 842 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 843 |
+
extensions: RequestExtensions | None = None,
|
| 844 |
+
) -> typing.Iterator[Response]:
|
| 845 |
+
"""
|
| 846 |
+
Alternative to `httpx.request()` that streams the response body
|
| 847 |
+
instead of loading it into memory at once.
|
| 848 |
+
|
| 849 |
+
**Parameters**: See `httpx.request`.
|
| 850 |
+
|
| 851 |
+
See also: [Streaming Responses][0]
|
| 852 |
+
|
| 853 |
+
[0]: /quickstart#streaming-responses
|
| 854 |
+
"""
|
| 855 |
+
request = self.build_request(
|
| 856 |
+
method=method,
|
| 857 |
+
url=url,
|
| 858 |
+
content=content,
|
| 859 |
+
data=data,
|
| 860 |
+
files=files,
|
| 861 |
+
json=json,
|
| 862 |
+
params=params,
|
| 863 |
+
headers=headers,
|
| 864 |
+
cookies=cookies,
|
| 865 |
+
timeout=timeout,
|
| 866 |
+
extensions=extensions,
|
| 867 |
+
)
|
| 868 |
+
response = self.send(
|
| 869 |
+
request=request,
|
| 870 |
+
auth=auth,
|
| 871 |
+
follow_redirects=follow_redirects,
|
| 872 |
+
stream=True,
|
| 873 |
+
)
|
| 874 |
+
try:
|
| 875 |
+
yield response
|
| 876 |
+
finally:
|
| 877 |
+
response.close()
|
| 878 |
+
|
| 879 |
+
def send(
|
| 880 |
+
self,
|
| 881 |
+
request: Request,
|
| 882 |
+
*,
|
| 883 |
+
stream: bool = False,
|
| 884 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 885 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 886 |
+
) -> Response:
|
| 887 |
+
"""
|
| 888 |
+
Send a request.
|
| 889 |
+
|
| 890 |
+
The request is sent as-is, unmodified.
|
| 891 |
+
|
| 892 |
+
Typically you'll want to build one with `Client.build_request()`
|
| 893 |
+
so that any client-level configuration is merged into the request,
|
| 894 |
+
but passing an explicit `httpx.Request()` is supported as well.
|
| 895 |
+
|
| 896 |
+
See also: [Request instances][0]
|
| 897 |
+
|
| 898 |
+
[0]: /advanced/clients/#request-instances
|
| 899 |
+
"""
|
| 900 |
+
if self._state == ClientState.CLOSED:
|
| 901 |
+
raise RuntimeError("Cannot send a request, as the client has been closed.")
|
| 902 |
+
|
| 903 |
+
self._state = ClientState.OPENED
|
| 904 |
+
follow_redirects = (
|
| 905 |
+
self.follow_redirects
|
| 906 |
+
if isinstance(follow_redirects, UseClientDefault)
|
| 907 |
+
else follow_redirects
|
| 908 |
+
)
|
| 909 |
+
|
| 910 |
+
self._set_timeout(request)
|
| 911 |
+
|
| 912 |
+
auth = self._build_request_auth(request, auth)
|
| 913 |
+
|
| 914 |
+
response = self._send_handling_auth(
|
| 915 |
+
request,
|
| 916 |
+
auth=auth,
|
| 917 |
+
follow_redirects=follow_redirects,
|
| 918 |
+
history=[],
|
| 919 |
+
)
|
| 920 |
+
try:
|
| 921 |
+
if not stream:
|
| 922 |
+
response.read()
|
| 923 |
+
|
| 924 |
+
return response
|
| 925 |
+
|
| 926 |
+
except BaseException as exc:
|
| 927 |
+
response.close()
|
| 928 |
+
raise exc
|
| 929 |
+
|
| 930 |
+
def _send_handling_auth(
|
| 931 |
+
self,
|
| 932 |
+
request: Request,
|
| 933 |
+
auth: Auth,
|
| 934 |
+
follow_redirects: bool,
|
| 935 |
+
history: list[Response],
|
| 936 |
+
) -> Response:
|
| 937 |
+
auth_flow = auth.sync_auth_flow(request)
|
| 938 |
+
try:
|
| 939 |
+
request = next(auth_flow)
|
| 940 |
+
|
| 941 |
+
while True:
|
| 942 |
+
response = self._send_handling_redirects(
|
| 943 |
+
request,
|
| 944 |
+
follow_redirects=follow_redirects,
|
| 945 |
+
history=history,
|
| 946 |
+
)
|
| 947 |
+
try:
|
| 948 |
+
try:
|
| 949 |
+
next_request = auth_flow.send(response)
|
| 950 |
+
except StopIteration:
|
| 951 |
+
return response
|
| 952 |
+
|
| 953 |
+
response.history = list(history)
|
| 954 |
+
response.read()
|
| 955 |
+
request = next_request
|
| 956 |
+
history.append(response)
|
| 957 |
+
|
| 958 |
+
except BaseException as exc:
|
| 959 |
+
response.close()
|
| 960 |
+
raise exc
|
| 961 |
+
finally:
|
| 962 |
+
auth_flow.close()
|
| 963 |
+
|
| 964 |
+
def _send_handling_redirects(
|
| 965 |
+
self,
|
| 966 |
+
request: Request,
|
| 967 |
+
follow_redirects: bool,
|
| 968 |
+
history: list[Response],
|
| 969 |
+
) -> Response:
|
| 970 |
+
while True:
|
| 971 |
+
if len(history) > self.max_redirects:
|
| 972 |
+
raise TooManyRedirects(
|
| 973 |
+
"Exceeded maximum allowed redirects.", request=request
|
| 974 |
+
)
|
| 975 |
+
|
| 976 |
+
for hook in self._event_hooks["request"]:
|
| 977 |
+
hook(request)
|
| 978 |
+
|
| 979 |
+
response = self._send_single_request(request)
|
| 980 |
+
try:
|
| 981 |
+
for hook in self._event_hooks["response"]:
|
| 982 |
+
hook(response)
|
| 983 |
+
response.history = list(history)
|
| 984 |
+
|
| 985 |
+
if not response.has_redirect_location:
|
| 986 |
+
return response
|
| 987 |
+
|
| 988 |
+
request = self._build_redirect_request(request, response)
|
| 989 |
+
history = history + [response]
|
| 990 |
+
|
| 991 |
+
if follow_redirects:
|
| 992 |
+
response.read()
|
| 993 |
+
else:
|
| 994 |
+
response.next_request = request
|
| 995 |
+
return response
|
| 996 |
+
|
| 997 |
+
except BaseException as exc:
|
| 998 |
+
response.close()
|
| 999 |
+
raise exc
|
| 1000 |
+
|
| 1001 |
+
def _send_single_request(self, request: Request) -> Response:
|
| 1002 |
+
"""
|
| 1003 |
+
Sends a single request, without handling any redirections.
|
| 1004 |
+
"""
|
| 1005 |
+
transport = self._transport_for_url(request.url)
|
| 1006 |
+
start = time.perf_counter()
|
| 1007 |
+
|
| 1008 |
+
if not isinstance(request.stream, SyncByteStream):
|
| 1009 |
+
raise RuntimeError(
|
| 1010 |
+
"Attempted to send an async request with a sync Client instance."
|
| 1011 |
+
)
|
| 1012 |
+
|
| 1013 |
+
with request_context(request=request):
|
| 1014 |
+
response = transport.handle_request(request)
|
| 1015 |
+
|
| 1016 |
+
assert isinstance(response.stream, SyncByteStream)
|
| 1017 |
+
|
| 1018 |
+
response.request = request
|
| 1019 |
+
response.stream = BoundSyncStream(
|
| 1020 |
+
response.stream, response=response, start=start
|
| 1021 |
+
)
|
| 1022 |
+
self.cookies.extract_cookies(response)
|
| 1023 |
+
response.default_encoding = self._default_encoding
|
| 1024 |
+
|
| 1025 |
+
logger.info(
|
| 1026 |
+
'HTTP Request: %s %s "%s %d %s"',
|
| 1027 |
+
request.method,
|
| 1028 |
+
request.url,
|
| 1029 |
+
response.http_version,
|
| 1030 |
+
response.status_code,
|
| 1031 |
+
response.reason_phrase,
|
| 1032 |
+
)
|
| 1033 |
+
|
| 1034 |
+
return response
|
| 1035 |
+
|
| 1036 |
+
def get(
|
| 1037 |
+
self,
|
| 1038 |
+
url: URL | str,
|
| 1039 |
+
*,
|
| 1040 |
+
params: QueryParamTypes | None = None,
|
| 1041 |
+
headers: HeaderTypes | None = None,
|
| 1042 |
+
cookies: CookieTypes | None = None,
|
| 1043 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1044 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1045 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1046 |
+
extensions: RequestExtensions | None = None,
|
| 1047 |
+
) -> Response:
|
| 1048 |
+
"""
|
| 1049 |
+
Send a `GET` request.
|
| 1050 |
+
|
| 1051 |
+
**Parameters**: See `httpx.request`.
|
| 1052 |
+
"""
|
| 1053 |
+
return self.request(
|
| 1054 |
+
"GET",
|
| 1055 |
+
url,
|
| 1056 |
+
params=params,
|
| 1057 |
+
headers=headers,
|
| 1058 |
+
cookies=cookies,
|
| 1059 |
+
auth=auth,
|
| 1060 |
+
follow_redirects=follow_redirects,
|
| 1061 |
+
timeout=timeout,
|
| 1062 |
+
extensions=extensions,
|
| 1063 |
+
)
|
| 1064 |
+
|
| 1065 |
+
def options(
|
| 1066 |
+
self,
|
| 1067 |
+
url: URL | str,
|
| 1068 |
+
*,
|
| 1069 |
+
params: QueryParamTypes | None = None,
|
| 1070 |
+
headers: HeaderTypes | None = None,
|
| 1071 |
+
cookies: CookieTypes | None = None,
|
| 1072 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1073 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1074 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1075 |
+
extensions: RequestExtensions | None = None,
|
| 1076 |
+
) -> Response:
|
| 1077 |
+
"""
|
| 1078 |
+
Send an `OPTIONS` request.
|
| 1079 |
+
|
| 1080 |
+
**Parameters**: See `httpx.request`.
|
| 1081 |
+
"""
|
| 1082 |
+
return self.request(
|
| 1083 |
+
"OPTIONS",
|
| 1084 |
+
url,
|
| 1085 |
+
params=params,
|
| 1086 |
+
headers=headers,
|
| 1087 |
+
cookies=cookies,
|
| 1088 |
+
auth=auth,
|
| 1089 |
+
follow_redirects=follow_redirects,
|
| 1090 |
+
timeout=timeout,
|
| 1091 |
+
extensions=extensions,
|
| 1092 |
+
)
|
| 1093 |
+
|
| 1094 |
+
def head(
|
| 1095 |
+
self,
|
| 1096 |
+
url: URL | str,
|
| 1097 |
+
*,
|
| 1098 |
+
params: QueryParamTypes | None = None,
|
| 1099 |
+
headers: HeaderTypes | None = None,
|
| 1100 |
+
cookies: CookieTypes | None = None,
|
| 1101 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1102 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1103 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1104 |
+
extensions: RequestExtensions | None = None,
|
| 1105 |
+
) -> Response:
|
| 1106 |
+
"""
|
| 1107 |
+
Send a `HEAD` request.
|
| 1108 |
+
|
| 1109 |
+
**Parameters**: See `httpx.request`.
|
| 1110 |
+
"""
|
| 1111 |
+
return self.request(
|
| 1112 |
+
"HEAD",
|
| 1113 |
+
url,
|
| 1114 |
+
params=params,
|
| 1115 |
+
headers=headers,
|
| 1116 |
+
cookies=cookies,
|
| 1117 |
+
auth=auth,
|
| 1118 |
+
follow_redirects=follow_redirects,
|
| 1119 |
+
timeout=timeout,
|
| 1120 |
+
extensions=extensions,
|
| 1121 |
+
)
|
| 1122 |
+
|
| 1123 |
+
def post(
|
| 1124 |
+
self,
|
| 1125 |
+
url: URL | str,
|
| 1126 |
+
*,
|
| 1127 |
+
content: RequestContent | None = None,
|
| 1128 |
+
data: RequestData | None = None,
|
| 1129 |
+
files: RequestFiles | None = None,
|
| 1130 |
+
json: typing.Any | None = None,
|
| 1131 |
+
params: QueryParamTypes | None = None,
|
| 1132 |
+
headers: HeaderTypes | None = None,
|
| 1133 |
+
cookies: CookieTypes | None = None,
|
| 1134 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1135 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1136 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1137 |
+
extensions: RequestExtensions | None = None,
|
| 1138 |
+
) -> Response:
|
| 1139 |
+
"""
|
| 1140 |
+
Send a `POST` request.
|
| 1141 |
+
|
| 1142 |
+
**Parameters**: See `httpx.request`.
|
| 1143 |
+
"""
|
| 1144 |
+
return self.request(
|
| 1145 |
+
"POST",
|
| 1146 |
+
url,
|
| 1147 |
+
content=content,
|
| 1148 |
+
data=data,
|
| 1149 |
+
files=files,
|
| 1150 |
+
json=json,
|
| 1151 |
+
params=params,
|
| 1152 |
+
headers=headers,
|
| 1153 |
+
cookies=cookies,
|
| 1154 |
+
auth=auth,
|
| 1155 |
+
follow_redirects=follow_redirects,
|
| 1156 |
+
timeout=timeout,
|
| 1157 |
+
extensions=extensions,
|
| 1158 |
+
)
|
| 1159 |
+
|
| 1160 |
+
def put(
|
| 1161 |
+
self,
|
| 1162 |
+
url: URL | str,
|
| 1163 |
+
*,
|
| 1164 |
+
content: RequestContent | None = None,
|
| 1165 |
+
data: RequestData | None = None,
|
| 1166 |
+
files: RequestFiles | None = None,
|
| 1167 |
+
json: typing.Any | None = None,
|
| 1168 |
+
params: QueryParamTypes | None = None,
|
| 1169 |
+
headers: HeaderTypes | None = None,
|
| 1170 |
+
cookies: CookieTypes | None = None,
|
| 1171 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1172 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1173 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1174 |
+
extensions: RequestExtensions | None = None,
|
| 1175 |
+
) -> Response:
|
| 1176 |
+
"""
|
| 1177 |
+
Send a `PUT` request.
|
| 1178 |
+
|
| 1179 |
+
**Parameters**: See `httpx.request`.
|
| 1180 |
+
"""
|
| 1181 |
+
return self.request(
|
| 1182 |
+
"PUT",
|
| 1183 |
+
url,
|
| 1184 |
+
content=content,
|
| 1185 |
+
data=data,
|
| 1186 |
+
files=files,
|
| 1187 |
+
json=json,
|
| 1188 |
+
params=params,
|
| 1189 |
+
headers=headers,
|
| 1190 |
+
cookies=cookies,
|
| 1191 |
+
auth=auth,
|
| 1192 |
+
follow_redirects=follow_redirects,
|
| 1193 |
+
timeout=timeout,
|
| 1194 |
+
extensions=extensions,
|
| 1195 |
+
)
|
| 1196 |
+
|
| 1197 |
+
def patch(
|
| 1198 |
+
self,
|
| 1199 |
+
url: URL | str,
|
| 1200 |
+
*,
|
| 1201 |
+
content: RequestContent | None = None,
|
| 1202 |
+
data: RequestData | None = None,
|
| 1203 |
+
files: RequestFiles | None = None,
|
| 1204 |
+
json: typing.Any | None = None,
|
| 1205 |
+
params: QueryParamTypes | None = None,
|
| 1206 |
+
headers: HeaderTypes | None = None,
|
| 1207 |
+
cookies: CookieTypes | None = None,
|
| 1208 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1209 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1210 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1211 |
+
extensions: RequestExtensions | None = None,
|
| 1212 |
+
) -> Response:
|
| 1213 |
+
"""
|
| 1214 |
+
Send a `PATCH` request.
|
| 1215 |
+
|
| 1216 |
+
**Parameters**: See `httpx.request`.
|
| 1217 |
+
"""
|
| 1218 |
+
return self.request(
|
| 1219 |
+
"PATCH",
|
| 1220 |
+
url,
|
| 1221 |
+
content=content,
|
| 1222 |
+
data=data,
|
| 1223 |
+
files=files,
|
| 1224 |
+
json=json,
|
| 1225 |
+
params=params,
|
| 1226 |
+
headers=headers,
|
| 1227 |
+
cookies=cookies,
|
| 1228 |
+
auth=auth,
|
| 1229 |
+
follow_redirects=follow_redirects,
|
| 1230 |
+
timeout=timeout,
|
| 1231 |
+
extensions=extensions,
|
| 1232 |
+
)
|
| 1233 |
+
|
| 1234 |
+
def delete(
|
| 1235 |
+
self,
|
| 1236 |
+
url: URL | str,
|
| 1237 |
+
*,
|
| 1238 |
+
params: QueryParamTypes | None = None,
|
| 1239 |
+
headers: HeaderTypes | None = None,
|
| 1240 |
+
cookies: CookieTypes | None = None,
|
| 1241 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1242 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1243 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1244 |
+
extensions: RequestExtensions | None = None,
|
| 1245 |
+
) -> Response:
|
| 1246 |
+
"""
|
| 1247 |
+
Send a `DELETE` request.
|
| 1248 |
+
|
| 1249 |
+
**Parameters**: See `httpx.request`.
|
| 1250 |
+
"""
|
| 1251 |
+
return self.request(
|
| 1252 |
+
"DELETE",
|
| 1253 |
+
url,
|
| 1254 |
+
params=params,
|
| 1255 |
+
headers=headers,
|
| 1256 |
+
cookies=cookies,
|
| 1257 |
+
auth=auth,
|
| 1258 |
+
follow_redirects=follow_redirects,
|
| 1259 |
+
timeout=timeout,
|
| 1260 |
+
extensions=extensions,
|
| 1261 |
+
)
|
| 1262 |
+
|
| 1263 |
+
def close(self) -> None:
|
| 1264 |
+
"""
|
| 1265 |
+
Close transport and proxies.
|
| 1266 |
+
"""
|
| 1267 |
+
if self._state != ClientState.CLOSED:
|
| 1268 |
+
self._state = ClientState.CLOSED
|
| 1269 |
+
|
| 1270 |
+
self._transport.close()
|
| 1271 |
+
for transport in self._mounts.values():
|
| 1272 |
+
if transport is not None:
|
| 1273 |
+
transport.close()
|
| 1274 |
+
|
| 1275 |
+
def __enter__(self: T) -> T:
|
| 1276 |
+
if self._state != ClientState.UNOPENED:
|
| 1277 |
+
msg = {
|
| 1278 |
+
ClientState.OPENED: "Cannot open a client instance more than once.",
|
| 1279 |
+
ClientState.CLOSED: (
|
| 1280 |
+
"Cannot reopen a client instance, once it has been closed."
|
| 1281 |
+
),
|
| 1282 |
+
}[self._state]
|
| 1283 |
+
raise RuntimeError(msg)
|
| 1284 |
+
|
| 1285 |
+
self._state = ClientState.OPENED
|
| 1286 |
+
|
| 1287 |
+
self._transport.__enter__()
|
| 1288 |
+
for transport in self._mounts.values():
|
| 1289 |
+
if transport is not None:
|
| 1290 |
+
transport.__enter__()
|
| 1291 |
+
return self
|
| 1292 |
+
|
| 1293 |
+
def __exit__(
|
| 1294 |
+
self,
|
| 1295 |
+
exc_type: type[BaseException] | None = None,
|
| 1296 |
+
exc_value: BaseException | None = None,
|
| 1297 |
+
traceback: TracebackType | None = None,
|
| 1298 |
+
) -> None:
|
| 1299 |
+
self._state = ClientState.CLOSED
|
| 1300 |
+
|
| 1301 |
+
self._transport.__exit__(exc_type, exc_value, traceback)
|
| 1302 |
+
for transport in self._mounts.values():
|
| 1303 |
+
if transport is not None:
|
| 1304 |
+
transport.__exit__(exc_type, exc_value, traceback)
|
| 1305 |
+
|
| 1306 |
+
|
| 1307 |
+
class AsyncClient(BaseClient):
|
| 1308 |
+
"""
|
| 1309 |
+
An asynchronous HTTP client, with connection pooling, HTTP/2, redirects,
|
| 1310 |
+
cookie persistence, etc.
|
| 1311 |
+
|
| 1312 |
+
It can be shared between tasks.
|
| 1313 |
+
|
| 1314 |
+
Usage:
|
| 1315 |
+
|
| 1316 |
+
```python
|
| 1317 |
+
>>> async with httpx.AsyncClient() as client:
|
| 1318 |
+
>>> response = await client.get('https://example.org')
|
| 1319 |
+
```
|
| 1320 |
+
|
| 1321 |
+
**Parameters:**
|
| 1322 |
+
|
| 1323 |
+
* **auth** - *(optional)* An authentication class to use when sending
|
| 1324 |
+
requests.
|
| 1325 |
+
* **params** - *(optional)* Query parameters to include in request URLs, as
|
| 1326 |
+
a string, dictionary, or sequence of two-tuples.
|
| 1327 |
+
* **headers** - *(optional)* Dictionary of HTTP headers to include when
|
| 1328 |
+
sending requests.
|
| 1329 |
+
* **cookies** - *(optional)* Dictionary of Cookie items to include when
|
| 1330 |
+
sending requests.
|
| 1331 |
+
* **verify** - *(optional)* Either `True` to use an SSL context with the
|
| 1332 |
+
default CA bundle, `False` to disable verification, or an instance of
|
| 1333 |
+
`ssl.SSLContext` to use a custom context.
|
| 1334 |
+
* **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
|
| 1335 |
+
enabled. Defaults to `False`.
|
| 1336 |
+
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
|
| 1337 |
+
* **timeout** - *(optional)* The timeout configuration to use when sending
|
| 1338 |
+
requests.
|
| 1339 |
+
* **limits** - *(optional)* The limits configuration to use.
|
| 1340 |
+
* **max_redirects** - *(optional)* The maximum number of redirect responses
|
| 1341 |
+
that should be followed.
|
| 1342 |
+
* **base_url** - *(optional)* A URL to use as the base when building
|
| 1343 |
+
request URLs.
|
| 1344 |
+
* **transport** - *(optional)* A transport class to use for sending requests
|
| 1345 |
+
over the network.
|
| 1346 |
+
* **trust_env** - *(optional)* Enables or disables usage of environment
|
| 1347 |
+
variables for configuration.
|
| 1348 |
+
* **default_encoding** - *(optional)* The default encoding to use for decoding
|
| 1349 |
+
response text, if no charset information is included in a response Content-Type
|
| 1350 |
+
header. Set to a callable for automatic character set detection. Default: "utf-8".
|
| 1351 |
+
"""
|
| 1352 |
+
|
| 1353 |
+
def __init__(
|
| 1354 |
+
self,
|
| 1355 |
+
*,
|
| 1356 |
+
auth: AuthTypes | None = None,
|
| 1357 |
+
params: QueryParamTypes | None = None,
|
| 1358 |
+
headers: HeaderTypes | None = None,
|
| 1359 |
+
cookies: CookieTypes | None = None,
|
| 1360 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 1361 |
+
cert: CertTypes | None = None,
|
| 1362 |
+
http1: bool = True,
|
| 1363 |
+
http2: bool = False,
|
| 1364 |
+
proxy: ProxyTypes | None = None,
|
| 1365 |
+
mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None,
|
| 1366 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 1367 |
+
follow_redirects: bool = False,
|
| 1368 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 1369 |
+
max_redirects: int = DEFAULT_MAX_REDIRECTS,
|
| 1370 |
+
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
|
| 1371 |
+
base_url: URL | str = "",
|
| 1372 |
+
transport: AsyncBaseTransport | None = None,
|
| 1373 |
+
trust_env: bool = True,
|
| 1374 |
+
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
|
| 1375 |
+
) -> None:
|
| 1376 |
+
super().__init__(
|
| 1377 |
+
auth=auth,
|
| 1378 |
+
params=params,
|
| 1379 |
+
headers=headers,
|
| 1380 |
+
cookies=cookies,
|
| 1381 |
+
timeout=timeout,
|
| 1382 |
+
follow_redirects=follow_redirects,
|
| 1383 |
+
max_redirects=max_redirects,
|
| 1384 |
+
event_hooks=event_hooks,
|
| 1385 |
+
base_url=base_url,
|
| 1386 |
+
trust_env=trust_env,
|
| 1387 |
+
default_encoding=default_encoding,
|
| 1388 |
+
)
|
| 1389 |
+
|
| 1390 |
+
if http2:
|
| 1391 |
+
try:
|
| 1392 |
+
import h2 # noqa
|
| 1393 |
+
except ImportError: # pragma: no cover
|
| 1394 |
+
raise ImportError(
|
| 1395 |
+
"Using http2=True, but the 'h2' package is not installed. "
|
| 1396 |
+
"Make sure to install httpx using `pip install httpx[http2]`."
|
| 1397 |
+
) from None
|
| 1398 |
+
|
| 1399 |
+
allow_env_proxies = trust_env and transport is None
|
| 1400 |
+
proxy_map = self._get_proxy_map(proxy, allow_env_proxies)
|
| 1401 |
+
|
| 1402 |
+
self._transport = self._init_transport(
|
| 1403 |
+
verify=verify,
|
| 1404 |
+
cert=cert,
|
| 1405 |
+
trust_env=trust_env,
|
| 1406 |
+
http1=http1,
|
| 1407 |
+
http2=http2,
|
| 1408 |
+
limits=limits,
|
| 1409 |
+
transport=transport,
|
| 1410 |
+
)
|
| 1411 |
+
|
| 1412 |
+
self._mounts: dict[URLPattern, AsyncBaseTransport | None] = {
|
| 1413 |
+
URLPattern(key): None
|
| 1414 |
+
if proxy is None
|
| 1415 |
+
else self._init_proxy_transport(
|
| 1416 |
+
proxy,
|
| 1417 |
+
verify=verify,
|
| 1418 |
+
cert=cert,
|
| 1419 |
+
trust_env=trust_env,
|
| 1420 |
+
http1=http1,
|
| 1421 |
+
http2=http2,
|
| 1422 |
+
limits=limits,
|
| 1423 |
+
)
|
| 1424 |
+
for key, proxy in proxy_map.items()
|
| 1425 |
+
}
|
| 1426 |
+
if mounts is not None:
|
| 1427 |
+
self._mounts.update(
|
| 1428 |
+
{URLPattern(key): transport for key, transport in mounts.items()}
|
| 1429 |
+
)
|
| 1430 |
+
self._mounts = dict(sorted(self._mounts.items()))
|
| 1431 |
+
|
| 1432 |
+
def _init_transport(
|
| 1433 |
+
self,
|
| 1434 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 1435 |
+
cert: CertTypes | None = None,
|
| 1436 |
+
trust_env: bool = True,
|
| 1437 |
+
http1: bool = True,
|
| 1438 |
+
http2: bool = False,
|
| 1439 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 1440 |
+
transport: AsyncBaseTransport | None = None,
|
| 1441 |
+
) -> AsyncBaseTransport:
|
| 1442 |
+
if transport is not None:
|
| 1443 |
+
return transport
|
| 1444 |
+
|
| 1445 |
+
return AsyncHTTPTransport(
|
| 1446 |
+
verify=verify,
|
| 1447 |
+
cert=cert,
|
| 1448 |
+
trust_env=trust_env,
|
| 1449 |
+
http1=http1,
|
| 1450 |
+
http2=http2,
|
| 1451 |
+
limits=limits,
|
| 1452 |
+
)
|
| 1453 |
+
|
| 1454 |
+
def _init_proxy_transport(
|
| 1455 |
+
self,
|
| 1456 |
+
proxy: Proxy,
|
| 1457 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 1458 |
+
cert: CertTypes | None = None,
|
| 1459 |
+
trust_env: bool = True,
|
| 1460 |
+
http1: bool = True,
|
| 1461 |
+
http2: bool = False,
|
| 1462 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 1463 |
+
) -> AsyncBaseTransport:
|
| 1464 |
+
return AsyncHTTPTransport(
|
| 1465 |
+
verify=verify,
|
| 1466 |
+
cert=cert,
|
| 1467 |
+
trust_env=trust_env,
|
| 1468 |
+
http1=http1,
|
| 1469 |
+
http2=http2,
|
| 1470 |
+
limits=limits,
|
| 1471 |
+
proxy=proxy,
|
| 1472 |
+
)
|
| 1473 |
+
|
| 1474 |
+
def _transport_for_url(self, url: URL) -> AsyncBaseTransport:
|
| 1475 |
+
"""
|
| 1476 |
+
Returns the transport instance that should be used for a given URL.
|
| 1477 |
+
This will either be the standard connection pool, or a proxy.
|
| 1478 |
+
"""
|
| 1479 |
+
for pattern, transport in self._mounts.items():
|
| 1480 |
+
if pattern.matches(url):
|
| 1481 |
+
return self._transport if transport is None else transport
|
| 1482 |
+
|
| 1483 |
+
return self._transport
|
| 1484 |
+
|
| 1485 |
+
async def request(
|
| 1486 |
+
self,
|
| 1487 |
+
method: str,
|
| 1488 |
+
url: URL | str,
|
| 1489 |
+
*,
|
| 1490 |
+
content: RequestContent | None = None,
|
| 1491 |
+
data: RequestData | None = None,
|
| 1492 |
+
files: RequestFiles | None = None,
|
| 1493 |
+
json: typing.Any | None = None,
|
| 1494 |
+
params: QueryParamTypes | None = None,
|
| 1495 |
+
headers: HeaderTypes | None = None,
|
| 1496 |
+
cookies: CookieTypes | None = None,
|
| 1497 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1498 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1499 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1500 |
+
extensions: RequestExtensions | None = None,
|
| 1501 |
+
) -> Response:
|
| 1502 |
+
"""
|
| 1503 |
+
Build and send a request.
|
| 1504 |
+
|
| 1505 |
+
Equivalent to:
|
| 1506 |
+
|
| 1507 |
+
```python
|
| 1508 |
+
request = client.build_request(...)
|
| 1509 |
+
response = await client.send(request, ...)
|
| 1510 |
+
```
|
| 1511 |
+
|
| 1512 |
+
See `AsyncClient.build_request()`, `AsyncClient.send()`
|
| 1513 |
+
and [Merging of configuration][0] for how the various parameters
|
| 1514 |
+
are merged with client-level configuration.
|
| 1515 |
+
|
| 1516 |
+
[0]: /advanced/clients/#merging-of-configuration
|
| 1517 |
+
"""
|
| 1518 |
+
|
| 1519 |
+
if cookies is not None: # pragma: no cover
|
| 1520 |
+
message = (
|
| 1521 |
+
"Setting per-request cookies=<...> is being deprecated, because "
|
| 1522 |
+
"the expected behaviour on cookie persistence is ambiguous. Set "
|
| 1523 |
+
"cookies directly on the client instance instead."
|
| 1524 |
+
)
|
| 1525 |
+
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
| 1526 |
+
|
| 1527 |
+
request = self.build_request(
|
| 1528 |
+
method=method,
|
| 1529 |
+
url=url,
|
| 1530 |
+
content=content,
|
| 1531 |
+
data=data,
|
| 1532 |
+
files=files,
|
| 1533 |
+
json=json,
|
| 1534 |
+
params=params,
|
| 1535 |
+
headers=headers,
|
| 1536 |
+
cookies=cookies,
|
| 1537 |
+
timeout=timeout,
|
| 1538 |
+
extensions=extensions,
|
| 1539 |
+
)
|
| 1540 |
+
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
|
| 1541 |
+
|
| 1542 |
+
@asynccontextmanager
|
| 1543 |
+
async def stream(
|
| 1544 |
+
self,
|
| 1545 |
+
method: str,
|
| 1546 |
+
url: URL | str,
|
| 1547 |
+
*,
|
| 1548 |
+
content: RequestContent | None = None,
|
| 1549 |
+
data: RequestData | None = None,
|
| 1550 |
+
files: RequestFiles | None = None,
|
| 1551 |
+
json: typing.Any | None = None,
|
| 1552 |
+
params: QueryParamTypes | None = None,
|
| 1553 |
+
headers: HeaderTypes | None = None,
|
| 1554 |
+
cookies: CookieTypes | None = None,
|
| 1555 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1556 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1557 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1558 |
+
extensions: RequestExtensions | None = None,
|
| 1559 |
+
) -> typing.AsyncIterator[Response]:
|
| 1560 |
+
"""
|
| 1561 |
+
Alternative to `httpx.request()` that streams the response body
|
| 1562 |
+
instead of loading it into memory at once.
|
| 1563 |
+
|
| 1564 |
+
**Parameters**: See `httpx.request`.
|
| 1565 |
+
|
| 1566 |
+
See also: [Streaming Responses][0]
|
| 1567 |
+
|
| 1568 |
+
[0]: /quickstart#streaming-responses
|
| 1569 |
+
"""
|
| 1570 |
+
request = self.build_request(
|
| 1571 |
+
method=method,
|
| 1572 |
+
url=url,
|
| 1573 |
+
content=content,
|
| 1574 |
+
data=data,
|
| 1575 |
+
files=files,
|
| 1576 |
+
json=json,
|
| 1577 |
+
params=params,
|
| 1578 |
+
headers=headers,
|
| 1579 |
+
cookies=cookies,
|
| 1580 |
+
timeout=timeout,
|
| 1581 |
+
extensions=extensions,
|
| 1582 |
+
)
|
| 1583 |
+
response = await self.send(
|
| 1584 |
+
request=request,
|
| 1585 |
+
auth=auth,
|
| 1586 |
+
follow_redirects=follow_redirects,
|
| 1587 |
+
stream=True,
|
| 1588 |
+
)
|
| 1589 |
+
try:
|
| 1590 |
+
yield response
|
| 1591 |
+
finally:
|
| 1592 |
+
await response.aclose()
|
| 1593 |
+
|
| 1594 |
+
async def send(
|
| 1595 |
+
self,
|
| 1596 |
+
request: Request,
|
| 1597 |
+
*,
|
| 1598 |
+
stream: bool = False,
|
| 1599 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1600 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1601 |
+
) -> Response:
|
| 1602 |
+
"""
|
| 1603 |
+
Send a request.
|
| 1604 |
+
|
| 1605 |
+
The request is sent as-is, unmodified.
|
| 1606 |
+
|
| 1607 |
+
Typically you'll want to build one with `AsyncClient.build_request()`
|
| 1608 |
+
so that any client-level configuration is merged into the request,
|
| 1609 |
+
but passing an explicit `httpx.Request()` is supported as well.
|
| 1610 |
+
|
| 1611 |
+
See also: [Request instances][0]
|
| 1612 |
+
|
| 1613 |
+
[0]: /advanced/clients/#request-instances
|
| 1614 |
+
"""
|
| 1615 |
+
if self._state == ClientState.CLOSED:
|
| 1616 |
+
raise RuntimeError("Cannot send a request, as the client has been closed.")
|
| 1617 |
+
|
| 1618 |
+
self._state = ClientState.OPENED
|
| 1619 |
+
follow_redirects = (
|
| 1620 |
+
self.follow_redirects
|
| 1621 |
+
if isinstance(follow_redirects, UseClientDefault)
|
| 1622 |
+
else follow_redirects
|
| 1623 |
+
)
|
| 1624 |
+
|
| 1625 |
+
self._set_timeout(request)
|
| 1626 |
+
|
| 1627 |
+
auth = self._build_request_auth(request, auth)
|
| 1628 |
+
|
| 1629 |
+
response = await self._send_handling_auth(
|
| 1630 |
+
request,
|
| 1631 |
+
auth=auth,
|
| 1632 |
+
follow_redirects=follow_redirects,
|
| 1633 |
+
history=[],
|
| 1634 |
+
)
|
| 1635 |
+
try:
|
| 1636 |
+
if not stream:
|
| 1637 |
+
await response.aread()
|
| 1638 |
+
|
| 1639 |
+
return response
|
| 1640 |
+
|
| 1641 |
+
except BaseException as exc:
|
| 1642 |
+
await response.aclose()
|
| 1643 |
+
raise exc
|
| 1644 |
+
|
| 1645 |
+
async def _send_handling_auth(
|
| 1646 |
+
self,
|
| 1647 |
+
request: Request,
|
| 1648 |
+
auth: Auth,
|
| 1649 |
+
follow_redirects: bool,
|
| 1650 |
+
history: list[Response],
|
| 1651 |
+
) -> Response:
|
| 1652 |
+
auth_flow = auth.async_auth_flow(request)
|
| 1653 |
+
try:
|
| 1654 |
+
request = await auth_flow.__anext__()
|
| 1655 |
+
|
| 1656 |
+
while True:
|
| 1657 |
+
response = await self._send_handling_redirects(
|
| 1658 |
+
request,
|
| 1659 |
+
follow_redirects=follow_redirects,
|
| 1660 |
+
history=history,
|
| 1661 |
+
)
|
| 1662 |
+
try:
|
| 1663 |
+
try:
|
| 1664 |
+
next_request = await auth_flow.asend(response)
|
| 1665 |
+
except StopAsyncIteration:
|
| 1666 |
+
return response
|
| 1667 |
+
|
| 1668 |
+
response.history = list(history)
|
| 1669 |
+
await response.aread()
|
| 1670 |
+
request = next_request
|
| 1671 |
+
history.append(response)
|
| 1672 |
+
|
| 1673 |
+
except BaseException as exc:
|
| 1674 |
+
await response.aclose()
|
| 1675 |
+
raise exc
|
| 1676 |
+
finally:
|
| 1677 |
+
await auth_flow.aclose()
|
| 1678 |
+
|
| 1679 |
+
async def _send_handling_redirects(
|
| 1680 |
+
self,
|
| 1681 |
+
request: Request,
|
| 1682 |
+
follow_redirects: bool,
|
| 1683 |
+
history: list[Response],
|
| 1684 |
+
) -> Response:
|
| 1685 |
+
while True:
|
| 1686 |
+
if len(history) > self.max_redirects:
|
| 1687 |
+
raise TooManyRedirects(
|
| 1688 |
+
"Exceeded maximum allowed redirects.", request=request
|
| 1689 |
+
)
|
| 1690 |
+
|
| 1691 |
+
for hook in self._event_hooks["request"]:
|
| 1692 |
+
await hook(request)
|
| 1693 |
+
|
| 1694 |
+
response = await self._send_single_request(request)
|
| 1695 |
+
try:
|
| 1696 |
+
for hook in self._event_hooks["response"]:
|
| 1697 |
+
await hook(response)
|
| 1698 |
+
|
| 1699 |
+
response.history = list(history)
|
| 1700 |
+
|
| 1701 |
+
if not response.has_redirect_location:
|
| 1702 |
+
return response
|
| 1703 |
+
|
| 1704 |
+
request = self._build_redirect_request(request, response)
|
| 1705 |
+
history = history + [response]
|
| 1706 |
+
|
| 1707 |
+
if follow_redirects:
|
| 1708 |
+
await response.aread()
|
| 1709 |
+
else:
|
| 1710 |
+
response.next_request = request
|
| 1711 |
+
return response
|
| 1712 |
+
|
| 1713 |
+
except BaseException as exc:
|
| 1714 |
+
await response.aclose()
|
| 1715 |
+
raise exc
|
| 1716 |
+
|
| 1717 |
+
async def _send_single_request(self, request: Request) -> Response:
|
| 1718 |
+
"""
|
| 1719 |
+
Sends a single request, without handling any redirections.
|
| 1720 |
+
"""
|
| 1721 |
+
transport = self._transport_for_url(request.url)
|
| 1722 |
+
start = time.perf_counter()
|
| 1723 |
+
|
| 1724 |
+
if not isinstance(request.stream, AsyncByteStream):
|
| 1725 |
+
raise RuntimeError(
|
| 1726 |
+
"Attempted to send an sync request with an AsyncClient instance."
|
| 1727 |
+
)
|
| 1728 |
+
|
| 1729 |
+
with request_context(request=request):
|
| 1730 |
+
response = await transport.handle_async_request(request)
|
| 1731 |
+
|
| 1732 |
+
assert isinstance(response.stream, AsyncByteStream)
|
| 1733 |
+
response.request = request
|
| 1734 |
+
response.stream = BoundAsyncStream(
|
| 1735 |
+
response.stream, response=response, start=start
|
| 1736 |
+
)
|
| 1737 |
+
self.cookies.extract_cookies(response)
|
| 1738 |
+
response.default_encoding = self._default_encoding
|
| 1739 |
+
|
| 1740 |
+
logger.info(
|
| 1741 |
+
'HTTP Request: %s %s "%s %d %s"',
|
| 1742 |
+
request.method,
|
| 1743 |
+
request.url,
|
| 1744 |
+
response.http_version,
|
| 1745 |
+
response.status_code,
|
| 1746 |
+
response.reason_phrase,
|
| 1747 |
+
)
|
| 1748 |
+
|
| 1749 |
+
return response
|
| 1750 |
+
|
| 1751 |
+
async def get(
|
| 1752 |
+
self,
|
| 1753 |
+
url: URL | str,
|
| 1754 |
+
*,
|
| 1755 |
+
params: QueryParamTypes | None = None,
|
| 1756 |
+
headers: HeaderTypes | None = None,
|
| 1757 |
+
cookies: CookieTypes | None = None,
|
| 1758 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1759 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1760 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1761 |
+
extensions: RequestExtensions | None = None,
|
| 1762 |
+
) -> Response:
|
| 1763 |
+
"""
|
| 1764 |
+
Send a `GET` request.
|
| 1765 |
+
|
| 1766 |
+
**Parameters**: See `httpx.request`.
|
| 1767 |
+
"""
|
| 1768 |
+
return await self.request(
|
| 1769 |
+
"GET",
|
| 1770 |
+
url,
|
| 1771 |
+
params=params,
|
| 1772 |
+
headers=headers,
|
| 1773 |
+
cookies=cookies,
|
| 1774 |
+
auth=auth,
|
| 1775 |
+
follow_redirects=follow_redirects,
|
| 1776 |
+
timeout=timeout,
|
| 1777 |
+
extensions=extensions,
|
| 1778 |
+
)
|
| 1779 |
+
|
| 1780 |
+
async def options(
|
| 1781 |
+
self,
|
| 1782 |
+
url: URL | str,
|
| 1783 |
+
*,
|
| 1784 |
+
params: QueryParamTypes | None = None,
|
| 1785 |
+
headers: HeaderTypes | None = None,
|
| 1786 |
+
cookies: CookieTypes | None = None,
|
| 1787 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1788 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1789 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1790 |
+
extensions: RequestExtensions | None = None,
|
| 1791 |
+
) -> Response:
|
| 1792 |
+
"""
|
| 1793 |
+
Send an `OPTIONS` request.
|
| 1794 |
+
|
| 1795 |
+
**Parameters**: See `httpx.request`.
|
| 1796 |
+
"""
|
| 1797 |
+
return await self.request(
|
| 1798 |
+
"OPTIONS",
|
| 1799 |
+
url,
|
| 1800 |
+
params=params,
|
| 1801 |
+
headers=headers,
|
| 1802 |
+
cookies=cookies,
|
| 1803 |
+
auth=auth,
|
| 1804 |
+
follow_redirects=follow_redirects,
|
| 1805 |
+
timeout=timeout,
|
| 1806 |
+
extensions=extensions,
|
| 1807 |
+
)
|
| 1808 |
+
|
| 1809 |
+
async def head(
|
| 1810 |
+
self,
|
| 1811 |
+
url: URL | str,
|
| 1812 |
+
*,
|
| 1813 |
+
params: QueryParamTypes | None = None,
|
| 1814 |
+
headers: HeaderTypes | None = None,
|
| 1815 |
+
cookies: CookieTypes | None = None,
|
| 1816 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1817 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1818 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1819 |
+
extensions: RequestExtensions | None = None,
|
| 1820 |
+
) -> Response:
|
| 1821 |
+
"""
|
| 1822 |
+
Send a `HEAD` request.
|
| 1823 |
+
|
| 1824 |
+
**Parameters**: See `httpx.request`.
|
| 1825 |
+
"""
|
| 1826 |
+
return await self.request(
|
| 1827 |
+
"HEAD",
|
| 1828 |
+
url,
|
| 1829 |
+
params=params,
|
| 1830 |
+
headers=headers,
|
| 1831 |
+
cookies=cookies,
|
| 1832 |
+
auth=auth,
|
| 1833 |
+
follow_redirects=follow_redirects,
|
| 1834 |
+
timeout=timeout,
|
| 1835 |
+
extensions=extensions,
|
| 1836 |
+
)
|
| 1837 |
+
|
| 1838 |
+
async def post(
|
| 1839 |
+
self,
|
| 1840 |
+
url: URL | str,
|
| 1841 |
+
*,
|
| 1842 |
+
content: RequestContent | None = None,
|
| 1843 |
+
data: RequestData | None = None,
|
| 1844 |
+
files: RequestFiles | None = None,
|
| 1845 |
+
json: typing.Any | None = None,
|
| 1846 |
+
params: QueryParamTypes | None = None,
|
| 1847 |
+
headers: HeaderTypes | None = None,
|
| 1848 |
+
cookies: CookieTypes | None = None,
|
| 1849 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1850 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1851 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1852 |
+
extensions: RequestExtensions | None = None,
|
| 1853 |
+
) -> Response:
|
| 1854 |
+
"""
|
| 1855 |
+
Send a `POST` request.
|
| 1856 |
+
|
| 1857 |
+
**Parameters**: See `httpx.request`.
|
| 1858 |
+
"""
|
| 1859 |
+
return await self.request(
|
| 1860 |
+
"POST",
|
| 1861 |
+
url,
|
| 1862 |
+
content=content,
|
| 1863 |
+
data=data,
|
| 1864 |
+
files=files,
|
| 1865 |
+
json=json,
|
| 1866 |
+
params=params,
|
| 1867 |
+
headers=headers,
|
| 1868 |
+
cookies=cookies,
|
| 1869 |
+
auth=auth,
|
| 1870 |
+
follow_redirects=follow_redirects,
|
| 1871 |
+
timeout=timeout,
|
| 1872 |
+
extensions=extensions,
|
| 1873 |
+
)
|
| 1874 |
+
|
| 1875 |
+
async def put(
|
| 1876 |
+
self,
|
| 1877 |
+
url: URL | str,
|
| 1878 |
+
*,
|
| 1879 |
+
content: RequestContent | None = None,
|
| 1880 |
+
data: RequestData | None = None,
|
| 1881 |
+
files: RequestFiles | None = None,
|
| 1882 |
+
json: typing.Any | None = None,
|
| 1883 |
+
params: QueryParamTypes | None = None,
|
| 1884 |
+
headers: HeaderTypes | None = None,
|
| 1885 |
+
cookies: CookieTypes | None = None,
|
| 1886 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1887 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1888 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1889 |
+
extensions: RequestExtensions | None = None,
|
| 1890 |
+
) -> Response:
|
| 1891 |
+
"""
|
| 1892 |
+
Send a `PUT` request.
|
| 1893 |
+
|
| 1894 |
+
**Parameters**: See `httpx.request`.
|
| 1895 |
+
"""
|
| 1896 |
+
return await self.request(
|
| 1897 |
+
"PUT",
|
| 1898 |
+
url,
|
| 1899 |
+
content=content,
|
| 1900 |
+
data=data,
|
| 1901 |
+
files=files,
|
| 1902 |
+
json=json,
|
| 1903 |
+
params=params,
|
| 1904 |
+
headers=headers,
|
| 1905 |
+
cookies=cookies,
|
| 1906 |
+
auth=auth,
|
| 1907 |
+
follow_redirects=follow_redirects,
|
| 1908 |
+
timeout=timeout,
|
| 1909 |
+
extensions=extensions,
|
| 1910 |
+
)
|
| 1911 |
+
|
| 1912 |
+
async def patch(
|
| 1913 |
+
self,
|
| 1914 |
+
url: URL | str,
|
| 1915 |
+
*,
|
| 1916 |
+
content: RequestContent | None = None,
|
| 1917 |
+
data: RequestData | None = None,
|
| 1918 |
+
files: RequestFiles | None = None,
|
| 1919 |
+
json: typing.Any | None = None,
|
| 1920 |
+
params: QueryParamTypes | None = None,
|
| 1921 |
+
headers: HeaderTypes | None = None,
|
| 1922 |
+
cookies: CookieTypes | None = None,
|
| 1923 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1924 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1925 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1926 |
+
extensions: RequestExtensions | None = None,
|
| 1927 |
+
) -> Response:
|
| 1928 |
+
"""
|
| 1929 |
+
Send a `PATCH` request.
|
| 1930 |
+
|
| 1931 |
+
**Parameters**: See `httpx.request`.
|
| 1932 |
+
"""
|
| 1933 |
+
return await self.request(
|
| 1934 |
+
"PATCH",
|
| 1935 |
+
url,
|
| 1936 |
+
content=content,
|
| 1937 |
+
data=data,
|
| 1938 |
+
files=files,
|
| 1939 |
+
json=json,
|
| 1940 |
+
params=params,
|
| 1941 |
+
headers=headers,
|
| 1942 |
+
cookies=cookies,
|
| 1943 |
+
auth=auth,
|
| 1944 |
+
follow_redirects=follow_redirects,
|
| 1945 |
+
timeout=timeout,
|
| 1946 |
+
extensions=extensions,
|
| 1947 |
+
)
|
| 1948 |
+
|
| 1949 |
+
async def delete(
|
| 1950 |
+
self,
|
| 1951 |
+
url: URL | str,
|
| 1952 |
+
*,
|
| 1953 |
+
params: QueryParamTypes | None = None,
|
| 1954 |
+
headers: HeaderTypes | None = None,
|
| 1955 |
+
cookies: CookieTypes | None = None,
|
| 1956 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1957 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1958 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1959 |
+
extensions: RequestExtensions | None = None,
|
| 1960 |
+
) -> Response:
|
| 1961 |
+
"""
|
| 1962 |
+
Send a `DELETE` request.
|
| 1963 |
+
|
| 1964 |
+
**Parameters**: See `httpx.request`.
|
| 1965 |
+
"""
|
| 1966 |
+
return await self.request(
|
| 1967 |
+
"DELETE",
|
| 1968 |
+
url,
|
| 1969 |
+
params=params,
|
| 1970 |
+
headers=headers,
|
| 1971 |
+
cookies=cookies,
|
| 1972 |
+
auth=auth,
|
| 1973 |
+
follow_redirects=follow_redirects,
|
| 1974 |
+
timeout=timeout,
|
| 1975 |
+
extensions=extensions,
|
| 1976 |
+
)
|
| 1977 |
+
|
| 1978 |
+
async def aclose(self) -> None:
|
| 1979 |
+
"""
|
| 1980 |
+
Close transport and proxies.
|
| 1981 |
+
"""
|
| 1982 |
+
if self._state != ClientState.CLOSED:
|
| 1983 |
+
self._state = ClientState.CLOSED
|
| 1984 |
+
|
| 1985 |
+
await self._transport.aclose()
|
| 1986 |
+
for proxy in self._mounts.values():
|
| 1987 |
+
if proxy is not None:
|
| 1988 |
+
await proxy.aclose()
|
| 1989 |
+
|
| 1990 |
+
async def __aenter__(self: U) -> U:
|
| 1991 |
+
if self._state != ClientState.UNOPENED:
|
| 1992 |
+
msg = {
|
| 1993 |
+
ClientState.OPENED: "Cannot open a client instance more than once.",
|
| 1994 |
+
ClientState.CLOSED: (
|
| 1995 |
+
"Cannot reopen a client instance, once it has been closed."
|
| 1996 |
+
),
|
| 1997 |
+
}[self._state]
|
| 1998 |
+
raise RuntimeError(msg)
|
| 1999 |
+
|
| 2000 |
+
self._state = ClientState.OPENED
|
| 2001 |
+
|
| 2002 |
+
await self._transport.__aenter__()
|
| 2003 |
+
for proxy in self._mounts.values():
|
| 2004 |
+
if proxy is not None:
|
| 2005 |
+
await proxy.__aenter__()
|
| 2006 |
+
return self
|
| 2007 |
+
|
| 2008 |
+
async def __aexit__(
|
| 2009 |
+
self,
|
| 2010 |
+
exc_type: type[BaseException] | None = None,
|
| 2011 |
+
exc_value: BaseException | None = None,
|
| 2012 |
+
traceback: TracebackType | None = None,
|
| 2013 |
+
) -> None:
|
| 2014 |
+
self._state = ClientState.CLOSED
|
| 2015 |
+
|
| 2016 |
+
await self._transport.__aexit__(exc_type, exc_value, traceback)
|
| 2017 |
+
for proxy in self._mounts.values():
|
| 2018 |
+
if proxy is not None:
|
| 2019 |
+
await proxy.__aexit__(exc_type, exc_value, traceback)
|
.venv/lib/python3.11/site-packages/httpx/_config.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import typing
|
| 5 |
+
|
| 6 |
+
from ._models import Headers
|
| 7 |
+
from ._types import CertTypes, HeaderTypes, TimeoutTypes
|
| 8 |
+
from ._urls import URL
|
| 9 |
+
|
| 10 |
+
if typing.TYPE_CHECKING:
|
| 11 |
+
import ssl # pragma: no cover
|
| 12 |
+
|
| 13 |
+
__all__ = ["Limits", "Proxy", "Timeout", "create_ssl_context"]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class UnsetType:
|
| 17 |
+
pass # pragma: no cover
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
UNSET = UnsetType()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def create_ssl_context(
|
| 24 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 25 |
+
cert: CertTypes | None = None,
|
| 26 |
+
trust_env: bool = True,
|
| 27 |
+
) -> ssl.SSLContext:
|
| 28 |
+
import ssl
|
| 29 |
+
import warnings
|
| 30 |
+
|
| 31 |
+
import certifi
|
| 32 |
+
|
| 33 |
+
if verify is True:
|
| 34 |
+
if trust_env and os.environ.get("SSL_CERT_FILE"): # pragma: nocover
|
| 35 |
+
ctx = ssl.create_default_context(cafile=os.environ["SSL_CERT_FILE"])
|
| 36 |
+
elif trust_env and os.environ.get("SSL_CERT_DIR"): # pragma: nocover
|
| 37 |
+
ctx = ssl.create_default_context(capath=os.environ["SSL_CERT_DIR"])
|
| 38 |
+
else:
|
| 39 |
+
# Default case...
|
| 40 |
+
ctx = ssl.create_default_context(cafile=certifi.where())
|
| 41 |
+
elif verify is False:
|
| 42 |
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
| 43 |
+
ctx.check_hostname = False
|
| 44 |
+
ctx.verify_mode = ssl.CERT_NONE
|
| 45 |
+
elif isinstance(verify, str): # pragma: nocover
|
| 46 |
+
message = (
|
| 47 |
+
"`verify=<str>` is deprecated. "
|
| 48 |
+
"Use `verify=ssl.create_default_context(cafile=...)` "
|
| 49 |
+
"or `verify=ssl.create_default_context(capath=...)` instead."
|
| 50 |
+
)
|
| 51 |
+
warnings.warn(message, DeprecationWarning)
|
| 52 |
+
if os.path.isdir(verify):
|
| 53 |
+
return ssl.create_default_context(capath=verify)
|
| 54 |
+
return ssl.create_default_context(cafile=verify)
|
| 55 |
+
else:
|
| 56 |
+
ctx = verify
|
| 57 |
+
|
| 58 |
+
if cert: # pragma: nocover
|
| 59 |
+
message = (
|
| 60 |
+
"`cert=...` is deprecated. Use `verify=<ssl_context>` instead,"
|
| 61 |
+
"with `.load_cert_chain()` to configure the certificate chain."
|
| 62 |
+
)
|
| 63 |
+
warnings.warn(message, DeprecationWarning)
|
| 64 |
+
if isinstance(cert, str):
|
| 65 |
+
ctx.load_cert_chain(cert)
|
| 66 |
+
else:
|
| 67 |
+
ctx.load_cert_chain(*cert)
|
| 68 |
+
|
| 69 |
+
return ctx
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
class Timeout:
|
| 73 |
+
"""
|
| 74 |
+
Timeout configuration.
|
| 75 |
+
|
| 76 |
+
**Usage**:
|
| 77 |
+
|
| 78 |
+
Timeout(None) # No timeouts.
|
| 79 |
+
Timeout(5.0) # 5s timeout on all operations.
|
| 80 |
+
Timeout(None, connect=5.0) # 5s timeout on connect, no other timeouts.
|
| 81 |
+
Timeout(5.0, connect=10.0) # 10s timeout on connect. 5s timeout elsewhere.
|
| 82 |
+
Timeout(5.0, pool=None) # No timeout on acquiring connection from pool.
|
| 83 |
+
# 5s timeout elsewhere.
|
| 84 |
+
"""
|
| 85 |
+
|
| 86 |
+
def __init__(
|
| 87 |
+
self,
|
| 88 |
+
timeout: TimeoutTypes | UnsetType = UNSET,
|
| 89 |
+
*,
|
| 90 |
+
connect: None | float | UnsetType = UNSET,
|
| 91 |
+
read: None | float | UnsetType = UNSET,
|
| 92 |
+
write: None | float | UnsetType = UNSET,
|
| 93 |
+
pool: None | float | UnsetType = UNSET,
|
| 94 |
+
) -> None:
|
| 95 |
+
if isinstance(timeout, Timeout):
|
| 96 |
+
# Passed as a single explicit Timeout.
|
| 97 |
+
assert connect is UNSET
|
| 98 |
+
assert read is UNSET
|
| 99 |
+
assert write is UNSET
|
| 100 |
+
assert pool is UNSET
|
| 101 |
+
self.connect = timeout.connect # type: typing.Optional[float]
|
| 102 |
+
self.read = timeout.read # type: typing.Optional[float]
|
| 103 |
+
self.write = timeout.write # type: typing.Optional[float]
|
| 104 |
+
self.pool = timeout.pool # type: typing.Optional[float]
|
| 105 |
+
elif isinstance(timeout, tuple):
|
| 106 |
+
# Passed as a tuple.
|
| 107 |
+
self.connect = timeout[0]
|
| 108 |
+
self.read = timeout[1]
|
| 109 |
+
self.write = None if len(timeout) < 3 else timeout[2]
|
| 110 |
+
self.pool = None if len(timeout) < 4 else timeout[3]
|
| 111 |
+
elif not (
|
| 112 |
+
isinstance(connect, UnsetType)
|
| 113 |
+
or isinstance(read, UnsetType)
|
| 114 |
+
or isinstance(write, UnsetType)
|
| 115 |
+
or isinstance(pool, UnsetType)
|
| 116 |
+
):
|
| 117 |
+
self.connect = connect
|
| 118 |
+
self.read = read
|
| 119 |
+
self.write = write
|
| 120 |
+
self.pool = pool
|
| 121 |
+
else:
|
| 122 |
+
if isinstance(timeout, UnsetType):
|
| 123 |
+
raise ValueError(
|
| 124 |
+
"httpx.Timeout must either include a default, or set all "
|
| 125 |
+
"four parameters explicitly."
|
| 126 |
+
)
|
| 127 |
+
self.connect = timeout if isinstance(connect, UnsetType) else connect
|
| 128 |
+
self.read = timeout if isinstance(read, UnsetType) else read
|
| 129 |
+
self.write = timeout if isinstance(write, UnsetType) else write
|
| 130 |
+
self.pool = timeout if isinstance(pool, UnsetType) else pool
|
| 131 |
+
|
| 132 |
+
def as_dict(self) -> dict[str, float | None]:
|
| 133 |
+
return {
|
| 134 |
+
"connect": self.connect,
|
| 135 |
+
"read": self.read,
|
| 136 |
+
"write": self.write,
|
| 137 |
+
"pool": self.pool,
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
def __eq__(self, other: typing.Any) -> bool:
|
| 141 |
+
return (
|
| 142 |
+
isinstance(other, self.__class__)
|
| 143 |
+
and self.connect == other.connect
|
| 144 |
+
and self.read == other.read
|
| 145 |
+
and self.write == other.write
|
| 146 |
+
and self.pool == other.pool
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
def __repr__(self) -> str:
|
| 150 |
+
class_name = self.__class__.__name__
|
| 151 |
+
if len({self.connect, self.read, self.write, self.pool}) == 1:
|
| 152 |
+
return f"{class_name}(timeout={self.connect})"
|
| 153 |
+
return (
|
| 154 |
+
f"{class_name}(connect={self.connect}, "
|
| 155 |
+
f"read={self.read}, write={self.write}, pool={self.pool})"
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class Limits:
|
| 160 |
+
"""
|
| 161 |
+
Configuration for limits to various client behaviors.
|
| 162 |
+
|
| 163 |
+
**Parameters:**
|
| 164 |
+
|
| 165 |
+
* **max_connections** - The maximum number of concurrent connections that may be
|
| 166 |
+
established.
|
| 167 |
+
* **max_keepalive_connections** - Allow the connection pool to maintain
|
| 168 |
+
keep-alive connections below this point. Should be less than or equal
|
| 169 |
+
to `max_connections`.
|
| 170 |
+
* **keepalive_expiry** - Time limit on idle keep-alive connections in seconds.
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
def __init__(
|
| 174 |
+
self,
|
| 175 |
+
*,
|
| 176 |
+
max_connections: int | None = None,
|
| 177 |
+
max_keepalive_connections: int | None = None,
|
| 178 |
+
keepalive_expiry: float | None = 5.0,
|
| 179 |
+
) -> None:
|
| 180 |
+
self.max_connections = max_connections
|
| 181 |
+
self.max_keepalive_connections = max_keepalive_connections
|
| 182 |
+
self.keepalive_expiry = keepalive_expiry
|
| 183 |
+
|
| 184 |
+
def __eq__(self, other: typing.Any) -> bool:
|
| 185 |
+
return (
|
| 186 |
+
isinstance(other, self.__class__)
|
| 187 |
+
and self.max_connections == other.max_connections
|
| 188 |
+
and self.max_keepalive_connections == other.max_keepalive_connections
|
| 189 |
+
and self.keepalive_expiry == other.keepalive_expiry
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
def __repr__(self) -> str:
|
| 193 |
+
class_name = self.__class__.__name__
|
| 194 |
+
return (
|
| 195 |
+
f"{class_name}(max_connections={self.max_connections}, "
|
| 196 |
+
f"max_keepalive_connections={self.max_keepalive_connections}, "
|
| 197 |
+
f"keepalive_expiry={self.keepalive_expiry})"
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
class Proxy:
|
| 202 |
+
def __init__(
|
| 203 |
+
self,
|
| 204 |
+
url: URL | str,
|
| 205 |
+
*,
|
| 206 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 207 |
+
auth: tuple[str, str] | None = None,
|
| 208 |
+
headers: HeaderTypes | None = None,
|
| 209 |
+
) -> None:
|
| 210 |
+
url = URL(url)
|
| 211 |
+
headers = Headers(headers)
|
| 212 |
+
|
| 213 |
+
if url.scheme not in ("http", "https", "socks5", "socks5h"):
|
| 214 |
+
raise ValueError(f"Unknown scheme for proxy URL {url!r}")
|
| 215 |
+
|
| 216 |
+
if url.username or url.password:
|
| 217 |
+
# Remove any auth credentials from the URL.
|
| 218 |
+
auth = (url.username, url.password)
|
| 219 |
+
url = url.copy_with(username=None, password=None)
|
| 220 |
+
|
| 221 |
+
self.url = url
|
| 222 |
+
self.auth = auth
|
| 223 |
+
self.headers = headers
|
| 224 |
+
self.ssl_context = ssl_context
|
| 225 |
+
|
| 226 |
+
@property
|
| 227 |
+
def raw_auth(self) -> tuple[bytes, bytes] | None:
|
| 228 |
+
# The proxy authentication as raw bytes.
|
| 229 |
+
return (
|
| 230 |
+
None
|
| 231 |
+
if self.auth is None
|
| 232 |
+
else (self.auth[0].encode("utf-8"), self.auth[1].encode("utf-8"))
|
| 233 |
+
)
|
| 234 |
+
|
| 235 |
+
def __repr__(self) -> str:
|
| 236 |
+
# The authentication is represented with the password component masked.
|
| 237 |
+
auth = (self.auth[0], "********") if self.auth else None
|
| 238 |
+
|
| 239 |
+
# Build a nice concise representation.
|
| 240 |
+
url_str = f"{str(self.url)!r}"
|
| 241 |
+
auth_str = f", auth={auth!r}" if auth else ""
|
| 242 |
+
headers_str = f", headers={dict(self.headers)!r}" if self.headers else ""
|
| 243 |
+
return f"Proxy({url_str}{auth_str}{headers_str})"
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
DEFAULT_TIMEOUT_CONFIG = Timeout(timeout=5.0)
|
| 247 |
+
DEFAULT_LIMITS = Limits(max_connections=100, max_keepalive_connections=20)
|
| 248 |
+
DEFAULT_MAX_REDIRECTS = 20
|
.venv/lib/python3.11/site-packages/httpx/_content.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import inspect
|
| 4 |
+
import warnings
|
| 5 |
+
from json import dumps as json_dumps
|
| 6 |
+
from typing import (
|
| 7 |
+
Any,
|
| 8 |
+
AsyncIterable,
|
| 9 |
+
AsyncIterator,
|
| 10 |
+
Iterable,
|
| 11 |
+
Iterator,
|
| 12 |
+
Mapping,
|
| 13 |
+
)
|
| 14 |
+
from urllib.parse import urlencode
|
| 15 |
+
|
| 16 |
+
from ._exceptions import StreamClosed, StreamConsumed
|
| 17 |
+
from ._multipart import MultipartStream
|
| 18 |
+
from ._types import (
|
| 19 |
+
AsyncByteStream,
|
| 20 |
+
RequestContent,
|
| 21 |
+
RequestData,
|
| 22 |
+
RequestFiles,
|
| 23 |
+
ResponseContent,
|
| 24 |
+
SyncByteStream,
|
| 25 |
+
)
|
| 26 |
+
from ._utils import peek_filelike_length, primitive_value_to_str
|
| 27 |
+
|
| 28 |
+
__all__ = ["ByteStream"]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class ByteStream(AsyncByteStream, SyncByteStream):
|
| 32 |
+
def __init__(self, stream: bytes) -> None:
|
| 33 |
+
self._stream = stream
|
| 34 |
+
|
| 35 |
+
def __iter__(self) -> Iterator[bytes]:
|
| 36 |
+
yield self._stream
|
| 37 |
+
|
| 38 |
+
async def __aiter__(self) -> AsyncIterator[bytes]:
|
| 39 |
+
yield self._stream
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class IteratorByteStream(SyncByteStream):
|
| 43 |
+
CHUNK_SIZE = 65_536
|
| 44 |
+
|
| 45 |
+
def __init__(self, stream: Iterable[bytes]) -> None:
|
| 46 |
+
self._stream = stream
|
| 47 |
+
self._is_stream_consumed = False
|
| 48 |
+
self._is_generator = inspect.isgenerator(stream)
|
| 49 |
+
|
| 50 |
+
def __iter__(self) -> Iterator[bytes]:
|
| 51 |
+
if self._is_stream_consumed and self._is_generator:
|
| 52 |
+
raise StreamConsumed()
|
| 53 |
+
|
| 54 |
+
self._is_stream_consumed = True
|
| 55 |
+
if hasattr(self._stream, "read"):
|
| 56 |
+
# File-like interfaces should use 'read' directly.
|
| 57 |
+
chunk = self._stream.read(self.CHUNK_SIZE)
|
| 58 |
+
while chunk:
|
| 59 |
+
yield chunk
|
| 60 |
+
chunk = self._stream.read(self.CHUNK_SIZE)
|
| 61 |
+
else:
|
| 62 |
+
# Otherwise iterate.
|
| 63 |
+
for part in self._stream:
|
| 64 |
+
yield part
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class AsyncIteratorByteStream(AsyncByteStream):
|
| 68 |
+
CHUNK_SIZE = 65_536
|
| 69 |
+
|
| 70 |
+
def __init__(self, stream: AsyncIterable[bytes]) -> None:
|
| 71 |
+
self._stream = stream
|
| 72 |
+
self._is_stream_consumed = False
|
| 73 |
+
self._is_generator = inspect.isasyncgen(stream)
|
| 74 |
+
|
| 75 |
+
async def __aiter__(self) -> AsyncIterator[bytes]:
|
| 76 |
+
if self._is_stream_consumed and self._is_generator:
|
| 77 |
+
raise StreamConsumed()
|
| 78 |
+
|
| 79 |
+
self._is_stream_consumed = True
|
| 80 |
+
if hasattr(self._stream, "aread"):
|
| 81 |
+
# File-like interfaces should use 'aread' directly.
|
| 82 |
+
chunk = await self._stream.aread(self.CHUNK_SIZE)
|
| 83 |
+
while chunk:
|
| 84 |
+
yield chunk
|
| 85 |
+
chunk = await self._stream.aread(self.CHUNK_SIZE)
|
| 86 |
+
else:
|
| 87 |
+
# Otherwise iterate.
|
| 88 |
+
async for part in self._stream:
|
| 89 |
+
yield part
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class UnattachedStream(AsyncByteStream, SyncByteStream):
|
| 93 |
+
"""
|
| 94 |
+
If a request or response is serialized using pickle, then it is no longer
|
| 95 |
+
attached to a stream for I/O purposes. Any stream operations should result
|
| 96 |
+
in `httpx.StreamClosed`.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
def __iter__(self) -> Iterator[bytes]:
|
| 100 |
+
raise StreamClosed()
|
| 101 |
+
|
| 102 |
+
async def __aiter__(self) -> AsyncIterator[bytes]:
|
| 103 |
+
raise StreamClosed()
|
| 104 |
+
yield b"" # pragma: no cover
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def encode_content(
|
| 108 |
+
content: str | bytes | Iterable[bytes] | AsyncIterable[bytes],
|
| 109 |
+
) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
|
| 110 |
+
if isinstance(content, (bytes, str)):
|
| 111 |
+
body = content.encode("utf-8") if isinstance(content, str) else content
|
| 112 |
+
content_length = len(body)
|
| 113 |
+
headers = {"Content-Length": str(content_length)} if body else {}
|
| 114 |
+
return headers, ByteStream(body)
|
| 115 |
+
|
| 116 |
+
elif isinstance(content, Iterable) and not isinstance(content, dict):
|
| 117 |
+
# `not isinstance(content, dict)` is a bit oddly specific, but it
|
| 118 |
+
# catches a case that's easy for users to make in error, and would
|
| 119 |
+
# otherwise pass through here, like any other bytes-iterable,
|
| 120 |
+
# because `dict` happens to be iterable. See issue #2491.
|
| 121 |
+
content_length_or_none = peek_filelike_length(content)
|
| 122 |
+
|
| 123 |
+
if content_length_or_none is None:
|
| 124 |
+
headers = {"Transfer-Encoding": "chunked"}
|
| 125 |
+
else:
|
| 126 |
+
headers = {"Content-Length": str(content_length_or_none)}
|
| 127 |
+
return headers, IteratorByteStream(content) # type: ignore
|
| 128 |
+
|
| 129 |
+
elif isinstance(content, AsyncIterable):
|
| 130 |
+
headers = {"Transfer-Encoding": "chunked"}
|
| 131 |
+
return headers, AsyncIteratorByteStream(content)
|
| 132 |
+
|
| 133 |
+
raise TypeError(f"Unexpected type for 'content', {type(content)!r}")
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def encode_urlencoded_data(
|
| 137 |
+
data: RequestData,
|
| 138 |
+
) -> tuple[dict[str, str], ByteStream]:
|
| 139 |
+
plain_data = []
|
| 140 |
+
for key, value in data.items():
|
| 141 |
+
if isinstance(value, (list, tuple)):
|
| 142 |
+
plain_data.extend([(key, primitive_value_to_str(item)) for item in value])
|
| 143 |
+
else:
|
| 144 |
+
plain_data.append((key, primitive_value_to_str(value)))
|
| 145 |
+
body = urlencode(plain_data, doseq=True).encode("utf-8")
|
| 146 |
+
content_length = str(len(body))
|
| 147 |
+
content_type = "application/x-www-form-urlencoded"
|
| 148 |
+
headers = {"Content-Length": content_length, "Content-Type": content_type}
|
| 149 |
+
return headers, ByteStream(body)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def encode_multipart_data(
|
| 153 |
+
data: RequestData, files: RequestFiles, boundary: bytes | None
|
| 154 |
+
) -> tuple[dict[str, str], MultipartStream]:
|
| 155 |
+
multipart = MultipartStream(data=data, files=files, boundary=boundary)
|
| 156 |
+
headers = multipart.get_headers()
|
| 157 |
+
return headers, multipart
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
def encode_text(text: str) -> tuple[dict[str, str], ByteStream]:
|
| 161 |
+
body = text.encode("utf-8")
|
| 162 |
+
content_length = str(len(body))
|
| 163 |
+
content_type = "text/plain; charset=utf-8"
|
| 164 |
+
headers = {"Content-Length": content_length, "Content-Type": content_type}
|
| 165 |
+
return headers, ByteStream(body)
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def encode_html(html: str) -> tuple[dict[str, str], ByteStream]:
|
| 169 |
+
body = html.encode("utf-8")
|
| 170 |
+
content_length = str(len(body))
|
| 171 |
+
content_type = "text/html; charset=utf-8"
|
| 172 |
+
headers = {"Content-Length": content_length, "Content-Type": content_type}
|
| 173 |
+
return headers, ByteStream(body)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def encode_json(json: Any) -> tuple[dict[str, str], ByteStream]:
|
| 177 |
+
body = json_dumps(
|
| 178 |
+
json, ensure_ascii=False, separators=(",", ":"), allow_nan=False
|
| 179 |
+
).encode("utf-8")
|
| 180 |
+
content_length = str(len(body))
|
| 181 |
+
content_type = "application/json"
|
| 182 |
+
headers = {"Content-Length": content_length, "Content-Type": content_type}
|
| 183 |
+
return headers, ByteStream(body)
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def encode_request(
|
| 187 |
+
content: RequestContent | None = None,
|
| 188 |
+
data: RequestData | None = None,
|
| 189 |
+
files: RequestFiles | None = None,
|
| 190 |
+
json: Any | None = None,
|
| 191 |
+
boundary: bytes | None = None,
|
| 192 |
+
) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
|
| 193 |
+
"""
|
| 194 |
+
Handles encoding the given `content`, `data`, `files`, and `json`,
|
| 195 |
+
returning a two-tuple of (<headers>, <stream>).
|
| 196 |
+
"""
|
| 197 |
+
if data is not None and not isinstance(data, Mapping):
|
| 198 |
+
# We prefer to separate `content=<bytes|str|byte iterator|bytes aiterator>`
|
| 199 |
+
# for raw request content, and `data=<form data>` for url encoded or
|
| 200 |
+
# multipart form content.
|
| 201 |
+
#
|
| 202 |
+
# However for compat with requests, we *do* still support
|
| 203 |
+
# `data=<bytes...>` usages. We deal with that case here, treating it
|
| 204 |
+
# as if `content=<...>` had been supplied instead.
|
| 205 |
+
message = "Use 'content=<...>' to upload raw bytes/text content."
|
| 206 |
+
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
| 207 |
+
return encode_content(data)
|
| 208 |
+
|
| 209 |
+
if content is not None:
|
| 210 |
+
return encode_content(content)
|
| 211 |
+
elif files:
|
| 212 |
+
return encode_multipart_data(data or {}, files, boundary)
|
| 213 |
+
elif data:
|
| 214 |
+
return encode_urlencoded_data(data)
|
| 215 |
+
elif json is not None:
|
| 216 |
+
return encode_json(json)
|
| 217 |
+
|
| 218 |
+
return {}, ByteStream(b"")
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def encode_response(
|
| 222 |
+
content: ResponseContent | None = None,
|
| 223 |
+
text: str | None = None,
|
| 224 |
+
html: str | None = None,
|
| 225 |
+
json: Any | None = None,
|
| 226 |
+
) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
|
| 227 |
+
"""
|
| 228 |
+
Handles encoding the given `content`, returning a two-tuple of
|
| 229 |
+
(<headers>, <stream>).
|
| 230 |
+
"""
|
| 231 |
+
if content is not None:
|
| 232 |
+
return encode_content(content)
|
| 233 |
+
elif text is not None:
|
| 234 |
+
return encode_text(text)
|
| 235 |
+
elif html is not None:
|
| 236 |
+
return encode_html(html)
|
| 237 |
+
elif json is not None:
|
| 238 |
+
return encode_json(json)
|
| 239 |
+
|
| 240 |
+
return {}, ByteStream(b"")
|
.venv/lib/python3.11/site-packages/httpx/_decoders.py
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Handlers for Content-Encoding.
|
| 3 |
+
|
| 4 |
+
See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import codecs
|
| 10 |
+
import io
|
| 11 |
+
import typing
|
| 12 |
+
import zlib
|
| 13 |
+
|
| 14 |
+
from ._exceptions import DecodingError
|
| 15 |
+
|
| 16 |
+
# Brotli support is optional
|
| 17 |
+
try:
|
| 18 |
+
# The C bindings in `brotli` are recommended for CPython.
|
| 19 |
+
import brotli
|
| 20 |
+
except ImportError: # pragma: no cover
|
| 21 |
+
try:
|
| 22 |
+
# The CFFI bindings in `brotlicffi` are recommended for PyPy
|
| 23 |
+
# and other environments.
|
| 24 |
+
import brotlicffi as brotli
|
| 25 |
+
except ImportError:
|
| 26 |
+
brotli = None
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# Zstandard support is optional
|
| 30 |
+
try:
|
| 31 |
+
import zstandard
|
| 32 |
+
except ImportError: # pragma: no cover
|
| 33 |
+
zstandard = None # type: ignore
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class ContentDecoder:
|
| 37 |
+
def decode(self, data: bytes) -> bytes:
|
| 38 |
+
raise NotImplementedError() # pragma: no cover
|
| 39 |
+
|
| 40 |
+
def flush(self) -> bytes:
|
| 41 |
+
raise NotImplementedError() # pragma: no cover
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class IdentityDecoder(ContentDecoder):
|
| 45 |
+
"""
|
| 46 |
+
Handle unencoded data.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def decode(self, data: bytes) -> bytes:
|
| 50 |
+
return data
|
| 51 |
+
|
| 52 |
+
def flush(self) -> bytes:
|
| 53 |
+
return b""
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class DeflateDecoder(ContentDecoder):
|
| 57 |
+
"""
|
| 58 |
+
Handle 'deflate' decoding.
|
| 59 |
+
|
| 60 |
+
See: https://stackoverflow.com/questions/1838699
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
def __init__(self) -> None:
|
| 64 |
+
self.first_attempt = True
|
| 65 |
+
self.decompressor = zlib.decompressobj()
|
| 66 |
+
|
| 67 |
+
def decode(self, data: bytes) -> bytes:
|
| 68 |
+
was_first_attempt = self.first_attempt
|
| 69 |
+
self.first_attempt = False
|
| 70 |
+
try:
|
| 71 |
+
return self.decompressor.decompress(data)
|
| 72 |
+
except zlib.error as exc:
|
| 73 |
+
if was_first_attempt:
|
| 74 |
+
self.decompressor = zlib.decompressobj(-zlib.MAX_WBITS)
|
| 75 |
+
return self.decode(data)
|
| 76 |
+
raise DecodingError(str(exc)) from exc
|
| 77 |
+
|
| 78 |
+
def flush(self) -> bytes:
|
| 79 |
+
try:
|
| 80 |
+
return self.decompressor.flush()
|
| 81 |
+
except zlib.error as exc: # pragma: no cover
|
| 82 |
+
raise DecodingError(str(exc)) from exc
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class GZipDecoder(ContentDecoder):
|
| 86 |
+
"""
|
| 87 |
+
Handle 'gzip' decoding.
|
| 88 |
+
|
| 89 |
+
See: https://stackoverflow.com/questions/1838699
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def __init__(self) -> None:
|
| 93 |
+
self.decompressor = zlib.decompressobj(zlib.MAX_WBITS | 16)
|
| 94 |
+
|
| 95 |
+
def decode(self, data: bytes) -> bytes:
|
| 96 |
+
try:
|
| 97 |
+
return self.decompressor.decompress(data)
|
| 98 |
+
except zlib.error as exc:
|
| 99 |
+
raise DecodingError(str(exc)) from exc
|
| 100 |
+
|
| 101 |
+
def flush(self) -> bytes:
|
| 102 |
+
try:
|
| 103 |
+
return self.decompressor.flush()
|
| 104 |
+
except zlib.error as exc: # pragma: no cover
|
| 105 |
+
raise DecodingError(str(exc)) from exc
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class BrotliDecoder(ContentDecoder):
|
| 109 |
+
"""
|
| 110 |
+
Handle 'brotli' decoding.
|
| 111 |
+
|
| 112 |
+
Requires `pip install brotlipy`. See: https://brotlipy.readthedocs.io/
|
| 113 |
+
or `pip install brotli`. See https://github.com/google/brotli
|
| 114 |
+
Supports both 'brotlipy' and 'Brotli' packages since they share an import
|
| 115 |
+
name. The top branches are for 'brotlipy' and bottom branches for 'Brotli'
|
| 116 |
+
"""
|
| 117 |
+
|
| 118 |
+
def __init__(self) -> None:
|
| 119 |
+
if brotli is None: # pragma: no cover
|
| 120 |
+
raise ImportError(
|
| 121 |
+
"Using 'BrotliDecoder', but neither of the 'brotlicffi' or 'brotli' "
|
| 122 |
+
"packages have been installed. "
|
| 123 |
+
"Make sure to install httpx using `pip install httpx[brotli]`."
|
| 124 |
+
) from None
|
| 125 |
+
|
| 126 |
+
self.decompressor = brotli.Decompressor()
|
| 127 |
+
self.seen_data = False
|
| 128 |
+
self._decompress: typing.Callable[[bytes], bytes]
|
| 129 |
+
if hasattr(self.decompressor, "decompress"):
|
| 130 |
+
# The 'brotlicffi' package.
|
| 131 |
+
self._decompress = self.decompressor.decompress # pragma: no cover
|
| 132 |
+
else:
|
| 133 |
+
# The 'brotli' package.
|
| 134 |
+
self._decompress = self.decompressor.process # pragma: no cover
|
| 135 |
+
|
| 136 |
+
def decode(self, data: bytes) -> bytes:
|
| 137 |
+
if not data:
|
| 138 |
+
return b""
|
| 139 |
+
self.seen_data = True
|
| 140 |
+
try:
|
| 141 |
+
return self._decompress(data)
|
| 142 |
+
except brotli.error as exc:
|
| 143 |
+
raise DecodingError(str(exc)) from exc
|
| 144 |
+
|
| 145 |
+
def flush(self) -> bytes:
|
| 146 |
+
if not self.seen_data:
|
| 147 |
+
return b""
|
| 148 |
+
try:
|
| 149 |
+
if hasattr(self.decompressor, "finish"):
|
| 150 |
+
# Only available in the 'brotlicffi' package.
|
| 151 |
+
|
| 152 |
+
# As the decompressor decompresses eagerly, this
|
| 153 |
+
# will never actually emit any data. However, it will potentially throw
|
| 154 |
+
# errors if a truncated or damaged data stream has been used.
|
| 155 |
+
self.decompressor.finish() # pragma: no cover
|
| 156 |
+
return b""
|
| 157 |
+
except brotli.error as exc: # pragma: no cover
|
| 158 |
+
raise DecodingError(str(exc)) from exc
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
class ZStandardDecoder(ContentDecoder):
|
| 162 |
+
"""
|
| 163 |
+
Handle 'zstd' RFC 8878 decoding.
|
| 164 |
+
|
| 165 |
+
Requires `pip install zstandard`.
|
| 166 |
+
Can be installed as a dependency of httpx using `pip install httpx[zstd]`.
|
| 167 |
+
"""
|
| 168 |
+
|
| 169 |
+
# inspired by the ZstdDecoder implementation in urllib3
|
| 170 |
+
def __init__(self) -> None:
|
| 171 |
+
if zstandard is None: # pragma: no cover
|
| 172 |
+
raise ImportError(
|
| 173 |
+
"Using 'ZStandardDecoder', ..."
|
| 174 |
+
"Make sure to install httpx using `pip install httpx[zstd]`."
|
| 175 |
+
) from None
|
| 176 |
+
|
| 177 |
+
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
|
| 178 |
+
self.seen_data = False
|
| 179 |
+
|
| 180 |
+
def decode(self, data: bytes) -> bytes:
|
| 181 |
+
assert zstandard is not None
|
| 182 |
+
self.seen_data = True
|
| 183 |
+
output = io.BytesIO()
|
| 184 |
+
try:
|
| 185 |
+
output.write(self.decompressor.decompress(data))
|
| 186 |
+
while self.decompressor.eof and self.decompressor.unused_data:
|
| 187 |
+
unused_data = self.decompressor.unused_data
|
| 188 |
+
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
|
| 189 |
+
output.write(self.decompressor.decompress(unused_data))
|
| 190 |
+
except zstandard.ZstdError as exc:
|
| 191 |
+
raise DecodingError(str(exc)) from exc
|
| 192 |
+
return output.getvalue()
|
| 193 |
+
|
| 194 |
+
def flush(self) -> bytes:
|
| 195 |
+
if not self.seen_data:
|
| 196 |
+
return b""
|
| 197 |
+
ret = self.decompressor.flush() # note: this is a no-op
|
| 198 |
+
if not self.decompressor.eof:
|
| 199 |
+
raise DecodingError("Zstandard data is incomplete") # pragma: no cover
|
| 200 |
+
return bytes(ret)
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class MultiDecoder(ContentDecoder):
|
| 204 |
+
"""
|
| 205 |
+
Handle the case where multiple encodings have been applied.
|
| 206 |
+
"""
|
| 207 |
+
|
| 208 |
+
def __init__(self, children: typing.Sequence[ContentDecoder]) -> None:
|
| 209 |
+
"""
|
| 210 |
+
'children' should be a sequence of decoders in the order in which
|
| 211 |
+
each was applied.
|
| 212 |
+
"""
|
| 213 |
+
# Note that we reverse the order for decoding.
|
| 214 |
+
self.children = list(reversed(children))
|
| 215 |
+
|
| 216 |
+
def decode(self, data: bytes) -> bytes:
|
| 217 |
+
for child in self.children:
|
| 218 |
+
data = child.decode(data)
|
| 219 |
+
return data
|
| 220 |
+
|
| 221 |
+
def flush(self) -> bytes:
|
| 222 |
+
data = b""
|
| 223 |
+
for child in self.children:
|
| 224 |
+
data = child.decode(data) + child.flush()
|
| 225 |
+
return data
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
class ByteChunker:
|
| 229 |
+
"""
|
| 230 |
+
Handles returning byte content in fixed-size chunks.
|
| 231 |
+
"""
|
| 232 |
+
|
| 233 |
+
def __init__(self, chunk_size: int | None = None) -> None:
|
| 234 |
+
self._buffer = io.BytesIO()
|
| 235 |
+
self._chunk_size = chunk_size
|
| 236 |
+
|
| 237 |
+
def decode(self, content: bytes) -> list[bytes]:
|
| 238 |
+
if self._chunk_size is None:
|
| 239 |
+
return [content] if content else []
|
| 240 |
+
|
| 241 |
+
self._buffer.write(content)
|
| 242 |
+
if self._buffer.tell() >= self._chunk_size:
|
| 243 |
+
value = self._buffer.getvalue()
|
| 244 |
+
chunks = [
|
| 245 |
+
value[i : i + self._chunk_size]
|
| 246 |
+
for i in range(0, len(value), self._chunk_size)
|
| 247 |
+
]
|
| 248 |
+
if len(chunks[-1]) == self._chunk_size:
|
| 249 |
+
self._buffer.seek(0)
|
| 250 |
+
self._buffer.truncate()
|
| 251 |
+
return chunks
|
| 252 |
+
else:
|
| 253 |
+
self._buffer.seek(0)
|
| 254 |
+
self._buffer.write(chunks[-1])
|
| 255 |
+
self._buffer.truncate()
|
| 256 |
+
return chunks[:-1]
|
| 257 |
+
else:
|
| 258 |
+
return []
|
| 259 |
+
|
| 260 |
+
def flush(self) -> list[bytes]:
|
| 261 |
+
value = self._buffer.getvalue()
|
| 262 |
+
self._buffer.seek(0)
|
| 263 |
+
self._buffer.truncate()
|
| 264 |
+
return [value] if value else []
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
class TextChunker:
|
| 268 |
+
"""
|
| 269 |
+
Handles returning text content in fixed-size chunks.
|
| 270 |
+
"""
|
| 271 |
+
|
| 272 |
+
def __init__(self, chunk_size: int | None = None) -> None:
|
| 273 |
+
self._buffer = io.StringIO()
|
| 274 |
+
self._chunk_size = chunk_size
|
| 275 |
+
|
| 276 |
+
def decode(self, content: str) -> list[str]:
|
| 277 |
+
if self._chunk_size is None:
|
| 278 |
+
return [content] if content else []
|
| 279 |
+
|
| 280 |
+
self._buffer.write(content)
|
| 281 |
+
if self._buffer.tell() >= self._chunk_size:
|
| 282 |
+
value = self._buffer.getvalue()
|
| 283 |
+
chunks = [
|
| 284 |
+
value[i : i + self._chunk_size]
|
| 285 |
+
for i in range(0, len(value), self._chunk_size)
|
| 286 |
+
]
|
| 287 |
+
if len(chunks[-1]) == self._chunk_size:
|
| 288 |
+
self._buffer.seek(0)
|
| 289 |
+
self._buffer.truncate()
|
| 290 |
+
return chunks
|
| 291 |
+
else:
|
| 292 |
+
self._buffer.seek(0)
|
| 293 |
+
self._buffer.write(chunks[-1])
|
| 294 |
+
self._buffer.truncate()
|
| 295 |
+
return chunks[:-1]
|
| 296 |
+
else:
|
| 297 |
+
return []
|
| 298 |
+
|
| 299 |
+
def flush(self) -> list[str]:
|
| 300 |
+
value = self._buffer.getvalue()
|
| 301 |
+
self._buffer.seek(0)
|
| 302 |
+
self._buffer.truncate()
|
| 303 |
+
return [value] if value else []
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
class TextDecoder:
|
| 307 |
+
"""
|
| 308 |
+
Handles incrementally decoding bytes into text
|
| 309 |
+
"""
|
| 310 |
+
|
| 311 |
+
def __init__(self, encoding: str = "utf-8") -> None:
|
| 312 |
+
self.decoder = codecs.getincrementaldecoder(encoding)(errors="replace")
|
| 313 |
+
|
| 314 |
+
def decode(self, data: bytes) -> str:
|
| 315 |
+
return self.decoder.decode(data)
|
| 316 |
+
|
| 317 |
+
def flush(self) -> str:
|
| 318 |
+
return self.decoder.decode(b"", True)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
class LineDecoder:
|
| 322 |
+
"""
|
| 323 |
+
Handles incrementally reading lines from text.
|
| 324 |
+
|
| 325 |
+
Has the same behaviour as the stdllib splitlines,
|
| 326 |
+
but handling the input iteratively.
|
| 327 |
+
"""
|
| 328 |
+
|
| 329 |
+
def __init__(self) -> None:
|
| 330 |
+
self.buffer: list[str] = []
|
| 331 |
+
self.trailing_cr: bool = False
|
| 332 |
+
|
| 333 |
+
def decode(self, text: str) -> list[str]:
|
| 334 |
+
# See https://docs.python.org/3/library/stdtypes.html#str.splitlines
|
| 335 |
+
NEWLINE_CHARS = "\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029"
|
| 336 |
+
|
| 337 |
+
# We always push a trailing `\r` into the next decode iteration.
|
| 338 |
+
if self.trailing_cr:
|
| 339 |
+
text = "\r" + text
|
| 340 |
+
self.trailing_cr = False
|
| 341 |
+
if text.endswith("\r"):
|
| 342 |
+
self.trailing_cr = True
|
| 343 |
+
text = text[:-1]
|
| 344 |
+
|
| 345 |
+
if not text:
|
| 346 |
+
# NOTE: the edge case input of empty text doesn't occur in practice,
|
| 347 |
+
# because other httpx internals filter out this value
|
| 348 |
+
return [] # pragma: no cover
|
| 349 |
+
|
| 350 |
+
trailing_newline = text[-1] in NEWLINE_CHARS
|
| 351 |
+
lines = text.splitlines()
|
| 352 |
+
|
| 353 |
+
if len(lines) == 1 and not trailing_newline:
|
| 354 |
+
# No new lines, buffer the input and continue.
|
| 355 |
+
self.buffer.append(lines[0])
|
| 356 |
+
return []
|
| 357 |
+
|
| 358 |
+
if self.buffer:
|
| 359 |
+
# Include any existing buffer in the first portion of the
|
| 360 |
+
# splitlines result.
|
| 361 |
+
lines = ["".join(self.buffer) + lines[0]] + lines[1:]
|
| 362 |
+
self.buffer = []
|
| 363 |
+
|
| 364 |
+
if not trailing_newline:
|
| 365 |
+
# If the last segment of splitlines is not newline terminated,
|
| 366 |
+
# then drop it from our output and start a new buffer.
|
| 367 |
+
self.buffer = [lines.pop()]
|
| 368 |
+
|
| 369 |
+
return lines
|
| 370 |
+
|
| 371 |
+
def flush(self) -> list[str]:
|
| 372 |
+
if not self.buffer and not self.trailing_cr:
|
| 373 |
+
return []
|
| 374 |
+
|
| 375 |
+
lines = ["".join(self.buffer)]
|
| 376 |
+
self.buffer = []
|
| 377 |
+
self.trailing_cr = False
|
| 378 |
+
return lines
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
SUPPORTED_DECODERS = {
|
| 382 |
+
"identity": IdentityDecoder,
|
| 383 |
+
"gzip": GZipDecoder,
|
| 384 |
+
"deflate": DeflateDecoder,
|
| 385 |
+
"br": BrotliDecoder,
|
| 386 |
+
"zstd": ZStandardDecoder,
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
if brotli is None:
|
| 391 |
+
SUPPORTED_DECODERS.pop("br") # pragma: no cover
|
| 392 |
+
if zstandard is None:
|
| 393 |
+
SUPPORTED_DECODERS.pop("zstd") # pragma: no cover
|
.venv/lib/python3.11/site-packages/httpx/_exceptions.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Our exception hierarchy:
|
| 3 |
+
|
| 4 |
+
* HTTPError
|
| 5 |
+
x RequestError
|
| 6 |
+
+ TransportError
|
| 7 |
+
- TimeoutException
|
| 8 |
+
· ConnectTimeout
|
| 9 |
+
· ReadTimeout
|
| 10 |
+
· WriteTimeout
|
| 11 |
+
· PoolTimeout
|
| 12 |
+
- NetworkError
|
| 13 |
+
· ConnectError
|
| 14 |
+
· ReadError
|
| 15 |
+
· WriteError
|
| 16 |
+
· CloseError
|
| 17 |
+
- ProtocolError
|
| 18 |
+
· LocalProtocolError
|
| 19 |
+
· RemoteProtocolError
|
| 20 |
+
- ProxyError
|
| 21 |
+
- UnsupportedProtocol
|
| 22 |
+
+ DecodingError
|
| 23 |
+
+ TooManyRedirects
|
| 24 |
+
x HTTPStatusError
|
| 25 |
+
* InvalidURL
|
| 26 |
+
* CookieConflict
|
| 27 |
+
* StreamError
|
| 28 |
+
x StreamConsumed
|
| 29 |
+
x StreamClosed
|
| 30 |
+
x ResponseNotRead
|
| 31 |
+
x RequestNotRead
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
from __future__ import annotations
|
| 35 |
+
|
| 36 |
+
import contextlib
|
| 37 |
+
import typing
|
| 38 |
+
|
| 39 |
+
if typing.TYPE_CHECKING:
|
| 40 |
+
from ._models import Request, Response # pragma: no cover
|
| 41 |
+
|
| 42 |
+
__all__ = [
|
| 43 |
+
"CloseError",
|
| 44 |
+
"ConnectError",
|
| 45 |
+
"ConnectTimeout",
|
| 46 |
+
"CookieConflict",
|
| 47 |
+
"DecodingError",
|
| 48 |
+
"HTTPError",
|
| 49 |
+
"HTTPStatusError",
|
| 50 |
+
"InvalidURL",
|
| 51 |
+
"LocalProtocolError",
|
| 52 |
+
"NetworkError",
|
| 53 |
+
"PoolTimeout",
|
| 54 |
+
"ProtocolError",
|
| 55 |
+
"ProxyError",
|
| 56 |
+
"ReadError",
|
| 57 |
+
"ReadTimeout",
|
| 58 |
+
"RemoteProtocolError",
|
| 59 |
+
"RequestError",
|
| 60 |
+
"RequestNotRead",
|
| 61 |
+
"ResponseNotRead",
|
| 62 |
+
"StreamClosed",
|
| 63 |
+
"StreamConsumed",
|
| 64 |
+
"StreamError",
|
| 65 |
+
"TimeoutException",
|
| 66 |
+
"TooManyRedirects",
|
| 67 |
+
"TransportError",
|
| 68 |
+
"UnsupportedProtocol",
|
| 69 |
+
"WriteError",
|
| 70 |
+
"WriteTimeout",
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class HTTPError(Exception):
|
| 75 |
+
"""
|
| 76 |
+
Base class for `RequestError` and `HTTPStatusError`.
|
| 77 |
+
|
| 78 |
+
Useful for `try...except` blocks when issuing a request,
|
| 79 |
+
and then calling `.raise_for_status()`.
|
| 80 |
+
|
| 81 |
+
For example:
|
| 82 |
+
|
| 83 |
+
```
|
| 84 |
+
try:
|
| 85 |
+
response = httpx.get("https://www.example.com")
|
| 86 |
+
response.raise_for_status()
|
| 87 |
+
except httpx.HTTPError as exc:
|
| 88 |
+
print(f"HTTP Exception for {exc.request.url} - {exc}")
|
| 89 |
+
```
|
| 90 |
+
"""
|
| 91 |
+
|
| 92 |
+
def __init__(self, message: str) -> None:
|
| 93 |
+
super().__init__(message)
|
| 94 |
+
self._request: Request | None = None
|
| 95 |
+
|
| 96 |
+
@property
|
| 97 |
+
def request(self) -> Request:
|
| 98 |
+
if self._request is None:
|
| 99 |
+
raise RuntimeError("The .request property has not been set.")
|
| 100 |
+
return self._request
|
| 101 |
+
|
| 102 |
+
@request.setter
|
| 103 |
+
def request(self, request: Request) -> None:
|
| 104 |
+
self._request = request
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class RequestError(HTTPError):
|
| 108 |
+
"""
|
| 109 |
+
Base class for all exceptions that may occur when issuing a `.request()`.
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
def __init__(self, message: str, *, request: Request | None = None) -> None:
|
| 113 |
+
super().__init__(message)
|
| 114 |
+
# At the point an exception is raised we won't typically have a request
|
| 115 |
+
# instance to associate it with.
|
| 116 |
+
#
|
| 117 |
+
# The 'request_context' context manager is used within the Client and
|
| 118 |
+
# Response methods in order to ensure that any raised exceptions
|
| 119 |
+
# have a `.request` property set on them.
|
| 120 |
+
self._request = request
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
class TransportError(RequestError):
|
| 124 |
+
"""
|
| 125 |
+
Base class for all exceptions that occur at the level of the Transport API.
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
# Timeout exceptions...
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
class TimeoutException(TransportError):
|
| 133 |
+
"""
|
| 134 |
+
The base class for timeout errors.
|
| 135 |
+
|
| 136 |
+
An operation has timed out.
|
| 137 |
+
"""
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class ConnectTimeout(TimeoutException):
|
| 141 |
+
"""
|
| 142 |
+
Timed out while connecting to the host.
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class ReadTimeout(TimeoutException):
|
| 147 |
+
"""
|
| 148 |
+
Timed out while receiving data from the host.
|
| 149 |
+
"""
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class WriteTimeout(TimeoutException):
|
| 153 |
+
"""
|
| 154 |
+
Timed out while sending data to the host.
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class PoolTimeout(TimeoutException):
|
| 159 |
+
"""
|
| 160 |
+
Timed out waiting to acquire a connection from the pool.
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
# Core networking exceptions...
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
class NetworkError(TransportError):
|
| 168 |
+
"""
|
| 169 |
+
The base class for network-related errors.
|
| 170 |
+
|
| 171 |
+
An error occurred while interacting with the network.
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
class ReadError(NetworkError):
|
| 176 |
+
"""
|
| 177 |
+
Failed to receive data from the network.
|
| 178 |
+
"""
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class WriteError(NetworkError):
|
| 182 |
+
"""
|
| 183 |
+
Failed to send data through the network.
|
| 184 |
+
"""
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
class ConnectError(NetworkError):
|
| 188 |
+
"""
|
| 189 |
+
Failed to establish a connection.
|
| 190 |
+
"""
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
class CloseError(NetworkError):
|
| 194 |
+
"""
|
| 195 |
+
Failed to close a connection.
|
| 196 |
+
"""
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
# Other transport exceptions...
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
class ProxyError(TransportError):
|
| 203 |
+
"""
|
| 204 |
+
An error occurred while establishing a proxy connection.
|
| 205 |
+
"""
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
class UnsupportedProtocol(TransportError):
|
| 209 |
+
"""
|
| 210 |
+
Attempted to make a request to an unsupported protocol.
|
| 211 |
+
|
| 212 |
+
For example issuing a request to `ftp://www.example.com`.
|
| 213 |
+
"""
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
class ProtocolError(TransportError):
|
| 217 |
+
"""
|
| 218 |
+
The protocol was violated.
|
| 219 |
+
"""
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
class LocalProtocolError(ProtocolError):
|
| 223 |
+
"""
|
| 224 |
+
A protocol was violated by the client.
|
| 225 |
+
|
| 226 |
+
For example if the user instantiated a `Request` instance explicitly,
|
| 227 |
+
failed to include the mandatory `Host:` header, and then issued it directly
|
| 228 |
+
using `client.send()`.
|
| 229 |
+
"""
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class RemoteProtocolError(ProtocolError):
|
| 233 |
+
"""
|
| 234 |
+
The protocol was violated by the server.
|
| 235 |
+
|
| 236 |
+
For example, returning malformed HTTP.
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
# Other request exceptions...
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
class DecodingError(RequestError):
|
| 244 |
+
"""
|
| 245 |
+
Decoding of the response failed, due to a malformed encoding.
|
| 246 |
+
"""
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
class TooManyRedirects(RequestError):
|
| 250 |
+
"""
|
| 251 |
+
Too many redirects.
|
| 252 |
+
"""
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
# Client errors
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
class HTTPStatusError(HTTPError):
|
| 259 |
+
"""
|
| 260 |
+
The response had an error HTTP status of 4xx or 5xx.
|
| 261 |
+
|
| 262 |
+
May be raised when calling `response.raise_for_status()`
|
| 263 |
+
"""
|
| 264 |
+
|
| 265 |
+
def __init__(self, message: str, *, request: Request, response: Response) -> None:
|
| 266 |
+
super().__init__(message)
|
| 267 |
+
self.request = request
|
| 268 |
+
self.response = response
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
class InvalidURL(Exception):
|
| 272 |
+
"""
|
| 273 |
+
URL is improperly formed or cannot be parsed.
|
| 274 |
+
"""
|
| 275 |
+
|
| 276 |
+
def __init__(self, message: str) -> None:
|
| 277 |
+
super().__init__(message)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
class CookieConflict(Exception):
|
| 281 |
+
"""
|
| 282 |
+
Attempted to lookup a cookie by name, but multiple cookies existed.
|
| 283 |
+
|
| 284 |
+
Can occur when calling `response.cookies.get(...)`.
|
| 285 |
+
"""
|
| 286 |
+
|
| 287 |
+
def __init__(self, message: str) -> None:
|
| 288 |
+
super().__init__(message)
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
# Stream exceptions...
|
| 292 |
+
|
| 293 |
+
# These may occur as the result of a programming error, by accessing
|
| 294 |
+
# the request/response stream in an invalid manner.
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
class StreamError(RuntimeError):
|
| 298 |
+
"""
|
| 299 |
+
The base class for stream exceptions.
|
| 300 |
+
|
| 301 |
+
The developer made an error in accessing the request stream in
|
| 302 |
+
an invalid way.
|
| 303 |
+
"""
|
| 304 |
+
|
| 305 |
+
def __init__(self, message: str) -> None:
|
| 306 |
+
super().__init__(message)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
class StreamConsumed(StreamError):
|
| 310 |
+
"""
|
| 311 |
+
Attempted to read or stream content, but the content has already
|
| 312 |
+
been streamed.
|
| 313 |
+
"""
|
| 314 |
+
|
| 315 |
+
def __init__(self) -> None:
|
| 316 |
+
message = (
|
| 317 |
+
"Attempted to read or stream some content, but the content has "
|
| 318 |
+
"already been streamed. For requests, this could be due to passing "
|
| 319 |
+
"a generator as request content, and then receiving a redirect "
|
| 320 |
+
"response or a secondary request as part of an authentication flow."
|
| 321 |
+
"For responses, this could be due to attempting to stream the response "
|
| 322 |
+
"content more than once."
|
| 323 |
+
)
|
| 324 |
+
super().__init__(message)
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
class StreamClosed(StreamError):
|
| 328 |
+
"""
|
| 329 |
+
Attempted to read or stream response content, but the request has been
|
| 330 |
+
closed.
|
| 331 |
+
"""
|
| 332 |
+
|
| 333 |
+
def __init__(self) -> None:
|
| 334 |
+
message = (
|
| 335 |
+
"Attempted to read or stream content, but the stream has " "been closed."
|
| 336 |
+
)
|
| 337 |
+
super().__init__(message)
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
class ResponseNotRead(StreamError):
|
| 341 |
+
"""
|
| 342 |
+
Attempted to access streaming response content, without having called `read()`.
|
| 343 |
+
"""
|
| 344 |
+
|
| 345 |
+
def __init__(self) -> None:
|
| 346 |
+
message = (
|
| 347 |
+
"Attempted to access streaming response content,"
|
| 348 |
+
" without having called `read()`."
|
| 349 |
+
)
|
| 350 |
+
super().__init__(message)
|
| 351 |
+
|
| 352 |
+
|
| 353 |
+
class RequestNotRead(StreamError):
|
| 354 |
+
"""
|
| 355 |
+
Attempted to access streaming request content, without having called `read()`.
|
| 356 |
+
"""
|
| 357 |
+
|
| 358 |
+
def __init__(self) -> None:
|
| 359 |
+
message = (
|
| 360 |
+
"Attempted to access streaming request content,"
|
| 361 |
+
" without having called `read()`."
|
| 362 |
+
)
|
| 363 |
+
super().__init__(message)
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
@contextlib.contextmanager
|
| 367 |
+
def request_context(
|
| 368 |
+
request: Request | None = None,
|
| 369 |
+
) -> typing.Iterator[None]:
|
| 370 |
+
"""
|
| 371 |
+
A context manager that can be used to attach the given request context
|
| 372 |
+
to any `RequestError` exceptions that are raised within the block.
|
| 373 |
+
"""
|
| 374 |
+
try:
|
| 375 |
+
yield
|
| 376 |
+
except RequestError as exc:
|
| 377 |
+
if request is not None:
|
| 378 |
+
exc.request = request
|
| 379 |
+
raise exc
|
.venv/lib/python3.11/site-packages/httpx/_main.py
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import functools
|
| 4 |
+
import json
|
| 5 |
+
import sys
|
| 6 |
+
import typing
|
| 7 |
+
|
| 8 |
+
import click
|
| 9 |
+
import pygments.lexers
|
| 10 |
+
import pygments.util
|
| 11 |
+
import rich.console
|
| 12 |
+
import rich.markup
|
| 13 |
+
import rich.progress
|
| 14 |
+
import rich.syntax
|
| 15 |
+
import rich.table
|
| 16 |
+
|
| 17 |
+
from ._client import Client
|
| 18 |
+
from ._exceptions import RequestError
|
| 19 |
+
from ._models import Response
|
| 20 |
+
from ._status_codes import codes
|
| 21 |
+
|
| 22 |
+
if typing.TYPE_CHECKING:
|
| 23 |
+
import httpcore # pragma: no cover
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def print_help() -> None:
|
| 27 |
+
console = rich.console.Console()
|
| 28 |
+
|
| 29 |
+
console.print("[bold]HTTPX :butterfly:", justify="center")
|
| 30 |
+
console.print()
|
| 31 |
+
console.print("A next generation HTTP client.", justify="center")
|
| 32 |
+
console.print()
|
| 33 |
+
console.print(
|
| 34 |
+
"Usage: [bold]httpx[/bold] [cyan]<URL> [OPTIONS][/cyan] ", justify="left"
|
| 35 |
+
)
|
| 36 |
+
console.print()
|
| 37 |
+
|
| 38 |
+
table = rich.table.Table.grid(padding=1, pad_edge=True)
|
| 39 |
+
table.add_column("Parameter", no_wrap=True, justify="left", style="bold")
|
| 40 |
+
table.add_column("Description")
|
| 41 |
+
table.add_row(
|
| 42 |
+
"-m, --method [cyan]METHOD",
|
| 43 |
+
"Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.\n"
|
| 44 |
+
"[Default: GET, or POST if a request body is included]",
|
| 45 |
+
)
|
| 46 |
+
table.add_row(
|
| 47 |
+
"-p, --params [cyan]<NAME VALUE> ...",
|
| 48 |
+
"Query parameters to include in the request URL.",
|
| 49 |
+
)
|
| 50 |
+
table.add_row(
|
| 51 |
+
"-c, --content [cyan]TEXT", "Byte content to include in the request body."
|
| 52 |
+
)
|
| 53 |
+
table.add_row(
|
| 54 |
+
"-d, --data [cyan]<NAME VALUE> ...", "Form data to include in the request body."
|
| 55 |
+
)
|
| 56 |
+
table.add_row(
|
| 57 |
+
"-f, --files [cyan]<NAME FILENAME> ...",
|
| 58 |
+
"Form files to include in the request body.",
|
| 59 |
+
)
|
| 60 |
+
table.add_row("-j, --json [cyan]TEXT", "JSON data to include in the request body.")
|
| 61 |
+
table.add_row(
|
| 62 |
+
"-h, --headers [cyan]<NAME VALUE> ...",
|
| 63 |
+
"Include additional HTTP headers in the request.",
|
| 64 |
+
)
|
| 65 |
+
table.add_row(
|
| 66 |
+
"--cookies [cyan]<NAME VALUE> ...", "Cookies to include in the request."
|
| 67 |
+
)
|
| 68 |
+
table.add_row(
|
| 69 |
+
"--auth [cyan]<USER PASS>",
|
| 70 |
+
"Username and password to include in the request. Specify '-' for the password"
|
| 71 |
+
" to use a password prompt. Note that using --verbose/-v will expose"
|
| 72 |
+
" the Authorization header, including the password encoding"
|
| 73 |
+
" in a trivially reversible format.",
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
table.add_row(
|
| 77 |
+
"--proxy [cyan]URL",
|
| 78 |
+
"Send the request via a proxy. Should be the URL giving the proxy address.",
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
table.add_row(
|
| 82 |
+
"--timeout [cyan]FLOAT",
|
| 83 |
+
"Timeout value to use for network operations, such as establishing the"
|
| 84 |
+
" connection, reading some data, etc... [Default: 5.0]",
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
table.add_row("--follow-redirects", "Automatically follow redirects.")
|
| 88 |
+
table.add_row("--no-verify", "Disable SSL verification.")
|
| 89 |
+
table.add_row(
|
| 90 |
+
"--http2", "Send the request using HTTP/2, if the remote server supports it."
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
table.add_row(
|
| 94 |
+
"--download [cyan]FILE",
|
| 95 |
+
"Save the response content as a file, rather than displaying it.",
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
table.add_row("-v, --verbose", "Verbose output. Show request as well as response.")
|
| 99 |
+
table.add_row("--help", "Show this message and exit.")
|
| 100 |
+
console.print(table)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def get_lexer_for_response(response: Response) -> str:
|
| 104 |
+
content_type = response.headers.get("Content-Type")
|
| 105 |
+
if content_type is not None:
|
| 106 |
+
mime_type, _, _ = content_type.partition(";")
|
| 107 |
+
try:
|
| 108 |
+
return typing.cast(
|
| 109 |
+
str, pygments.lexers.get_lexer_for_mimetype(mime_type.strip()).name
|
| 110 |
+
)
|
| 111 |
+
except pygments.util.ClassNotFound: # pragma: no cover
|
| 112 |
+
pass
|
| 113 |
+
return "" # pragma: no cover
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def format_request_headers(request: httpcore.Request, http2: bool = False) -> str:
|
| 117 |
+
version = "HTTP/2" if http2 else "HTTP/1.1"
|
| 118 |
+
headers = [
|
| 119 |
+
(name.lower() if http2 else name, value) for name, value in request.headers
|
| 120 |
+
]
|
| 121 |
+
method = request.method.decode("ascii")
|
| 122 |
+
target = request.url.target.decode("ascii")
|
| 123 |
+
lines = [f"{method} {target} {version}"] + [
|
| 124 |
+
f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers
|
| 125 |
+
]
|
| 126 |
+
return "\n".join(lines)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def format_response_headers(
|
| 130 |
+
http_version: bytes,
|
| 131 |
+
status: int,
|
| 132 |
+
reason_phrase: bytes | None,
|
| 133 |
+
headers: list[tuple[bytes, bytes]],
|
| 134 |
+
) -> str:
|
| 135 |
+
version = http_version.decode("ascii")
|
| 136 |
+
reason = (
|
| 137 |
+
codes.get_reason_phrase(status)
|
| 138 |
+
if reason_phrase is None
|
| 139 |
+
else reason_phrase.decode("ascii")
|
| 140 |
+
)
|
| 141 |
+
lines = [f"{version} {status} {reason}"] + [
|
| 142 |
+
f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers
|
| 143 |
+
]
|
| 144 |
+
return "\n".join(lines)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def print_request_headers(request: httpcore.Request, http2: bool = False) -> None:
|
| 148 |
+
console = rich.console.Console()
|
| 149 |
+
http_text = format_request_headers(request, http2=http2)
|
| 150 |
+
syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True)
|
| 151 |
+
console.print(syntax)
|
| 152 |
+
syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True)
|
| 153 |
+
console.print(syntax)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def print_response_headers(
|
| 157 |
+
http_version: bytes,
|
| 158 |
+
status: int,
|
| 159 |
+
reason_phrase: bytes | None,
|
| 160 |
+
headers: list[tuple[bytes, bytes]],
|
| 161 |
+
) -> None:
|
| 162 |
+
console = rich.console.Console()
|
| 163 |
+
http_text = format_response_headers(http_version, status, reason_phrase, headers)
|
| 164 |
+
syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True)
|
| 165 |
+
console.print(syntax)
|
| 166 |
+
syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True)
|
| 167 |
+
console.print(syntax)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def print_response(response: Response) -> None:
|
| 171 |
+
console = rich.console.Console()
|
| 172 |
+
lexer_name = get_lexer_for_response(response)
|
| 173 |
+
if lexer_name:
|
| 174 |
+
if lexer_name.lower() == "json":
|
| 175 |
+
try:
|
| 176 |
+
data = response.json()
|
| 177 |
+
text = json.dumps(data, indent=4)
|
| 178 |
+
except ValueError: # pragma: no cover
|
| 179 |
+
text = response.text
|
| 180 |
+
else:
|
| 181 |
+
text = response.text
|
| 182 |
+
|
| 183 |
+
syntax = rich.syntax.Syntax(text, lexer_name, theme="ansi_dark", word_wrap=True)
|
| 184 |
+
console.print(syntax)
|
| 185 |
+
else:
|
| 186 |
+
console.print(f"<{len(response.content)} bytes of binary data>")
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
_PCTRTT = typing.Tuple[typing.Tuple[str, str], ...]
|
| 190 |
+
_PCTRTTT = typing.Tuple[_PCTRTT, ...]
|
| 191 |
+
_PeerCertRetDictType = typing.Dict[str, typing.Union[str, _PCTRTTT, _PCTRTT]]
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def format_certificate(cert: _PeerCertRetDictType) -> str: # pragma: no cover
|
| 195 |
+
lines = []
|
| 196 |
+
for key, value in cert.items():
|
| 197 |
+
if isinstance(value, (list, tuple)):
|
| 198 |
+
lines.append(f"* {key}:")
|
| 199 |
+
for item in value:
|
| 200 |
+
if key in ("subject", "issuer"):
|
| 201 |
+
for sub_item in item:
|
| 202 |
+
lines.append(f"* {sub_item[0]}: {sub_item[1]!r}")
|
| 203 |
+
elif isinstance(item, tuple) and len(item) == 2:
|
| 204 |
+
lines.append(f"* {item[0]}: {item[1]!r}")
|
| 205 |
+
else:
|
| 206 |
+
lines.append(f"* {item!r}")
|
| 207 |
+
else:
|
| 208 |
+
lines.append(f"* {key}: {value!r}")
|
| 209 |
+
return "\n".join(lines)
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def trace(
|
| 213 |
+
name: str, info: typing.Mapping[str, typing.Any], verbose: bool = False
|
| 214 |
+
) -> None:
|
| 215 |
+
console = rich.console.Console()
|
| 216 |
+
if name == "connection.connect_tcp.started" and verbose:
|
| 217 |
+
host = info["host"]
|
| 218 |
+
console.print(f"* Connecting to {host!r}")
|
| 219 |
+
elif name == "connection.connect_tcp.complete" and verbose:
|
| 220 |
+
stream = info["return_value"]
|
| 221 |
+
server_addr = stream.get_extra_info("server_addr")
|
| 222 |
+
console.print(f"* Connected to {server_addr[0]!r} on port {server_addr[1]}")
|
| 223 |
+
elif name == "connection.start_tls.complete" and verbose: # pragma: no cover
|
| 224 |
+
stream = info["return_value"]
|
| 225 |
+
ssl_object = stream.get_extra_info("ssl_object")
|
| 226 |
+
version = ssl_object.version()
|
| 227 |
+
cipher = ssl_object.cipher()
|
| 228 |
+
server_cert = ssl_object.getpeercert()
|
| 229 |
+
alpn = ssl_object.selected_alpn_protocol()
|
| 230 |
+
console.print(f"* SSL established using {version!r} / {cipher[0]!r}")
|
| 231 |
+
console.print(f"* Selected ALPN protocol: {alpn!r}")
|
| 232 |
+
if server_cert:
|
| 233 |
+
console.print("* Server certificate:")
|
| 234 |
+
console.print(format_certificate(server_cert))
|
| 235 |
+
elif name == "http11.send_request_headers.started" and verbose:
|
| 236 |
+
request = info["request"]
|
| 237 |
+
print_request_headers(request, http2=False)
|
| 238 |
+
elif name == "http2.send_request_headers.started" and verbose: # pragma: no cover
|
| 239 |
+
request = info["request"]
|
| 240 |
+
print_request_headers(request, http2=True)
|
| 241 |
+
elif name == "http11.receive_response_headers.complete":
|
| 242 |
+
http_version, status, reason_phrase, headers = info["return_value"]
|
| 243 |
+
print_response_headers(http_version, status, reason_phrase, headers)
|
| 244 |
+
elif name == "http2.receive_response_headers.complete": # pragma: no cover
|
| 245 |
+
status, headers = info["return_value"]
|
| 246 |
+
http_version = b"HTTP/2"
|
| 247 |
+
reason_phrase = None
|
| 248 |
+
print_response_headers(http_version, status, reason_phrase, headers)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def download_response(response: Response, download: typing.BinaryIO) -> None:
|
| 252 |
+
console = rich.console.Console()
|
| 253 |
+
console.print()
|
| 254 |
+
content_length = response.headers.get("Content-Length")
|
| 255 |
+
with rich.progress.Progress(
|
| 256 |
+
"[progress.description]{task.description}",
|
| 257 |
+
"[progress.percentage]{task.percentage:>3.0f}%",
|
| 258 |
+
rich.progress.BarColumn(bar_width=None),
|
| 259 |
+
rich.progress.DownloadColumn(),
|
| 260 |
+
rich.progress.TransferSpeedColumn(),
|
| 261 |
+
) as progress:
|
| 262 |
+
description = f"Downloading [bold]{rich.markup.escape(download.name)}"
|
| 263 |
+
download_task = progress.add_task(
|
| 264 |
+
description,
|
| 265 |
+
total=int(content_length or 0),
|
| 266 |
+
start=content_length is not None,
|
| 267 |
+
)
|
| 268 |
+
for chunk in response.iter_bytes():
|
| 269 |
+
download.write(chunk)
|
| 270 |
+
progress.update(download_task, completed=response.num_bytes_downloaded)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
def validate_json(
|
| 274 |
+
ctx: click.Context,
|
| 275 |
+
param: click.Option | click.Parameter,
|
| 276 |
+
value: typing.Any,
|
| 277 |
+
) -> typing.Any:
|
| 278 |
+
if value is None:
|
| 279 |
+
return None
|
| 280 |
+
|
| 281 |
+
try:
|
| 282 |
+
return json.loads(value)
|
| 283 |
+
except json.JSONDecodeError: # pragma: no cover
|
| 284 |
+
raise click.BadParameter("Not valid JSON")
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def validate_auth(
|
| 288 |
+
ctx: click.Context,
|
| 289 |
+
param: click.Option | click.Parameter,
|
| 290 |
+
value: typing.Any,
|
| 291 |
+
) -> typing.Any:
|
| 292 |
+
if value == (None, None):
|
| 293 |
+
return None
|
| 294 |
+
|
| 295 |
+
username, password = value
|
| 296 |
+
if password == "-": # pragma: no cover
|
| 297 |
+
password = click.prompt("Password", hide_input=True)
|
| 298 |
+
return (username, password)
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def handle_help(
|
| 302 |
+
ctx: click.Context,
|
| 303 |
+
param: click.Option | click.Parameter,
|
| 304 |
+
value: typing.Any,
|
| 305 |
+
) -> None:
|
| 306 |
+
if not value or ctx.resilient_parsing:
|
| 307 |
+
return
|
| 308 |
+
|
| 309 |
+
print_help()
|
| 310 |
+
ctx.exit()
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@click.command(add_help_option=False)
|
| 314 |
+
@click.argument("url", type=str)
|
| 315 |
+
@click.option(
|
| 316 |
+
"--method",
|
| 317 |
+
"-m",
|
| 318 |
+
"method",
|
| 319 |
+
type=str,
|
| 320 |
+
help=(
|
| 321 |
+
"Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. "
|
| 322 |
+
"[Default: GET, or POST if a request body is included]"
|
| 323 |
+
),
|
| 324 |
+
)
|
| 325 |
+
@click.option(
|
| 326 |
+
"--params",
|
| 327 |
+
"-p",
|
| 328 |
+
"params",
|
| 329 |
+
type=(str, str),
|
| 330 |
+
multiple=True,
|
| 331 |
+
help="Query parameters to include in the request URL.",
|
| 332 |
+
)
|
| 333 |
+
@click.option(
|
| 334 |
+
"--content",
|
| 335 |
+
"-c",
|
| 336 |
+
"content",
|
| 337 |
+
type=str,
|
| 338 |
+
help="Byte content to include in the request body.",
|
| 339 |
+
)
|
| 340 |
+
@click.option(
|
| 341 |
+
"--data",
|
| 342 |
+
"-d",
|
| 343 |
+
"data",
|
| 344 |
+
type=(str, str),
|
| 345 |
+
multiple=True,
|
| 346 |
+
help="Form data to include in the request body.",
|
| 347 |
+
)
|
| 348 |
+
@click.option(
|
| 349 |
+
"--files",
|
| 350 |
+
"-f",
|
| 351 |
+
"files",
|
| 352 |
+
type=(str, click.File(mode="rb")),
|
| 353 |
+
multiple=True,
|
| 354 |
+
help="Form files to include in the request body.",
|
| 355 |
+
)
|
| 356 |
+
@click.option(
|
| 357 |
+
"--json",
|
| 358 |
+
"-j",
|
| 359 |
+
"json",
|
| 360 |
+
type=str,
|
| 361 |
+
callback=validate_json,
|
| 362 |
+
help="JSON data to include in the request body.",
|
| 363 |
+
)
|
| 364 |
+
@click.option(
|
| 365 |
+
"--headers",
|
| 366 |
+
"-h",
|
| 367 |
+
"headers",
|
| 368 |
+
type=(str, str),
|
| 369 |
+
multiple=True,
|
| 370 |
+
help="Include additional HTTP headers in the request.",
|
| 371 |
+
)
|
| 372 |
+
@click.option(
|
| 373 |
+
"--cookies",
|
| 374 |
+
"cookies",
|
| 375 |
+
type=(str, str),
|
| 376 |
+
multiple=True,
|
| 377 |
+
help="Cookies to include in the request.",
|
| 378 |
+
)
|
| 379 |
+
@click.option(
|
| 380 |
+
"--auth",
|
| 381 |
+
"auth",
|
| 382 |
+
type=(str, str),
|
| 383 |
+
default=(None, None),
|
| 384 |
+
callback=validate_auth,
|
| 385 |
+
help=(
|
| 386 |
+
"Username and password to include in the request. "
|
| 387 |
+
"Specify '-' for the password to use a password prompt. "
|
| 388 |
+
"Note that using --verbose/-v will expose the Authorization header, "
|
| 389 |
+
"including the password encoding in a trivially reversible format."
|
| 390 |
+
),
|
| 391 |
+
)
|
| 392 |
+
@click.option(
|
| 393 |
+
"--proxy",
|
| 394 |
+
"proxy",
|
| 395 |
+
type=str,
|
| 396 |
+
default=None,
|
| 397 |
+
help="Send the request via a proxy. Should be the URL giving the proxy address.",
|
| 398 |
+
)
|
| 399 |
+
@click.option(
|
| 400 |
+
"--timeout",
|
| 401 |
+
"timeout",
|
| 402 |
+
type=float,
|
| 403 |
+
default=5.0,
|
| 404 |
+
help=(
|
| 405 |
+
"Timeout value to use for network operations, such as establishing the "
|
| 406 |
+
"connection, reading some data, etc... [Default: 5.0]"
|
| 407 |
+
),
|
| 408 |
+
)
|
| 409 |
+
@click.option(
|
| 410 |
+
"--follow-redirects",
|
| 411 |
+
"follow_redirects",
|
| 412 |
+
is_flag=True,
|
| 413 |
+
default=False,
|
| 414 |
+
help="Automatically follow redirects.",
|
| 415 |
+
)
|
| 416 |
+
@click.option(
|
| 417 |
+
"--no-verify",
|
| 418 |
+
"verify",
|
| 419 |
+
is_flag=True,
|
| 420 |
+
default=True,
|
| 421 |
+
help="Disable SSL verification.",
|
| 422 |
+
)
|
| 423 |
+
@click.option(
|
| 424 |
+
"--http2",
|
| 425 |
+
"http2",
|
| 426 |
+
type=bool,
|
| 427 |
+
is_flag=True,
|
| 428 |
+
default=False,
|
| 429 |
+
help="Send the request using HTTP/2, if the remote server supports it.",
|
| 430 |
+
)
|
| 431 |
+
@click.option(
|
| 432 |
+
"--download",
|
| 433 |
+
type=click.File("wb"),
|
| 434 |
+
help="Save the response content as a file, rather than displaying it.",
|
| 435 |
+
)
|
| 436 |
+
@click.option(
|
| 437 |
+
"--verbose",
|
| 438 |
+
"-v",
|
| 439 |
+
type=bool,
|
| 440 |
+
is_flag=True,
|
| 441 |
+
default=False,
|
| 442 |
+
help="Verbose. Show request as well as response.",
|
| 443 |
+
)
|
| 444 |
+
@click.option(
|
| 445 |
+
"--help",
|
| 446 |
+
is_flag=True,
|
| 447 |
+
is_eager=True,
|
| 448 |
+
expose_value=False,
|
| 449 |
+
callback=handle_help,
|
| 450 |
+
help="Show this message and exit.",
|
| 451 |
+
)
|
| 452 |
+
def main(
|
| 453 |
+
url: str,
|
| 454 |
+
method: str,
|
| 455 |
+
params: list[tuple[str, str]],
|
| 456 |
+
content: str,
|
| 457 |
+
data: list[tuple[str, str]],
|
| 458 |
+
files: list[tuple[str, click.File]],
|
| 459 |
+
json: str,
|
| 460 |
+
headers: list[tuple[str, str]],
|
| 461 |
+
cookies: list[tuple[str, str]],
|
| 462 |
+
auth: tuple[str, str] | None,
|
| 463 |
+
proxy: str,
|
| 464 |
+
timeout: float,
|
| 465 |
+
follow_redirects: bool,
|
| 466 |
+
verify: bool,
|
| 467 |
+
http2: bool,
|
| 468 |
+
download: typing.BinaryIO | None,
|
| 469 |
+
verbose: bool,
|
| 470 |
+
) -> None:
|
| 471 |
+
"""
|
| 472 |
+
An HTTP command line client.
|
| 473 |
+
Sends a request and displays the response.
|
| 474 |
+
"""
|
| 475 |
+
if not method:
|
| 476 |
+
method = "POST" if content or data or files or json else "GET"
|
| 477 |
+
|
| 478 |
+
try:
|
| 479 |
+
with Client(proxy=proxy, timeout=timeout, http2=http2, verify=verify) as client:
|
| 480 |
+
with client.stream(
|
| 481 |
+
method,
|
| 482 |
+
url,
|
| 483 |
+
params=list(params),
|
| 484 |
+
content=content,
|
| 485 |
+
data=dict(data),
|
| 486 |
+
files=files, # type: ignore
|
| 487 |
+
json=json,
|
| 488 |
+
headers=headers,
|
| 489 |
+
cookies=dict(cookies),
|
| 490 |
+
auth=auth,
|
| 491 |
+
follow_redirects=follow_redirects,
|
| 492 |
+
extensions={"trace": functools.partial(trace, verbose=verbose)},
|
| 493 |
+
) as response:
|
| 494 |
+
if download is not None:
|
| 495 |
+
download_response(response, download)
|
| 496 |
+
else:
|
| 497 |
+
response.read()
|
| 498 |
+
if response.content:
|
| 499 |
+
print_response(response)
|
| 500 |
+
|
| 501 |
+
except RequestError as exc:
|
| 502 |
+
console = rich.console.Console()
|
| 503 |
+
console.print(f"[red]{type(exc).__name__}[/red]: {exc}")
|
| 504 |
+
sys.exit(1)
|
| 505 |
+
|
| 506 |
+
sys.exit(0 if response.is_success else 1)
|
.venv/lib/python3.11/site-packages/httpx/_models.py
ADDED
|
@@ -0,0 +1,1277 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import codecs
|
| 4 |
+
import datetime
|
| 5 |
+
import email.message
|
| 6 |
+
import json as jsonlib
|
| 7 |
+
import re
|
| 8 |
+
import typing
|
| 9 |
+
import urllib.request
|
| 10 |
+
from collections.abc import Mapping
|
| 11 |
+
from http.cookiejar import Cookie, CookieJar
|
| 12 |
+
|
| 13 |
+
from ._content import ByteStream, UnattachedStream, encode_request, encode_response
|
| 14 |
+
from ._decoders import (
|
| 15 |
+
SUPPORTED_DECODERS,
|
| 16 |
+
ByteChunker,
|
| 17 |
+
ContentDecoder,
|
| 18 |
+
IdentityDecoder,
|
| 19 |
+
LineDecoder,
|
| 20 |
+
MultiDecoder,
|
| 21 |
+
TextChunker,
|
| 22 |
+
TextDecoder,
|
| 23 |
+
)
|
| 24 |
+
from ._exceptions import (
|
| 25 |
+
CookieConflict,
|
| 26 |
+
HTTPStatusError,
|
| 27 |
+
RequestNotRead,
|
| 28 |
+
ResponseNotRead,
|
| 29 |
+
StreamClosed,
|
| 30 |
+
StreamConsumed,
|
| 31 |
+
request_context,
|
| 32 |
+
)
|
| 33 |
+
from ._multipart import get_multipart_boundary_from_content_type
|
| 34 |
+
from ._status_codes import codes
|
| 35 |
+
from ._types import (
|
| 36 |
+
AsyncByteStream,
|
| 37 |
+
CookieTypes,
|
| 38 |
+
HeaderTypes,
|
| 39 |
+
QueryParamTypes,
|
| 40 |
+
RequestContent,
|
| 41 |
+
RequestData,
|
| 42 |
+
RequestExtensions,
|
| 43 |
+
RequestFiles,
|
| 44 |
+
ResponseContent,
|
| 45 |
+
ResponseExtensions,
|
| 46 |
+
SyncByteStream,
|
| 47 |
+
)
|
| 48 |
+
from ._urls import URL
|
| 49 |
+
from ._utils import to_bytes_or_str, to_str
|
| 50 |
+
|
| 51 |
+
__all__ = ["Cookies", "Headers", "Request", "Response"]
|
| 52 |
+
|
| 53 |
+
SENSITIVE_HEADERS = {"authorization", "proxy-authorization"}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def _is_known_encoding(encoding: str) -> bool:
|
| 57 |
+
"""
|
| 58 |
+
Return `True` if `encoding` is a known codec.
|
| 59 |
+
"""
|
| 60 |
+
try:
|
| 61 |
+
codecs.lookup(encoding)
|
| 62 |
+
except LookupError:
|
| 63 |
+
return False
|
| 64 |
+
return True
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _normalize_header_key(key: str | bytes, encoding: str | None = None) -> bytes:
|
| 68 |
+
"""
|
| 69 |
+
Coerce str/bytes into a strictly byte-wise HTTP header key.
|
| 70 |
+
"""
|
| 71 |
+
return key if isinstance(key, bytes) else key.encode(encoding or "ascii")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _normalize_header_value(value: str | bytes, encoding: str | None = None) -> bytes:
|
| 75 |
+
"""
|
| 76 |
+
Coerce str/bytes into a strictly byte-wise HTTP header value.
|
| 77 |
+
"""
|
| 78 |
+
if isinstance(value, bytes):
|
| 79 |
+
return value
|
| 80 |
+
if not isinstance(value, str):
|
| 81 |
+
raise TypeError(f"Header value must be str or bytes, not {type(value)}")
|
| 82 |
+
return value.encode(encoding or "ascii")
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _parse_content_type_charset(content_type: str) -> str | None:
|
| 86 |
+
# We used to use `cgi.parse_header()` here, but `cgi` became a dead battery.
|
| 87 |
+
# See: https://peps.python.org/pep-0594/#cgi
|
| 88 |
+
msg = email.message.Message()
|
| 89 |
+
msg["content-type"] = content_type
|
| 90 |
+
return msg.get_content_charset(failobj=None)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _parse_header_links(value: str) -> list[dict[str, str]]:
|
| 94 |
+
"""
|
| 95 |
+
Returns a list of parsed link headers, for more info see:
|
| 96 |
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
|
| 97 |
+
The generic syntax of those is:
|
| 98 |
+
Link: < uri-reference >; param1=value1; param2="value2"
|
| 99 |
+
So for instance:
|
| 100 |
+
Link; '<http:/.../front.jpeg>; type="image/jpeg",<http://.../back.jpeg>;'
|
| 101 |
+
would return
|
| 102 |
+
[
|
| 103 |
+
{"url": "http:/.../front.jpeg", "type": "image/jpeg"},
|
| 104 |
+
{"url": "http://.../back.jpeg"},
|
| 105 |
+
]
|
| 106 |
+
:param value: HTTP Link entity-header field
|
| 107 |
+
:return: list of parsed link headers
|
| 108 |
+
"""
|
| 109 |
+
links: list[dict[str, str]] = []
|
| 110 |
+
replace_chars = " '\""
|
| 111 |
+
value = value.strip(replace_chars)
|
| 112 |
+
if not value:
|
| 113 |
+
return links
|
| 114 |
+
for val in re.split(", *<", value):
|
| 115 |
+
try:
|
| 116 |
+
url, params = val.split(";", 1)
|
| 117 |
+
except ValueError:
|
| 118 |
+
url, params = val, ""
|
| 119 |
+
link = {"url": url.strip("<> '\"")}
|
| 120 |
+
for param in params.split(";"):
|
| 121 |
+
try:
|
| 122 |
+
key, value = param.split("=")
|
| 123 |
+
except ValueError:
|
| 124 |
+
break
|
| 125 |
+
link[key.strip(replace_chars)] = value.strip(replace_chars)
|
| 126 |
+
links.append(link)
|
| 127 |
+
return links
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _obfuscate_sensitive_headers(
|
| 131 |
+
items: typing.Iterable[tuple[typing.AnyStr, typing.AnyStr]],
|
| 132 |
+
) -> typing.Iterator[tuple[typing.AnyStr, typing.AnyStr]]:
|
| 133 |
+
for k, v in items:
|
| 134 |
+
if to_str(k.lower()) in SENSITIVE_HEADERS:
|
| 135 |
+
v = to_bytes_or_str("[secure]", match_type_of=v)
|
| 136 |
+
yield k, v
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
class Headers(typing.MutableMapping[str, str]):
|
| 140 |
+
"""
|
| 141 |
+
HTTP headers, as a case-insensitive multi-dict.
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
def __init__(
|
| 145 |
+
self,
|
| 146 |
+
headers: HeaderTypes | None = None,
|
| 147 |
+
encoding: str | None = None,
|
| 148 |
+
) -> None:
|
| 149 |
+
self._list = [] # type: typing.List[typing.Tuple[bytes, bytes, bytes]]
|
| 150 |
+
|
| 151 |
+
if isinstance(headers, Headers):
|
| 152 |
+
self._list = list(headers._list)
|
| 153 |
+
elif isinstance(headers, Mapping):
|
| 154 |
+
for k, v in headers.items():
|
| 155 |
+
bytes_key = _normalize_header_key(k, encoding)
|
| 156 |
+
bytes_value = _normalize_header_value(v, encoding)
|
| 157 |
+
self._list.append((bytes_key, bytes_key.lower(), bytes_value))
|
| 158 |
+
elif headers is not None:
|
| 159 |
+
for k, v in headers:
|
| 160 |
+
bytes_key = _normalize_header_key(k, encoding)
|
| 161 |
+
bytes_value = _normalize_header_value(v, encoding)
|
| 162 |
+
self._list.append((bytes_key, bytes_key.lower(), bytes_value))
|
| 163 |
+
|
| 164 |
+
self._encoding = encoding
|
| 165 |
+
|
| 166 |
+
@property
|
| 167 |
+
def encoding(self) -> str:
|
| 168 |
+
"""
|
| 169 |
+
Header encoding is mandated as ascii, but we allow fallbacks to utf-8
|
| 170 |
+
or iso-8859-1.
|
| 171 |
+
"""
|
| 172 |
+
if self._encoding is None:
|
| 173 |
+
for encoding in ["ascii", "utf-8"]:
|
| 174 |
+
for key, value in self.raw:
|
| 175 |
+
try:
|
| 176 |
+
key.decode(encoding)
|
| 177 |
+
value.decode(encoding)
|
| 178 |
+
except UnicodeDecodeError:
|
| 179 |
+
break
|
| 180 |
+
else:
|
| 181 |
+
# The else block runs if 'break' did not occur, meaning
|
| 182 |
+
# all values fitted the encoding.
|
| 183 |
+
self._encoding = encoding
|
| 184 |
+
break
|
| 185 |
+
else:
|
| 186 |
+
# The ISO-8859-1 encoding covers all 256 code points in a byte,
|
| 187 |
+
# so will never raise decode errors.
|
| 188 |
+
self._encoding = "iso-8859-1"
|
| 189 |
+
return self._encoding
|
| 190 |
+
|
| 191 |
+
@encoding.setter
|
| 192 |
+
def encoding(self, value: str) -> None:
|
| 193 |
+
self._encoding = value
|
| 194 |
+
|
| 195 |
+
@property
|
| 196 |
+
def raw(self) -> list[tuple[bytes, bytes]]:
|
| 197 |
+
"""
|
| 198 |
+
Returns a list of the raw header items, as byte pairs.
|
| 199 |
+
"""
|
| 200 |
+
return [(raw_key, value) for raw_key, _, value in self._list]
|
| 201 |
+
|
| 202 |
+
def keys(self) -> typing.KeysView[str]:
|
| 203 |
+
return {key.decode(self.encoding): None for _, key, value in self._list}.keys()
|
| 204 |
+
|
| 205 |
+
def values(self) -> typing.ValuesView[str]:
|
| 206 |
+
values_dict: dict[str, str] = {}
|
| 207 |
+
for _, key, value in self._list:
|
| 208 |
+
str_key = key.decode(self.encoding)
|
| 209 |
+
str_value = value.decode(self.encoding)
|
| 210 |
+
if str_key in values_dict:
|
| 211 |
+
values_dict[str_key] += f", {str_value}"
|
| 212 |
+
else:
|
| 213 |
+
values_dict[str_key] = str_value
|
| 214 |
+
return values_dict.values()
|
| 215 |
+
|
| 216 |
+
def items(self) -> typing.ItemsView[str, str]:
|
| 217 |
+
"""
|
| 218 |
+
Return `(key, value)` items of headers. Concatenate headers
|
| 219 |
+
into a single comma separated value when a key occurs multiple times.
|
| 220 |
+
"""
|
| 221 |
+
values_dict: dict[str, str] = {}
|
| 222 |
+
for _, key, value in self._list:
|
| 223 |
+
str_key = key.decode(self.encoding)
|
| 224 |
+
str_value = value.decode(self.encoding)
|
| 225 |
+
if str_key in values_dict:
|
| 226 |
+
values_dict[str_key] += f", {str_value}"
|
| 227 |
+
else:
|
| 228 |
+
values_dict[str_key] = str_value
|
| 229 |
+
return values_dict.items()
|
| 230 |
+
|
| 231 |
+
def multi_items(self) -> list[tuple[str, str]]:
|
| 232 |
+
"""
|
| 233 |
+
Return a list of `(key, value)` pairs of headers. Allow multiple
|
| 234 |
+
occurrences of the same key without concatenating into a single
|
| 235 |
+
comma separated value.
|
| 236 |
+
"""
|
| 237 |
+
return [
|
| 238 |
+
(key.decode(self.encoding), value.decode(self.encoding))
|
| 239 |
+
for _, key, value in self._list
|
| 240 |
+
]
|
| 241 |
+
|
| 242 |
+
def get(self, key: str, default: typing.Any = None) -> typing.Any:
|
| 243 |
+
"""
|
| 244 |
+
Return a header value. If multiple occurrences of the header occur
|
| 245 |
+
then concatenate them together with commas.
|
| 246 |
+
"""
|
| 247 |
+
try:
|
| 248 |
+
return self[key]
|
| 249 |
+
except KeyError:
|
| 250 |
+
return default
|
| 251 |
+
|
| 252 |
+
def get_list(self, key: str, split_commas: bool = False) -> list[str]:
|
| 253 |
+
"""
|
| 254 |
+
Return a list of all header values for a given key.
|
| 255 |
+
If `split_commas=True` is passed, then any comma separated header
|
| 256 |
+
values are split into multiple return strings.
|
| 257 |
+
"""
|
| 258 |
+
get_header_key = key.lower().encode(self.encoding)
|
| 259 |
+
|
| 260 |
+
values = [
|
| 261 |
+
item_value.decode(self.encoding)
|
| 262 |
+
for _, item_key, item_value in self._list
|
| 263 |
+
if item_key.lower() == get_header_key
|
| 264 |
+
]
|
| 265 |
+
|
| 266 |
+
if not split_commas:
|
| 267 |
+
return values
|
| 268 |
+
|
| 269 |
+
split_values = []
|
| 270 |
+
for value in values:
|
| 271 |
+
split_values.extend([item.strip() for item in value.split(",")])
|
| 272 |
+
return split_values
|
| 273 |
+
|
| 274 |
+
def update(self, headers: HeaderTypes | None = None) -> None: # type: ignore
|
| 275 |
+
headers = Headers(headers)
|
| 276 |
+
for key in headers.keys():
|
| 277 |
+
if key in self:
|
| 278 |
+
self.pop(key)
|
| 279 |
+
self._list.extend(headers._list)
|
| 280 |
+
|
| 281 |
+
def copy(self) -> Headers:
|
| 282 |
+
return Headers(self, encoding=self.encoding)
|
| 283 |
+
|
| 284 |
+
def __getitem__(self, key: str) -> str:
|
| 285 |
+
"""
|
| 286 |
+
Return a single header value.
|
| 287 |
+
|
| 288 |
+
If there are multiple headers with the same key, then we concatenate
|
| 289 |
+
them with commas. See: https://tools.ietf.org/html/rfc7230#section-3.2.2
|
| 290 |
+
"""
|
| 291 |
+
normalized_key = key.lower().encode(self.encoding)
|
| 292 |
+
|
| 293 |
+
items = [
|
| 294 |
+
header_value.decode(self.encoding)
|
| 295 |
+
for _, header_key, header_value in self._list
|
| 296 |
+
if header_key == normalized_key
|
| 297 |
+
]
|
| 298 |
+
|
| 299 |
+
if items:
|
| 300 |
+
return ", ".join(items)
|
| 301 |
+
|
| 302 |
+
raise KeyError(key)
|
| 303 |
+
|
| 304 |
+
def __setitem__(self, key: str, value: str) -> None:
|
| 305 |
+
"""
|
| 306 |
+
Set the header `key` to `value`, removing any duplicate entries.
|
| 307 |
+
Retains insertion order.
|
| 308 |
+
"""
|
| 309 |
+
set_key = key.encode(self._encoding or "utf-8")
|
| 310 |
+
set_value = value.encode(self._encoding or "utf-8")
|
| 311 |
+
lookup_key = set_key.lower()
|
| 312 |
+
|
| 313 |
+
found_indexes = [
|
| 314 |
+
idx
|
| 315 |
+
for idx, (_, item_key, _) in enumerate(self._list)
|
| 316 |
+
if item_key == lookup_key
|
| 317 |
+
]
|
| 318 |
+
|
| 319 |
+
for idx in reversed(found_indexes[1:]):
|
| 320 |
+
del self._list[idx]
|
| 321 |
+
|
| 322 |
+
if found_indexes:
|
| 323 |
+
idx = found_indexes[0]
|
| 324 |
+
self._list[idx] = (set_key, lookup_key, set_value)
|
| 325 |
+
else:
|
| 326 |
+
self._list.append((set_key, lookup_key, set_value))
|
| 327 |
+
|
| 328 |
+
def __delitem__(self, key: str) -> None:
|
| 329 |
+
"""
|
| 330 |
+
Remove the header `key`.
|
| 331 |
+
"""
|
| 332 |
+
del_key = key.lower().encode(self.encoding)
|
| 333 |
+
|
| 334 |
+
pop_indexes = [
|
| 335 |
+
idx
|
| 336 |
+
for idx, (_, item_key, _) in enumerate(self._list)
|
| 337 |
+
if item_key.lower() == del_key
|
| 338 |
+
]
|
| 339 |
+
|
| 340 |
+
if not pop_indexes:
|
| 341 |
+
raise KeyError(key)
|
| 342 |
+
|
| 343 |
+
for idx in reversed(pop_indexes):
|
| 344 |
+
del self._list[idx]
|
| 345 |
+
|
| 346 |
+
def __contains__(self, key: typing.Any) -> bool:
|
| 347 |
+
header_key = key.lower().encode(self.encoding)
|
| 348 |
+
return header_key in [key for _, key, _ in self._list]
|
| 349 |
+
|
| 350 |
+
def __iter__(self) -> typing.Iterator[typing.Any]:
|
| 351 |
+
return iter(self.keys())
|
| 352 |
+
|
| 353 |
+
def __len__(self) -> int:
|
| 354 |
+
return len(self._list)
|
| 355 |
+
|
| 356 |
+
def __eq__(self, other: typing.Any) -> bool:
|
| 357 |
+
try:
|
| 358 |
+
other_headers = Headers(other)
|
| 359 |
+
except ValueError:
|
| 360 |
+
return False
|
| 361 |
+
|
| 362 |
+
self_list = [(key, value) for _, key, value in self._list]
|
| 363 |
+
other_list = [(key, value) for _, key, value in other_headers._list]
|
| 364 |
+
return sorted(self_list) == sorted(other_list)
|
| 365 |
+
|
| 366 |
+
def __repr__(self) -> str:
|
| 367 |
+
class_name = self.__class__.__name__
|
| 368 |
+
|
| 369 |
+
encoding_str = ""
|
| 370 |
+
if self.encoding != "ascii":
|
| 371 |
+
encoding_str = f", encoding={self.encoding!r}"
|
| 372 |
+
|
| 373 |
+
as_list = list(_obfuscate_sensitive_headers(self.multi_items()))
|
| 374 |
+
as_dict = dict(as_list)
|
| 375 |
+
|
| 376 |
+
no_duplicate_keys = len(as_dict) == len(as_list)
|
| 377 |
+
if no_duplicate_keys:
|
| 378 |
+
return f"{class_name}({as_dict!r}{encoding_str})"
|
| 379 |
+
return f"{class_name}({as_list!r}{encoding_str})"
|
| 380 |
+
|
| 381 |
+
|
| 382 |
+
class Request:
|
| 383 |
+
def __init__(
|
| 384 |
+
self,
|
| 385 |
+
method: str,
|
| 386 |
+
url: URL | str,
|
| 387 |
+
*,
|
| 388 |
+
params: QueryParamTypes | None = None,
|
| 389 |
+
headers: HeaderTypes | None = None,
|
| 390 |
+
cookies: CookieTypes | None = None,
|
| 391 |
+
content: RequestContent | None = None,
|
| 392 |
+
data: RequestData | None = None,
|
| 393 |
+
files: RequestFiles | None = None,
|
| 394 |
+
json: typing.Any | None = None,
|
| 395 |
+
stream: SyncByteStream | AsyncByteStream | None = None,
|
| 396 |
+
extensions: RequestExtensions | None = None,
|
| 397 |
+
) -> None:
|
| 398 |
+
self.method = method.upper()
|
| 399 |
+
self.url = URL(url) if params is None else URL(url, params=params)
|
| 400 |
+
self.headers = Headers(headers)
|
| 401 |
+
self.extensions = {} if extensions is None else dict(extensions)
|
| 402 |
+
|
| 403 |
+
if cookies:
|
| 404 |
+
Cookies(cookies).set_cookie_header(self)
|
| 405 |
+
|
| 406 |
+
if stream is None:
|
| 407 |
+
content_type: str | None = self.headers.get("content-type")
|
| 408 |
+
headers, stream = encode_request(
|
| 409 |
+
content=content,
|
| 410 |
+
data=data,
|
| 411 |
+
files=files,
|
| 412 |
+
json=json,
|
| 413 |
+
boundary=get_multipart_boundary_from_content_type(
|
| 414 |
+
content_type=content_type.encode(self.headers.encoding)
|
| 415 |
+
if content_type
|
| 416 |
+
else None
|
| 417 |
+
),
|
| 418 |
+
)
|
| 419 |
+
self._prepare(headers)
|
| 420 |
+
self.stream = stream
|
| 421 |
+
# Load the request body, except for streaming content.
|
| 422 |
+
if isinstance(stream, ByteStream):
|
| 423 |
+
self.read()
|
| 424 |
+
else:
|
| 425 |
+
# There's an important distinction between `Request(content=...)`,
|
| 426 |
+
# and `Request(stream=...)`.
|
| 427 |
+
#
|
| 428 |
+
# Using `content=...` implies automatically populated `Host` and content
|
| 429 |
+
# headers, of either `Content-Length: ...` or `Transfer-Encoding: chunked`.
|
| 430 |
+
#
|
| 431 |
+
# Using `stream=...` will not automatically include *any*
|
| 432 |
+
# auto-populated headers.
|
| 433 |
+
#
|
| 434 |
+
# As an end-user you don't really need `stream=...`. It's only
|
| 435 |
+
# useful when:
|
| 436 |
+
#
|
| 437 |
+
# * Preserving the request stream when copying requests, eg for redirects.
|
| 438 |
+
# * Creating request instances on the *server-side* of the transport API.
|
| 439 |
+
self.stream = stream
|
| 440 |
+
|
| 441 |
+
def _prepare(self, default_headers: dict[str, str]) -> None:
|
| 442 |
+
for key, value in default_headers.items():
|
| 443 |
+
# Ignore Transfer-Encoding if the Content-Length has been set explicitly.
|
| 444 |
+
if key.lower() == "transfer-encoding" and "Content-Length" in self.headers:
|
| 445 |
+
continue
|
| 446 |
+
self.headers.setdefault(key, value)
|
| 447 |
+
|
| 448 |
+
auto_headers: list[tuple[bytes, bytes]] = []
|
| 449 |
+
|
| 450 |
+
has_host = "Host" in self.headers
|
| 451 |
+
has_content_length = (
|
| 452 |
+
"Content-Length" in self.headers or "Transfer-Encoding" in self.headers
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
if not has_host and self.url.host:
|
| 456 |
+
auto_headers.append((b"Host", self.url.netloc))
|
| 457 |
+
if not has_content_length and self.method in ("POST", "PUT", "PATCH"):
|
| 458 |
+
auto_headers.append((b"Content-Length", b"0"))
|
| 459 |
+
|
| 460 |
+
self.headers = Headers(auto_headers + self.headers.raw)
|
| 461 |
+
|
| 462 |
+
@property
|
| 463 |
+
def content(self) -> bytes:
|
| 464 |
+
if not hasattr(self, "_content"):
|
| 465 |
+
raise RequestNotRead()
|
| 466 |
+
return self._content
|
| 467 |
+
|
| 468 |
+
def read(self) -> bytes:
|
| 469 |
+
"""
|
| 470 |
+
Read and return the request content.
|
| 471 |
+
"""
|
| 472 |
+
if not hasattr(self, "_content"):
|
| 473 |
+
assert isinstance(self.stream, typing.Iterable)
|
| 474 |
+
self._content = b"".join(self.stream)
|
| 475 |
+
if not isinstance(self.stream, ByteStream):
|
| 476 |
+
# If a streaming request has been read entirely into memory, then
|
| 477 |
+
# we can replace the stream with a raw bytes implementation,
|
| 478 |
+
# to ensure that any non-replayable streams can still be used.
|
| 479 |
+
self.stream = ByteStream(self._content)
|
| 480 |
+
return self._content
|
| 481 |
+
|
| 482 |
+
async def aread(self) -> bytes:
|
| 483 |
+
"""
|
| 484 |
+
Read and return the request content.
|
| 485 |
+
"""
|
| 486 |
+
if not hasattr(self, "_content"):
|
| 487 |
+
assert isinstance(self.stream, typing.AsyncIterable)
|
| 488 |
+
self._content = b"".join([part async for part in self.stream])
|
| 489 |
+
if not isinstance(self.stream, ByteStream):
|
| 490 |
+
# If a streaming request has been read entirely into memory, then
|
| 491 |
+
# we can replace the stream with a raw bytes implementation,
|
| 492 |
+
# to ensure that any non-replayable streams can still be used.
|
| 493 |
+
self.stream = ByteStream(self._content)
|
| 494 |
+
return self._content
|
| 495 |
+
|
| 496 |
+
def __repr__(self) -> str:
|
| 497 |
+
class_name = self.__class__.__name__
|
| 498 |
+
url = str(self.url)
|
| 499 |
+
return f"<{class_name}({self.method!r}, {url!r})>"
|
| 500 |
+
|
| 501 |
+
def __getstate__(self) -> dict[str, typing.Any]:
|
| 502 |
+
return {
|
| 503 |
+
name: value
|
| 504 |
+
for name, value in self.__dict__.items()
|
| 505 |
+
if name not in ["extensions", "stream"]
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
def __setstate__(self, state: dict[str, typing.Any]) -> None:
|
| 509 |
+
for name, value in state.items():
|
| 510 |
+
setattr(self, name, value)
|
| 511 |
+
self.extensions = {}
|
| 512 |
+
self.stream = UnattachedStream()
|
| 513 |
+
|
| 514 |
+
|
| 515 |
+
class Response:
|
| 516 |
+
def __init__(
|
| 517 |
+
self,
|
| 518 |
+
status_code: int,
|
| 519 |
+
*,
|
| 520 |
+
headers: HeaderTypes | None = None,
|
| 521 |
+
content: ResponseContent | None = None,
|
| 522 |
+
text: str | None = None,
|
| 523 |
+
html: str | None = None,
|
| 524 |
+
json: typing.Any = None,
|
| 525 |
+
stream: SyncByteStream | AsyncByteStream | None = None,
|
| 526 |
+
request: Request | None = None,
|
| 527 |
+
extensions: ResponseExtensions | None = None,
|
| 528 |
+
history: list[Response] | None = None,
|
| 529 |
+
default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
|
| 530 |
+
) -> None:
|
| 531 |
+
self.status_code = status_code
|
| 532 |
+
self.headers = Headers(headers)
|
| 533 |
+
|
| 534 |
+
self._request: Request | None = request
|
| 535 |
+
|
| 536 |
+
# When follow_redirects=False and a redirect is received,
|
| 537 |
+
# the client will set `response.next_request`.
|
| 538 |
+
self.next_request: Request | None = None
|
| 539 |
+
|
| 540 |
+
self.extensions = {} if extensions is None else dict(extensions)
|
| 541 |
+
self.history = [] if history is None else list(history)
|
| 542 |
+
|
| 543 |
+
self.is_closed = False
|
| 544 |
+
self.is_stream_consumed = False
|
| 545 |
+
|
| 546 |
+
self.default_encoding = default_encoding
|
| 547 |
+
|
| 548 |
+
if stream is None:
|
| 549 |
+
headers, stream = encode_response(content, text, html, json)
|
| 550 |
+
self._prepare(headers)
|
| 551 |
+
self.stream = stream
|
| 552 |
+
if isinstance(stream, ByteStream):
|
| 553 |
+
# Load the response body, except for streaming content.
|
| 554 |
+
self.read()
|
| 555 |
+
else:
|
| 556 |
+
# There's an important distinction between `Response(content=...)`,
|
| 557 |
+
# and `Response(stream=...)`.
|
| 558 |
+
#
|
| 559 |
+
# Using `content=...` implies automatically populated content headers,
|
| 560 |
+
# of either `Content-Length: ...` or `Transfer-Encoding: chunked`.
|
| 561 |
+
#
|
| 562 |
+
# Using `stream=...` will not automatically include any content headers.
|
| 563 |
+
#
|
| 564 |
+
# As an end-user you don't really need `stream=...`. It's only
|
| 565 |
+
# useful when creating response instances having received a stream
|
| 566 |
+
# from the transport API.
|
| 567 |
+
self.stream = stream
|
| 568 |
+
|
| 569 |
+
self._num_bytes_downloaded = 0
|
| 570 |
+
|
| 571 |
+
def _prepare(self, default_headers: dict[str, str]) -> None:
|
| 572 |
+
for key, value in default_headers.items():
|
| 573 |
+
# Ignore Transfer-Encoding if the Content-Length has been set explicitly.
|
| 574 |
+
if key.lower() == "transfer-encoding" and "content-length" in self.headers:
|
| 575 |
+
continue
|
| 576 |
+
self.headers.setdefault(key, value)
|
| 577 |
+
|
| 578 |
+
@property
|
| 579 |
+
def elapsed(self) -> datetime.timedelta:
|
| 580 |
+
"""
|
| 581 |
+
Returns the time taken for the complete request/response
|
| 582 |
+
cycle to complete.
|
| 583 |
+
"""
|
| 584 |
+
if not hasattr(self, "_elapsed"):
|
| 585 |
+
raise RuntimeError(
|
| 586 |
+
"'.elapsed' may only be accessed after the response "
|
| 587 |
+
"has been read or closed."
|
| 588 |
+
)
|
| 589 |
+
return self._elapsed
|
| 590 |
+
|
| 591 |
+
@elapsed.setter
|
| 592 |
+
def elapsed(self, elapsed: datetime.timedelta) -> None:
|
| 593 |
+
self._elapsed = elapsed
|
| 594 |
+
|
| 595 |
+
@property
|
| 596 |
+
def request(self) -> Request:
|
| 597 |
+
"""
|
| 598 |
+
Returns the request instance associated to the current response.
|
| 599 |
+
"""
|
| 600 |
+
if self._request is None:
|
| 601 |
+
raise RuntimeError(
|
| 602 |
+
"The request instance has not been set on this response."
|
| 603 |
+
)
|
| 604 |
+
return self._request
|
| 605 |
+
|
| 606 |
+
@request.setter
|
| 607 |
+
def request(self, value: Request) -> None:
|
| 608 |
+
self._request = value
|
| 609 |
+
|
| 610 |
+
@property
|
| 611 |
+
def http_version(self) -> str:
|
| 612 |
+
try:
|
| 613 |
+
http_version: bytes = self.extensions["http_version"]
|
| 614 |
+
except KeyError:
|
| 615 |
+
return "HTTP/1.1"
|
| 616 |
+
else:
|
| 617 |
+
return http_version.decode("ascii", errors="ignore")
|
| 618 |
+
|
| 619 |
+
@property
|
| 620 |
+
def reason_phrase(self) -> str:
|
| 621 |
+
try:
|
| 622 |
+
reason_phrase: bytes = self.extensions["reason_phrase"]
|
| 623 |
+
except KeyError:
|
| 624 |
+
return codes.get_reason_phrase(self.status_code)
|
| 625 |
+
else:
|
| 626 |
+
return reason_phrase.decode("ascii", errors="ignore")
|
| 627 |
+
|
| 628 |
+
@property
|
| 629 |
+
def url(self) -> URL:
|
| 630 |
+
"""
|
| 631 |
+
Returns the URL for which the request was made.
|
| 632 |
+
"""
|
| 633 |
+
return self.request.url
|
| 634 |
+
|
| 635 |
+
@property
|
| 636 |
+
def content(self) -> bytes:
|
| 637 |
+
if not hasattr(self, "_content"):
|
| 638 |
+
raise ResponseNotRead()
|
| 639 |
+
return self._content
|
| 640 |
+
|
| 641 |
+
@property
|
| 642 |
+
def text(self) -> str:
|
| 643 |
+
if not hasattr(self, "_text"):
|
| 644 |
+
content = self.content
|
| 645 |
+
if not content:
|
| 646 |
+
self._text = ""
|
| 647 |
+
else:
|
| 648 |
+
decoder = TextDecoder(encoding=self.encoding or "utf-8")
|
| 649 |
+
self._text = "".join([decoder.decode(self.content), decoder.flush()])
|
| 650 |
+
return self._text
|
| 651 |
+
|
| 652 |
+
@property
|
| 653 |
+
def encoding(self) -> str | None:
|
| 654 |
+
"""
|
| 655 |
+
Return an encoding to use for decoding the byte content into text.
|
| 656 |
+
The priority for determining this is given by...
|
| 657 |
+
|
| 658 |
+
* `.encoding = <>` has been set explicitly.
|
| 659 |
+
* The encoding as specified by the charset parameter in the Content-Type header.
|
| 660 |
+
* The encoding as determined by `default_encoding`, which may either be
|
| 661 |
+
a string like "utf-8" indicating the encoding to use, or may be a callable
|
| 662 |
+
which enables charset autodetection.
|
| 663 |
+
"""
|
| 664 |
+
if not hasattr(self, "_encoding"):
|
| 665 |
+
encoding = self.charset_encoding
|
| 666 |
+
if encoding is None or not _is_known_encoding(encoding):
|
| 667 |
+
if isinstance(self.default_encoding, str):
|
| 668 |
+
encoding = self.default_encoding
|
| 669 |
+
elif hasattr(self, "_content"):
|
| 670 |
+
encoding = self.default_encoding(self._content)
|
| 671 |
+
self._encoding = encoding or "utf-8"
|
| 672 |
+
return self._encoding
|
| 673 |
+
|
| 674 |
+
@encoding.setter
|
| 675 |
+
def encoding(self, value: str) -> None:
|
| 676 |
+
"""
|
| 677 |
+
Set the encoding to use for decoding the byte content into text.
|
| 678 |
+
|
| 679 |
+
If the `text` attribute has been accessed, attempting to set the
|
| 680 |
+
encoding will throw a ValueError.
|
| 681 |
+
"""
|
| 682 |
+
if hasattr(self, "_text"):
|
| 683 |
+
raise ValueError(
|
| 684 |
+
"Setting encoding after `text` has been accessed is not allowed."
|
| 685 |
+
)
|
| 686 |
+
self._encoding = value
|
| 687 |
+
|
| 688 |
+
@property
|
| 689 |
+
def charset_encoding(self) -> str | None:
|
| 690 |
+
"""
|
| 691 |
+
Return the encoding, as specified by the Content-Type header.
|
| 692 |
+
"""
|
| 693 |
+
content_type = self.headers.get("Content-Type")
|
| 694 |
+
if content_type is None:
|
| 695 |
+
return None
|
| 696 |
+
|
| 697 |
+
return _parse_content_type_charset(content_type)
|
| 698 |
+
|
| 699 |
+
def _get_content_decoder(self) -> ContentDecoder:
|
| 700 |
+
"""
|
| 701 |
+
Returns a decoder instance which can be used to decode the raw byte
|
| 702 |
+
content, depending on the Content-Encoding used in the response.
|
| 703 |
+
"""
|
| 704 |
+
if not hasattr(self, "_decoder"):
|
| 705 |
+
decoders: list[ContentDecoder] = []
|
| 706 |
+
values = self.headers.get_list("content-encoding", split_commas=True)
|
| 707 |
+
for value in values:
|
| 708 |
+
value = value.strip().lower()
|
| 709 |
+
try:
|
| 710 |
+
decoder_cls = SUPPORTED_DECODERS[value]
|
| 711 |
+
decoders.append(decoder_cls())
|
| 712 |
+
except KeyError:
|
| 713 |
+
continue
|
| 714 |
+
|
| 715 |
+
if len(decoders) == 1:
|
| 716 |
+
self._decoder = decoders[0]
|
| 717 |
+
elif len(decoders) > 1:
|
| 718 |
+
self._decoder = MultiDecoder(children=decoders)
|
| 719 |
+
else:
|
| 720 |
+
self._decoder = IdentityDecoder()
|
| 721 |
+
|
| 722 |
+
return self._decoder
|
| 723 |
+
|
| 724 |
+
@property
|
| 725 |
+
def is_informational(self) -> bool:
|
| 726 |
+
"""
|
| 727 |
+
A property which is `True` for 1xx status codes, `False` otherwise.
|
| 728 |
+
"""
|
| 729 |
+
return codes.is_informational(self.status_code)
|
| 730 |
+
|
| 731 |
+
@property
|
| 732 |
+
def is_success(self) -> bool:
|
| 733 |
+
"""
|
| 734 |
+
A property which is `True` for 2xx status codes, `False` otherwise.
|
| 735 |
+
"""
|
| 736 |
+
return codes.is_success(self.status_code)
|
| 737 |
+
|
| 738 |
+
@property
|
| 739 |
+
def is_redirect(self) -> bool:
|
| 740 |
+
"""
|
| 741 |
+
A property which is `True` for 3xx status codes, `False` otherwise.
|
| 742 |
+
|
| 743 |
+
Note that not all responses with a 3xx status code indicate a URL redirect.
|
| 744 |
+
|
| 745 |
+
Use `response.has_redirect_location` to determine responses with a properly
|
| 746 |
+
formed URL redirection.
|
| 747 |
+
"""
|
| 748 |
+
return codes.is_redirect(self.status_code)
|
| 749 |
+
|
| 750 |
+
@property
|
| 751 |
+
def is_client_error(self) -> bool:
|
| 752 |
+
"""
|
| 753 |
+
A property which is `True` for 4xx status codes, `False` otherwise.
|
| 754 |
+
"""
|
| 755 |
+
return codes.is_client_error(self.status_code)
|
| 756 |
+
|
| 757 |
+
@property
|
| 758 |
+
def is_server_error(self) -> bool:
|
| 759 |
+
"""
|
| 760 |
+
A property which is `True` for 5xx status codes, `False` otherwise.
|
| 761 |
+
"""
|
| 762 |
+
return codes.is_server_error(self.status_code)
|
| 763 |
+
|
| 764 |
+
@property
|
| 765 |
+
def is_error(self) -> bool:
|
| 766 |
+
"""
|
| 767 |
+
A property which is `True` for 4xx and 5xx status codes, `False` otherwise.
|
| 768 |
+
"""
|
| 769 |
+
return codes.is_error(self.status_code)
|
| 770 |
+
|
| 771 |
+
@property
|
| 772 |
+
def has_redirect_location(self) -> bool:
|
| 773 |
+
"""
|
| 774 |
+
Returns True for 3xx responses with a properly formed URL redirection,
|
| 775 |
+
`False` otherwise.
|
| 776 |
+
"""
|
| 777 |
+
return (
|
| 778 |
+
self.status_code
|
| 779 |
+
in (
|
| 780 |
+
# 301 (Cacheable redirect. Method may change to GET.)
|
| 781 |
+
codes.MOVED_PERMANENTLY,
|
| 782 |
+
# 302 (Uncacheable redirect. Method may change to GET.)
|
| 783 |
+
codes.FOUND,
|
| 784 |
+
# 303 (Client should make a GET or HEAD request.)
|
| 785 |
+
codes.SEE_OTHER,
|
| 786 |
+
# 307 (Equiv. 302, but retain method)
|
| 787 |
+
codes.TEMPORARY_REDIRECT,
|
| 788 |
+
# 308 (Equiv. 301, but retain method)
|
| 789 |
+
codes.PERMANENT_REDIRECT,
|
| 790 |
+
)
|
| 791 |
+
and "Location" in self.headers
|
| 792 |
+
)
|
| 793 |
+
|
| 794 |
+
def raise_for_status(self) -> Response:
|
| 795 |
+
"""
|
| 796 |
+
Raise the `HTTPStatusError` if one occurred.
|
| 797 |
+
"""
|
| 798 |
+
request = self._request
|
| 799 |
+
if request is None:
|
| 800 |
+
raise RuntimeError(
|
| 801 |
+
"Cannot call `raise_for_status` as the request "
|
| 802 |
+
"instance has not been set on this response."
|
| 803 |
+
)
|
| 804 |
+
|
| 805 |
+
if self.is_success:
|
| 806 |
+
return self
|
| 807 |
+
|
| 808 |
+
if self.has_redirect_location:
|
| 809 |
+
message = (
|
| 810 |
+
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
|
| 811 |
+
"Redirect location: '{0.headers[location]}'\n"
|
| 812 |
+
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
|
| 813 |
+
)
|
| 814 |
+
else:
|
| 815 |
+
message = (
|
| 816 |
+
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
|
| 817 |
+
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
|
| 818 |
+
)
|
| 819 |
+
|
| 820 |
+
status_class = self.status_code // 100
|
| 821 |
+
error_types = {
|
| 822 |
+
1: "Informational response",
|
| 823 |
+
3: "Redirect response",
|
| 824 |
+
4: "Client error",
|
| 825 |
+
5: "Server error",
|
| 826 |
+
}
|
| 827 |
+
error_type = error_types.get(status_class, "Invalid status code")
|
| 828 |
+
message = message.format(self, error_type=error_type)
|
| 829 |
+
raise HTTPStatusError(message, request=request, response=self)
|
| 830 |
+
|
| 831 |
+
def json(self, **kwargs: typing.Any) -> typing.Any:
|
| 832 |
+
return jsonlib.loads(self.content, **kwargs)
|
| 833 |
+
|
| 834 |
+
@property
|
| 835 |
+
def cookies(self) -> Cookies:
|
| 836 |
+
if not hasattr(self, "_cookies"):
|
| 837 |
+
self._cookies = Cookies()
|
| 838 |
+
self._cookies.extract_cookies(self)
|
| 839 |
+
return self._cookies
|
| 840 |
+
|
| 841 |
+
@property
|
| 842 |
+
def links(self) -> dict[str | None, dict[str, str]]:
|
| 843 |
+
"""
|
| 844 |
+
Returns the parsed header links of the response, if any
|
| 845 |
+
"""
|
| 846 |
+
header = self.headers.get("link")
|
| 847 |
+
if header is None:
|
| 848 |
+
return {}
|
| 849 |
+
|
| 850 |
+
return {
|
| 851 |
+
(link.get("rel") or link.get("url")): link
|
| 852 |
+
for link in _parse_header_links(header)
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
@property
|
| 856 |
+
def num_bytes_downloaded(self) -> int:
|
| 857 |
+
return self._num_bytes_downloaded
|
| 858 |
+
|
| 859 |
+
def __repr__(self) -> str:
|
| 860 |
+
return f"<Response [{self.status_code} {self.reason_phrase}]>"
|
| 861 |
+
|
| 862 |
+
def __getstate__(self) -> dict[str, typing.Any]:
|
| 863 |
+
return {
|
| 864 |
+
name: value
|
| 865 |
+
for name, value in self.__dict__.items()
|
| 866 |
+
if name not in ["extensions", "stream", "is_closed", "_decoder"]
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
def __setstate__(self, state: dict[str, typing.Any]) -> None:
|
| 870 |
+
for name, value in state.items():
|
| 871 |
+
setattr(self, name, value)
|
| 872 |
+
self.is_closed = True
|
| 873 |
+
self.extensions = {}
|
| 874 |
+
self.stream = UnattachedStream()
|
| 875 |
+
|
| 876 |
+
def read(self) -> bytes:
|
| 877 |
+
"""
|
| 878 |
+
Read and return the response content.
|
| 879 |
+
"""
|
| 880 |
+
if not hasattr(self, "_content"):
|
| 881 |
+
self._content = b"".join(self.iter_bytes())
|
| 882 |
+
return self._content
|
| 883 |
+
|
| 884 |
+
def iter_bytes(self, chunk_size: int | None = None) -> typing.Iterator[bytes]:
|
| 885 |
+
"""
|
| 886 |
+
A byte-iterator over the decoded response content.
|
| 887 |
+
This allows us to handle gzip, deflate, brotli, and zstd encoded responses.
|
| 888 |
+
"""
|
| 889 |
+
if hasattr(self, "_content"):
|
| 890 |
+
chunk_size = len(self._content) if chunk_size is None else chunk_size
|
| 891 |
+
for i in range(0, len(self._content), max(chunk_size, 1)):
|
| 892 |
+
yield self._content[i : i + chunk_size]
|
| 893 |
+
else:
|
| 894 |
+
decoder = self._get_content_decoder()
|
| 895 |
+
chunker = ByteChunker(chunk_size=chunk_size)
|
| 896 |
+
with request_context(request=self._request):
|
| 897 |
+
for raw_bytes in self.iter_raw():
|
| 898 |
+
decoded = decoder.decode(raw_bytes)
|
| 899 |
+
for chunk in chunker.decode(decoded):
|
| 900 |
+
yield chunk
|
| 901 |
+
decoded = decoder.flush()
|
| 902 |
+
for chunk in chunker.decode(decoded):
|
| 903 |
+
yield chunk # pragma: no cover
|
| 904 |
+
for chunk in chunker.flush():
|
| 905 |
+
yield chunk
|
| 906 |
+
|
| 907 |
+
def iter_text(self, chunk_size: int | None = None) -> typing.Iterator[str]:
|
| 908 |
+
"""
|
| 909 |
+
A str-iterator over the decoded response content
|
| 910 |
+
that handles both gzip, deflate, etc but also detects the content's
|
| 911 |
+
string encoding.
|
| 912 |
+
"""
|
| 913 |
+
decoder = TextDecoder(encoding=self.encoding or "utf-8")
|
| 914 |
+
chunker = TextChunker(chunk_size=chunk_size)
|
| 915 |
+
with request_context(request=self._request):
|
| 916 |
+
for byte_content in self.iter_bytes():
|
| 917 |
+
text_content = decoder.decode(byte_content)
|
| 918 |
+
for chunk in chunker.decode(text_content):
|
| 919 |
+
yield chunk
|
| 920 |
+
text_content = decoder.flush()
|
| 921 |
+
for chunk in chunker.decode(text_content):
|
| 922 |
+
yield chunk # pragma: no cover
|
| 923 |
+
for chunk in chunker.flush():
|
| 924 |
+
yield chunk
|
| 925 |
+
|
| 926 |
+
def iter_lines(self) -> typing.Iterator[str]:
|
| 927 |
+
decoder = LineDecoder()
|
| 928 |
+
with request_context(request=self._request):
|
| 929 |
+
for text in self.iter_text():
|
| 930 |
+
for line in decoder.decode(text):
|
| 931 |
+
yield line
|
| 932 |
+
for line in decoder.flush():
|
| 933 |
+
yield line
|
| 934 |
+
|
| 935 |
+
def iter_raw(self, chunk_size: int | None = None) -> typing.Iterator[bytes]:
|
| 936 |
+
"""
|
| 937 |
+
A byte-iterator over the raw response content.
|
| 938 |
+
"""
|
| 939 |
+
if self.is_stream_consumed:
|
| 940 |
+
raise StreamConsumed()
|
| 941 |
+
if self.is_closed:
|
| 942 |
+
raise StreamClosed()
|
| 943 |
+
if not isinstance(self.stream, SyncByteStream):
|
| 944 |
+
raise RuntimeError("Attempted to call a sync iterator on an async stream.")
|
| 945 |
+
|
| 946 |
+
self.is_stream_consumed = True
|
| 947 |
+
self._num_bytes_downloaded = 0
|
| 948 |
+
chunker = ByteChunker(chunk_size=chunk_size)
|
| 949 |
+
|
| 950 |
+
with request_context(request=self._request):
|
| 951 |
+
for raw_stream_bytes in self.stream:
|
| 952 |
+
self._num_bytes_downloaded += len(raw_stream_bytes)
|
| 953 |
+
for chunk in chunker.decode(raw_stream_bytes):
|
| 954 |
+
yield chunk
|
| 955 |
+
|
| 956 |
+
for chunk in chunker.flush():
|
| 957 |
+
yield chunk
|
| 958 |
+
|
| 959 |
+
self.close()
|
| 960 |
+
|
| 961 |
+
def close(self) -> None:
|
| 962 |
+
"""
|
| 963 |
+
Close the response and release the connection.
|
| 964 |
+
Automatically called if the response body is read to completion.
|
| 965 |
+
"""
|
| 966 |
+
if not isinstance(self.stream, SyncByteStream):
|
| 967 |
+
raise RuntimeError("Attempted to call an sync close on an async stream.")
|
| 968 |
+
|
| 969 |
+
if not self.is_closed:
|
| 970 |
+
self.is_closed = True
|
| 971 |
+
with request_context(request=self._request):
|
| 972 |
+
self.stream.close()
|
| 973 |
+
|
| 974 |
+
async def aread(self) -> bytes:
|
| 975 |
+
"""
|
| 976 |
+
Read and return the response content.
|
| 977 |
+
"""
|
| 978 |
+
if not hasattr(self, "_content"):
|
| 979 |
+
self._content = b"".join([part async for part in self.aiter_bytes()])
|
| 980 |
+
return self._content
|
| 981 |
+
|
| 982 |
+
async def aiter_bytes(
|
| 983 |
+
self, chunk_size: int | None = None
|
| 984 |
+
) -> typing.AsyncIterator[bytes]:
|
| 985 |
+
"""
|
| 986 |
+
A byte-iterator over the decoded response content.
|
| 987 |
+
This allows us to handle gzip, deflate, brotli, and zstd encoded responses.
|
| 988 |
+
"""
|
| 989 |
+
if hasattr(self, "_content"):
|
| 990 |
+
chunk_size = len(self._content) if chunk_size is None else chunk_size
|
| 991 |
+
for i in range(0, len(self._content), max(chunk_size, 1)):
|
| 992 |
+
yield self._content[i : i + chunk_size]
|
| 993 |
+
else:
|
| 994 |
+
decoder = self._get_content_decoder()
|
| 995 |
+
chunker = ByteChunker(chunk_size=chunk_size)
|
| 996 |
+
with request_context(request=self._request):
|
| 997 |
+
async for raw_bytes in self.aiter_raw():
|
| 998 |
+
decoded = decoder.decode(raw_bytes)
|
| 999 |
+
for chunk in chunker.decode(decoded):
|
| 1000 |
+
yield chunk
|
| 1001 |
+
decoded = decoder.flush()
|
| 1002 |
+
for chunk in chunker.decode(decoded):
|
| 1003 |
+
yield chunk # pragma: no cover
|
| 1004 |
+
for chunk in chunker.flush():
|
| 1005 |
+
yield chunk
|
| 1006 |
+
|
| 1007 |
+
async def aiter_text(
|
| 1008 |
+
self, chunk_size: int | None = None
|
| 1009 |
+
) -> typing.AsyncIterator[str]:
|
| 1010 |
+
"""
|
| 1011 |
+
A str-iterator over the decoded response content
|
| 1012 |
+
that handles both gzip, deflate, etc but also detects the content's
|
| 1013 |
+
string encoding.
|
| 1014 |
+
"""
|
| 1015 |
+
decoder = TextDecoder(encoding=self.encoding or "utf-8")
|
| 1016 |
+
chunker = TextChunker(chunk_size=chunk_size)
|
| 1017 |
+
with request_context(request=self._request):
|
| 1018 |
+
async for byte_content in self.aiter_bytes():
|
| 1019 |
+
text_content = decoder.decode(byte_content)
|
| 1020 |
+
for chunk in chunker.decode(text_content):
|
| 1021 |
+
yield chunk
|
| 1022 |
+
text_content = decoder.flush()
|
| 1023 |
+
for chunk in chunker.decode(text_content):
|
| 1024 |
+
yield chunk # pragma: no cover
|
| 1025 |
+
for chunk in chunker.flush():
|
| 1026 |
+
yield chunk
|
| 1027 |
+
|
| 1028 |
+
async def aiter_lines(self) -> typing.AsyncIterator[str]:
|
| 1029 |
+
decoder = LineDecoder()
|
| 1030 |
+
with request_context(request=self._request):
|
| 1031 |
+
async for text in self.aiter_text():
|
| 1032 |
+
for line in decoder.decode(text):
|
| 1033 |
+
yield line
|
| 1034 |
+
for line in decoder.flush():
|
| 1035 |
+
yield line
|
| 1036 |
+
|
| 1037 |
+
async def aiter_raw(
|
| 1038 |
+
self, chunk_size: int | None = None
|
| 1039 |
+
) -> typing.AsyncIterator[bytes]:
|
| 1040 |
+
"""
|
| 1041 |
+
A byte-iterator over the raw response content.
|
| 1042 |
+
"""
|
| 1043 |
+
if self.is_stream_consumed:
|
| 1044 |
+
raise StreamConsumed()
|
| 1045 |
+
if self.is_closed:
|
| 1046 |
+
raise StreamClosed()
|
| 1047 |
+
if not isinstance(self.stream, AsyncByteStream):
|
| 1048 |
+
raise RuntimeError("Attempted to call an async iterator on an sync stream.")
|
| 1049 |
+
|
| 1050 |
+
self.is_stream_consumed = True
|
| 1051 |
+
self._num_bytes_downloaded = 0
|
| 1052 |
+
chunker = ByteChunker(chunk_size=chunk_size)
|
| 1053 |
+
|
| 1054 |
+
with request_context(request=self._request):
|
| 1055 |
+
async for raw_stream_bytes in self.stream:
|
| 1056 |
+
self._num_bytes_downloaded += len(raw_stream_bytes)
|
| 1057 |
+
for chunk in chunker.decode(raw_stream_bytes):
|
| 1058 |
+
yield chunk
|
| 1059 |
+
|
| 1060 |
+
for chunk in chunker.flush():
|
| 1061 |
+
yield chunk
|
| 1062 |
+
|
| 1063 |
+
await self.aclose()
|
| 1064 |
+
|
| 1065 |
+
async def aclose(self) -> None:
|
| 1066 |
+
"""
|
| 1067 |
+
Close the response and release the connection.
|
| 1068 |
+
Automatically called if the response body is read to completion.
|
| 1069 |
+
"""
|
| 1070 |
+
if not isinstance(self.stream, AsyncByteStream):
|
| 1071 |
+
raise RuntimeError("Attempted to call an async close on an sync stream.")
|
| 1072 |
+
|
| 1073 |
+
if not self.is_closed:
|
| 1074 |
+
self.is_closed = True
|
| 1075 |
+
with request_context(request=self._request):
|
| 1076 |
+
await self.stream.aclose()
|
| 1077 |
+
|
| 1078 |
+
|
| 1079 |
+
class Cookies(typing.MutableMapping[str, str]):
|
| 1080 |
+
"""
|
| 1081 |
+
HTTP Cookies, as a mutable mapping.
|
| 1082 |
+
"""
|
| 1083 |
+
|
| 1084 |
+
def __init__(self, cookies: CookieTypes | None = None) -> None:
|
| 1085 |
+
if cookies is None or isinstance(cookies, dict):
|
| 1086 |
+
self.jar = CookieJar()
|
| 1087 |
+
if isinstance(cookies, dict):
|
| 1088 |
+
for key, value in cookies.items():
|
| 1089 |
+
self.set(key, value)
|
| 1090 |
+
elif isinstance(cookies, list):
|
| 1091 |
+
self.jar = CookieJar()
|
| 1092 |
+
for key, value in cookies:
|
| 1093 |
+
self.set(key, value)
|
| 1094 |
+
elif isinstance(cookies, Cookies):
|
| 1095 |
+
self.jar = CookieJar()
|
| 1096 |
+
for cookie in cookies.jar:
|
| 1097 |
+
self.jar.set_cookie(cookie)
|
| 1098 |
+
else:
|
| 1099 |
+
self.jar = cookies
|
| 1100 |
+
|
| 1101 |
+
def extract_cookies(self, response: Response) -> None:
|
| 1102 |
+
"""
|
| 1103 |
+
Loads any cookies based on the response `Set-Cookie` headers.
|
| 1104 |
+
"""
|
| 1105 |
+
urllib_response = self._CookieCompatResponse(response)
|
| 1106 |
+
urllib_request = self._CookieCompatRequest(response.request)
|
| 1107 |
+
|
| 1108 |
+
self.jar.extract_cookies(urllib_response, urllib_request) # type: ignore
|
| 1109 |
+
|
| 1110 |
+
def set_cookie_header(self, request: Request) -> None:
|
| 1111 |
+
"""
|
| 1112 |
+
Sets an appropriate 'Cookie:' HTTP header on the `Request`.
|
| 1113 |
+
"""
|
| 1114 |
+
urllib_request = self._CookieCompatRequest(request)
|
| 1115 |
+
self.jar.add_cookie_header(urllib_request)
|
| 1116 |
+
|
| 1117 |
+
def set(self, name: str, value: str, domain: str = "", path: str = "/") -> None:
|
| 1118 |
+
"""
|
| 1119 |
+
Set a cookie value by name. May optionally include domain and path.
|
| 1120 |
+
"""
|
| 1121 |
+
kwargs = {
|
| 1122 |
+
"version": 0,
|
| 1123 |
+
"name": name,
|
| 1124 |
+
"value": value,
|
| 1125 |
+
"port": None,
|
| 1126 |
+
"port_specified": False,
|
| 1127 |
+
"domain": domain,
|
| 1128 |
+
"domain_specified": bool(domain),
|
| 1129 |
+
"domain_initial_dot": domain.startswith("."),
|
| 1130 |
+
"path": path,
|
| 1131 |
+
"path_specified": bool(path),
|
| 1132 |
+
"secure": False,
|
| 1133 |
+
"expires": None,
|
| 1134 |
+
"discard": True,
|
| 1135 |
+
"comment": None,
|
| 1136 |
+
"comment_url": None,
|
| 1137 |
+
"rest": {"HttpOnly": None},
|
| 1138 |
+
"rfc2109": False,
|
| 1139 |
+
}
|
| 1140 |
+
cookie = Cookie(**kwargs) # type: ignore
|
| 1141 |
+
self.jar.set_cookie(cookie)
|
| 1142 |
+
|
| 1143 |
+
def get( # type: ignore
|
| 1144 |
+
self,
|
| 1145 |
+
name: str,
|
| 1146 |
+
default: str | None = None,
|
| 1147 |
+
domain: str | None = None,
|
| 1148 |
+
path: str | None = None,
|
| 1149 |
+
) -> str | None:
|
| 1150 |
+
"""
|
| 1151 |
+
Get a cookie by name. May optionally include domain and path
|
| 1152 |
+
in order to specify exactly which cookie to retrieve.
|
| 1153 |
+
"""
|
| 1154 |
+
value = None
|
| 1155 |
+
for cookie in self.jar:
|
| 1156 |
+
if cookie.name == name:
|
| 1157 |
+
if domain is None or cookie.domain == domain:
|
| 1158 |
+
if path is None or cookie.path == path:
|
| 1159 |
+
if value is not None:
|
| 1160 |
+
message = f"Multiple cookies exist with name={name}"
|
| 1161 |
+
raise CookieConflict(message)
|
| 1162 |
+
value = cookie.value
|
| 1163 |
+
|
| 1164 |
+
if value is None:
|
| 1165 |
+
return default
|
| 1166 |
+
return value
|
| 1167 |
+
|
| 1168 |
+
def delete(
|
| 1169 |
+
self,
|
| 1170 |
+
name: str,
|
| 1171 |
+
domain: str | None = None,
|
| 1172 |
+
path: str | None = None,
|
| 1173 |
+
) -> None:
|
| 1174 |
+
"""
|
| 1175 |
+
Delete a cookie by name. May optionally include domain and path
|
| 1176 |
+
in order to specify exactly which cookie to delete.
|
| 1177 |
+
"""
|
| 1178 |
+
if domain is not None and path is not None:
|
| 1179 |
+
return self.jar.clear(domain, path, name)
|
| 1180 |
+
|
| 1181 |
+
remove = [
|
| 1182 |
+
cookie
|
| 1183 |
+
for cookie in self.jar
|
| 1184 |
+
if cookie.name == name
|
| 1185 |
+
and (domain is None or cookie.domain == domain)
|
| 1186 |
+
and (path is None or cookie.path == path)
|
| 1187 |
+
]
|
| 1188 |
+
|
| 1189 |
+
for cookie in remove:
|
| 1190 |
+
self.jar.clear(cookie.domain, cookie.path, cookie.name)
|
| 1191 |
+
|
| 1192 |
+
def clear(self, domain: str | None = None, path: str | None = None) -> None:
|
| 1193 |
+
"""
|
| 1194 |
+
Delete all cookies. Optionally include a domain and path in
|
| 1195 |
+
order to only delete a subset of all the cookies.
|
| 1196 |
+
"""
|
| 1197 |
+
args = []
|
| 1198 |
+
if domain is not None:
|
| 1199 |
+
args.append(domain)
|
| 1200 |
+
if path is not None:
|
| 1201 |
+
assert domain is not None
|
| 1202 |
+
args.append(path)
|
| 1203 |
+
self.jar.clear(*args)
|
| 1204 |
+
|
| 1205 |
+
def update(self, cookies: CookieTypes | None = None) -> None: # type: ignore
|
| 1206 |
+
cookies = Cookies(cookies)
|
| 1207 |
+
for cookie in cookies.jar:
|
| 1208 |
+
self.jar.set_cookie(cookie)
|
| 1209 |
+
|
| 1210 |
+
def __setitem__(self, name: str, value: str) -> None:
|
| 1211 |
+
return self.set(name, value)
|
| 1212 |
+
|
| 1213 |
+
def __getitem__(self, name: str) -> str:
|
| 1214 |
+
value = self.get(name)
|
| 1215 |
+
if value is None:
|
| 1216 |
+
raise KeyError(name)
|
| 1217 |
+
return value
|
| 1218 |
+
|
| 1219 |
+
def __delitem__(self, name: str) -> None:
|
| 1220 |
+
return self.delete(name)
|
| 1221 |
+
|
| 1222 |
+
def __len__(self) -> int:
|
| 1223 |
+
return len(self.jar)
|
| 1224 |
+
|
| 1225 |
+
def __iter__(self) -> typing.Iterator[str]:
|
| 1226 |
+
return (cookie.name for cookie in self.jar)
|
| 1227 |
+
|
| 1228 |
+
def __bool__(self) -> bool:
|
| 1229 |
+
for _ in self.jar:
|
| 1230 |
+
return True
|
| 1231 |
+
return False
|
| 1232 |
+
|
| 1233 |
+
def __repr__(self) -> str:
|
| 1234 |
+
cookies_repr = ", ".join(
|
| 1235 |
+
[
|
| 1236 |
+
f"<Cookie {cookie.name}={cookie.value} for {cookie.domain} />"
|
| 1237 |
+
for cookie in self.jar
|
| 1238 |
+
]
|
| 1239 |
+
)
|
| 1240 |
+
|
| 1241 |
+
return f"<Cookies[{cookies_repr}]>"
|
| 1242 |
+
|
| 1243 |
+
class _CookieCompatRequest(urllib.request.Request):
|
| 1244 |
+
"""
|
| 1245 |
+
Wraps a `Request` instance up in a compatibility interface suitable
|
| 1246 |
+
for use with `CookieJar` operations.
|
| 1247 |
+
"""
|
| 1248 |
+
|
| 1249 |
+
def __init__(self, request: Request) -> None:
|
| 1250 |
+
super().__init__(
|
| 1251 |
+
url=str(request.url),
|
| 1252 |
+
headers=dict(request.headers),
|
| 1253 |
+
method=request.method,
|
| 1254 |
+
)
|
| 1255 |
+
self.request = request
|
| 1256 |
+
|
| 1257 |
+
def add_unredirected_header(self, key: str, value: str) -> None:
|
| 1258 |
+
super().add_unredirected_header(key, value)
|
| 1259 |
+
self.request.headers[key] = value
|
| 1260 |
+
|
| 1261 |
+
class _CookieCompatResponse:
|
| 1262 |
+
"""
|
| 1263 |
+
Wraps a `Request` instance up in a compatibility interface suitable
|
| 1264 |
+
for use with `CookieJar` operations.
|
| 1265 |
+
"""
|
| 1266 |
+
|
| 1267 |
+
def __init__(self, response: Response) -> None:
|
| 1268 |
+
self.response = response
|
| 1269 |
+
|
| 1270 |
+
def info(self) -> email.message.Message:
|
| 1271 |
+
info = email.message.Message()
|
| 1272 |
+
for key, value in self.response.headers.multi_items():
|
| 1273 |
+
# Note that setting `info[key]` here is an "append" operation,
|
| 1274 |
+
# not a "replace" operation.
|
| 1275 |
+
# https://docs.python.org/3/library/email.compat32-message.html#email.message.Message.__setitem__
|
| 1276 |
+
info[key] = value
|
| 1277 |
+
return info
|
.venv/lib/python3.11/site-packages/httpx/_multipart.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import io
|
| 4 |
+
import mimetypes
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
import typing
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from ._types import (
|
| 11 |
+
AsyncByteStream,
|
| 12 |
+
FileContent,
|
| 13 |
+
FileTypes,
|
| 14 |
+
RequestData,
|
| 15 |
+
RequestFiles,
|
| 16 |
+
SyncByteStream,
|
| 17 |
+
)
|
| 18 |
+
from ._utils import (
|
| 19 |
+
peek_filelike_length,
|
| 20 |
+
primitive_value_to_str,
|
| 21 |
+
to_bytes,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"}
|
| 25 |
+
_HTML5_FORM_ENCODING_REPLACEMENTS.update(
|
| 26 |
+
{chr(c): "%{:02X}".format(c) for c in range(0x1F + 1) if c != 0x1B}
|
| 27 |
+
)
|
| 28 |
+
_HTML5_FORM_ENCODING_RE = re.compile(
|
| 29 |
+
r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()])
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _format_form_param(name: str, value: str) -> bytes:
|
| 34 |
+
"""
|
| 35 |
+
Encode a name/value pair within a multipart form.
|
| 36 |
+
"""
|
| 37 |
+
|
| 38 |
+
def replacer(match: typing.Match[str]) -> str:
|
| 39 |
+
return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)]
|
| 40 |
+
|
| 41 |
+
value = _HTML5_FORM_ENCODING_RE.sub(replacer, value)
|
| 42 |
+
return f'{name}="{value}"'.encode()
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _guess_content_type(filename: str | None) -> str | None:
|
| 46 |
+
"""
|
| 47 |
+
Guesses the mimetype based on a filename. Defaults to `application/octet-stream`.
|
| 48 |
+
|
| 49 |
+
Returns `None` if `filename` is `None` or empty.
|
| 50 |
+
"""
|
| 51 |
+
if filename:
|
| 52 |
+
return mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
| 53 |
+
return None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def get_multipart_boundary_from_content_type(
|
| 57 |
+
content_type: bytes | None,
|
| 58 |
+
) -> bytes | None:
|
| 59 |
+
if not content_type or not content_type.startswith(b"multipart/form-data"):
|
| 60 |
+
return None
|
| 61 |
+
# parse boundary according to
|
| 62 |
+
# https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1
|
| 63 |
+
if b";" in content_type:
|
| 64 |
+
for section in content_type.split(b";"):
|
| 65 |
+
if section.strip().lower().startswith(b"boundary="):
|
| 66 |
+
return section.strip()[len(b"boundary=") :].strip(b'"')
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class DataField:
|
| 71 |
+
"""
|
| 72 |
+
A single form field item, within a multipart form field.
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
def __init__(self, name: str, value: str | bytes | int | float | None) -> None:
|
| 76 |
+
if not isinstance(name, str):
|
| 77 |
+
raise TypeError(
|
| 78 |
+
f"Invalid type for name. Expected str, got {type(name)}: {name!r}"
|
| 79 |
+
)
|
| 80 |
+
if value is not None and not isinstance(value, (str, bytes, int, float)):
|
| 81 |
+
raise TypeError(
|
| 82 |
+
"Invalid type for value. Expected primitive type,"
|
| 83 |
+
f" got {type(value)}: {value!r}"
|
| 84 |
+
)
|
| 85 |
+
self.name = name
|
| 86 |
+
self.value: str | bytes = (
|
| 87 |
+
value if isinstance(value, bytes) else primitive_value_to_str(value)
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
def render_headers(self) -> bytes:
|
| 91 |
+
if not hasattr(self, "_headers"):
|
| 92 |
+
name = _format_form_param("name", self.name)
|
| 93 |
+
self._headers = b"".join(
|
| 94 |
+
[b"Content-Disposition: form-data; ", name, b"\r\n\r\n"]
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
return self._headers
|
| 98 |
+
|
| 99 |
+
def render_data(self) -> bytes:
|
| 100 |
+
if not hasattr(self, "_data"):
|
| 101 |
+
self._data = to_bytes(self.value)
|
| 102 |
+
|
| 103 |
+
return self._data
|
| 104 |
+
|
| 105 |
+
def get_length(self) -> int:
|
| 106 |
+
headers = self.render_headers()
|
| 107 |
+
data = self.render_data()
|
| 108 |
+
return len(headers) + len(data)
|
| 109 |
+
|
| 110 |
+
def render(self) -> typing.Iterator[bytes]:
|
| 111 |
+
yield self.render_headers()
|
| 112 |
+
yield self.render_data()
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class FileField:
|
| 116 |
+
"""
|
| 117 |
+
A single file field item, within a multipart form field.
|
| 118 |
+
"""
|
| 119 |
+
|
| 120 |
+
CHUNK_SIZE = 64 * 1024
|
| 121 |
+
|
| 122 |
+
def __init__(self, name: str, value: FileTypes) -> None:
|
| 123 |
+
self.name = name
|
| 124 |
+
|
| 125 |
+
fileobj: FileContent
|
| 126 |
+
|
| 127 |
+
headers: dict[str, str] = {}
|
| 128 |
+
content_type: str | None = None
|
| 129 |
+
|
| 130 |
+
# This large tuple based API largely mirror's requests' API
|
| 131 |
+
# It would be good to think of better APIs for this that we could
|
| 132 |
+
# include in httpx 2.0 since variable length tuples(especially of 4 elements)
|
| 133 |
+
# are quite unwieldly
|
| 134 |
+
if isinstance(value, tuple):
|
| 135 |
+
if len(value) == 2:
|
| 136 |
+
# neither the 3rd parameter (content_type) nor the 4th (headers)
|
| 137 |
+
# was included
|
| 138 |
+
filename, fileobj = value
|
| 139 |
+
elif len(value) == 3:
|
| 140 |
+
filename, fileobj, content_type = value
|
| 141 |
+
else:
|
| 142 |
+
# all 4 parameters included
|
| 143 |
+
filename, fileobj, content_type, headers = value # type: ignore
|
| 144 |
+
else:
|
| 145 |
+
filename = Path(str(getattr(value, "name", "upload"))).name
|
| 146 |
+
fileobj = value
|
| 147 |
+
|
| 148 |
+
if content_type is None:
|
| 149 |
+
content_type = _guess_content_type(filename)
|
| 150 |
+
|
| 151 |
+
has_content_type_header = any("content-type" in key.lower() for key in headers)
|
| 152 |
+
if content_type is not None and not has_content_type_header:
|
| 153 |
+
# note that unlike requests, we ignore the content_type provided in the 3rd
|
| 154 |
+
# tuple element if it is also included in the headers requests does
|
| 155 |
+
# the opposite (it overwrites the headerwith the 3rd tuple element)
|
| 156 |
+
headers["Content-Type"] = content_type
|
| 157 |
+
|
| 158 |
+
if isinstance(fileobj, io.StringIO):
|
| 159 |
+
raise TypeError(
|
| 160 |
+
"Multipart file uploads require 'io.BytesIO', not 'io.StringIO'."
|
| 161 |
+
)
|
| 162 |
+
if isinstance(fileobj, io.TextIOBase):
|
| 163 |
+
raise TypeError(
|
| 164 |
+
"Multipart file uploads must be opened in binary mode, not text mode."
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
self.filename = filename
|
| 168 |
+
self.file = fileobj
|
| 169 |
+
self.headers = headers
|
| 170 |
+
|
| 171 |
+
def get_length(self) -> int | None:
|
| 172 |
+
headers = self.render_headers()
|
| 173 |
+
|
| 174 |
+
if isinstance(self.file, (str, bytes)):
|
| 175 |
+
return len(headers) + len(to_bytes(self.file))
|
| 176 |
+
|
| 177 |
+
file_length = peek_filelike_length(self.file)
|
| 178 |
+
|
| 179 |
+
# If we can't determine the filesize without reading it into memory,
|
| 180 |
+
# then return `None` here, to indicate an unknown file length.
|
| 181 |
+
if file_length is None:
|
| 182 |
+
return None
|
| 183 |
+
|
| 184 |
+
return len(headers) + file_length
|
| 185 |
+
|
| 186 |
+
def render_headers(self) -> bytes:
|
| 187 |
+
if not hasattr(self, "_headers"):
|
| 188 |
+
parts = [
|
| 189 |
+
b"Content-Disposition: form-data; ",
|
| 190 |
+
_format_form_param("name", self.name),
|
| 191 |
+
]
|
| 192 |
+
if self.filename:
|
| 193 |
+
filename = _format_form_param("filename", self.filename)
|
| 194 |
+
parts.extend([b"; ", filename])
|
| 195 |
+
for header_name, header_value in self.headers.items():
|
| 196 |
+
key, val = f"\r\n{header_name}: ".encode(), header_value.encode()
|
| 197 |
+
parts.extend([key, val])
|
| 198 |
+
parts.append(b"\r\n\r\n")
|
| 199 |
+
self._headers = b"".join(parts)
|
| 200 |
+
|
| 201 |
+
return self._headers
|
| 202 |
+
|
| 203 |
+
def render_data(self) -> typing.Iterator[bytes]:
|
| 204 |
+
if isinstance(self.file, (str, bytes)):
|
| 205 |
+
yield to_bytes(self.file)
|
| 206 |
+
return
|
| 207 |
+
|
| 208 |
+
if hasattr(self.file, "seek"):
|
| 209 |
+
try:
|
| 210 |
+
self.file.seek(0)
|
| 211 |
+
except io.UnsupportedOperation:
|
| 212 |
+
pass
|
| 213 |
+
|
| 214 |
+
chunk = self.file.read(self.CHUNK_SIZE)
|
| 215 |
+
while chunk:
|
| 216 |
+
yield to_bytes(chunk)
|
| 217 |
+
chunk = self.file.read(self.CHUNK_SIZE)
|
| 218 |
+
|
| 219 |
+
def render(self) -> typing.Iterator[bytes]:
|
| 220 |
+
yield self.render_headers()
|
| 221 |
+
yield from self.render_data()
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
class MultipartStream(SyncByteStream, AsyncByteStream):
|
| 225 |
+
"""
|
| 226 |
+
Request content as streaming multipart encoded form data.
|
| 227 |
+
"""
|
| 228 |
+
|
| 229 |
+
def __init__(
|
| 230 |
+
self,
|
| 231 |
+
data: RequestData,
|
| 232 |
+
files: RequestFiles,
|
| 233 |
+
boundary: bytes | None = None,
|
| 234 |
+
) -> None:
|
| 235 |
+
if boundary is None:
|
| 236 |
+
boundary = os.urandom(16).hex().encode("ascii")
|
| 237 |
+
|
| 238 |
+
self.boundary = boundary
|
| 239 |
+
self.content_type = "multipart/form-data; boundary=%s" % boundary.decode(
|
| 240 |
+
"ascii"
|
| 241 |
+
)
|
| 242 |
+
self.fields = list(self._iter_fields(data, files))
|
| 243 |
+
|
| 244 |
+
def _iter_fields(
|
| 245 |
+
self, data: RequestData, files: RequestFiles
|
| 246 |
+
) -> typing.Iterator[FileField | DataField]:
|
| 247 |
+
for name, value in data.items():
|
| 248 |
+
if isinstance(value, (tuple, list)):
|
| 249 |
+
for item in value:
|
| 250 |
+
yield DataField(name=name, value=item)
|
| 251 |
+
else:
|
| 252 |
+
yield DataField(name=name, value=value)
|
| 253 |
+
|
| 254 |
+
file_items = files.items() if isinstance(files, typing.Mapping) else files
|
| 255 |
+
for name, value in file_items:
|
| 256 |
+
yield FileField(name=name, value=value)
|
| 257 |
+
|
| 258 |
+
def iter_chunks(self) -> typing.Iterator[bytes]:
|
| 259 |
+
for field in self.fields:
|
| 260 |
+
yield b"--%s\r\n" % self.boundary
|
| 261 |
+
yield from field.render()
|
| 262 |
+
yield b"\r\n"
|
| 263 |
+
yield b"--%s--\r\n" % self.boundary
|
| 264 |
+
|
| 265 |
+
def get_content_length(self) -> int | None:
|
| 266 |
+
"""
|
| 267 |
+
Return the length of the multipart encoded content, or `None` if
|
| 268 |
+
any of the files have a length that cannot be determined upfront.
|
| 269 |
+
"""
|
| 270 |
+
boundary_length = len(self.boundary)
|
| 271 |
+
length = 0
|
| 272 |
+
|
| 273 |
+
for field in self.fields:
|
| 274 |
+
field_length = field.get_length()
|
| 275 |
+
if field_length is None:
|
| 276 |
+
return None
|
| 277 |
+
|
| 278 |
+
length += 2 + boundary_length + 2 # b"--{boundary}\r\n"
|
| 279 |
+
length += field_length
|
| 280 |
+
length += 2 # b"\r\n"
|
| 281 |
+
|
| 282 |
+
length += 2 + boundary_length + 4 # b"--{boundary}--\r\n"
|
| 283 |
+
return length
|
| 284 |
+
|
| 285 |
+
# Content stream interface.
|
| 286 |
+
|
| 287 |
+
def get_headers(self) -> dict[str, str]:
|
| 288 |
+
content_length = self.get_content_length()
|
| 289 |
+
content_type = self.content_type
|
| 290 |
+
if content_length is None:
|
| 291 |
+
return {"Transfer-Encoding": "chunked", "Content-Type": content_type}
|
| 292 |
+
return {"Content-Length": str(content_length), "Content-Type": content_type}
|
| 293 |
+
|
| 294 |
+
def __iter__(self) -> typing.Iterator[bytes]:
|
| 295 |
+
for chunk in self.iter_chunks():
|
| 296 |
+
yield chunk
|
| 297 |
+
|
| 298 |
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
| 299 |
+
for chunk in self.iter_chunks():
|
| 300 |
+
yield chunk
|
.venv/lib/python3.11/site-packages/httpx/_status_codes.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from enum import IntEnum
|
| 4 |
+
|
| 5 |
+
__all__ = ["codes"]
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class codes(IntEnum):
|
| 9 |
+
"""HTTP status codes and reason phrases
|
| 10 |
+
|
| 11 |
+
Status codes from the following RFCs are all observed:
|
| 12 |
+
|
| 13 |
+
* RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616
|
| 14 |
+
* RFC 6585: Additional HTTP Status Codes
|
| 15 |
+
* RFC 3229: Delta encoding in HTTP
|
| 16 |
+
* RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518
|
| 17 |
+
* RFC 5842: Binding Extensions to WebDAV
|
| 18 |
+
* RFC 7238: Permanent Redirect
|
| 19 |
+
* RFC 2295: Transparent Content Negotiation in HTTP
|
| 20 |
+
* RFC 2774: An HTTP Extension Framework
|
| 21 |
+
* RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2)
|
| 22 |
+
* RFC 2324: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0)
|
| 23 |
+
* RFC 7725: An HTTP Status Code to Report Legal Obstacles
|
| 24 |
+
* RFC 8297: An HTTP Status Code for Indicating Hints
|
| 25 |
+
* RFC 8470: Using Early Data in HTTP
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
def __new__(cls, value: int, phrase: str = "") -> codes:
|
| 29 |
+
obj = int.__new__(cls, value)
|
| 30 |
+
obj._value_ = value
|
| 31 |
+
|
| 32 |
+
obj.phrase = phrase # type: ignore[attr-defined]
|
| 33 |
+
return obj
|
| 34 |
+
|
| 35 |
+
def __str__(self) -> str:
|
| 36 |
+
return str(self.value)
|
| 37 |
+
|
| 38 |
+
@classmethod
|
| 39 |
+
def get_reason_phrase(cls, value: int) -> str:
|
| 40 |
+
try:
|
| 41 |
+
return codes(value).phrase # type: ignore
|
| 42 |
+
except ValueError:
|
| 43 |
+
return ""
|
| 44 |
+
|
| 45 |
+
@classmethod
|
| 46 |
+
def is_informational(cls, value: int) -> bool:
|
| 47 |
+
"""
|
| 48 |
+
Returns `True` for 1xx status codes, `False` otherwise.
|
| 49 |
+
"""
|
| 50 |
+
return 100 <= value <= 199
|
| 51 |
+
|
| 52 |
+
@classmethod
|
| 53 |
+
def is_success(cls, value: int) -> bool:
|
| 54 |
+
"""
|
| 55 |
+
Returns `True` for 2xx status codes, `False` otherwise.
|
| 56 |
+
"""
|
| 57 |
+
return 200 <= value <= 299
|
| 58 |
+
|
| 59 |
+
@classmethod
|
| 60 |
+
def is_redirect(cls, value: int) -> bool:
|
| 61 |
+
"""
|
| 62 |
+
Returns `True` for 3xx status codes, `False` otherwise.
|
| 63 |
+
"""
|
| 64 |
+
return 300 <= value <= 399
|
| 65 |
+
|
| 66 |
+
@classmethod
|
| 67 |
+
def is_client_error(cls, value: int) -> bool:
|
| 68 |
+
"""
|
| 69 |
+
Returns `True` for 4xx status codes, `False` otherwise.
|
| 70 |
+
"""
|
| 71 |
+
return 400 <= value <= 499
|
| 72 |
+
|
| 73 |
+
@classmethod
|
| 74 |
+
def is_server_error(cls, value: int) -> bool:
|
| 75 |
+
"""
|
| 76 |
+
Returns `True` for 5xx status codes, `False` otherwise.
|
| 77 |
+
"""
|
| 78 |
+
return 500 <= value <= 599
|
| 79 |
+
|
| 80 |
+
@classmethod
|
| 81 |
+
def is_error(cls, value: int) -> bool:
|
| 82 |
+
"""
|
| 83 |
+
Returns `True` for 4xx or 5xx status codes, `False` otherwise.
|
| 84 |
+
"""
|
| 85 |
+
return 400 <= value <= 599
|
| 86 |
+
|
| 87 |
+
# informational
|
| 88 |
+
CONTINUE = 100, "Continue"
|
| 89 |
+
SWITCHING_PROTOCOLS = 101, "Switching Protocols"
|
| 90 |
+
PROCESSING = 102, "Processing"
|
| 91 |
+
EARLY_HINTS = 103, "Early Hints"
|
| 92 |
+
|
| 93 |
+
# success
|
| 94 |
+
OK = 200, "OK"
|
| 95 |
+
CREATED = 201, "Created"
|
| 96 |
+
ACCEPTED = 202, "Accepted"
|
| 97 |
+
NON_AUTHORITATIVE_INFORMATION = 203, "Non-Authoritative Information"
|
| 98 |
+
NO_CONTENT = 204, "No Content"
|
| 99 |
+
RESET_CONTENT = 205, "Reset Content"
|
| 100 |
+
PARTIAL_CONTENT = 206, "Partial Content"
|
| 101 |
+
MULTI_STATUS = 207, "Multi-Status"
|
| 102 |
+
ALREADY_REPORTED = 208, "Already Reported"
|
| 103 |
+
IM_USED = 226, "IM Used"
|
| 104 |
+
|
| 105 |
+
# redirection
|
| 106 |
+
MULTIPLE_CHOICES = 300, "Multiple Choices"
|
| 107 |
+
MOVED_PERMANENTLY = 301, "Moved Permanently"
|
| 108 |
+
FOUND = 302, "Found"
|
| 109 |
+
SEE_OTHER = 303, "See Other"
|
| 110 |
+
NOT_MODIFIED = 304, "Not Modified"
|
| 111 |
+
USE_PROXY = 305, "Use Proxy"
|
| 112 |
+
TEMPORARY_REDIRECT = 307, "Temporary Redirect"
|
| 113 |
+
PERMANENT_REDIRECT = 308, "Permanent Redirect"
|
| 114 |
+
|
| 115 |
+
# client error
|
| 116 |
+
BAD_REQUEST = 400, "Bad Request"
|
| 117 |
+
UNAUTHORIZED = 401, "Unauthorized"
|
| 118 |
+
PAYMENT_REQUIRED = 402, "Payment Required"
|
| 119 |
+
FORBIDDEN = 403, "Forbidden"
|
| 120 |
+
NOT_FOUND = 404, "Not Found"
|
| 121 |
+
METHOD_NOT_ALLOWED = 405, "Method Not Allowed"
|
| 122 |
+
NOT_ACCEPTABLE = 406, "Not Acceptable"
|
| 123 |
+
PROXY_AUTHENTICATION_REQUIRED = 407, "Proxy Authentication Required"
|
| 124 |
+
REQUEST_TIMEOUT = 408, "Request Timeout"
|
| 125 |
+
CONFLICT = 409, "Conflict"
|
| 126 |
+
GONE = 410, "Gone"
|
| 127 |
+
LENGTH_REQUIRED = 411, "Length Required"
|
| 128 |
+
PRECONDITION_FAILED = 412, "Precondition Failed"
|
| 129 |
+
REQUEST_ENTITY_TOO_LARGE = 413, "Request Entity Too Large"
|
| 130 |
+
REQUEST_URI_TOO_LONG = 414, "Request-URI Too Long"
|
| 131 |
+
UNSUPPORTED_MEDIA_TYPE = 415, "Unsupported Media Type"
|
| 132 |
+
REQUESTED_RANGE_NOT_SATISFIABLE = 416, "Requested Range Not Satisfiable"
|
| 133 |
+
EXPECTATION_FAILED = 417, "Expectation Failed"
|
| 134 |
+
IM_A_TEAPOT = 418, "I'm a teapot"
|
| 135 |
+
MISDIRECTED_REQUEST = 421, "Misdirected Request"
|
| 136 |
+
UNPROCESSABLE_ENTITY = 422, "Unprocessable Entity"
|
| 137 |
+
LOCKED = 423, "Locked"
|
| 138 |
+
FAILED_DEPENDENCY = 424, "Failed Dependency"
|
| 139 |
+
TOO_EARLY = 425, "Too Early"
|
| 140 |
+
UPGRADE_REQUIRED = 426, "Upgrade Required"
|
| 141 |
+
PRECONDITION_REQUIRED = 428, "Precondition Required"
|
| 142 |
+
TOO_MANY_REQUESTS = 429, "Too Many Requests"
|
| 143 |
+
REQUEST_HEADER_FIELDS_TOO_LARGE = 431, "Request Header Fields Too Large"
|
| 144 |
+
UNAVAILABLE_FOR_LEGAL_REASONS = 451, "Unavailable For Legal Reasons"
|
| 145 |
+
|
| 146 |
+
# server errors
|
| 147 |
+
INTERNAL_SERVER_ERROR = 500, "Internal Server Error"
|
| 148 |
+
NOT_IMPLEMENTED = 501, "Not Implemented"
|
| 149 |
+
BAD_GATEWAY = 502, "Bad Gateway"
|
| 150 |
+
SERVICE_UNAVAILABLE = 503, "Service Unavailable"
|
| 151 |
+
GATEWAY_TIMEOUT = 504, "Gateway Timeout"
|
| 152 |
+
HTTP_VERSION_NOT_SUPPORTED = 505, "HTTP Version Not Supported"
|
| 153 |
+
VARIANT_ALSO_NEGOTIATES = 506, "Variant Also Negotiates"
|
| 154 |
+
INSUFFICIENT_STORAGE = 507, "Insufficient Storage"
|
| 155 |
+
LOOP_DETECTED = 508, "Loop Detected"
|
| 156 |
+
NOT_EXTENDED = 510, "Not Extended"
|
| 157 |
+
NETWORK_AUTHENTICATION_REQUIRED = 511, "Network Authentication Required"
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
# Include lower-case styles for `requests` compatibility.
|
| 161 |
+
for code in codes:
|
| 162 |
+
setattr(codes, code._name_.lower(), int(code))
|
.venv/lib/python3.11/site-packages/httpx/_transports/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .asgi import *
|
| 2 |
+
from .base import *
|
| 3 |
+
from .default import *
|
| 4 |
+
from .mock import *
|
| 5 |
+
from .wsgi import *
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"ASGITransport",
|
| 9 |
+
"AsyncBaseTransport",
|
| 10 |
+
"BaseTransport",
|
| 11 |
+
"AsyncHTTPTransport",
|
| 12 |
+
"HTTPTransport",
|
| 13 |
+
"MockTransport",
|
| 14 |
+
"WSGITransport",
|
| 15 |
+
]
|
.venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (475 Bytes). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/asgi.cpython-311.pyc
ADDED
|
Binary file (8.12 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/base.cpython-311.pyc
ADDED
|
Binary file (4.2 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/default.cpython-311.pyc
ADDED
|
Binary file (17.5 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/mock.cpython-311.pyc
ADDED
|
Binary file (2.12 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/_transports/__pycache__/wsgi.cpython-311.pyc
ADDED
|
Binary file (7.39 kB). View file
|
|
|
.venv/lib/python3.11/site-packages/httpx/_transports/asgi.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
|
| 5 |
+
from .._models import Request, Response
|
| 6 |
+
from .._types import AsyncByteStream
|
| 7 |
+
from .base import AsyncBaseTransport
|
| 8 |
+
|
| 9 |
+
if typing.TYPE_CHECKING: # pragma: no cover
|
| 10 |
+
import asyncio
|
| 11 |
+
|
| 12 |
+
import trio
|
| 13 |
+
|
| 14 |
+
Event = typing.Union[asyncio.Event, trio.Event]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
_Message = typing.MutableMapping[str, typing.Any]
|
| 18 |
+
_Receive = typing.Callable[[], typing.Awaitable[_Message]]
|
| 19 |
+
_Send = typing.Callable[
|
| 20 |
+
[typing.MutableMapping[str, typing.Any]], typing.Awaitable[None]
|
| 21 |
+
]
|
| 22 |
+
_ASGIApp = typing.Callable[
|
| 23 |
+
[typing.MutableMapping[str, typing.Any], _Receive, _Send], typing.Awaitable[None]
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
__all__ = ["ASGITransport"]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def is_running_trio() -> bool:
|
| 30 |
+
try:
|
| 31 |
+
# sniffio is a dependency of trio.
|
| 32 |
+
|
| 33 |
+
# See https://github.com/python-trio/trio/issues/2802
|
| 34 |
+
import sniffio
|
| 35 |
+
|
| 36 |
+
if sniffio.current_async_library() == "trio":
|
| 37 |
+
return True
|
| 38 |
+
except ImportError: # pragma: nocover
|
| 39 |
+
pass
|
| 40 |
+
|
| 41 |
+
return False
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def create_event() -> Event:
|
| 45 |
+
if is_running_trio():
|
| 46 |
+
import trio
|
| 47 |
+
|
| 48 |
+
return trio.Event()
|
| 49 |
+
|
| 50 |
+
import asyncio
|
| 51 |
+
|
| 52 |
+
return asyncio.Event()
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class ASGIResponseStream(AsyncByteStream):
|
| 56 |
+
def __init__(self, body: list[bytes]) -> None:
|
| 57 |
+
self._body = body
|
| 58 |
+
|
| 59 |
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
| 60 |
+
yield b"".join(self._body)
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class ASGITransport(AsyncBaseTransport):
|
| 64 |
+
"""
|
| 65 |
+
A custom AsyncTransport that handles sending requests directly to an ASGI app.
|
| 66 |
+
|
| 67 |
+
```python
|
| 68 |
+
transport = httpx.ASGITransport(
|
| 69 |
+
app=app,
|
| 70 |
+
root_path="/submount",
|
| 71 |
+
client=("1.2.3.4", 123)
|
| 72 |
+
)
|
| 73 |
+
client = httpx.AsyncClient(transport=transport)
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
Arguments:
|
| 77 |
+
|
| 78 |
+
* `app` - The ASGI application.
|
| 79 |
+
* `raise_app_exceptions` - Boolean indicating if exceptions in the application
|
| 80 |
+
should be raised. Default to `True`. Can be set to `False` for use cases
|
| 81 |
+
such as testing the content of a client 500 response.
|
| 82 |
+
* `root_path` - The root path on which the ASGI application should be mounted.
|
| 83 |
+
* `client` - A two-tuple indicating the client IP and port of incoming requests.
|
| 84 |
+
```
|
| 85 |
+
"""
|
| 86 |
+
|
| 87 |
+
def __init__(
|
| 88 |
+
self,
|
| 89 |
+
app: _ASGIApp,
|
| 90 |
+
raise_app_exceptions: bool = True,
|
| 91 |
+
root_path: str = "",
|
| 92 |
+
client: tuple[str, int] = ("127.0.0.1", 123),
|
| 93 |
+
) -> None:
|
| 94 |
+
self.app = app
|
| 95 |
+
self.raise_app_exceptions = raise_app_exceptions
|
| 96 |
+
self.root_path = root_path
|
| 97 |
+
self.client = client
|
| 98 |
+
|
| 99 |
+
async def handle_async_request(
|
| 100 |
+
self,
|
| 101 |
+
request: Request,
|
| 102 |
+
) -> Response:
|
| 103 |
+
assert isinstance(request.stream, AsyncByteStream)
|
| 104 |
+
|
| 105 |
+
# ASGI scope.
|
| 106 |
+
scope = {
|
| 107 |
+
"type": "http",
|
| 108 |
+
"asgi": {"version": "3.0"},
|
| 109 |
+
"http_version": "1.1",
|
| 110 |
+
"method": request.method,
|
| 111 |
+
"headers": [(k.lower(), v) for (k, v) in request.headers.raw],
|
| 112 |
+
"scheme": request.url.scheme,
|
| 113 |
+
"path": request.url.path,
|
| 114 |
+
"raw_path": request.url.raw_path.split(b"?")[0],
|
| 115 |
+
"query_string": request.url.query,
|
| 116 |
+
"server": (request.url.host, request.url.port),
|
| 117 |
+
"client": self.client,
|
| 118 |
+
"root_path": self.root_path,
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
# Request.
|
| 122 |
+
request_body_chunks = request.stream.__aiter__()
|
| 123 |
+
request_complete = False
|
| 124 |
+
|
| 125 |
+
# Response.
|
| 126 |
+
status_code = None
|
| 127 |
+
response_headers = None
|
| 128 |
+
body_parts = []
|
| 129 |
+
response_started = False
|
| 130 |
+
response_complete = create_event()
|
| 131 |
+
|
| 132 |
+
# ASGI callables.
|
| 133 |
+
|
| 134 |
+
async def receive() -> dict[str, typing.Any]:
|
| 135 |
+
nonlocal request_complete
|
| 136 |
+
|
| 137 |
+
if request_complete:
|
| 138 |
+
await response_complete.wait()
|
| 139 |
+
return {"type": "http.disconnect"}
|
| 140 |
+
|
| 141 |
+
try:
|
| 142 |
+
body = await request_body_chunks.__anext__()
|
| 143 |
+
except StopAsyncIteration:
|
| 144 |
+
request_complete = True
|
| 145 |
+
return {"type": "http.request", "body": b"", "more_body": False}
|
| 146 |
+
return {"type": "http.request", "body": body, "more_body": True}
|
| 147 |
+
|
| 148 |
+
async def send(message: typing.MutableMapping[str, typing.Any]) -> None:
|
| 149 |
+
nonlocal status_code, response_headers, response_started
|
| 150 |
+
|
| 151 |
+
if message["type"] == "http.response.start":
|
| 152 |
+
assert not response_started
|
| 153 |
+
|
| 154 |
+
status_code = message["status"]
|
| 155 |
+
response_headers = message.get("headers", [])
|
| 156 |
+
response_started = True
|
| 157 |
+
|
| 158 |
+
elif message["type"] == "http.response.body":
|
| 159 |
+
assert not response_complete.is_set()
|
| 160 |
+
body = message.get("body", b"")
|
| 161 |
+
more_body = message.get("more_body", False)
|
| 162 |
+
|
| 163 |
+
if body and request.method != "HEAD":
|
| 164 |
+
body_parts.append(body)
|
| 165 |
+
|
| 166 |
+
if not more_body:
|
| 167 |
+
response_complete.set()
|
| 168 |
+
|
| 169 |
+
try:
|
| 170 |
+
await self.app(scope, receive, send)
|
| 171 |
+
except Exception: # noqa: PIE-786
|
| 172 |
+
if self.raise_app_exceptions:
|
| 173 |
+
raise
|
| 174 |
+
|
| 175 |
+
response_complete.set()
|
| 176 |
+
if status_code is None:
|
| 177 |
+
status_code = 500
|
| 178 |
+
if response_headers is None:
|
| 179 |
+
response_headers = {}
|
| 180 |
+
|
| 181 |
+
assert response_complete.is_set()
|
| 182 |
+
assert status_code is not None
|
| 183 |
+
assert response_headers is not None
|
| 184 |
+
|
| 185 |
+
stream = ASGIResponseStream(body_parts)
|
| 186 |
+
|
| 187 |
+
return Response(status_code, headers=response_headers, stream=stream)
|
.venv/lib/python3.11/site-packages/httpx/_transports/base.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
from types import TracebackType
|
| 5 |
+
|
| 6 |
+
from .._models import Request, Response
|
| 7 |
+
|
| 8 |
+
T = typing.TypeVar("T", bound="BaseTransport")
|
| 9 |
+
A = typing.TypeVar("A", bound="AsyncBaseTransport")
|
| 10 |
+
|
| 11 |
+
__all__ = ["AsyncBaseTransport", "BaseTransport"]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class BaseTransport:
|
| 15 |
+
def __enter__(self: T) -> T:
|
| 16 |
+
return self
|
| 17 |
+
|
| 18 |
+
def __exit__(
|
| 19 |
+
self,
|
| 20 |
+
exc_type: type[BaseException] | None = None,
|
| 21 |
+
exc_value: BaseException | None = None,
|
| 22 |
+
traceback: TracebackType | None = None,
|
| 23 |
+
) -> None:
|
| 24 |
+
self.close()
|
| 25 |
+
|
| 26 |
+
def handle_request(self, request: Request) -> Response:
|
| 27 |
+
"""
|
| 28 |
+
Send a single HTTP request and return a response.
|
| 29 |
+
|
| 30 |
+
Developers shouldn't typically ever need to call into this API directly,
|
| 31 |
+
since the Client class provides all the higher level user-facing API
|
| 32 |
+
niceties.
|
| 33 |
+
|
| 34 |
+
In order to properly release any network resources, the response
|
| 35 |
+
stream should *either* be consumed immediately, with a call to
|
| 36 |
+
`response.stream.read()`, or else the `handle_request` call should
|
| 37 |
+
be followed with a try/finally block to ensuring the stream is
|
| 38 |
+
always closed.
|
| 39 |
+
|
| 40 |
+
Example usage:
|
| 41 |
+
|
| 42 |
+
with httpx.HTTPTransport() as transport:
|
| 43 |
+
req = httpx.Request(
|
| 44 |
+
method=b"GET",
|
| 45 |
+
url=(b"https", b"www.example.com", 443, b"/"),
|
| 46 |
+
headers=[(b"Host", b"www.example.com")],
|
| 47 |
+
)
|
| 48 |
+
resp = transport.handle_request(req)
|
| 49 |
+
body = resp.stream.read()
|
| 50 |
+
print(resp.status_code, resp.headers, body)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
Takes a `Request` instance as the only argument.
|
| 54 |
+
|
| 55 |
+
Returns a `Response` instance.
|
| 56 |
+
"""
|
| 57 |
+
raise NotImplementedError(
|
| 58 |
+
"The 'handle_request' method must be implemented."
|
| 59 |
+
) # pragma: no cover
|
| 60 |
+
|
| 61 |
+
def close(self) -> None:
|
| 62 |
+
pass
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class AsyncBaseTransport:
|
| 66 |
+
async def __aenter__(self: A) -> A:
|
| 67 |
+
return self
|
| 68 |
+
|
| 69 |
+
async def __aexit__(
|
| 70 |
+
self,
|
| 71 |
+
exc_type: type[BaseException] | None = None,
|
| 72 |
+
exc_value: BaseException | None = None,
|
| 73 |
+
traceback: TracebackType | None = None,
|
| 74 |
+
) -> None:
|
| 75 |
+
await self.aclose()
|
| 76 |
+
|
| 77 |
+
async def handle_async_request(
|
| 78 |
+
self,
|
| 79 |
+
request: Request,
|
| 80 |
+
) -> Response:
|
| 81 |
+
raise NotImplementedError(
|
| 82 |
+
"The 'handle_async_request' method must be implemented."
|
| 83 |
+
) # pragma: no cover
|
| 84 |
+
|
| 85 |
+
async def aclose(self) -> None:
|
| 86 |
+
pass
|
.venv/lib/python3.11/site-packages/httpx/_transports/default.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Custom transports, with nicely configured defaults.
|
| 3 |
+
|
| 4 |
+
The following additional keyword arguments are currently supported by httpcore...
|
| 5 |
+
|
| 6 |
+
* uds: str
|
| 7 |
+
* local_address: str
|
| 8 |
+
* retries: int
|
| 9 |
+
|
| 10 |
+
Example usages...
|
| 11 |
+
|
| 12 |
+
# Disable HTTP/2 on a single specific domain.
|
| 13 |
+
mounts = {
|
| 14 |
+
"all://": httpx.HTTPTransport(http2=True),
|
| 15 |
+
"all://*example.org": httpx.HTTPTransport()
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
# Using advanced httpcore configuration, with connection retries.
|
| 19 |
+
transport = httpx.HTTPTransport(retries=1)
|
| 20 |
+
client = httpx.Client(transport=transport)
|
| 21 |
+
|
| 22 |
+
# Using advanced httpcore configuration, with unix domain sockets.
|
| 23 |
+
transport = httpx.HTTPTransport(uds="socket.uds")
|
| 24 |
+
client = httpx.Client(transport=transport)
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import contextlib
|
| 30 |
+
import typing
|
| 31 |
+
from types import TracebackType
|
| 32 |
+
|
| 33 |
+
if typing.TYPE_CHECKING:
|
| 34 |
+
import ssl # pragma: no cover
|
| 35 |
+
|
| 36 |
+
import httpx # pragma: no cover
|
| 37 |
+
|
| 38 |
+
from .._config import DEFAULT_LIMITS, Limits, Proxy, create_ssl_context
|
| 39 |
+
from .._exceptions import (
|
| 40 |
+
ConnectError,
|
| 41 |
+
ConnectTimeout,
|
| 42 |
+
LocalProtocolError,
|
| 43 |
+
NetworkError,
|
| 44 |
+
PoolTimeout,
|
| 45 |
+
ProtocolError,
|
| 46 |
+
ProxyError,
|
| 47 |
+
ReadError,
|
| 48 |
+
ReadTimeout,
|
| 49 |
+
RemoteProtocolError,
|
| 50 |
+
TimeoutException,
|
| 51 |
+
UnsupportedProtocol,
|
| 52 |
+
WriteError,
|
| 53 |
+
WriteTimeout,
|
| 54 |
+
)
|
| 55 |
+
from .._models import Request, Response
|
| 56 |
+
from .._types import AsyncByteStream, CertTypes, ProxyTypes, SyncByteStream
|
| 57 |
+
from .._urls import URL
|
| 58 |
+
from .base import AsyncBaseTransport, BaseTransport
|
| 59 |
+
|
| 60 |
+
T = typing.TypeVar("T", bound="HTTPTransport")
|
| 61 |
+
A = typing.TypeVar("A", bound="AsyncHTTPTransport")
|
| 62 |
+
|
| 63 |
+
SOCKET_OPTION = typing.Union[
|
| 64 |
+
typing.Tuple[int, int, int],
|
| 65 |
+
typing.Tuple[int, int, typing.Union[bytes, bytearray]],
|
| 66 |
+
typing.Tuple[int, int, None, int],
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
__all__ = ["AsyncHTTPTransport", "HTTPTransport"]
|
| 70 |
+
|
| 71 |
+
HTTPCORE_EXC_MAP: dict[type[Exception], type[httpx.HTTPError]] = {}
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _load_httpcore_exceptions() -> dict[type[Exception], type[httpx.HTTPError]]:
|
| 75 |
+
import httpcore
|
| 76 |
+
|
| 77 |
+
return {
|
| 78 |
+
httpcore.TimeoutException: TimeoutException,
|
| 79 |
+
httpcore.ConnectTimeout: ConnectTimeout,
|
| 80 |
+
httpcore.ReadTimeout: ReadTimeout,
|
| 81 |
+
httpcore.WriteTimeout: WriteTimeout,
|
| 82 |
+
httpcore.PoolTimeout: PoolTimeout,
|
| 83 |
+
httpcore.NetworkError: NetworkError,
|
| 84 |
+
httpcore.ConnectError: ConnectError,
|
| 85 |
+
httpcore.ReadError: ReadError,
|
| 86 |
+
httpcore.WriteError: WriteError,
|
| 87 |
+
httpcore.ProxyError: ProxyError,
|
| 88 |
+
httpcore.UnsupportedProtocol: UnsupportedProtocol,
|
| 89 |
+
httpcore.ProtocolError: ProtocolError,
|
| 90 |
+
httpcore.LocalProtocolError: LocalProtocolError,
|
| 91 |
+
httpcore.RemoteProtocolError: RemoteProtocolError,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@contextlib.contextmanager
|
| 96 |
+
def map_httpcore_exceptions() -> typing.Iterator[None]:
|
| 97 |
+
global HTTPCORE_EXC_MAP
|
| 98 |
+
if len(HTTPCORE_EXC_MAP) == 0:
|
| 99 |
+
HTTPCORE_EXC_MAP = _load_httpcore_exceptions()
|
| 100 |
+
try:
|
| 101 |
+
yield
|
| 102 |
+
except Exception as exc:
|
| 103 |
+
mapped_exc = None
|
| 104 |
+
|
| 105 |
+
for from_exc, to_exc in HTTPCORE_EXC_MAP.items():
|
| 106 |
+
if not isinstance(exc, from_exc):
|
| 107 |
+
continue
|
| 108 |
+
# We want to map to the most specific exception we can find.
|
| 109 |
+
# Eg if `exc` is an `httpcore.ReadTimeout`, we want to map to
|
| 110 |
+
# `httpx.ReadTimeout`, not just `httpx.TimeoutException`.
|
| 111 |
+
if mapped_exc is None or issubclass(to_exc, mapped_exc):
|
| 112 |
+
mapped_exc = to_exc
|
| 113 |
+
|
| 114 |
+
if mapped_exc is None: # pragma: no cover
|
| 115 |
+
raise
|
| 116 |
+
|
| 117 |
+
message = str(exc)
|
| 118 |
+
raise mapped_exc(message) from exc
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
class ResponseStream(SyncByteStream):
|
| 122 |
+
def __init__(self, httpcore_stream: typing.Iterable[bytes]) -> None:
|
| 123 |
+
self._httpcore_stream = httpcore_stream
|
| 124 |
+
|
| 125 |
+
def __iter__(self) -> typing.Iterator[bytes]:
|
| 126 |
+
with map_httpcore_exceptions():
|
| 127 |
+
for part in self._httpcore_stream:
|
| 128 |
+
yield part
|
| 129 |
+
|
| 130 |
+
def close(self) -> None:
|
| 131 |
+
if hasattr(self._httpcore_stream, "close"):
|
| 132 |
+
self._httpcore_stream.close()
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class HTTPTransport(BaseTransport):
|
| 136 |
+
def __init__(
|
| 137 |
+
self,
|
| 138 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 139 |
+
cert: CertTypes | None = None,
|
| 140 |
+
trust_env: bool = True,
|
| 141 |
+
http1: bool = True,
|
| 142 |
+
http2: bool = False,
|
| 143 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 144 |
+
proxy: ProxyTypes | None = None,
|
| 145 |
+
uds: str | None = None,
|
| 146 |
+
local_address: str | None = None,
|
| 147 |
+
retries: int = 0,
|
| 148 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 149 |
+
) -> None:
|
| 150 |
+
import httpcore
|
| 151 |
+
|
| 152 |
+
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
|
| 153 |
+
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
|
| 154 |
+
|
| 155 |
+
if proxy is None:
|
| 156 |
+
self._pool = httpcore.ConnectionPool(
|
| 157 |
+
ssl_context=ssl_context,
|
| 158 |
+
max_connections=limits.max_connections,
|
| 159 |
+
max_keepalive_connections=limits.max_keepalive_connections,
|
| 160 |
+
keepalive_expiry=limits.keepalive_expiry,
|
| 161 |
+
http1=http1,
|
| 162 |
+
http2=http2,
|
| 163 |
+
uds=uds,
|
| 164 |
+
local_address=local_address,
|
| 165 |
+
retries=retries,
|
| 166 |
+
socket_options=socket_options,
|
| 167 |
+
)
|
| 168 |
+
elif proxy.url.scheme in ("http", "https"):
|
| 169 |
+
self._pool = httpcore.HTTPProxy(
|
| 170 |
+
proxy_url=httpcore.URL(
|
| 171 |
+
scheme=proxy.url.raw_scheme,
|
| 172 |
+
host=proxy.url.raw_host,
|
| 173 |
+
port=proxy.url.port,
|
| 174 |
+
target=proxy.url.raw_path,
|
| 175 |
+
),
|
| 176 |
+
proxy_auth=proxy.raw_auth,
|
| 177 |
+
proxy_headers=proxy.headers.raw,
|
| 178 |
+
ssl_context=ssl_context,
|
| 179 |
+
proxy_ssl_context=proxy.ssl_context,
|
| 180 |
+
max_connections=limits.max_connections,
|
| 181 |
+
max_keepalive_connections=limits.max_keepalive_connections,
|
| 182 |
+
keepalive_expiry=limits.keepalive_expiry,
|
| 183 |
+
http1=http1,
|
| 184 |
+
http2=http2,
|
| 185 |
+
socket_options=socket_options,
|
| 186 |
+
)
|
| 187 |
+
elif proxy.url.scheme in ("socks5", "socks5h"):
|
| 188 |
+
try:
|
| 189 |
+
import socksio # noqa
|
| 190 |
+
except ImportError: # pragma: no cover
|
| 191 |
+
raise ImportError(
|
| 192 |
+
"Using SOCKS proxy, but the 'socksio' package is not installed. "
|
| 193 |
+
"Make sure to install httpx using `pip install httpx[socks]`."
|
| 194 |
+
) from None
|
| 195 |
+
|
| 196 |
+
self._pool = httpcore.SOCKSProxy(
|
| 197 |
+
proxy_url=httpcore.URL(
|
| 198 |
+
scheme=proxy.url.raw_scheme,
|
| 199 |
+
host=proxy.url.raw_host,
|
| 200 |
+
port=proxy.url.port,
|
| 201 |
+
target=proxy.url.raw_path,
|
| 202 |
+
),
|
| 203 |
+
proxy_auth=proxy.raw_auth,
|
| 204 |
+
ssl_context=ssl_context,
|
| 205 |
+
max_connections=limits.max_connections,
|
| 206 |
+
max_keepalive_connections=limits.max_keepalive_connections,
|
| 207 |
+
keepalive_expiry=limits.keepalive_expiry,
|
| 208 |
+
http1=http1,
|
| 209 |
+
http2=http2,
|
| 210 |
+
)
|
| 211 |
+
else: # pragma: no cover
|
| 212 |
+
raise ValueError(
|
| 213 |
+
"Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h',"
|
| 214 |
+
f" but got {proxy.url.scheme!r}."
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
def __enter__(self: T) -> T: # Use generics for subclass support.
|
| 218 |
+
self._pool.__enter__()
|
| 219 |
+
return self
|
| 220 |
+
|
| 221 |
+
def __exit__(
|
| 222 |
+
self,
|
| 223 |
+
exc_type: type[BaseException] | None = None,
|
| 224 |
+
exc_value: BaseException | None = None,
|
| 225 |
+
traceback: TracebackType | None = None,
|
| 226 |
+
) -> None:
|
| 227 |
+
with map_httpcore_exceptions():
|
| 228 |
+
self._pool.__exit__(exc_type, exc_value, traceback)
|
| 229 |
+
|
| 230 |
+
def handle_request(
|
| 231 |
+
self,
|
| 232 |
+
request: Request,
|
| 233 |
+
) -> Response:
|
| 234 |
+
assert isinstance(request.stream, SyncByteStream)
|
| 235 |
+
import httpcore
|
| 236 |
+
|
| 237 |
+
req = httpcore.Request(
|
| 238 |
+
method=request.method,
|
| 239 |
+
url=httpcore.URL(
|
| 240 |
+
scheme=request.url.raw_scheme,
|
| 241 |
+
host=request.url.raw_host,
|
| 242 |
+
port=request.url.port,
|
| 243 |
+
target=request.url.raw_path,
|
| 244 |
+
),
|
| 245 |
+
headers=request.headers.raw,
|
| 246 |
+
content=request.stream,
|
| 247 |
+
extensions=request.extensions,
|
| 248 |
+
)
|
| 249 |
+
with map_httpcore_exceptions():
|
| 250 |
+
resp = self._pool.handle_request(req)
|
| 251 |
+
|
| 252 |
+
assert isinstance(resp.stream, typing.Iterable)
|
| 253 |
+
|
| 254 |
+
return Response(
|
| 255 |
+
status_code=resp.status,
|
| 256 |
+
headers=resp.headers,
|
| 257 |
+
stream=ResponseStream(resp.stream),
|
| 258 |
+
extensions=resp.extensions,
|
| 259 |
+
)
|
| 260 |
+
|
| 261 |
+
def close(self) -> None:
|
| 262 |
+
self._pool.close()
|
| 263 |
+
|
| 264 |
+
|
| 265 |
+
class AsyncResponseStream(AsyncByteStream):
|
| 266 |
+
def __init__(self, httpcore_stream: typing.AsyncIterable[bytes]) -> None:
|
| 267 |
+
self._httpcore_stream = httpcore_stream
|
| 268 |
+
|
| 269 |
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
| 270 |
+
with map_httpcore_exceptions():
|
| 271 |
+
async for part in self._httpcore_stream:
|
| 272 |
+
yield part
|
| 273 |
+
|
| 274 |
+
async def aclose(self) -> None:
|
| 275 |
+
if hasattr(self._httpcore_stream, "aclose"):
|
| 276 |
+
await self._httpcore_stream.aclose()
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
class AsyncHTTPTransport(AsyncBaseTransport):
|
| 280 |
+
def __init__(
|
| 281 |
+
self,
|
| 282 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 283 |
+
cert: CertTypes | None = None,
|
| 284 |
+
trust_env: bool = True,
|
| 285 |
+
http1: bool = True,
|
| 286 |
+
http2: bool = False,
|
| 287 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 288 |
+
proxy: ProxyTypes | None = None,
|
| 289 |
+
uds: str | None = None,
|
| 290 |
+
local_address: str | None = None,
|
| 291 |
+
retries: int = 0,
|
| 292 |
+
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
|
| 293 |
+
) -> None:
|
| 294 |
+
import httpcore
|
| 295 |
+
|
| 296 |
+
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
|
| 297 |
+
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
|
| 298 |
+
|
| 299 |
+
if proxy is None:
|
| 300 |
+
self._pool = httpcore.AsyncConnectionPool(
|
| 301 |
+
ssl_context=ssl_context,
|
| 302 |
+
max_connections=limits.max_connections,
|
| 303 |
+
max_keepalive_connections=limits.max_keepalive_connections,
|
| 304 |
+
keepalive_expiry=limits.keepalive_expiry,
|
| 305 |
+
http1=http1,
|
| 306 |
+
http2=http2,
|
| 307 |
+
uds=uds,
|
| 308 |
+
local_address=local_address,
|
| 309 |
+
retries=retries,
|
| 310 |
+
socket_options=socket_options,
|
| 311 |
+
)
|
| 312 |
+
elif proxy.url.scheme in ("http", "https"):
|
| 313 |
+
self._pool = httpcore.AsyncHTTPProxy(
|
| 314 |
+
proxy_url=httpcore.URL(
|
| 315 |
+
scheme=proxy.url.raw_scheme,
|
| 316 |
+
host=proxy.url.raw_host,
|
| 317 |
+
port=proxy.url.port,
|
| 318 |
+
target=proxy.url.raw_path,
|
| 319 |
+
),
|
| 320 |
+
proxy_auth=proxy.raw_auth,
|
| 321 |
+
proxy_headers=proxy.headers.raw,
|
| 322 |
+
proxy_ssl_context=proxy.ssl_context,
|
| 323 |
+
ssl_context=ssl_context,
|
| 324 |
+
max_connections=limits.max_connections,
|
| 325 |
+
max_keepalive_connections=limits.max_keepalive_connections,
|
| 326 |
+
keepalive_expiry=limits.keepalive_expiry,
|
| 327 |
+
http1=http1,
|
| 328 |
+
http2=http2,
|
| 329 |
+
socket_options=socket_options,
|
| 330 |
+
)
|
| 331 |
+
elif proxy.url.scheme in ("socks5", "socks5h"):
|
| 332 |
+
try:
|
| 333 |
+
import socksio # noqa
|
| 334 |
+
except ImportError: # pragma: no cover
|
| 335 |
+
raise ImportError(
|
| 336 |
+
"Using SOCKS proxy, but the 'socksio' package is not installed. "
|
| 337 |
+
"Make sure to install httpx using `pip install httpx[socks]`."
|
| 338 |
+
) from None
|
| 339 |
+
|
| 340 |
+
self._pool = httpcore.AsyncSOCKSProxy(
|
| 341 |
+
proxy_url=httpcore.URL(
|
| 342 |
+
scheme=proxy.url.raw_scheme,
|
| 343 |
+
host=proxy.url.raw_host,
|
| 344 |
+
port=proxy.url.port,
|
| 345 |
+
target=proxy.url.raw_path,
|
| 346 |
+
),
|
| 347 |
+
proxy_auth=proxy.raw_auth,
|
| 348 |
+
ssl_context=ssl_context,
|
| 349 |
+
max_connections=limits.max_connections,
|
| 350 |
+
max_keepalive_connections=limits.max_keepalive_connections,
|
| 351 |
+
keepalive_expiry=limits.keepalive_expiry,
|
| 352 |
+
http1=http1,
|
| 353 |
+
http2=http2,
|
| 354 |
+
)
|
| 355 |
+
else: # pragma: no cover
|
| 356 |
+
raise ValueError(
|
| 357 |
+
"Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h',"
|
| 358 |
+
" but got {proxy.url.scheme!r}."
|
| 359 |
+
)
|
| 360 |
+
|
| 361 |
+
async def __aenter__(self: A) -> A: # Use generics for subclass support.
|
| 362 |
+
await self._pool.__aenter__()
|
| 363 |
+
return self
|
| 364 |
+
|
| 365 |
+
async def __aexit__(
|
| 366 |
+
self,
|
| 367 |
+
exc_type: type[BaseException] | None = None,
|
| 368 |
+
exc_value: BaseException | None = None,
|
| 369 |
+
traceback: TracebackType | None = None,
|
| 370 |
+
) -> None:
|
| 371 |
+
with map_httpcore_exceptions():
|
| 372 |
+
await self._pool.__aexit__(exc_type, exc_value, traceback)
|
| 373 |
+
|
| 374 |
+
async def handle_async_request(
|
| 375 |
+
self,
|
| 376 |
+
request: Request,
|
| 377 |
+
) -> Response:
|
| 378 |
+
assert isinstance(request.stream, AsyncByteStream)
|
| 379 |
+
import httpcore
|
| 380 |
+
|
| 381 |
+
req = httpcore.Request(
|
| 382 |
+
method=request.method,
|
| 383 |
+
url=httpcore.URL(
|
| 384 |
+
scheme=request.url.raw_scheme,
|
| 385 |
+
host=request.url.raw_host,
|
| 386 |
+
port=request.url.port,
|
| 387 |
+
target=request.url.raw_path,
|
| 388 |
+
),
|
| 389 |
+
headers=request.headers.raw,
|
| 390 |
+
content=request.stream,
|
| 391 |
+
extensions=request.extensions,
|
| 392 |
+
)
|
| 393 |
+
with map_httpcore_exceptions():
|
| 394 |
+
resp = await self._pool.handle_async_request(req)
|
| 395 |
+
|
| 396 |
+
assert isinstance(resp.stream, typing.AsyncIterable)
|
| 397 |
+
|
| 398 |
+
return Response(
|
| 399 |
+
status_code=resp.status,
|
| 400 |
+
headers=resp.headers,
|
| 401 |
+
stream=AsyncResponseStream(resp.stream),
|
| 402 |
+
extensions=resp.extensions,
|
| 403 |
+
)
|
| 404 |
+
|
| 405 |
+
async def aclose(self) -> None:
|
| 406 |
+
await self._pool.aclose()
|
.venv/lib/python3.11/site-packages/httpx/_transports/mock.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
|
| 5 |
+
from .._models import Request, Response
|
| 6 |
+
from .base import AsyncBaseTransport, BaseTransport
|
| 7 |
+
|
| 8 |
+
SyncHandler = typing.Callable[[Request], Response]
|
| 9 |
+
AsyncHandler = typing.Callable[[Request], typing.Coroutine[None, None, Response]]
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
__all__ = ["MockTransport"]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class MockTransport(AsyncBaseTransport, BaseTransport):
|
| 16 |
+
def __init__(self, handler: SyncHandler | AsyncHandler) -> None:
|
| 17 |
+
self.handler = handler
|
| 18 |
+
|
| 19 |
+
def handle_request(
|
| 20 |
+
self,
|
| 21 |
+
request: Request,
|
| 22 |
+
) -> Response:
|
| 23 |
+
request.read()
|
| 24 |
+
response = self.handler(request)
|
| 25 |
+
if not isinstance(response, Response): # pragma: no cover
|
| 26 |
+
raise TypeError("Cannot use an async handler in a sync Client")
|
| 27 |
+
return response
|
| 28 |
+
|
| 29 |
+
async def handle_async_request(
|
| 30 |
+
self,
|
| 31 |
+
request: Request,
|
| 32 |
+
) -> Response:
|
| 33 |
+
await request.aread()
|
| 34 |
+
response = self.handler(request)
|
| 35 |
+
|
| 36 |
+
# Allow handler to *optionally* be an `async` function.
|
| 37 |
+
# If it is, then the `response` variable need to be awaited to actually
|
| 38 |
+
# return the result.
|
| 39 |
+
|
| 40 |
+
if not isinstance(response, Response):
|
| 41 |
+
response = await response
|
| 42 |
+
|
| 43 |
+
return response
|
.venv/lib/python3.11/site-packages/httpx/_transports/wsgi.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import io
|
| 4 |
+
import itertools
|
| 5 |
+
import sys
|
| 6 |
+
import typing
|
| 7 |
+
|
| 8 |
+
from .._models import Request, Response
|
| 9 |
+
from .._types import SyncByteStream
|
| 10 |
+
from .base import BaseTransport
|
| 11 |
+
|
| 12 |
+
if typing.TYPE_CHECKING:
|
| 13 |
+
from _typeshed import OptExcInfo # pragma: no cover
|
| 14 |
+
from _typeshed.wsgi import WSGIApplication # pragma: no cover
|
| 15 |
+
|
| 16 |
+
_T = typing.TypeVar("_T")
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
__all__ = ["WSGITransport"]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _skip_leading_empty_chunks(body: typing.Iterable[_T]) -> typing.Iterable[_T]:
|
| 23 |
+
body = iter(body)
|
| 24 |
+
for chunk in body:
|
| 25 |
+
if chunk:
|
| 26 |
+
return itertools.chain([chunk], body)
|
| 27 |
+
return []
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class WSGIByteStream(SyncByteStream):
|
| 31 |
+
def __init__(self, result: typing.Iterable[bytes]) -> None:
|
| 32 |
+
self._close = getattr(result, "close", None)
|
| 33 |
+
self._result = _skip_leading_empty_chunks(result)
|
| 34 |
+
|
| 35 |
+
def __iter__(self) -> typing.Iterator[bytes]:
|
| 36 |
+
for part in self._result:
|
| 37 |
+
yield part
|
| 38 |
+
|
| 39 |
+
def close(self) -> None:
|
| 40 |
+
if self._close is not None:
|
| 41 |
+
self._close()
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class WSGITransport(BaseTransport):
|
| 45 |
+
"""
|
| 46 |
+
A custom transport that handles sending requests directly to an WSGI app.
|
| 47 |
+
The simplest way to use this functionality is to use the `app` argument.
|
| 48 |
+
|
| 49 |
+
```
|
| 50 |
+
client = httpx.Client(app=app)
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
Alternatively, you can setup the transport instance explicitly.
|
| 54 |
+
This allows you to include any additional configuration arguments specific
|
| 55 |
+
to the WSGITransport class:
|
| 56 |
+
|
| 57 |
+
```
|
| 58 |
+
transport = httpx.WSGITransport(
|
| 59 |
+
app=app,
|
| 60 |
+
script_name="/submount",
|
| 61 |
+
remote_addr="1.2.3.4"
|
| 62 |
+
)
|
| 63 |
+
client = httpx.Client(transport=transport)
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
Arguments:
|
| 67 |
+
|
| 68 |
+
* `app` - The WSGI application.
|
| 69 |
+
* `raise_app_exceptions` - Boolean indicating if exceptions in the application
|
| 70 |
+
should be raised. Default to `True`. Can be set to `False` for use cases
|
| 71 |
+
such as testing the content of a client 500 response.
|
| 72 |
+
* `script_name` - The root path on which the WSGI application should be mounted.
|
| 73 |
+
* `remote_addr` - A string indicating the client IP of incoming requests.
|
| 74 |
+
```
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
def __init__(
|
| 78 |
+
self,
|
| 79 |
+
app: WSGIApplication,
|
| 80 |
+
raise_app_exceptions: bool = True,
|
| 81 |
+
script_name: str = "",
|
| 82 |
+
remote_addr: str = "127.0.0.1",
|
| 83 |
+
wsgi_errors: typing.TextIO | None = None,
|
| 84 |
+
) -> None:
|
| 85 |
+
self.app = app
|
| 86 |
+
self.raise_app_exceptions = raise_app_exceptions
|
| 87 |
+
self.script_name = script_name
|
| 88 |
+
self.remote_addr = remote_addr
|
| 89 |
+
self.wsgi_errors = wsgi_errors
|
| 90 |
+
|
| 91 |
+
def handle_request(self, request: Request) -> Response:
|
| 92 |
+
request.read()
|
| 93 |
+
wsgi_input = io.BytesIO(request.content)
|
| 94 |
+
|
| 95 |
+
port = request.url.port or {"http": 80, "https": 443}[request.url.scheme]
|
| 96 |
+
environ = {
|
| 97 |
+
"wsgi.version": (1, 0),
|
| 98 |
+
"wsgi.url_scheme": request.url.scheme,
|
| 99 |
+
"wsgi.input": wsgi_input,
|
| 100 |
+
"wsgi.errors": self.wsgi_errors or sys.stderr,
|
| 101 |
+
"wsgi.multithread": True,
|
| 102 |
+
"wsgi.multiprocess": False,
|
| 103 |
+
"wsgi.run_once": False,
|
| 104 |
+
"REQUEST_METHOD": request.method,
|
| 105 |
+
"SCRIPT_NAME": self.script_name,
|
| 106 |
+
"PATH_INFO": request.url.path,
|
| 107 |
+
"QUERY_STRING": request.url.query.decode("ascii"),
|
| 108 |
+
"SERVER_NAME": request.url.host,
|
| 109 |
+
"SERVER_PORT": str(port),
|
| 110 |
+
"SERVER_PROTOCOL": "HTTP/1.1",
|
| 111 |
+
"REMOTE_ADDR": self.remote_addr,
|
| 112 |
+
}
|
| 113 |
+
for header_key, header_value in request.headers.raw:
|
| 114 |
+
key = header_key.decode("ascii").upper().replace("-", "_")
|
| 115 |
+
if key not in ("CONTENT_TYPE", "CONTENT_LENGTH"):
|
| 116 |
+
key = "HTTP_" + key
|
| 117 |
+
environ[key] = header_value.decode("ascii")
|
| 118 |
+
|
| 119 |
+
seen_status = None
|
| 120 |
+
seen_response_headers = None
|
| 121 |
+
seen_exc_info = None
|
| 122 |
+
|
| 123 |
+
def start_response(
|
| 124 |
+
status: str,
|
| 125 |
+
response_headers: list[tuple[str, str]],
|
| 126 |
+
exc_info: OptExcInfo | None = None,
|
| 127 |
+
) -> typing.Callable[[bytes], typing.Any]:
|
| 128 |
+
nonlocal seen_status, seen_response_headers, seen_exc_info
|
| 129 |
+
seen_status = status
|
| 130 |
+
seen_response_headers = response_headers
|
| 131 |
+
seen_exc_info = exc_info
|
| 132 |
+
return lambda _: None
|
| 133 |
+
|
| 134 |
+
result = self.app(environ, start_response)
|
| 135 |
+
|
| 136 |
+
stream = WSGIByteStream(result)
|
| 137 |
+
|
| 138 |
+
assert seen_status is not None
|
| 139 |
+
assert seen_response_headers is not None
|
| 140 |
+
if seen_exc_info and seen_exc_info[0] and self.raise_app_exceptions:
|
| 141 |
+
raise seen_exc_info[1]
|
| 142 |
+
|
| 143 |
+
status_code = int(seen_status.split()[0])
|
| 144 |
+
headers = [
|
| 145 |
+
(key.encode("ascii"), value.encode("ascii"))
|
| 146 |
+
for key, value in seen_response_headers
|
| 147 |
+
]
|
| 148 |
+
|
| 149 |
+
return Response(status_code, headers=headers, stream=stream)
|
.venv/lib/python3.11/site-packages/httpx/_types.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Type definitions for type checking purposes.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from http.cookiejar import CookieJar
|
| 6 |
+
from typing import (
|
| 7 |
+
IO,
|
| 8 |
+
TYPE_CHECKING,
|
| 9 |
+
Any,
|
| 10 |
+
AsyncIterable,
|
| 11 |
+
AsyncIterator,
|
| 12 |
+
Callable,
|
| 13 |
+
Dict,
|
| 14 |
+
Iterable,
|
| 15 |
+
Iterator,
|
| 16 |
+
List,
|
| 17 |
+
Mapping,
|
| 18 |
+
Optional,
|
| 19 |
+
Sequence,
|
| 20 |
+
Tuple,
|
| 21 |
+
Union,
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
if TYPE_CHECKING: # pragma: no cover
|
| 25 |
+
from ._auth import Auth # noqa: F401
|
| 26 |
+
from ._config import Proxy, Timeout # noqa: F401
|
| 27 |
+
from ._models import Cookies, Headers, Request # noqa: F401
|
| 28 |
+
from ._urls import URL, QueryParams # noqa: F401
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
PrimitiveData = Optional[Union[str, int, float, bool]]
|
| 32 |
+
|
| 33 |
+
URLTypes = Union["URL", str]
|
| 34 |
+
|
| 35 |
+
QueryParamTypes = Union[
|
| 36 |
+
"QueryParams",
|
| 37 |
+
Mapping[str, Union[PrimitiveData, Sequence[PrimitiveData]]],
|
| 38 |
+
List[Tuple[str, PrimitiveData]],
|
| 39 |
+
Tuple[Tuple[str, PrimitiveData], ...],
|
| 40 |
+
str,
|
| 41 |
+
bytes,
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
HeaderTypes = Union[
|
| 45 |
+
"Headers",
|
| 46 |
+
Mapping[str, str],
|
| 47 |
+
Mapping[bytes, bytes],
|
| 48 |
+
Sequence[Tuple[str, str]],
|
| 49 |
+
Sequence[Tuple[bytes, bytes]],
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
CookieTypes = Union["Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]]
|
| 53 |
+
|
| 54 |
+
TimeoutTypes = Union[
|
| 55 |
+
Optional[float],
|
| 56 |
+
Tuple[Optional[float], Optional[float], Optional[float], Optional[float]],
|
| 57 |
+
"Timeout",
|
| 58 |
+
]
|
| 59 |
+
ProxyTypes = Union["URL", str, "Proxy"]
|
| 60 |
+
CertTypes = Union[str, Tuple[str, str], Tuple[str, str, str]]
|
| 61 |
+
|
| 62 |
+
AuthTypes = Union[
|
| 63 |
+
Tuple[Union[str, bytes], Union[str, bytes]],
|
| 64 |
+
Callable[["Request"], "Request"],
|
| 65 |
+
"Auth",
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
|
| 69 |
+
ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
|
| 70 |
+
ResponseExtensions = Mapping[str, Any]
|
| 71 |
+
|
| 72 |
+
RequestData = Mapping[str, Any]
|
| 73 |
+
|
| 74 |
+
FileContent = Union[IO[bytes], bytes, str]
|
| 75 |
+
FileTypes = Union[
|
| 76 |
+
# file (or bytes)
|
| 77 |
+
FileContent,
|
| 78 |
+
# (filename, file (or bytes))
|
| 79 |
+
Tuple[Optional[str], FileContent],
|
| 80 |
+
# (filename, file (or bytes), content_type)
|
| 81 |
+
Tuple[Optional[str], FileContent, Optional[str]],
|
| 82 |
+
# (filename, file (or bytes), content_type, headers)
|
| 83 |
+
Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]],
|
| 84 |
+
]
|
| 85 |
+
RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]]
|
| 86 |
+
|
| 87 |
+
RequestExtensions = Mapping[str, Any]
|
| 88 |
+
|
| 89 |
+
__all__ = ["AsyncByteStream", "SyncByteStream"]
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
class SyncByteStream:
|
| 93 |
+
def __iter__(self) -> Iterator[bytes]:
|
| 94 |
+
raise NotImplementedError(
|
| 95 |
+
"The '__iter__' method must be implemented."
|
| 96 |
+
) # pragma: no cover
|
| 97 |
+
yield b"" # pragma: no cover
|
| 98 |
+
|
| 99 |
+
def close(self) -> None:
|
| 100 |
+
"""
|
| 101 |
+
Subclasses can override this method to release any network resources
|
| 102 |
+
after a request/response cycle is complete.
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class AsyncByteStream:
|
| 107 |
+
async def __aiter__(self) -> AsyncIterator[bytes]:
|
| 108 |
+
raise NotImplementedError(
|
| 109 |
+
"The '__aiter__' method must be implemented."
|
| 110 |
+
) # pragma: no cover
|
| 111 |
+
yield b"" # pragma: no cover
|
| 112 |
+
|
| 113 |
+
async def aclose(self) -> None:
|
| 114 |
+
pass
|
.venv/lib/python3.11/site-packages/httpx/_urlparse.py
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
An implementation of `urlparse` that provides URL validation and normalization
|
| 3 |
+
as described by RFC3986.
|
| 4 |
+
|
| 5 |
+
We rely on this implementation rather than the one in Python's stdlib, because:
|
| 6 |
+
|
| 7 |
+
* It provides more complete URL validation.
|
| 8 |
+
* It properly differentiates between an empty querystring and an absent querystring,
|
| 9 |
+
to distinguish URLs with a trailing '?'.
|
| 10 |
+
* It handles scheme, hostname, port, and path normalization.
|
| 11 |
+
* It supports IDNA hostnames, normalizing them to their encoded form.
|
| 12 |
+
* The API supports passing individual components, as well as the complete URL string.
|
| 13 |
+
|
| 14 |
+
Previously we relied on the excellent `rfc3986` package to handle URL parsing and
|
| 15 |
+
validation, but this module provides a simpler alternative, with less indirection
|
| 16 |
+
required.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import ipaddress
|
| 22 |
+
import re
|
| 23 |
+
import typing
|
| 24 |
+
|
| 25 |
+
import idna
|
| 26 |
+
|
| 27 |
+
from ._exceptions import InvalidURL
|
| 28 |
+
|
| 29 |
+
MAX_URL_LENGTH = 65536
|
| 30 |
+
|
| 31 |
+
# https://datatracker.ietf.org/doc/html/rfc3986.html#section-2.3
|
| 32 |
+
UNRESERVED_CHARACTERS = (
|
| 33 |
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
| 34 |
+
)
|
| 35 |
+
SUB_DELIMS = "!$&'()*+,;="
|
| 36 |
+
|
| 37 |
+
PERCENT_ENCODED_REGEX = re.compile("%[A-Fa-f0-9]{2}")
|
| 38 |
+
|
| 39 |
+
# https://url.spec.whatwg.org/#percent-encoded-bytes
|
| 40 |
+
|
| 41 |
+
# The fragment percent-encode set is the C0 control percent-encode set
|
| 42 |
+
# and U+0020 SPACE, U+0022 ("), U+003C (<), U+003E (>), and U+0060 (`).
|
| 43 |
+
FRAG_SAFE = "".join(
|
| 44 |
+
[chr(i) for i in range(0x20, 0x7F) if i not in (0x20, 0x22, 0x3C, 0x3E, 0x60)]
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
# The query percent-encode set is the C0 control percent-encode set
|
| 48 |
+
# and U+0020 SPACE, U+0022 ("), U+0023 (#), U+003C (<), and U+003E (>).
|
| 49 |
+
QUERY_SAFE = "".join(
|
| 50 |
+
[chr(i) for i in range(0x20, 0x7F) if i not in (0x20, 0x22, 0x23, 0x3C, 0x3E)]
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# The path percent-encode set is the query percent-encode set
|
| 54 |
+
# and U+003F (?), U+0060 (`), U+007B ({), and U+007D (}).
|
| 55 |
+
PATH_SAFE = "".join(
|
| 56 |
+
[
|
| 57 |
+
chr(i)
|
| 58 |
+
for i in range(0x20, 0x7F)
|
| 59 |
+
if i not in (0x20, 0x22, 0x23, 0x3C, 0x3E) + (0x3F, 0x60, 0x7B, 0x7D)
|
| 60 |
+
]
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# The userinfo percent-encode set is the path percent-encode set
|
| 64 |
+
# and U+002F (/), U+003A (:), U+003B (;), U+003D (=), U+0040 (@),
|
| 65 |
+
# U+005B ([) to U+005E (^), inclusive, and U+007C (|).
|
| 66 |
+
USERNAME_SAFE = "".join(
|
| 67 |
+
[
|
| 68 |
+
chr(i)
|
| 69 |
+
for i in range(0x20, 0x7F)
|
| 70 |
+
if i
|
| 71 |
+
not in (0x20, 0x22, 0x23, 0x3C, 0x3E)
|
| 72 |
+
+ (0x3F, 0x60, 0x7B, 0x7D)
|
| 73 |
+
+ (0x2F, 0x3A, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C)
|
| 74 |
+
]
|
| 75 |
+
)
|
| 76 |
+
PASSWORD_SAFE = "".join(
|
| 77 |
+
[
|
| 78 |
+
chr(i)
|
| 79 |
+
for i in range(0x20, 0x7F)
|
| 80 |
+
if i
|
| 81 |
+
not in (0x20, 0x22, 0x23, 0x3C, 0x3E)
|
| 82 |
+
+ (0x3F, 0x60, 0x7B, 0x7D)
|
| 83 |
+
+ (0x2F, 0x3A, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C)
|
| 84 |
+
]
|
| 85 |
+
)
|
| 86 |
+
# Note... The terminology 'userinfo' percent-encode set in the WHATWG document
|
| 87 |
+
# is used for the username and password quoting. For the joint userinfo component
|
| 88 |
+
# we remove U+003A (:) from the safe set.
|
| 89 |
+
USERINFO_SAFE = "".join(
|
| 90 |
+
[
|
| 91 |
+
chr(i)
|
| 92 |
+
for i in range(0x20, 0x7F)
|
| 93 |
+
if i
|
| 94 |
+
not in (0x20, 0x22, 0x23, 0x3C, 0x3E)
|
| 95 |
+
+ (0x3F, 0x60, 0x7B, 0x7D)
|
| 96 |
+
+ (0x2F, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C)
|
| 97 |
+
]
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# {scheme}: (optional)
|
| 102 |
+
# //{authority} (optional)
|
| 103 |
+
# {path}
|
| 104 |
+
# ?{query} (optional)
|
| 105 |
+
# #{fragment} (optional)
|
| 106 |
+
URL_REGEX = re.compile(
|
| 107 |
+
(
|
| 108 |
+
r"(?:(?P<scheme>{scheme}):)?"
|
| 109 |
+
r"(?://(?P<authority>{authority}))?"
|
| 110 |
+
r"(?P<path>{path})"
|
| 111 |
+
r"(?:\?(?P<query>{query}))?"
|
| 112 |
+
r"(?:#(?P<fragment>{fragment}))?"
|
| 113 |
+
).format(
|
| 114 |
+
scheme="([a-zA-Z][a-zA-Z0-9+.-]*)?",
|
| 115 |
+
authority="[^/?#]*",
|
| 116 |
+
path="[^?#]*",
|
| 117 |
+
query="[^#]*",
|
| 118 |
+
fragment=".*",
|
| 119 |
+
)
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
# {userinfo}@ (optional)
|
| 123 |
+
# {host}
|
| 124 |
+
# :{port} (optional)
|
| 125 |
+
AUTHORITY_REGEX = re.compile(
|
| 126 |
+
(
|
| 127 |
+
r"(?:(?P<userinfo>{userinfo})@)?" r"(?P<host>{host})" r":?(?P<port>{port})?"
|
| 128 |
+
).format(
|
| 129 |
+
userinfo=".*", # Any character sequence.
|
| 130 |
+
host="(\\[.*\\]|[^:@]*)", # Either any character sequence excluding ':' or '@',
|
| 131 |
+
# or an IPv6 address enclosed within square brackets.
|
| 132 |
+
port=".*", # Any character sequence.
|
| 133 |
+
)
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# If we call urlparse with an individual component, then we need to regex
|
| 138 |
+
# validate that component individually.
|
| 139 |
+
# Note that we're duplicating the same strings as above. Shock! Horror!!
|
| 140 |
+
COMPONENT_REGEX = {
|
| 141 |
+
"scheme": re.compile("([a-zA-Z][a-zA-Z0-9+.-]*)?"),
|
| 142 |
+
"authority": re.compile("[^/?#]*"),
|
| 143 |
+
"path": re.compile("[^?#]*"),
|
| 144 |
+
"query": re.compile("[^#]*"),
|
| 145 |
+
"fragment": re.compile(".*"),
|
| 146 |
+
"userinfo": re.compile("[^@]*"),
|
| 147 |
+
"host": re.compile("(\\[.*\\]|[^:]*)"),
|
| 148 |
+
"port": re.compile(".*"),
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# We use these simple regexs as a first pass before handing off to
|
| 153 |
+
# the stdlib 'ipaddress' module for IP address validation.
|
| 154 |
+
IPv4_STYLE_HOSTNAME = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$")
|
| 155 |
+
IPv6_STYLE_HOSTNAME = re.compile(r"^\[.*\]$")
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
class ParseResult(typing.NamedTuple):
|
| 159 |
+
scheme: str
|
| 160 |
+
userinfo: str
|
| 161 |
+
host: str
|
| 162 |
+
port: int | None
|
| 163 |
+
path: str
|
| 164 |
+
query: str | None
|
| 165 |
+
fragment: str | None
|
| 166 |
+
|
| 167 |
+
@property
|
| 168 |
+
def authority(self) -> str:
|
| 169 |
+
return "".join(
|
| 170 |
+
[
|
| 171 |
+
f"{self.userinfo}@" if self.userinfo else "",
|
| 172 |
+
f"[{self.host}]" if ":" in self.host else self.host,
|
| 173 |
+
f":{self.port}" if self.port is not None else "",
|
| 174 |
+
]
|
| 175 |
+
)
|
| 176 |
+
|
| 177 |
+
@property
|
| 178 |
+
def netloc(self) -> str:
|
| 179 |
+
return "".join(
|
| 180 |
+
[
|
| 181 |
+
f"[{self.host}]" if ":" in self.host else self.host,
|
| 182 |
+
f":{self.port}" if self.port is not None else "",
|
| 183 |
+
]
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
def copy_with(self, **kwargs: str | None) -> ParseResult:
|
| 187 |
+
if not kwargs:
|
| 188 |
+
return self
|
| 189 |
+
|
| 190 |
+
defaults = {
|
| 191 |
+
"scheme": self.scheme,
|
| 192 |
+
"authority": self.authority,
|
| 193 |
+
"path": self.path,
|
| 194 |
+
"query": self.query,
|
| 195 |
+
"fragment": self.fragment,
|
| 196 |
+
}
|
| 197 |
+
defaults.update(kwargs)
|
| 198 |
+
return urlparse("", **defaults)
|
| 199 |
+
|
| 200 |
+
def __str__(self) -> str:
|
| 201 |
+
authority = self.authority
|
| 202 |
+
return "".join(
|
| 203 |
+
[
|
| 204 |
+
f"{self.scheme}:" if self.scheme else "",
|
| 205 |
+
f"//{authority}" if authority else "",
|
| 206 |
+
self.path,
|
| 207 |
+
f"?{self.query}" if self.query is not None else "",
|
| 208 |
+
f"#{self.fragment}" if self.fragment is not None else "",
|
| 209 |
+
]
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def urlparse(url: str = "", **kwargs: str | None) -> ParseResult:
|
| 214 |
+
# Initial basic checks on allowable URLs.
|
| 215 |
+
# ---------------------------------------
|
| 216 |
+
|
| 217 |
+
# Hard limit the maximum allowable URL length.
|
| 218 |
+
if len(url) > MAX_URL_LENGTH:
|
| 219 |
+
raise InvalidURL("URL too long")
|
| 220 |
+
|
| 221 |
+
# If a URL includes any ASCII control characters including \t, \r, \n,
|
| 222 |
+
# then treat it as invalid.
|
| 223 |
+
if any(char.isascii() and not char.isprintable() for char in url):
|
| 224 |
+
char = next(char for char in url if char.isascii() and not char.isprintable())
|
| 225 |
+
idx = url.find(char)
|
| 226 |
+
error = (
|
| 227 |
+
f"Invalid non-printable ASCII character in URL, {char!r} at position {idx}."
|
| 228 |
+
)
|
| 229 |
+
raise InvalidURL(error)
|
| 230 |
+
|
| 231 |
+
# Some keyword arguments require special handling.
|
| 232 |
+
# ------------------------------------------------
|
| 233 |
+
|
| 234 |
+
# Coerce "port" to a string, if it is provided as an integer.
|
| 235 |
+
if "port" in kwargs:
|
| 236 |
+
port = kwargs["port"]
|
| 237 |
+
kwargs["port"] = str(port) if isinstance(port, int) else port
|
| 238 |
+
|
| 239 |
+
# Replace "netloc" with "host and "port".
|
| 240 |
+
if "netloc" in kwargs:
|
| 241 |
+
netloc = kwargs.pop("netloc") or ""
|
| 242 |
+
kwargs["host"], _, kwargs["port"] = netloc.partition(":")
|
| 243 |
+
|
| 244 |
+
# Replace "username" and/or "password" with "userinfo".
|
| 245 |
+
if "username" in kwargs or "password" in kwargs:
|
| 246 |
+
username = quote(kwargs.pop("username", "") or "", safe=USERNAME_SAFE)
|
| 247 |
+
password = quote(kwargs.pop("password", "") or "", safe=PASSWORD_SAFE)
|
| 248 |
+
kwargs["userinfo"] = f"{username}:{password}" if password else username
|
| 249 |
+
|
| 250 |
+
# Replace "raw_path" with "path" and "query".
|
| 251 |
+
if "raw_path" in kwargs:
|
| 252 |
+
raw_path = kwargs.pop("raw_path") or ""
|
| 253 |
+
kwargs["path"], seperator, kwargs["query"] = raw_path.partition("?")
|
| 254 |
+
if not seperator:
|
| 255 |
+
kwargs["query"] = None
|
| 256 |
+
|
| 257 |
+
# Ensure that IPv6 "host" addresses are always escaped with "[...]".
|
| 258 |
+
if "host" in kwargs:
|
| 259 |
+
host = kwargs.get("host") or ""
|
| 260 |
+
if ":" in host and not (host.startswith("[") and host.endswith("]")):
|
| 261 |
+
kwargs["host"] = f"[{host}]"
|
| 262 |
+
|
| 263 |
+
# If any keyword arguments are provided, ensure they are valid.
|
| 264 |
+
# -------------------------------------------------------------
|
| 265 |
+
|
| 266 |
+
for key, value in kwargs.items():
|
| 267 |
+
if value is not None:
|
| 268 |
+
if len(value) > MAX_URL_LENGTH:
|
| 269 |
+
raise InvalidURL(f"URL component '{key}' too long")
|
| 270 |
+
|
| 271 |
+
# If a component includes any ASCII control characters including \t, \r, \n,
|
| 272 |
+
# then treat it as invalid.
|
| 273 |
+
if any(char.isascii() and not char.isprintable() for char in value):
|
| 274 |
+
char = next(
|
| 275 |
+
char for char in value if char.isascii() and not char.isprintable()
|
| 276 |
+
)
|
| 277 |
+
idx = value.find(char)
|
| 278 |
+
error = (
|
| 279 |
+
f"Invalid non-printable ASCII character in URL {key} component, "
|
| 280 |
+
f"{char!r} at position {idx}."
|
| 281 |
+
)
|
| 282 |
+
raise InvalidURL(error)
|
| 283 |
+
|
| 284 |
+
# Ensure that keyword arguments match as a valid regex.
|
| 285 |
+
if not COMPONENT_REGEX[key].fullmatch(value):
|
| 286 |
+
raise InvalidURL(f"Invalid URL component '{key}'")
|
| 287 |
+
|
| 288 |
+
# The URL_REGEX will always match, but may have empty components.
|
| 289 |
+
url_match = URL_REGEX.match(url)
|
| 290 |
+
assert url_match is not None
|
| 291 |
+
url_dict = url_match.groupdict()
|
| 292 |
+
|
| 293 |
+
# * 'scheme', 'authority', and 'path' may be empty strings.
|
| 294 |
+
# * 'query' may be 'None', indicating no trailing "?" portion.
|
| 295 |
+
# Any string including the empty string, indicates a trailing "?".
|
| 296 |
+
# * 'fragment' may be 'None', indicating no trailing "#" portion.
|
| 297 |
+
# Any string including the empty string, indicates a trailing "#".
|
| 298 |
+
scheme = kwargs.get("scheme", url_dict["scheme"]) or ""
|
| 299 |
+
authority = kwargs.get("authority", url_dict["authority"]) or ""
|
| 300 |
+
path = kwargs.get("path", url_dict["path"]) or ""
|
| 301 |
+
query = kwargs.get("query", url_dict["query"])
|
| 302 |
+
frag = kwargs.get("fragment", url_dict["fragment"])
|
| 303 |
+
|
| 304 |
+
# The AUTHORITY_REGEX will always match, but may have empty components.
|
| 305 |
+
authority_match = AUTHORITY_REGEX.match(authority)
|
| 306 |
+
assert authority_match is not None
|
| 307 |
+
authority_dict = authority_match.groupdict()
|
| 308 |
+
|
| 309 |
+
# * 'userinfo' and 'host' may be empty strings.
|
| 310 |
+
# * 'port' may be 'None'.
|
| 311 |
+
userinfo = kwargs.get("userinfo", authority_dict["userinfo"]) or ""
|
| 312 |
+
host = kwargs.get("host", authority_dict["host"]) or ""
|
| 313 |
+
port = kwargs.get("port", authority_dict["port"])
|
| 314 |
+
|
| 315 |
+
# Normalize and validate each component.
|
| 316 |
+
# We end up with a parsed representation of the URL,
|
| 317 |
+
# with components that are plain ASCII bytestrings.
|
| 318 |
+
parsed_scheme: str = scheme.lower()
|
| 319 |
+
parsed_userinfo: str = quote(userinfo, safe=USERINFO_SAFE)
|
| 320 |
+
parsed_host: str = encode_host(host)
|
| 321 |
+
parsed_port: int | None = normalize_port(port, scheme)
|
| 322 |
+
|
| 323 |
+
has_scheme = parsed_scheme != ""
|
| 324 |
+
has_authority = (
|
| 325 |
+
parsed_userinfo != "" or parsed_host != "" or parsed_port is not None
|
| 326 |
+
)
|
| 327 |
+
validate_path(path, has_scheme=has_scheme, has_authority=has_authority)
|
| 328 |
+
if has_scheme or has_authority:
|
| 329 |
+
path = normalize_path(path)
|
| 330 |
+
|
| 331 |
+
parsed_path: str = quote(path, safe=PATH_SAFE)
|
| 332 |
+
parsed_query: str | None = None if query is None else quote(query, safe=QUERY_SAFE)
|
| 333 |
+
parsed_frag: str | None = None if frag is None else quote(frag, safe=FRAG_SAFE)
|
| 334 |
+
|
| 335 |
+
# The parsed ASCII bytestrings are our canonical form.
|
| 336 |
+
# All properties of the URL are derived from these.
|
| 337 |
+
return ParseResult(
|
| 338 |
+
parsed_scheme,
|
| 339 |
+
parsed_userinfo,
|
| 340 |
+
parsed_host,
|
| 341 |
+
parsed_port,
|
| 342 |
+
parsed_path,
|
| 343 |
+
parsed_query,
|
| 344 |
+
parsed_frag,
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def encode_host(host: str) -> str:
|
| 349 |
+
if not host:
|
| 350 |
+
return ""
|
| 351 |
+
|
| 352 |
+
elif IPv4_STYLE_HOSTNAME.match(host):
|
| 353 |
+
# Validate IPv4 hostnames like #.#.#.#
|
| 354 |
+
#
|
| 355 |
+
# From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2
|
| 356 |
+
#
|
| 357 |
+
# IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
|
| 358 |
+
try:
|
| 359 |
+
ipaddress.IPv4Address(host)
|
| 360 |
+
except ipaddress.AddressValueError:
|
| 361 |
+
raise InvalidURL(f"Invalid IPv4 address: {host!r}")
|
| 362 |
+
return host
|
| 363 |
+
|
| 364 |
+
elif IPv6_STYLE_HOSTNAME.match(host):
|
| 365 |
+
# Validate IPv6 hostnames like [...]
|
| 366 |
+
#
|
| 367 |
+
# From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2
|
| 368 |
+
#
|
| 369 |
+
# "A host identified by an Internet Protocol literal address, version 6
|
| 370 |
+
# [RFC3513] or later, is distinguished by enclosing the IP literal
|
| 371 |
+
# within square brackets ("[" and "]"). This is the only place where
|
| 372 |
+
# square bracket characters are allowed in the URI syntax."
|
| 373 |
+
try:
|
| 374 |
+
ipaddress.IPv6Address(host[1:-1])
|
| 375 |
+
except ipaddress.AddressValueError:
|
| 376 |
+
raise InvalidURL(f"Invalid IPv6 address: {host!r}")
|
| 377 |
+
return host[1:-1]
|
| 378 |
+
|
| 379 |
+
elif host.isascii():
|
| 380 |
+
# Regular ASCII hostnames
|
| 381 |
+
#
|
| 382 |
+
# From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2
|
| 383 |
+
#
|
| 384 |
+
# reg-name = *( unreserved / pct-encoded / sub-delims )
|
| 385 |
+
WHATWG_SAFE = '"`{}%|\\'
|
| 386 |
+
return quote(host.lower(), safe=SUB_DELIMS + WHATWG_SAFE)
|
| 387 |
+
|
| 388 |
+
# IDNA hostnames
|
| 389 |
+
try:
|
| 390 |
+
return idna.encode(host.lower()).decode("ascii")
|
| 391 |
+
except idna.IDNAError:
|
| 392 |
+
raise InvalidURL(f"Invalid IDNA hostname: {host!r}")
|
| 393 |
+
|
| 394 |
+
|
| 395 |
+
def normalize_port(port: str | int | None, scheme: str) -> int | None:
|
| 396 |
+
# From https://tools.ietf.org/html/rfc3986#section-3.2.3
|
| 397 |
+
#
|
| 398 |
+
# "A scheme may define a default port. For example, the "http" scheme
|
| 399 |
+
# defines a default port of "80", corresponding to its reserved TCP
|
| 400 |
+
# port number. The type of port designated by the port number (e.g.,
|
| 401 |
+
# TCP, UDP, SCTP) is defined by the URI scheme. URI producers and
|
| 402 |
+
# normalizers should omit the port component and its ":" delimiter if
|
| 403 |
+
# port is empty or if its value would be the same as that of the
|
| 404 |
+
# scheme's default."
|
| 405 |
+
if port is None or port == "":
|
| 406 |
+
return None
|
| 407 |
+
|
| 408 |
+
try:
|
| 409 |
+
port_as_int = int(port)
|
| 410 |
+
except ValueError:
|
| 411 |
+
raise InvalidURL(f"Invalid port: {port!r}")
|
| 412 |
+
|
| 413 |
+
# See https://url.spec.whatwg.org/#url-miscellaneous
|
| 414 |
+
default_port = {"ftp": 21, "http": 80, "https": 443, "ws": 80, "wss": 443}.get(
|
| 415 |
+
scheme
|
| 416 |
+
)
|
| 417 |
+
if port_as_int == default_port:
|
| 418 |
+
return None
|
| 419 |
+
return port_as_int
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def validate_path(path: str, has_scheme: bool, has_authority: bool) -> None:
|
| 423 |
+
"""
|
| 424 |
+
Path validation rules that depend on if the URL contains
|
| 425 |
+
a scheme or authority component.
|
| 426 |
+
|
| 427 |
+
See https://datatracker.ietf.org/doc/html/rfc3986.html#section-3.3
|
| 428 |
+
"""
|
| 429 |
+
if has_authority:
|
| 430 |
+
# If a URI contains an authority component, then the path component
|
| 431 |
+
# must either be empty or begin with a slash ("/") character."
|
| 432 |
+
if path and not path.startswith("/"):
|
| 433 |
+
raise InvalidURL("For absolute URLs, path must be empty or begin with '/'")
|
| 434 |
+
|
| 435 |
+
if not has_scheme and not has_authority:
|
| 436 |
+
# If a URI does not contain an authority component, then the path cannot begin
|
| 437 |
+
# with two slash characters ("//").
|
| 438 |
+
if path.startswith("//"):
|
| 439 |
+
raise InvalidURL("Relative URLs cannot have a path starting with '//'")
|
| 440 |
+
|
| 441 |
+
# In addition, a URI reference (Section 4.1) may be a relative-path reference,
|
| 442 |
+
# in which case the first path segment cannot contain a colon (":") character.
|
| 443 |
+
if path.startswith(":"):
|
| 444 |
+
raise InvalidURL("Relative URLs cannot have a path starting with ':'")
|
| 445 |
+
|
| 446 |
+
|
| 447 |
+
def normalize_path(path: str) -> str:
|
| 448 |
+
"""
|
| 449 |
+
Drop "." and ".." segments from a URL path.
|
| 450 |
+
|
| 451 |
+
For example:
|
| 452 |
+
|
| 453 |
+
normalize_path("/path/./to/somewhere/..") == "/path/to"
|
| 454 |
+
"""
|
| 455 |
+
# Fast return when no '.' characters in the path.
|
| 456 |
+
if "." not in path:
|
| 457 |
+
return path
|
| 458 |
+
|
| 459 |
+
components = path.split("/")
|
| 460 |
+
|
| 461 |
+
# Fast return when no '.' or '..' components in the path.
|
| 462 |
+
if "." not in components and ".." not in components:
|
| 463 |
+
return path
|
| 464 |
+
|
| 465 |
+
# https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
|
| 466 |
+
output: list[str] = []
|
| 467 |
+
for component in components:
|
| 468 |
+
if component == ".":
|
| 469 |
+
pass
|
| 470 |
+
elif component == "..":
|
| 471 |
+
if output and output != [""]:
|
| 472 |
+
output.pop()
|
| 473 |
+
else:
|
| 474 |
+
output.append(component)
|
| 475 |
+
return "/".join(output)
|
| 476 |
+
|
| 477 |
+
|
| 478 |
+
def PERCENT(string: str) -> str:
|
| 479 |
+
return "".join([f"%{byte:02X}" for byte in string.encode("utf-8")])
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def percent_encoded(string: str, safe: str) -> str:
|
| 483 |
+
"""
|
| 484 |
+
Use percent-encoding to quote a string.
|
| 485 |
+
"""
|
| 486 |
+
NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe
|
| 487 |
+
|
| 488 |
+
# Fast path for strings that don't need escaping.
|
| 489 |
+
if not string.rstrip(NON_ESCAPED_CHARS):
|
| 490 |
+
return string
|
| 491 |
+
|
| 492 |
+
return "".join(
|
| 493 |
+
[char if char in NON_ESCAPED_CHARS else PERCENT(char) for char in string]
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
|
| 497 |
+
def quote(string: str, safe: str) -> str:
|
| 498 |
+
"""
|
| 499 |
+
Use percent-encoding to quote a string, omitting existing '%xx' escape sequences.
|
| 500 |
+
|
| 501 |
+
See: https://www.rfc-editor.org/rfc/rfc3986#section-2.1
|
| 502 |
+
|
| 503 |
+
* `string`: The string to be percent-escaped.
|
| 504 |
+
* `safe`: A string containing characters that may be treated as safe, and do not
|
| 505 |
+
need to be escaped. Unreserved characters are always treated as safe.
|
| 506 |
+
See: https://www.rfc-editor.org/rfc/rfc3986#section-2.3
|
| 507 |
+
"""
|
| 508 |
+
parts = []
|
| 509 |
+
current_position = 0
|
| 510 |
+
for match in re.finditer(PERCENT_ENCODED_REGEX, string):
|
| 511 |
+
start_position, end_position = match.start(), match.end()
|
| 512 |
+
matched_text = match.group(0)
|
| 513 |
+
# Add any text up to the '%xx' escape sequence.
|
| 514 |
+
if start_position != current_position:
|
| 515 |
+
leading_text = string[current_position:start_position]
|
| 516 |
+
parts.append(percent_encoded(leading_text, safe=safe))
|
| 517 |
+
|
| 518 |
+
# Add the '%xx' escape sequence.
|
| 519 |
+
parts.append(matched_text)
|
| 520 |
+
current_position = end_position
|
| 521 |
+
|
| 522 |
+
# Add any text after the final '%xx' escape sequence.
|
| 523 |
+
if current_position != len(string):
|
| 524 |
+
trailing_text = string[current_position:]
|
| 525 |
+
parts.append(percent_encoded(trailing_text, safe=safe))
|
| 526 |
+
|
| 527 |
+
return "".join(parts)
|
.venv/lib/python3.11/site-packages/httpx/_urls.py
ADDED
|
@@ -0,0 +1,641 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
from urllib.parse import parse_qs, unquote, urlencode
|
| 5 |
+
|
| 6 |
+
import idna
|
| 7 |
+
|
| 8 |
+
from ._types import QueryParamTypes
|
| 9 |
+
from ._urlparse import urlparse
|
| 10 |
+
from ._utils import primitive_value_to_str
|
| 11 |
+
|
| 12 |
+
__all__ = ["URL", "QueryParams"]
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class URL:
|
| 16 |
+
"""
|
| 17 |
+
url = httpx.URL("HTTPS://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink")
|
| 18 |
+
|
| 19 |
+
assert url.scheme == "https"
|
| 20 |
+
assert url.username == "jo@email.com"
|
| 21 |
+
assert url.password == "a secret"
|
| 22 |
+
assert url.userinfo == b"jo%40email.com:a%20secret"
|
| 23 |
+
assert url.host == "müller.de"
|
| 24 |
+
assert url.raw_host == b"xn--mller-kva.de"
|
| 25 |
+
assert url.port == 1234
|
| 26 |
+
assert url.netloc == b"xn--mller-kva.de:1234"
|
| 27 |
+
assert url.path == "/pa th"
|
| 28 |
+
assert url.query == b"?search=ab"
|
| 29 |
+
assert url.raw_path == b"/pa%20th?search=ab"
|
| 30 |
+
assert url.fragment == "anchorlink"
|
| 31 |
+
|
| 32 |
+
The components of a URL are broken down like this:
|
| 33 |
+
|
| 34 |
+
https://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink
|
| 35 |
+
[scheme] [ username ] [password] [ host ][port][ path ] [ query ] [fragment]
|
| 36 |
+
[ userinfo ] [ netloc ][ raw_path ]
|
| 37 |
+
|
| 38 |
+
Note that:
|
| 39 |
+
|
| 40 |
+
* `url.scheme` is normalized to always be lowercased.
|
| 41 |
+
|
| 42 |
+
* `url.host` is normalized to always be lowercased. Internationalized domain
|
| 43 |
+
names are represented in unicode, without IDNA encoding applied. For instance:
|
| 44 |
+
|
| 45 |
+
url = httpx.URL("http://中国.icom.museum")
|
| 46 |
+
assert url.host == "中国.icom.museum"
|
| 47 |
+
url = httpx.URL("http://xn--fiqs8s.icom.museum")
|
| 48 |
+
assert url.host == "中国.icom.museum"
|
| 49 |
+
|
| 50 |
+
* `url.raw_host` is normalized to always be lowercased, and is IDNA encoded.
|
| 51 |
+
|
| 52 |
+
url = httpx.URL("http://中国.icom.museum")
|
| 53 |
+
assert url.raw_host == b"xn--fiqs8s.icom.museum"
|
| 54 |
+
url = httpx.URL("http://xn--fiqs8s.icom.museum")
|
| 55 |
+
assert url.raw_host == b"xn--fiqs8s.icom.museum"
|
| 56 |
+
|
| 57 |
+
* `url.port` is either None or an integer. URLs that include the default port for
|
| 58 |
+
"http", "https", "ws", "wss", and "ftp" schemes have their port
|
| 59 |
+
normalized to `None`.
|
| 60 |
+
|
| 61 |
+
assert httpx.URL("http://example.com") == httpx.URL("http://example.com:80")
|
| 62 |
+
assert httpx.URL("http://example.com").port is None
|
| 63 |
+
assert httpx.URL("http://example.com:80").port is None
|
| 64 |
+
|
| 65 |
+
* `url.userinfo` is raw bytes, without URL escaping. Usually you'll want to work
|
| 66 |
+
with `url.username` and `url.password` instead, which handle the URL escaping.
|
| 67 |
+
|
| 68 |
+
* `url.raw_path` is raw bytes of both the path and query, without URL escaping.
|
| 69 |
+
This portion is used as the target when constructing HTTP requests. Usually you'll
|
| 70 |
+
want to work with `url.path` instead.
|
| 71 |
+
|
| 72 |
+
* `url.query` is raw bytes, without URL escaping. A URL query string portion can
|
| 73 |
+
only be properly URL escaped when decoding the parameter names and values
|
| 74 |
+
themselves.
|
| 75 |
+
"""
|
| 76 |
+
|
| 77 |
+
def __init__(self, url: URL | str = "", **kwargs: typing.Any) -> None:
|
| 78 |
+
if kwargs:
|
| 79 |
+
allowed = {
|
| 80 |
+
"scheme": str,
|
| 81 |
+
"username": str,
|
| 82 |
+
"password": str,
|
| 83 |
+
"userinfo": bytes,
|
| 84 |
+
"host": str,
|
| 85 |
+
"port": int,
|
| 86 |
+
"netloc": bytes,
|
| 87 |
+
"path": str,
|
| 88 |
+
"query": bytes,
|
| 89 |
+
"raw_path": bytes,
|
| 90 |
+
"fragment": str,
|
| 91 |
+
"params": object,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
# Perform type checking for all supported keyword arguments.
|
| 95 |
+
for key, value in kwargs.items():
|
| 96 |
+
if key not in allowed:
|
| 97 |
+
message = f"{key!r} is an invalid keyword argument for URL()"
|
| 98 |
+
raise TypeError(message)
|
| 99 |
+
if value is not None and not isinstance(value, allowed[key]):
|
| 100 |
+
expected = allowed[key].__name__
|
| 101 |
+
seen = type(value).__name__
|
| 102 |
+
message = f"Argument {key!r} must be {expected} but got {seen}"
|
| 103 |
+
raise TypeError(message)
|
| 104 |
+
if isinstance(value, bytes):
|
| 105 |
+
kwargs[key] = value.decode("ascii")
|
| 106 |
+
|
| 107 |
+
if "params" in kwargs:
|
| 108 |
+
# Replace any "params" keyword with the raw "query" instead.
|
| 109 |
+
#
|
| 110 |
+
# Ensure that empty params use `kwargs["query"] = None` rather
|
| 111 |
+
# than `kwargs["query"] = ""`, so that generated URLs do not
|
| 112 |
+
# include an empty trailing "?".
|
| 113 |
+
params = kwargs.pop("params")
|
| 114 |
+
kwargs["query"] = None if not params else str(QueryParams(params))
|
| 115 |
+
|
| 116 |
+
if isinstance(url, str):
|
| 117 |
+
self._uri_reference = urlparse(url, **kwargs)
|
| 118 |
+
elif isinstance(url, URL):
|
| 119 |
+
self._uri_reference = url._uri_reference.copy_with(**kwargs)
|
| 120 |
+
else:
|
| 121 |
+
raise TypeError(
|
| 122 |
+
"Invalid type for url. Expected str or httpx.URL,"
|
| 123 |
+
f" got {type(url)}: {url!r}"
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
@property
|
| 127 |
+
def scheme(self) -> str:
|
| 128 |
+
"""
|
| 129 |
+
The URL scheme, such as "http", "https".
|
| 130 |
+
Always normalised to lowercase.
|
| 131 |
+
"""
|
| 132 |
+
return self._uri_reference.scheme
|
| 133 |
+
|
| 134 |
+
@property
|
| 135 |
+
def raw_scheme(self) -> bytes:
|
| 136 |
+
"""
|
| 137 |
+
The raw bytes representation of the URL scheme, such as b"http", b"https".
|
| 138 |
+
Always normalised to lowercase.
|
| 139 |
+
"""
|
| 140 |
+
return self._uri_reference.scheme.encode("ascii")
|
| 141 |
+
|
| 142 |
+
@property
|
| 143 |
+
def userinfo(self) -> bytes:
|
| 144 |
+
"""
|
| 145 |
+
The URL userinfo as a raw bytestring.
|
| 146 |
+
For example: b"jo%40email.com:a%20secret".
|
| 147 |
+
"""
|
| 148 |
+
return self._uri_reference.userinfo.encode("ascii")
|
| 149 |
+
|
| 150 |
+
@property
|
| 151 |
+
def username(self) -> str:
|
| 152 |
+
"""
|
| 153 |
+
The URL username as a string, with URL decoding applied.
|
| 154 |
+
For example: "jo@email.com"
|
| 155 |
+
"""
|
| 156 |
+
userinfo = self._uri_reference.userinfo
|
| 157 |
+
return unquote(userinfo.partition(":")[0])
|
| 158 |
+
|
| 159 |
+
@property
|
| 160 |
+
def password(self) -> str:
|
| 161 |
+
"""
|
| 162 |
+
The URL password as a string, with URL decoding applied.
|
| 163 |
+
For example: "a secret"
|
| 164 |
+
"""
|
| 165 |
+
userinfo = self._uri_reference.userinfo
|
| 166 |
+
return unquote(userinfo.partition(":")[2])
|
| 167 |
+
|
| 168 |
+
@property
|
| 169 |
+
def host(self) -> str:
|
| 170 |
+
"""
|
| 171 |
+
The URL host as a string.
|
| 172 |
+
Always normalized to lowercase, with IDNA hosts decoded into unicode.
|
| 173 |
+
|
| 174 |
+
Examples:
|
| 175 |
+
|
| 176 |
+
url = httpx.URL("http://www.EXAMPLE.org")
|
| 177 |
+
assert url.host == "www.example.org"
|
| 178 |
+
|
| 179 |
+
url = httpx.URL("http://中国.icom.museum")
|
| 180 |
+
assert url.host == "中国.icom.museum"
|
| 181 |
+
|
| 182 |
+
url = httpx.URL("http://xn--fiqs8s.icom.museum")
|
| 183 |
+
assert url.host == "中国.icom.museum"
|
| 184 |
+
|
| 185 |
+
url = httpx.URL("https://[::ffff:192.168.0.1]")
|
| 186 |
+
assert url.host == "::ffff:192.168.0.1"
|
| 187 |
+
"""
|
| 188 |
+
host: str = self._uri_reference.host
|
| 189 |
+
|
| 190 |
+
if host.startswith("xn--"):
|
| 191 |
+
host = idna.decode(host)
|
| 192 |
+
|
| 193 |
+
return host
|
| 194 |
+
|
| 195 |
+
@property
|
| 196 |
+
def raw_host(self) -> bytes:
|
| 197 |
+
"""
|
| 198 |
+
The raw bytes representation of the URL host.
|
| 199 |
+
Always normalized to lowercase, and IDNA encoded.
|
| 200 |
+
|
| 201 |
+
Examples:
|
| 202 |
+
|
| 203 |
+
url = httpx.URL("http://www.EXAMPLE.org")
|
| 204 |
+
assert url.raw_host == b"www.example.org"
|
| 205 |
+
|
| 206 |
+
url = httpx.URL("http://中国.icom.museum")
|
| 207 |
+
assert url.raw_host == b"xn--fiqs8s.icom.museum"
|
| 208 |
+
|
| 209 |
+
url = httpx.URL("http://xn--fiqs8s.icom.museum")
|
| 210 |
+
assert url.raw_host == b"xn--fiqs8s.icom.museum"
|
| 211 |
+
|
| 212 |
+
url = httpx.URL("https://[::ffff:192.168.0.1]")
|
| 213 |
+
assert url.raw_host == b"::ffff:192.168.0.1"
|
| 214 |
+
"""
|
| 215 |
+
return self._uri_reference.host.encode("ascii")
|
| 216 |
+
|
| 217 |
+
@property
|
| 218 |
+
def port(self) -> int | None:
|
| 219 |
+
"""
|
| 220 |
+
The URL port as an integer.
|
| 221 |
+
|
| 222 |
+
Note that the URL class performs port normalization as per the WHATWG spec.
|
| 223 |
+
Default ports for "http", "https", "ws", "wss", and "ftp" schemes are always
|
| 224 |
+
treated as `None`.
|
| 225 |
+
|
| 226 |
+
For example:
|
| 227 |
+
|
| 228 |
+
assert httpx.URL("http://www.example.com") == httpx.URL("http://www.example.com:80")
|
| 229 |
+
assert httpx.URL("http://www.example.com:80").port is None
|
| 230 |
+
"""
|
| 231 |
+
return self._uri_reference.port
|
| 232 |
+
|
| 233 |
+
@property
|
| 234 |
+
def netloc(self) -> bytes:
|
| 235 |
+
"""
|
| 236 |
+
Either `<host>` or `<host>:<port>` as bytes.
|
| 237 |
+
Always normalized to lowercase, and IDNA encoded.
|
| 238 |
+
|
| 239 |
+
This property may be used for generating the value of a request
|
| 240 |
+
"Host" header.
|
| 241 |
+
"""
|
| 242 |
+
return self._uri_reference.netloc.encode("ascii")
|
| 243 |
+
|
| 244 |
+
@property
|
| 245 |
+
def path(self) -> str:
|
| 246 |
+
"""
|
| 247 |
+
The URL path as a string. Excluding the query string, and URL decoded.
|
| 248 |
+
|
| 249 |
+
For example:
|
| 250 |
+
|
| 251 |
+
url = httpx.URL("https://example.com/pa%20th")
|
| 252 |
+
assert url.path == "/pa th"
|
| 253 |
+
"""
|
| 254 |
+
path = self._uri_reference.path or "/"
|
| 255 |
+
return unquote(path)
|
| 256 |
+
|
| 257 |
+
@property
|
| 258 |
+
def query(self) -> bytes:
|
| 259 |
+
"""
|
| 260 |
+
The URL query string, as raw bytes, excluding the leading b"?".
|
| 261 |
+
|
| 262 |
+
This is necessarily a bytewise interface, because we cannot
|
| 263 |
+
perform URL decoding of this representation until we've parsed
|
| 264 |
+
the keys and values into a QueryParams instance.
|
| 265 |
+
|
| 266 |
+
For example:
|
| 267 |
+
|
| 268 |
+
url = httpx.URL("https://example.com/?filter=some%20search%20terms")
|
| 269 |
+
assert url.query == b"filter=some%20search%20terms"
|
| 270 |
+
"""
|
| 271 |
+
query = self._uri_reference.query or ""
|
| 272 |
+
return query.encode("ascii")
|
| 273 |
+
|
| 274 |
+
@property
|
| 275 |
+
def params(self) -> QueryParams:
|
| 276 |
+
"""
|
| 277 |
+
The URL query parameters, neatly parsed and packaged into an immutable
|
| 278 |
+
multidict representation.
|
| 279 |
+
"""
|
| 280 |
+
return QueryParams(self._uri_reference.query)
|
| 281 |
+
|
| 282 |
+
@property
|
| 283 |
+
def raw_path(self) -> bytes:
|
| 284 |
+
"""
|
| 285 |
+
The complete URL path and query string as raw bytes.
|
| 286 |
+
Used as the target when constructing HTTP requests.
|
| 287 |
+
|
| 288 |
+
For example:
|
| 289 |
+
|
| 290 |
+
GET /users?search=some%20text HTTP/1.1
|
| 291 |
+
Host: www.example.org
|
| 292 |
+
Connection: close
|
| 293 |
+
"""
|
| 294 |
+
path = self._uri_reference.path or "/"
|
| 295 |
+
if self._uri_reference.query is not None:
|
| 296 |
+
path += "?" + self._uri_reference.query
|
| 297 |
+
return path.encode("ascii")
|
| 298 |
+
|
| 299 |
+
@property
|
| 300 |
+
def fragment(self) -> str:
|
| 301 |
+
"""
|
| 302 |
+
The URL fragments, as used in HTML anchors.
|
| 303 |
+
As a string, without the leading '#'.
|
| 304 |
+
"""
|
| 305 |
+
return unquote(self._uri_reference.fragment or "")
|
| 306 |
+
|
| 307 |
+
@property
|
| 308 |
+
def is_absolute_url(self) -> bool:
|
| 309 |
+
"""
|
| 310 |
+
Return `True` for absolute URLs such as 'http://example.com/path',
|
| 311 |
+
and `False` for relative URLs such as '/path'.
|
| 312 |
+
"""
|
| 313 |
+
# We don't use `.is_absolute` from `rfc3986` because it treats
|
| 314 |
+
# URLs with a fragment portion as not absolute.
|
| 315 |
+
# What we actually care about is if the URL provides
|
| 316 |
+
# a scheme and hostname to which connections should be made.
|
| 317 |
+
return bool(self._uri_reference.scheme and self._uri_reference.host)
|
| 318 |
+
|
| 319 |
+
@property
|
| 320 |
+
def is_relative_url(self) -> bool:
|
| 321 |
+
"""
|
| 322 |
+
Return `False` for absolute URLs such as 'http://example.com/path',
|
| 323 |
+
and `True` for relative URLs such as '/path'.
|
| 324 |
+
"""
|
| 325 |
+
return not self.is_absolute_url
|
| 326 |
+
|
| 327 |
+
def copy_with(self, **kwargs: typing.Any) -> URL:
|
| 328 |
+
"""
|
| 329 |
+
Copy this URL, returning a new URL with some components altered.
|
| 330 |
+
Accepts the same set of parameters as the components that are made
|
| 331 |
+
available via properties on the `URL` class.
|
| 332 |
+
|
| 333 |
+
For example:
|
| 334 |
+
|
| 335 |
+
url = httpx.URL("https://www.example.com").copy_with(
|
| 336 |
+
username="jo@gmail.com", password="a secret"
|
| 337 |
+
)
|
| 338 |
+
assert url == "https://jo%40email.com:a%20secret@www.example.com"
|
| 339 |
+
"""
|
| 340 |
+
return URL(self, **kwargs)
|
| 341 |
+
|
| 342 |
+
def copy_set_param(self, key: str, value: typing.Any = None) -> URL:
|
| 343 |
+
return self.copy_with(params=self.params.set(key, value))
|
| 344 |
+
|
| 345 |
+
def copy_add_param(self, key: str, value: typing.Any = None) -> URL:
|
| 346 |
+
return self.copy_with(params=self.params.add(key, value))
|
| 347 |
+
|
| 348 |
+
def copy_remove_param(self, key: str) -> URL:
|
| 349 |
+
return self.copy_with(params=self.params.remove(key))
|
| 350 |
+
|
| 351 |
+
def copy_merge_params(self, params: QueryParamTypes) -> URL:
|
| 352 |
+
return self.copy_with(params=self.params.merge(params))
|
| 353 |
+
|
| 354 |
+
def join(self, url: URL | str) -> URL:
|
| 355 |
+
"""
|
| 356 |
+
Return an absolute URL, using this URL as the base.
|
| 357 |
+
|
| 358 |
+
Eg.
|
| 359 |
+
|
| 360 |
+
url = httpx.URL("https://www.example.com/test")
|
| 361 |
+
url = url.join("/new/path")
|
| 362 |
+
assert url == "https://www.example.com/new/path"
|
| 363 |
+
"""
|
| 364 |
+
from urllib.parse import urljoin
|
| 365 |
+
|
| 366 |
+
return URL(urljoin(str(self), str(URL(url))))
|
| 367 |
+
|
| 368 |
+
def __hash__(self) -> int:
|
| 369 |
+
return hash(str(self))
|
| 370 |
+
|
| 371 |
+
def __eq__(self, other: typing.Any) -> bool:
|
| 372 |
+
return isinstance(other, (URL, str)) and str(self) == str(URL(other))
|
| 373 |
+
|
| 374 |
+
def __str__(self) -> str:
|
| 375 |
+
return str(self._uri_reference)
|
| 376 |
+
|
| 377 |
+
def __repr__(self) -> str:
|
| 378 |
+
scheme, userinfo, host, port, path, query, fragment = self._uri_reference
|
| 379 |
+
|
| 380 |
+
if ":" in userinfo:
|
| 381 |
+
# Mask any password component.
|
| 382 |
+
userinfo = f'{userinfo.split(":")[0]}:[secure]'
|
| 383 |
+
|
| 384 |
+
authority = "".join(
|
| 385 |
+
[
|
| 386 |
+
f"{userinfo}@" if userinfo else "",
|
| 387 |
+
f"[{host}]" if ":" in host else host,
|
| 388 |
+
f":{port}" if port is not None else "",
|
| 389 |
+
]
|
| 390 |
+
)
|
| 391 |
+
url = "".join(
|
| 392 |
+
[
|
| 393 |
+
f"{self.scheme}:" if scheme else "",
|
| 394 |
+
f"//{authority}" if authority else "",
|
| 395 |
+
path,
|
| 396 |
+
f"?{query}" if query is not None else "",
|
| 397 |
+
f"#{fragment}" if fragment is not None else "",
|
| 398 |
+
]
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
return f"{self.__class__.__name__}({url!r})"
|
| 402 |
+
|
| 403 |
+
@property
|
| 404 |
+
def raw(self) -> tuple[bytes, bytes, int, bytes]: # pragma: nocover
|
| 405 |
+
import collections
|
| 406 |
+
import warnings
|
| 407 |
+
|
| 408 |
+
warnings.warn("URL.raw is deprecated.")
|
| 409 |
+
RawURL = collections.namedtuple(
|
| 410 |
+
"RawURL", ["raw_scheme", "raw_host", "port", "raw_path"]
|
| 411 |
+
)
|
| 412 |
+
return RawURL(
|
| 413 |
+
raw_scheme=self.raw_scheme,
|
| 414 |
+
raw_host=self.raw_host,
|
| 415 |
+
port=self.port,
|
| 416 |
+
raw_path=self.raw_path,
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
class QueryParams(typing.Mapping[str, str]):
|
| 421 |
+
"""
|
| 422 |
+
URL query parameters, as a multi-dict.
|
| 423 |
+
"""
|
| 424 |
+
|
| 425 |
+
def __init__(self, *args: QueryParamTypes | None, **kwargs: typing.Any) -> None:
|
| 426 |
+
assert len(args) < 2, "Too many arguments."
|
| 427 |
+
assert not (args and kwargs), "Cannot mix named and unnamed arguments."
|
| 428 |
+
|
| 429 |
+
value = args[0] if args else kwargs
|
| 430 |
+
|
| 431 |
+
if value is None or isinstance(value, (str, bytes)):
|
| 432 |
+
value = value.decode("ascii") if isinstance(value, bytes) else value
|
| 433 |
+
self._dict = parse_qs(value, keep_blank_values=True)
|
| 434 |
+
elif isinstance(value, QueryParams):
|
| 435 |
+
self._dict = {k: list(v) for k, v in value._dict.items()}
|
| 436 |
+
else:
|
| 437 |
+
dict_value: dict[typing.Any, list[typing.Any]] = {}
|
| 438 |
+
if isinstance(value, (list, tuple)):
|
| 439 |
+
# Convert list inputs like:
|
| 440 |
+
# [("a", "123"), ("a", "456"), ("b", "789")]
|
| 441 |
+
# To a dict representation, like:
|
| 442 |
+
# {"a": ["123", "456"], "b": ["789"]}
|
| 443 |
+
for item in value:
|
| 444 |
+
dict_value.setdefault(item[0], []).append(item[1])
|
| 445 |
+
else:
|
| 446 |
+
# Convert dict inputs like:
|
| 447 |
+
# {"a": "123", "b": ["456", "789"]}
|
| 448 |
+
# To dict inputs where values are always lists, like:
|
| 449 |
+
# {"a": ["123"], "b": ["456", "789"]}
|
| 450 |
+
dict_value = {
|
| 451 |
+
k: list(v) if isinstance(v, (list, tuple)) else [v]
|
| 452 |
+
for k, v in value.items()
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
# Ensure that keys and values are neatly coerced to strings.
|
| 456 |
+
# We coerce values `True` and `False` to JSON-like "true" and "false"
|
| 457 |
+
# representations, and coerce `None` values to the empty string.
|
| 458 |
+
self._dict = {
|
| 459 |
+
str(k): [primitive_value_to_str(item) for item in v]
|
| 460 |
+
for k, v in dict_value.items()
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
def keys(self) -> typing.KeysView[str]:
|
| 464 |
+
"""
|
| 465 |
+
Return all the keys in the query params.
|
| 466 |
+
|
| 467 |
+
Usage:
|
| 468 |
+
|
| 469 |
+
q = httpx.QueryParams("a=123&a=456&b=789")
|
| 470 |
+
assert list(q.keys()) == ["a", "b"]
|
| 471 |
+
"""
|
| 472 |
+
return self._dict.keys()
|
| 473 |
+
|
| 474 |
+
def values(self) -> typing.ValuesView[str]:
|
| 475 |
+
"""
|
| 476 |
+
Return all the values in the query params. If a key occurs more than once
|
| 477 |
+
only the first item for that key is returned.
|
| 478 |
+
|
| 479 |
+
Usage:
|
| 480 |
+
|
| 481 |
+
q = httpx.QueryParams("a=123&a=456&b=789")
|
| 482 |
+
assert list(q.values()) == ["123", "789"]
|
| 483 |
+
"""
|
| 484 |
+
return {k: v[0] for k, v in self._dict.items()}.values()
|
| 485 |
+
|
| 486 |
+
def items(self) -> typing.ItemsView[str, str]:
|
| 487 |
+
"""
|
| 488 |
+
Return all items in the query params. If a key occurs more than once
|
| 489 |
+
only the first item for that key is returned.
|
| 490 |
+
|
| 491 |
+
Usage:
|
| 492 |
+
|
| 493 |
+
q = httpx.QueryParams("a=123&a=456&b=789")
|
| 494 |
+
assert list(q.items()) == [("a", "123"), ("b", "789")]
|
| 495 |
+
"""
|
| 496 |
+
return {k: v[0] for k, v in self._dict.items()}.items()
|
| 497 |
+
|
| 498 |
+
def multi_items(self) -> list[tuple[str, str]]:
|
| 499 |
+
"""
|
| 500 |
+
Return all items in the query params. Allow duplicate keys to occur.
|
| 501 |
+
|
| 502 |
+
Usage:
|
| 503 |
+
|
| 504 |
+
q = httpx.QueryParams("a=123&a=456&b=789")
|
| 505 |
+
assert list(q.multi_items()) == [("a", "123"), ("a", "456"), ("b", "789")]
|
| 506 |
+
"""
|
| 507 |
+
multi_items: list[tuple[str, str]] = []
|
| 508 |
+
for k, v in self._dict.items():
|
| 509 |
+
multi_items.extend([(k, i) for i in v])
|
| 510 |
+
return multi_items
|
| 511 |
+
|
| 512 |
+
def get(self, key: typing.Any, default: typing.Any = None) -> typing.Any:
|
| 513 |
+
"""
|
| 514 |
+
Get a value from the query param for a given key. If the key occurs
|
| 515 |
+
more than once, then only the first value is returned.
|
| 516 |
+
|
| 517 |
+
Usage:
|
| 518 |
+
|
| 519 |
+
q = httpx.QueryParams("a=123&a=456&b=789")
|
| 520 |
+
assert q.get("a") == "123"
|
| 521 |
+
"""
|
| 522 |
+
if key in self._dict:
|
| 523 |
+
return self._dict[str(key)][0]
|
| 524 |
+
return default
|
| 525 |
+
|
| 526 |
+
def get_list(self, key: str) -> list[str]:
|
| 527 |
+
"""
|
| 528 |
+
Get all values from the query param for a given key.
|
| 529 |
+
|
| 530 |
+
Usage:
|
| 531 |
+
|
| 532 |
+
q = httpx.QueryParams("a=123&a=456&b=789")
|
| 533 |
+
assert q.get_list("a") == ["123", "456"]
|
| 534 |
+
"""
|
| 535 |
+
return list(self._dict.get(str(key), []))
|
| 536 |
+
|
| 537 |
+
def set(self, key: str, value: typing.Any = None) -> QueryParams:
|
| 538 |
+
"""
|
| 539 |
+
Return a new QueryParams instance, setting the value of a key.
|
| 540 |
+
|
| 541 |
+
Usage:
|
| 542 |
+
|
| 543 |
+
q = httpx.QueryParams("a=123")
|
| 544 |
+
q = q.set("a", "456")
|
| 545 |
+
assert q == httpx.QueryParams("a=456")
|
| 546 |
+
"""
|
| 547 |
+
q = QueryParams()
|
| 548 |
+
q._dict = dict(self._dict)
|
| 549 |
+
q._dict[str(key)] = [primitive_value_to_str(value)]
|
| 550 |
+
return q
|
| 551 |
+
|
| 552 |
+
def add(self, key: str, value: typing.Any = None) -> QueryParams:
|
| 553 |
+
"""
|
| 554 |
+
Return a new QueryParams instance, setting or appending the value of a key.
|
| 555 |
+
|
| 556 |
+
Usage:
|
| 557 |
+
|
| 558 |
+
q = httpx.QueryParams("a=123")
|
| 559 |
+
q = q.add("a", "456")
|
| 560 |
+
assert q == httpx.QueryParams("a=123&a=456")
|
| 561 |
+
"""
|
| 562 |
+
q = QueryParams()
|
| 563 |
+
q._dict = dict(self._dict)
|
| 564 |
+
q._dict[str(key)] = q.get_list(key) + [primitive_value_to_str(value)]
|
| 565 |
+
return q
|
| 566 |
+
|
| 567 |
+
def remove(self, key: str) -> QueryParams:
|
| 568 |
+
"""
|
| 569 |
+
Return a new QueryParams instance, removing the value of a key.
|
| 570 |
+
|
| 571 |
+
Usage:
|
| 572 |
+
|
| 573 |
+
q = httpx.QueryParams("a=123")
|
| 574 |
+
q = q.remove("a")
|
| 575 |
+
assert q == httpx.QueryParams("")
|
| 576 |
+
"""
|
| 577 |
+
q = QueryParams()
|
| 578 |
+
q._dict = dict(self._dict)
|
| 579 |
+
q._dict.pop(str(key), None)
|
| 580 |
+
return q
|
| 581 |
+
|
| 582 |
+
def merge(self, params: QueryParamTypes | None = None) -> QueryParams:
|
| 583 |
+
"""
|
| 584 |
+
Return a new QueryParams instance, updated with.
|
| 585 |
+
|
| 586 |
+
Usage:
|
| 587 |
+
|
| 588 |
+
q = httpx.QueryParams("a=123")
|
| 589 |
+
q = q.merge({"b": "456"})
|
| 590 |
+
assert q == httpx.QueryParams("a=123&b=456")
|
| 591 |
+
|
| 592 |
+
q = httpx.QueryParams("a=123")
|
| 593 |
+
q = q.merge({"a": "456", "b": "789"})
|
| 594 |
+
assert q == httpx.QueryParams("a=456&b=789")
|
| 595 |
+
"""
|
| 596 |
+
q = QueryParams(params)
|
| 597 |
+
q._dict = {**self._dict, **q._dict}
|
| 598 |
+
return q
|
| 599 |
+
|
| 600 |
+
def __getitem__(self, key: typing.Any) -> str:
|
| 601 |
+
return self._dict[key][0]
|
| 602 |
+
|
| 603 |
+
def __contains__(self, key: typing.Any) -> bool:
|
| 604 |
+
return key in self._dict
|
| 605 |
+
|
| 606 |
+
def __iter__(self) -> typing.Iterator[typing.Any]:
|
| 607 |
+
return iter(self.keys())
|
| 608 |
+
|
| 609 |
+
def __len__(self) -> int:
|
| 610 |
+
return len(self._dict)
|
| 611 |
+
|
| 612 |
+
def __bool__(self) -> bool:
|
| 613 |
+
return bool(self._dict)
|
| 614 |
+
|
| 615 |
+
def __hash__(self) -> int:
|
| 616 |
+
return hash(str(self))
|
| 617 |
+
|
| 618 |
+
def __eq__(self, other: typing.Any) -> bool:
|
| 619 |
+
if not isinstance(other, self.__class__):
|
| 620 |
+
return False
|
| 621 |
+
return sorted(self.multi_items()) == sorted(other.multi_items())
|
| 622 |
+
|
| 623 |
+
def __str__(self) -> str:
|
| 624 |
+
return urlencode(self.multi_items())
|
| 625 |
+
|
| 626 |
+
def __repr__(self) -> str:
|
| 627 |
+
class_name = self.__class__.__name__
|
| 628 |
+
query_string = str(self)
|
| 629 |
+
return f"{class_name}({query_string!r})"
|
| 630 |
+
|
| 631 |
+
def update(self, params: QueryParamTypes | None = None) -> None:
|
| 632 |
+
raise RuntimeError(
|
| 633 |
+
"QueryParams are immutable since 0.18.0. "
|
| 634 |
+
"Use `q = q.merge(...)` to create an updated copy."
|
| 635 |
+
)
|
| 636 |
+
|
| 637 |
+
def __setitem__(self, key: str, value: str) -> None:
|
| 638 |
+
raise RuntimeError(
|
| 639 |
+
"QueryParams are immutable since 0.18.0. "
|
| 640 |
+
"Use `q = q.set(key, value)` to create an updated copy."
|
| 641 |
+
)
|
.venv/lib/python3.11/site-packages/httpx/_utils.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import ipaddress
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
import typing
|
| 7 |
+
from urllib.request import getproxies
|
| 8 |
+
|
| 9 |
+
from ._types import PrimitiveData
|
| 10 |
+
|
| 11 |
+
if typing.TYPE_CHECKING: # pragma: no cover
|
| 12 |
+
from ._urls import URL
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def primitive_value_to_str(value: PrimitiveData) -> str:
|
| 16 |
+
"""
|
| 17 |
+
Coerce a primitive data type into a string value.
|
| 18 |
+
|
| 19 |
+
Note that we prefer JSON-style 'true'/'false' for boolean values here.
|
| 20 |
+
"""
|
| 21 |
+
if value is True:
|
| 22 |
+
return "true"
|
| 23 |
+
elif value is False:
|
| 24 |
+
return "false"
|
| 25 |
+
elif value is None:
|
| 26 |
+
return ""
|
| 27 |
+
return str(value)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def get_environment_proxies() -> dict[str, str | None]:
|
| 31 |
+
"""Gets proxy information from the environment"""
|
| 32 |
+
|
| 33 |
+
# urllib.request.getproxies() falls back on System
|
| 34 |
+
# Registry and Config for proxies on Windows and macOS.
|
| 35 |
+
# We don't want to propagate non-HTTP proxies into
|
| 36 |
+
# our configuration such as 'TRAVIS_APT_PROXY'.
|
| 37 |
+
proxy_info = getproxies()
|
| 38 |
+
mounts: dict[str, str | None] = {}
|
| 39 |
+
|
| 40 |
+
for scheme in ("http", "https", "all"):
|
| 41 |
+
if proxy_info.get(scheme):
|
| 42 |
+
hostname = proxy_info[scheme]
|
| 43 |
+
mounts[f"{scheme}://"] = (
|
| 44 |
+
hostname if "://" in hostname else f"http://{hostname}"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
no_proxy_hosts = [host.strip() for host in proxy_info.get("no", "").split(",")]
|
| 48 |
+
for hostname in no_proxy_hosts:
|
| 49 |
+
# See https://curl.haxx.se/libcurl/c/CURLOPT_NOPROXY.html for details
|
| 50 |
+
# on how names in `NO_PROXY` are handled.
|
| 51 |
+
if hostname == "*":
|
| 52 |
+
# If NO_PROXY=* is used or if "*" occurs as any one of the comma
|
| 53 |
+
# separated hostnames, then we should just bypass any information
|
| 54 |
+
# from HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and always ignore
|
| 55 |
+
# proxies.
|
| 56 |
+
return {}
|
| 57 |
+
elif hostname:
|
| 58 |
+
# NO_PROXY=.google.com is marked as "all://*.google.com,
|
| 59 |
+
# which disables "www.google.com" but not "google.com"
|
| 60 |
+
# NO_PROXY=google.com is marked as "all://*google.com,
|
| 61 |
+
# which disables "www.google.com" and "google.com".
|
| 62 |
+
# (But not "wwwgoogle.com")
|
| 63 |
+
# NO_PROXY can include domains, IPv6, IPv4 addresses and "localhost"
|
| 64 |
+
# NO_PROXY=example.com,::1,localhost,192.168.0.0/16
|
| 65 |
+
if "://" in hostname:
|
| 66 |
+
mounts[hostname] = None
|
| 67 |
+
elif is_ipv4_hostname(hostname):
|
| 68 |
+
mounts[f"all://{hostname}"] = None
|
| 69 |
+
elif is_ipv6_hostname(hostname):
|
| 70 |
+
mounts[f"all://[{hostname}]"] = None
|
| 71 |
+
elif hostname.lower() == "localhost":
|
| 72 |
+
mounts[f"all://{hostname}"] = None
|
| 73 |
+
else:
|
| 74 |
+
mounts[f"all://*{hostname}"] = None
|
| 75 |
+
|
| 76 |
+
return mounts
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def to_bytes(value: str | bytes, encoding: str = "utf-8") -> bytes:
|
| 80 |
+
return value.encode(encoding) if isinstance(value, str) else value
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def to_str(value: str | bytes, encoding: str = "utf-8") -> str:
|
| 84 |
+
return value if isinstance(value, str) else value.decode(encoding)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def to_bytes_or_str(value: str, match_type_of: typing.AnyStr) -> typing.AnyStr:
|
| 88 |
+
return value if isinstance(match_type_of, str) else value.encode()
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def unquote(value: str) -> str:
|
| 92 |
+
return value[1:-1] if value[0] == value[-1] == '"' else value
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def peek_filelike_length(stream: typing.Any) -> int | None:
|
| 96 |
+
"""
|
| 97 |
+
Given a file-like stream object, return its length in number of bytes
|
| 98 |
+
without reading it into memory.
|
| 99 |
+
"""
|
| 100 |
+
try:
|
| 101 |
+
# Is it an actual file?
|
| 102 |
+
fd = stream.fileno()
|
| 103 |
+
# Yup, seems to be an actual file.
|
| 104 |
+
length = os.fstat(fd).st_size
|
| 105 |
+
except (AttributeError, OSError):
|
| 106 |
+
# No... Maybe it's something that supports random access, like `io.BytesIO`?
|
| 107 |
+
try:
|
| 108 |
+
# Assuming so, go to end of stream to figure out its length,
|
| 109 |
+
# then put it back in place.
|
| 110 |
+
offset = stream.tell()
|
| 111 |
+
length = stream.seek(0, os.SEEK_END)
|
| 112 |
+
stream.seek(offset)
|
| 113 |
+
except (AttributeError, OSError):
|
| 114 |
+
# Not even that? Sorry, we're doomed...
|
| 115 |
+
return None
|
| 116 |
+
|
| 117 |
+
return length
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class URLPattern:
|
| 121 |
+
"""
|
| 122 |
+
A utility class currently used for making lookups against proxy keys...
|
| 123 |
+
|
| 124 |
+
# Wildcard matching...
|
| 125 |
+
>>> pattern = URLPattern("all://")
|
| 126 |
+
>>> pattern.matches(httpx.URL("http://example.com"))
|
| 127 |
+
True
|
| 128 |
+
|
| 129 |
+
# Witch scheme matching...
|
| 130 |
+
>>> pattern = URLPattern("https://")
|
| 131 |
+
>>> pattern.matches(httpx.URL("https://example.com"))
|
| 132 |
+
True
|
| 133 |
+
>>> pattern.matches(httpx.URL("http://example.com"))
|
| 134 |
+
False
|
| 135 |
+
|
| 136 |
+
# With domain matching...
|
| 137 |
+
>>> pattern = URLPattern("https://example.com")
|
| 138 |
+
>>> pattern.matches(httpx.URL("https://example.com"))
|
| 139 |
+
True
|
| 140 |
+
>>> pattern.matches(httpx.URL("http://example.com"))
|
| 141 |
+
False
|
| 142 |
+
>>> pattern.matches(httpx.URL("https://other.com"))
|
| 143 |
+
False
|
| 144 |
+
|
| 145 |
+
# Wildcard scheme, with domain matching...
|
| 146 |
+
>>> pattern = URLPattern("all://example.com")
|
| 147 |
+
>>> pattern.matches(httpx.URL("https://example.com"))
|
| 148 |
+
True
|
| 149 |
+
>>> pattern.matches(httpx.URL("http://example.com"))
|
| 150 |
+
True
|
| 151 |
+
>>> pattern.matches(httpx.URL("https://other.com"))
|
| 152 |
+
False
|
| 153 |
+
|
| 154 |
+
# With port matching...
|
| 155 |
+
>>> pattern = URLPattern("https://example.com:1234")
|
| 156 |
+
>>> pattern.matches(httpx.URL("https://example.com:1234"))
|
| 157 |
+
True
|
| 158 |
+
>>> pattern.matches(httpx.URL("https://example.com"))
|
| 159 |
+
False
|
| 160 |
+
"""
|
| 161 |
+
|
| 162 |
+
def __init__(self, pattern: str) -> None:
|
| 163 |
+
from ._urls import URL
|
| 164 |
+
|
| 165 |
+
if pattern and ":" not in pattern:
|
| 166 |
+
raise ValueError(
|
| 167 |
+
f"Proxy keys should use proper URL forms rather "
|
| 168 |
+
f"than plain scheme strings. "
|
| 169 |
+
f'Instead of "{pattern}", use "{pattern}://"'
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
url = URL(pattern)
|
| 173 |
+
self.pattern = pattern
|
| 174 |
+
self.scheme = "" if url.scheme == "all" else url.scheme
|
| 175 |
+
self.host = "" if url.host == "*" else url.host
|
| 176 |
+
self.port = url.port
|
| 177 |
+
if not url.host or url.host == "*":
|
| 178 |
+
self.host_regex: typing.Pattern[str] | None = None
|
| 179 |
+
elif url.host.startswith("*."):
|
| 180 |
+
# *.example.com should match "www.example.com", but not "example.com"
|
| 181 |
+
domain = re.escape(url.host[2:])
|
| 182 |
+
self.host_regex = re.compile(f"^.+\\.{domain}$")
|
| 183 |
+
elif url.host.startswith("*"):
|
| 184 |
+
# *example.com should match "www.example.com" and "example.com"
|
| 185 |
+
domain = re.escape(url.host[1:])
|
| 186 |
+
self.host_regex = re.compile(f"^(.+\\.)?{domain}$")
|
| 187 |
+
else:
|
| 188 |
+
# example.com should match "example.com" but not "www.example.com"
|
| 189 |
+
domain = re.escape(url.host)
|
| 190 |
+
self.host_regex = re.compile(f"^{domain}$")
|
| 191 |
+
|
| 192 |
+
def matches(self, other: URL) -> bool:
|
| 193 |
+
if self.scheme and self.scheme != other.scheme:
|
| 194 |
+
return False
|
| 195 |
+
if (
|
| 196 |
+
self.host
|
| 197 |
+
and self.host_regex is not None
|
| 198 |
+
and not self.host_regex.match(other.host)
|
| 199 |
+
):
|
| 200 |
+
return False
|
| 201 |
+
if self.port is not None and self.port != other.port:
|
| 202 |
+
return False
|
| 203 |
+
return True
|
| 204 |
+
|
| 205 |
+
@property
|
| 206 |
+
def priority(self) -> tuple[int, int, int]:
|
| 207 |
+
"""
|
| 208 |
+
The priority allows URLPattern instances to be sortable, so that
|
| 209 |
+
we can match from most specific to least specific.
|
| 210 |
+
"""
|
| 211 |
+
# URLs with a port should take priority over URLs without a port.
|
| 212 |
+
port_priority = 0 if self.port is not None else 1
|
| 213 |
+
# Longer hostnames should match first.
|
| 214 |
+
host_priority = -len(self.host)
|
| 215 |
+
# Longer schemes should match first.
|
| 216 |
+
scheme_priority = -len(self.scheme)
|
| 217 |
+
return (port_priority, host_priority, scheme_priority)
|
| 218 |
+
|
| 219 |
+
def __hash__(self) -> int:
|
| 220 |
+
return hash(self.pattern)
|
| 221 |
+
|
| 222 |
+
def __lt__(self, other: URLPattern) -> bool:
|
| 223 |
+
return self.priority < other.priority
|
| 224 |
+
|
| 225 |
+
def __eq__(self, other: typing.Any) -> bool:
|
| 226 |
+
return isinstance(other, URLPattern) and self.pattern == other.pattern
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def is_ipv4_hostname(hostname: str) -> bool:
|
| 230 |
+
try:
|
| 231 |
+
ipaddress.IPv4Address(hostname.split("/")[0])
|
| 232 |
+
except Exception:
|
| 233 |
+
return False
|
| 234 |
+
return True
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def is_ipv6_hostname(hostname: str) -> bool:
|
| 238 |
+
try:
|
| 239 |
+
ipaddress.IPv6Address(hostname.split("/")[0])
|
| 240 |
+
except Exception:
|
| 241 |
+
return False
|
| 242 |
+
return True
|
.venv/lib/python3.11/site-packages/httpx/py.typed
ADDED
|
File without changes
|
.venv/lib/python3.11/site-packages/propcache-0.2.1.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
.venv/lib/python3.11/site-packages/propcache-0.2.1.dist-info/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|