Instructions to use dl1wjd2/Logistics_AX with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- ultralytics
How to use dl1wjd2/Logistics_AX with ultralytics:
from huggingface_hub import hf_hub_download from ultralytics import YOLO # pick the weights file from this repo's "Files and versions" tab weights = hf_hub_download("dl1wjd2/Logistics_AX", "<weights>.pt") model = YOLO(weights) source = 'http://images.cocodataset.org/val2017/000000039769.jpg' model.predict(source=source, save=True) - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub (part 7)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +14 -0
- hug/lib/python3.12/site-packages/httpx-0.28.1.dist-info/RECORD +54 -0
- hug/lib/python3.12/site-packages/httpx-0.28.1.dist-info/WHEEL +4 -0
- hug/lib/python3.12/site-packages/httpx-0.28.1.dist-info/entry_points.txt +2 -0
- hug/lib/python3.12/site-packages/httpx-0.28.1.dist-info/licenses/LICENSE.md +12 -0
- hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/INSTALLER +1 -0
- hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/METADATA +230 -0
- hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/RECORD +70 -0
- hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/WHEEL +4 -0
- hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/entry_points.txt +2 -0
- hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/licenses/LICENSE.md +13 -0
- hug/lib/python3.12/site-packages/httpx2/__init__.py +130 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/__init__.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/__version__.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_alias.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_api.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_auth.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_client.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_config.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_content.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_cookies.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_decoders.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_exceptions.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_main.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_models.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_multipart.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_sse.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_status_codes.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_types.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_urlparse.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_urls.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__pycache__/_utils.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/__version__.py +5 -0
- hug/lib/python3.12/site-packages/httpx2/_alias.py +71 -0
- hug/lib/python3.12/site-packages/httpx2/_api.py +532 -0
- hug/lib/python3.12/site-packages/httpx2/_auth.py +335 -0
- hug/lib/python3.12/site-packages/httpx2/_client.py +2236 -0
- hug/lib/python3.12/site-packages/httpx2/_config.py +221 -0
- hug/lib/python3.12/site-packages/httpx2/_content.py +237 -0
- hug/lib/python3.12/site-packages/httpx2/_cookies.py +74 -0
- hug/lib/python3.12/site-packages/httpx2/_decoders.py +458 -0
- hug/lib/python3.12/site-packages/httpx2/_exceptions.py +380 -0
- hug/lib/python3.12/site-packages/httpx2/_main.py +483 -0
- hug/lib/python3.12/site-packages/httpx2/_models.py +1274 -0
- hug/lib/python3.12/site-packages/httpx2/_multipart.py +284 -0
- hug/lib/python3.12/site-packages/httpx2/_sse.py +226 -0
- hug/lib/python3.12/site-packages/httpx2/_status_codes.py +189 -0
- hug/lib/python3.12/site-packages/httpx2/_transports/__init__.py +27 -0
- hug/lib/python3.12/site-packages/httpx2/_transports/__pycache__/__init__.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/httpx2/_transports/__pycache__/asgi.cpython-312.pyc +0 -0
.gitattributes
CHANGED
|
@@ -91,3 +91,17 @@ hug/lib/python3.12/site-packages/fontTools/ttLib/tables/__pycache__/otTables.cpy
|
|
| 91 |
hug/lib/python3.12/site-packages/fontTools/varLib/instancer/__pycache__/__init__.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 92 |
hug/lib/python3.12/site-packages/fontTools/varLib/iup.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 93 |
hug/lib/python3.12/site-packages/hf_xet/hf_xet.abi3.so filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
hug/lib/python3.12/site-packages/fontTools/varLib/instancer/__pycache__/__init__.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 92 |
hug/lib/python3.12/site-packages/fontTools/varLib/iup.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 93 |
hug/lib/python3.12/site-packages/hf_xet/hf_xet.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 94 |
+
hug/lib/python3.12/site-packages/huggingface_hub/__pycache__/_sandbox.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 95 |
+
hug/lib/python3.12/site-packages/huggingface_hub/__pycache__/hf_api.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 96 |
+
hug/lib/python3.12/site-packages/huggingface_hub/cli/__pycache__/jobs.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 97 |
+
hug/lib/python3.12/site-packages/huggingface_hub/inference/__pycache__/_client.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 98 |
+
hug/lib/python3.12/site-packages/huggingface_hub/inference/_generated/__pycache__/_async_client.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 99 |
+
hug/lib/python3.12/site-packages/jinja2/__pycache__/compiler.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 100 |
+
hug/lib/python3.12/site-packages/kiwisolver/_cext.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 101 |
+
hug/lib/python3.12/site-packages/matplotlib/__pycache__/_cm_listed.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 102 |
+
hug/lib/python3.12/site-packages/matplotlib/__pycache__/_mathtext.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 103 |
+
hug/lib/python3.12/site-packages/matplotlib/__pycache__/axis.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 104 |
+
hug/lib/python3.12/site-packages/matplotlib/__pycache__/backend_bases.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 105 |
+
hug/lib/python3.12/site-packages/matplotlib/__pycache__/cbook.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 106 |
+
hug/lib/python3.12/site-packages/matplotlib/__pycache__/collections.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 107 |
+
hug/lib/python3.12/site-packages/matplotlib/__pycache__/colors.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
hug/lib/python3.12/site-packages/httpx-0.28.1.dist-info/RECORD
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
../../../bin/httpx,sha256=XlpDske_YLN2L2KlF8lWwvkH3UD4wovGisP3RvxKvNk,175
|
| 2 |
+
httpx-0.28.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 3 |
+
httpx-0.28.1.dist-info/METADATA,sha256=_rubD48-gNV8gZnDBPNcQzboWB0dGNeYPJJ2a4J5OyU,7052
|
| 4 |
+
httpx-0.28.1.dist-info/RECORD,,
|
| 5 |
+
httpx-0.28.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
| 6 |
+
httpx-0.28.1.dist-info/entry_points.txt,sha256=2lVkdQmxLA1pNMgSN2eV89o90HCZezhmNwsy6ryKDSA,37
|
| 7 |
+
httpx-0.28.1.dist-info/licenses/LICENSE.md,sha256=TsWdVE8StfU5o6cW_TIaxYzNgDC0ZSIfLIgCAM3yjY0,1508
|
| 8 |
+
httpx/__init__.py,sha256=CsaZe6yZj0rHg6322AWKWHGTMVr9txgEfD5P3_Rrz60,2171
|
| 9 |
+
httpx/__pycache__/__init__.cpython-312.pyc,,
|
| 10 |
+
httpx/__pycache__/__version__.cpython-312.pyc,,
|
| 11 |
+
httpx/__pycache__/_api.cpython-312.pyc,,
|
| 12 |
+
httpx/__pycache__/_auth.cpython-312.pyc,,
|
| 13 |
+
httpx/__pycache__/_client.cpython-312.pyc,,
|
| 14 |
+
httpx/__pycache__/_config.cpython-312.pyc,,
|
| 15 |
+
httpx/__pycache__/_content.cpython-312.pyc,,
|
| 16 |
+
httpx/__pycache__/_decoders.cpython-312.pyc,,
|
| 17 |
+
httpx/__pycache__/_exceptions.cpython-312.pyc,,
|
| 18 |
+
httpx/__pycache__/_main.cpython-312.pyc,,
|
| 19 |
+
httpx/__pycache__/_models.cpython-312.pyc,,
|
| 20 |
+
httpx/__pycache__/_multipart.cpython-312.pyc,,
|
| 21 |
+
httpx/__pycache__/_status_codes.cpython-312.pyc,,
|
| 22 |
+
httpx/__pycache__/_types.cpython-312.pyc,,
|
| 23 |
+
httpx/__pycache__/_urlparse.cpython-312.pyc,,
|
| 24 |
+
httpx/__pycache__/_urls.cpython-312.pyc,,
|
| 25 |
+
httpx/__pycache__/_utils.cpython-312.pyc,,
|
| 26 |
+
httpx/__version__.py,sha256=LoUyYeOXTieGzuP_64UL0wxdtxjuu_QbOvE7NOg-IqU,108
|
| 27 |
+
httpx/_api.py,sha256=r_Zgs4jIpcPJLqK5dbbSayqo_iVMKFaxZCd-oOHxLEs,11743
|
| 28 |
+
httpx/_auth.py,sha256=Yr3QwaUSK17rGYx-7j-FdicFIzz4Y9FFV-1F4-7RXX4,11891
|
| 29 |
+
httpx/_client.py,sha256=xD-UG67-WMkeltAAOeGGj-cZ2RRTAm19sWRxlFY7_40,65714
|
| 30 |
+
httpx/_config.py,sha256=pPp2U-wicfcKsF-KYRE1LYdt3e6ERGeIoXZ8Gjo3LWc,8547
|
| 31 |
+
httpx/_content.py,sha256=LGGzrJTR3OvN4Mb1GVVNLXkXJH-6oKlwAttO9p5w_yg,8161
|
| 32 |
+
httpx/_decoders.py,sha256=p0dX8I0NEHexs3UGp4SsZutiMhsXrrWl6-GnqVb0iKM,12041
|
| 33 |
+
httpx/_exceptions.py,sha256=bxW7fxzgVMAdNTbwT0Vnq04gJDW1_gI_GFiQPuMyjL0,8527
|
| 34 |
+
httpx/_main.py,sha256=Cg9GMabiTT_swaDfUgIRitSwxLRMSwUDOm7LdSGqlA4,15626
|
| 35 |
+
httpx/_models.py,sha256=4__Guyv1gLxuZChwim8kfQNiIOcJ9acreFOSurvZfms,44700
|
| 36 |
+
httpx/_multipart.py,sha256=KOHEZZl6oohg9mPaKyyu345qq1rJLg35TUG3YAzXB3Y,9843
|
| 37 |
+
httpx/_status_codes.py,sha256=DYn-2ufBgMeXy5s8x3_TB7wjAuAAMewTakPrm5rXEsc,5639
|
| 38 |
+
httpx/_transports/__init__.py,sha256=GbUoBSAOp7z-l-9j5YhMhR3DMIcn6FVLhj072O3Nnno,275
|
| 39 |
+
httpx/_transports/__pycache__/__init__.cpython-312.pyc,,
|
| 40 |
+
httpx/_transports/__pycache__/asgi.cpython-312.pyc,,
|
| 41 |
+
httpx/_transports/__pycache__/base.cpython-312.pyc,,
|
| 42 |
+
httpx/_transports/__pycache__/default.cpython-312.pyc,,
|
| 43 |
+
httpx/_transports/__pycache__/mock.cpython-312.pyc,,
|
| 44 |
+
httpx/_transports/__pycache__/wsgi.cpython-312.pyc,,
|
| 45 |
+
httpx/_transports/asgi.py,sha256=HRfiDYMPt4wQH2gFgHZg4c-i3sblo6bL5GTqcET-xz8,5501
|
| 46 |
+
httpx/_transports/base.py,sha256=kZS_VMbViYfF570pogUCJ1bulz-ybfL51Pqs9yktebU,2523
|
| 47 |
+
httpx/_transports/default.py,sha256=AzeaRUyVwCccTyyNJexDf0n1dFfzzydpdIQgvw7PLnk,13983
|
| 48 |
+
httpx/_transports/mock.py,sha256=PTo0d567RITXxGrki6kN7_67wwAxfwiMDcuXJiZCjEo,1232
|
| 49 |
+
httpx/_transports/wsgi.py,sha256=NcPX3Xap_EwCFZWO_OaSyQNuInCYx1QMNbO8GAei6jY,4825
|
| 50 |
+
httpx/_types.py,sha256=Jyh41GQq7AOev8IOWKDAg7zCbvHAfufmW5g_PiTtErY,2965
|
| 51 |
+
httpx/_urlparse.py,sha256=ZAmH47ONfkxrrj-PPYhGeiHjb6AjKCS-ANWIN4OL_KY,18546
|
| 52 |
+
httpx/_urls.py,sha256=dX99VR1DSOHpgo9Aq7PzYO4FKdxqKjwyNp8grf8dHN0,21550
|
| 53 |
+
httpx/_utils.py,sha256=_TVeqAKvxJkKHdz7dFeb4s0LZqQXgeFkXSgfiHBK_1o,8285
|
| 54 |
+
httpx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
hug/lib/python3.12/site-packages/httpx-0.28.1.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: hatchling 1.26.3
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
hug/lib/python3.12/site-packages/httpx-0.28.1.dist-info/entry_points.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[console_scripts]
|
| 2 |
+
httpx = httpx:main
|
hug/lib/python3.12/site-packages/httpx-0.28.1.dist-info/licenses/LICENSE.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright © 2019, [Encode OSS Ltd](https://www.encode.io/).
|
| 2 |
+
All rights reserved.
|
| 3 |
+
|
| 4 |
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
| 5 |
+
|
| 6 |
+
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
| 7 |
+
|
| 8 |
+
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
| 9 |
+
|
| 10 |
+
* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
| 11 |
+
|
| 12 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/METADATA
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.5
|
| 2 |
+
Name: httpx2
|
| 3 |
+
Version: 2.16.0
|
| 4 |
+
Summary: The next generation HTTP client.
|
| 5 |
+
Project-URL: Changelog, https://github.com/pydantic/httpx2/blob/main/src/httpx2/CHANGELOG.md
|
| 6 |
+
Project-URL: Homepage, https://github.com/pydantic/httpx2
|
| 7 |
+
Project-URL: Source, https://github.com/pydantic/httpx2
|
| 8 |
+
Author-email: Tom Christie <tom@tomchristie.com>
|
| 9 |
+
Maintainer-email: "Pydantic Services Inc." <engineering@pydantic.dev>
|
| 10 |
+
License-Expression: BSD-3-Clause
|
| 11 |
+
License-File: LICENSE.md
|
| 12 |
+
Classifier: Development Status :: 5 - Production/Stable
|
| 13 |
+
Classifier: Environment :: Web Environment
|
| 14 |
+
Classifier: Framework :: AnyIO
|
| 15 |
+
Classifier: Framework :: AsyncIO
|
| 16 |
+
Classifier: Framework :: Trio
|
| 17 |
+
Classifier: Intended Audience :: Developers
|
| 18 |
+
Classifier: License :: OSI Approved :: BSD License
|
| 19 |
+
Classifier: Operating System :: OS Independent
|
| 20 |
+
Classifier: Programming Language :: Python :: 3
|
| 21 |
+
Classifier: Programming Language :: Python :: 3 :: Only
|
| 22 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 23 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 24 |
+
Classifier: Programming Language :: Python :: 3.13
|
| 25 |
+
Classifier: Programming Language :: Python :: 3.14
|
| 26 |
+
Classifier: Programming Language :: Python :: 3.15
|
| 27 |
+
Classifier: Topic :: Internet :: WWW/HTTP
|
| 28 |
+
Requires-Python: >=3.11
|
| 29 |
+
Requires-Dist: anyio>=4.10; sys_platform != 'emscripten'
|
| 30 |
+
Requires-Dist: httpcore2==2.16.0; sys_platform != 'emscripten'
|
| 31 |
+
Requires-Dist: httpx2-jsfetch; sys_platform == 'emscripten' and python_version >= '3.12'
|
| 32 |
+
Requires-Dist: idna>=3.18
|
| 33 |
+
Requires-Dist: truststore>=0.10; sys_platform != 'emscripten'
|
| 34 |
+
Requires-Dist: typing-extensions>=4.5.0; python_version < '3.13'
|
| 35 |
+
Provides-Extra: brotli
|
| 36 |
+
Requires-Dist: brotli>=1.2.0; (platform_python_implementation == 'CPython') and extra == 'brotli'
|
| 37 |
+
Requires-Dist: brotlicffi>=1.2.0.2; (platform_python_implementation != 'CPython') and extra == 'brotli'
|
| 38 |
+
Provides-Extra: cli
|
| 39 |
+
Requires-Dist: click>=8.4.2; extra == 'cli'
|
| 40 |
+
Requires-Dist: pygments==2.*; extra == 'cli'
|
| 41 |
+
Requires-Dist: rich<16,>=10; extra == 'cli'
|
| 42 |
+
Provides-Extra: http2
|
| 43 |
+
Requires-Dist: h2<5,>=3; extra == 'http2'
|
| 44 |
+
Provides-Extra: socks
|
| 45 |
+
Requires-Dist: socksio==1.*; extra == 'socks'
|
| 46 |
+
Provides-Extra: ws
|
| 47 |
+
Requires-Dist: wsproto>=1.2; extra == 'ws'
|
| 48 |
+
Provides-Extra: zstd
|
| 49 |
+
Requires-Dist: backports-zstd>=1.0.0; (python_version <= '3.13') and extra == 'zstd'
|
| 50 |
+
Description-Content-Type: text/markdown
|
| 51 |
+
|
| 52 |
+
<h1 align="center">HTTPX2</h1>
|
| 53 |
+
|
| 54 |
+
<p align="center"><em>A next-generation HTTP client for Python.</em></p>
|
| 55 |
+
|
| 56 |
+
<p align="center">
|
| 57 |
+
<a href="https://github.com/pydantic/httpx2/actions"><img src="https://github.com/pydantic/httpx2/workflows/Test%20Suite/badge.svg" alt="Test Suite"></a>
|
| 58 |
+
<a href="https://pypi.org/project/httpx2/"><img src="https://badge.fury.io/py/httpx2.svg" alt="Package version"></a>
|
| 59 |
+
<a href="https://pydantic.dev/docs/logfire/join-slack/"><img src="https://img.shields.io/badge/Slack-Join%20Slack-4A154B?logo=slack" alt="Join Slack"></a>
|
| 60 |
+
</p>
|
| 61 |
+
|
| 62 |
+
HTTPX2 is a fully featured HTTP client library for Python. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**.
|
| 63 |
+
|
| 64 |
+
> [!NOTE]
|
| 65 |
+
> HTTPX2 is a continuation of the wonderful work started by [@lovelydinosaur](https://github.com/lovelydinosaur) and the broader HTTPX community. We're enormously grateful for everything that has gone into HTTPX over the years - it has been a foundational piece of the modern Python ecosystem, and this project would not exist without it.
|
| 66 |
+
>
|
| 67 |
+
> With HTTPX itself seeing limited activity recently, Pydantic is picking up stewardship under the HTTPX2 name so that users have a reliably maintained path forward - including timely security updates for a library that sits in the critical path of so many production systems. Our aim is to honour the original project's design, keep it stable for everyone relying on it, and continue evolving it carefully. Thank you to [@lovelydinosaur](https://github.com/lovelydinosaur) and every past contributor for laying such a strong foundation. 💙
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
Install HTTPX2 using pip:
|
| 72 |
+
|
| 73 |
+
```shell
|
| 74 |
+
pip install httpx2
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
Now, let's get started:
|
| 78 |
+
|
| 79 |
+
```pycon
|
| 80 |
+
>>> import httpx2
|
| 81 |
+
>>> r = httpx2.get('https://www.example.org/')
|
| 82 |
+
>>> r
|
| 83 |
+
<Response [200 OK]>
|
| 84 |
+
>>> r.status_code
|
| 85 |
+
200
|
| 86 |
+
>>> r.headers['content-type']
|
| 87 |
+
'text/html; charset=UTF-8'
|
| 88 |
+
>>> r.text
|
| 89 |
+
'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>...'
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
Or, using the command-line client.
|
| 93 |
+
|
| 94 |
+
```shell
|
| 95 |
+
pip install 'httpx2[cli]' # The command line client is an optional dependency.
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Which now allows us to use HTTPX2 directly from the command-line:
|
| 99 |
+
|
| 100 |
+
```shell
|
| 101 |
+
httpx2 --help
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## Features
|
| 105 |
+
|
| 106 |
+
HTTPX2 builds on the well-established usability of `requests`, and gives you:
|
| 107 |
+
|
| 108 |
+
* A broadly [requests-compatible API](https://pydantic.dev/docs/httpx2/guides/compatibility/).
|
| 109 |
+
* An integrated command-line client.
|
| 110 |
+
* HTTP/1.1 [and HTTP/2 support](https://pydantic.dev/docs/httpx2/guides/http2/).
|
| 111 |
+
* Standard synchronous interface, but with [async support if you need it](https://pydantic.dev/docs/httpx2/guides/async/).
|
| 112 |
+
* Ability to make requests directly to [WSGI applications](https://pydantic.dev/docs/httpx2/advanced/transports/#wsgi-transport) or [ASGI applications](https://pydantic.dev/docs/httpx2/advanced/transports/#asgi-transport).
|
| 113 |
+
* Strict timeouts everywhere.
|
| 114 |
+
* Fully type annotated.
|
| 115 |
+
* 100% test coverage.
|
| 116 |
+
|
| 117 |
+
Plus all the standard features of `requests`...
|
| 118 |
+
|
| 119 |
+
* International Domains and URLs
|
| 120 |
+
* Keep-Alive & Connection Pooling
|
| 121 |
+
* Sessions with Cookie Persistence
|
| 122 |
+
* Browser-style SSL Verification
|
| 123 |
+
* Basic/Digest Authentication
|
| 124 |
+
* Elegant Key/Value Cookies
|
| 125 |
+
* Automatic Decompression
|
| 126 |
+
* Automatic Content Decoding
|
| 127 |
+
* Unicode Response Bodies
|
| 128 |
+
* Multipart File Uploads
|
| 129 |
+
* HTTP(S) Proxy Support
|
| 130 |
+
* Connection Timeouts
|
| 131 |
+
* Streaming Downloads
|
| 132 |
+
* .netrc Support
|
| 133 |
+
* Chunked Requests
|
| 134 |
+
|
| 135 |
+
## Installation
|
| 136 |
+
|
| 137 |
+
Install with pip:
|
| 138 |
+
|
| 139 |
+
```shell
|
| 140 |
+
pip install httpx2
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
Or, to include the optional HTTP/2 support, use:
|
| 144 |
+
|
| 145 |
+
```shell
|
| 146 |
+
pip install httpx2[http2]
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
## Documentation
|
| 150 |
+
|
| 151 |
+
Project documentation is available at [https://pydantic.dev/docs/httpx2/](https://pydantic.dev/docs/httpx2/).
|
| 152 |
+
|
| 153 |
+
For a run-through of all the basics, head over to the [QuickStart](https://pydantic.dev/docs/httpx2/get-started/quickstart/).
|
| 154 |
+
|
| 155 |
+
For more advanced topics, see the [Advanced Usage](https://pydantic.dev/docs/httpx2/advanced/clients/) section, the [async support](https://pydantic.dev/docs/httpx2/guides/async/) section, or the [HTTP/2](https://pydantic.dev/docs/httpx2/guides/http2/) section.
|
| 156 |
+
|
| 157 |
+
The [Developer Interface](https://pydantic.dev/docs/httpx2/api/api/) provides a comprehensive API reference.
|
| 158 |
+
|
| 159 |
+
To find out about tools that integrate with HTTPX, see [Third Party Packages](https://pydantic.dev/docs/httpx2/community/third_party_packages/).
|
| 160 |
+
|
| 161 |
+
## Observability with Pydantic Logfire
|
| 162 |
+
|
| 163 |
+
Install Logfire with HTTPX instrumentation support:
|
| 164 |
+
|
| 165 |
+
```shell
|
| 166 |
+
pip install 'logfire[httpx]'
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
HTTPX2 works out of the box with [Pydantic Logfire](https://pydantic.dev/logfire), our observability platform built on OpenTelemetry. One line instruments every request, giving you traces, timings, and status codes with no other changes:
|
| 170 |
+
|
| 171 |
+
```python
|
| 172 |
+
import logfire
|
| 173 |
+
import httpx2
|
| 174 |
+
|
| 175 |
+
logfire.configure()
|
| 176 |
+
logfire.instrument_httpx()
|
| 177 |
+
|
| 178 |
+
httpx2.get("https://pydantic.dev/")
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
This works the same way for explicit `httpx2.Client()` and `httpx2.AsyncClient()` instances, and you can scope instrumentation to a single client by passing it in: `logfire.instrument_httpx(client)`. Pass `capture_all=True` to also record headers and bodies. See the [Logfire HTTPX docs](https://logfire.pydantic.dev/docs/integrations/http-clients/httpx/) for the full set of options.
|
| 182 |
+
|
| 183 |
+
## Contribute
|
| 184 |
+
|
| 185 |
+
If you want to contribute with HTTPX2 check out the [Contributing Guide](https://pydantic.dev/docs/httpx2/community/contributing/) to learn how to start.
|
| 186 |
+
|
| 187 |
+
## Dependencies
|
| 188 |
+
|
| 189 |
+
The HTTPX2 project relies on these excellent libraries:
|
| 190 |
+
|
| 191 |
+
* `httpcore2` - The underlying transport implementation for `httpx2`.
|
| 192 |
+
* `h11` - HTTP/1.1 support.
|
| 193 |
+
* `anyio` - Structured concurrency primitives, used to support both `asyncio` and `trio`.
|
| 194 |
+
* `truststore` - SSL verification using the operating system's trust store.
|
| 195 |
+
* `idna` - Internationalized domain name support.
|
| 196 |
+
|
| 197 |
+
As well as these optional installs:
|
| 198 |
+
|
| 199 |
+
* `h2` - HTTP/2 support. *(Optional, with `httpx2[http2]`)*
|
| 200 |
+
* `socksio` - SOCKS proxy support. *(Optional, with `httpx2[socks]`)*
|
| 201 |
+
* `rich` - Rich terminal support. *(Optional, with `httpx2[cli]`)*
|
| 202 |
+
* `click` - Command line client support. *(Optional, with `httpx2[cli]`)*
|
| 203 |
+
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx2[brotli]`)*
|
| 204 |
+
* `backports.zstd` - Decoding for "zstd" compressed responses on Python 3.13 and below. *(Optional, with `httpx2[zstd]`. On Python 3.14+, `zstd` is supported via the stdlib [`compression.zstd`](https://docs.python.org/3/library/compression.zstd.html) module when it is available, and the `httpx2[zstd]` extra installs nothing.)*
|
| 205 |
+
|
| 206 |
+
A huge amount of credit is due to `requests` for the API layout that
|
| 207 |
+
much of this work follows, as well as to `urllib3` for plenty of design
|
| 208 |
+
inspiration around the lower-level networking details.
|
| 209 |
+
|
| 210 |
+
---
|
| 211 |
+
|
| 212 |
+
<p align="center"><i>HTTPX2 is <a href="https://github.com/pydantic/httpx2/blob/main/LICENSE.md">BSD licensed</a> code.<br/>Designed & crafted with care.</i><br/>— 🦋 —</p>
|
| 213 |
+
|
| 214 |
+
## Release Information
|
| 215 |
+
|
| 216 |
+
## 2.16.0 (October 21st, 2026)
|
| 217 |
+
|
| 218 |
+
### Changed
|
| 219 |
+
|
| 220 |
+
* Look up only the cookie domains that match the request host, and skip the expired cookie sweep until a cookie is due. With 1,000 cookies across 500 domains, building the `Cookie` header is about 18 times faster. Cookies from different domains are now sent from the least to the most specific domain.
|
| 221 |
+
([#1285](https://github.com/pydantic/httpx2/pull/1285))
|
| 222 |
+
|
| 223 |
+
### Fixed
|
| 224 |
+
|
| 225 |
+
* Reuse the client cookie jar instead of copying it on every request and redirect. A custom policy set with `client.cookies.jar.set_policy()` now applies to outgoing requests.
|
| 226 |
+
([#1284](https://github.com/pydantic/httpx2/pull/1284))
|
| 227 |
+
|
| 228 |
+
---
|
| 229 |
+
|
| 230 |
+
[Full changelog](https://github.com/pydantic/httpx2/blob/main/src/httpx2/CHANGELOG.md)
|
hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/RECORD
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
../../../bin/httpx2,sha256=IVmLURpO8FtaxmmWQKsF87pgLAN-RkeJpwronCJIbZ0,182
|
| 2 |
+
httpx2-2.16.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 3 |
+
httpx2-2.16.0.dist-info/METADATA,sha256=GS8gBGIA_1CRGnJ4wh377h4aTG-nlDInr-kjHSje_D8,10019
|
| 4 |
+
httpx2-2.16.0.dist-info/RECORD,,
|
| 5 |
+
httpx2-2.16.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
| 6 |
+
httpx2-2.16.0.dist-info/entry_points.txt,sha256=3uiZorRQlQAFNoHaDxuMsmkdjt3rUTmuBz81tLXFdQY,45
|
| 7 |
+
httpx2-2.16.0.dist-info/licenses/LICENSE.md,sha256=fn1tuvf-Fg05_W6E_QLDPKuWvL6jTTaZjVRbzrShnDA,1589
|
| 8 |
+
httpx2/__init__.py,sha256=BfjyqGaJJhHp5NsWDrQ7GJUjqr1zGXNoqV3vb5eUozo,2661
|
| 9 |
+
httpx2/__pycache__/__init__.cpython-312.pyc,,
|
| 10 |
+
httpx2/__pycache__/__version__.cpython-312.pyc,,
|
| 11 |
+
httpx2/__pycache__/_alias.cpython-312.pyc,,
|
| 12 |
+
httpx2/__pycache__/_api.cpython-312.pyc,,
|
| 13 |
+
httpx2/__pycache__/_auth.cpython-312.pyc,,
|
| 14 |
+
httpx2/__pycache__/_client.cpython-312.pyc,,
|
| 15 |
+
httpx2/__pycache__/_config.cpython-312.pyc,,
|
| 16 |
+
httpx2/__pycache__/_content.cpython-312.pyc,,
|
| 17 |
+
httpx2/__pycache__/_cookies.cpython-312.pyc,,
|
| 18 |
+
httpx2/__pycache__/_decoders.cpython-312.pyc,,
|
| 19 |
+
httpx2/__pycache__/_exceptions.cpython-312.pyc,,
|
| 20 |
+
httpx2/__pycache__/_main.cpython-312.pyc,,
|
| 21 |
+
httpx2/__pycache__/_models.cpython-312.pyc,,
|
| 22 |
+
httpx2/__pycache__/_multipart.cpython-312.pyc,,
|
| 23 |
+
httpx2/__pycache__/_sse.cpython-312.pyc,,
|
| 24 |
+
httpx2/__pycache__/_status_codes.cpython-312.pyc,,
|
| 25 |
+
httpx2/__pycache__/_types.cpython-312.pyc,,
|
| 26 |
+
httpx2/__pycache__/_urlparse.cpython-312.pyc,,
|
| 27 |
+
httpx2/__pycache__/_urls.cpython-312.pyc,,
|
| 28 |
+
httpx2/__pycache__/_utils.cpython-312.pyc,,
|
| 29 |
+
httpx2/__version__.py,sha256=XWKDNx4TZNJGkuO6vv16RvfV8lhdgdMIbQ8Bt0cR4UQ,158
|
| 30 |
+
httpx2/_alias.py,sha256=Evn6S3BWugzd5XgDAYOsQf45_tAQpgae4JJ6U6n02Bw,2431
|
| 31 |
+
httpx2/_api.py,sha256=IhvMLaySB7-pEER2l_qCbSDjTmG28hVPXIJJMdiHze0,14794
|
| 32 |
+
httpx2/_auth.py,sha256=VUO3zJfK3GGOp3dnQqYrAu6Lkip-DpK2mutFPmVbjjA,11969
|
| 33 |
+
httpx2/_client.py,sha256=pDgPmEKjiJZDBhhnPM72ZIalzb1mU5h9WBuGnHLfm1M,75349
|
| 34 |
+
httpx2/_config.py,sha256=DAJBDxmJX4GmlCqtNE1L1dgEr5sBmo8tvLcn48R2oTA,7438
|
| 35 |
+
httpx2/_content.py,sha256=sNp9IVai11gXO_4P5Uka8XqrT_A8Ab5xkgRlKD4xeY0,8490
|
| 36 |
+
httpx2/_cookies.py,sha256=lMivxsZyHM5zBJHim6WVPJ5QBKeAW79pHh5jDtTl5_0,2965
|
| 37 |
+
httpx2/_decoders.py,sha256=C479GSHULT-xFz3Rr5TwZ0uUn2ih_afezn7QcgoIfAA,15046
|
| 38 |
+
httpx2/_exceptions.py,sha256=69TPZgHvTkkjV1P0y5DSVOmDkxwCRsKhCfhd_qTTaVc,8857
|
| 39 |
+
httpx2/_main.py,sha256=JT_VdROzHpKs8DNBIvXZXF2jYr6jxo0NVyk1FK6ff5A,15434
|
| 40 |
+
httpx2/_models.py,sha256=INk4VXTudl3jGI34fJJKknUmfF_dwzNuqerCiPo9T6k,45856
|
| 41 |
+
httpx2/_multipart.py,sha256=ksgIJarALvO7EcgDmc40GX6GyVO6XG0faszehXdRaTg,10018
|
| 42 |
+
httpx2/_sse.py,sha256=IZ1Lf6NrgoZa7FP5-n_VhKhk0eEYFkPZtOC8g5JpQCk,7552
|
| 43 |
+
httpx2/_status_codes.py,sha256=0CWpuD1dw8PYrt9f7epZ0bZZQEnmqe-z6FK_IzIeh14,6625
|
| 44 |
+
httpx2/_transports/__init__.py,sha256=e65w9X4RMK9O_0Q6kPqpXr1ijeEf6b9xbl4KfrAYDCI,711
|
| 45 |
+
httpx2/_transports/__pycache__/__init__.cpython-312.pyc,,
|
| 46 |
+
httpx2/_transports/__pycache__/asgi.cpython-312.pyc,,
|
| 47 |
+
httpx2/_transports/__pycache__/base.cpython-312.pyc,,
|
| 48 |
+
httpx2/_transports/__pycache__/default.cpython-312.pyc,,
|
| 49 |
+
httpx2/_transports/__pycache__/mock.cpython-312.pyc,,
|
| 50 |
+
httpx2/_transports/__pycache__/wsgi.cpython-312.pyc,,
|
| 51 |
+
httpx2/_transports/asgi.py,sha256=PWe4gtw8l2NqK5VusB4uprs6l46fJ4ewLX8FUEOozuk,5429
|
| 52 |
+
httpx2/_transports/base.py,sha256=KY5zBVbdG6ge2sE-j0ZKyJ1C1LWBZVHQnOg0BdQ6idE,2584
|
| 53 |
+
httpx2/_transports/default.py,sha256=j8Q6KILUca_3UTZqfyjpHF4xgxpLKao7tOZpMaIUjpc,13913
|
| 54 |
+
httpx2/_transports/mock.py,sha256=3LZowN_YXbXezA2P4D4g29bCemTiT5Gr8U_vBvxtgxs,1209
|
| 55 |
+
httpx2/_transports/wsgi.py,sha256=R5UApE_xXvl3BwKMgokN5irXiXGRFbOrtTdiKXFEBkI,4904
|
| 56 |
+
httpx2/_types.py,sha256=pVETNjBR2OoWs9-sY7tRTAQM8sZx8-kNwZ4DjZcEYZU,2671
|
| 57 |
+
httpx2/_urlparse.py,sha256=X-_rLDsC9QMIGc1FXr9DVRTYsLLB_SOdaCOtF2-DRIA,18323
|
| 58 |
+
httpx2/_urls.py,sha256=t2wZOCuWtmP8vHnwq2hLhLZ_g2Y36K3SMpU0u_3J8CY,23184
|
| 59 |
+
httpx2/_utils.py,sha256=-2nvGVPvYfJxZXPU-RbQXxGazILdRp5kJUJK98mF5R4,10524
|
| 60 |
+
httpx2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 61 |
+
httpx2/websockets/__init__.py,sha256=K66d2Zczl0Ewa0ey0Q2ETx5utzVH6n5G6J22mR3GbV0,1826
|
| 62 |
+
httpx2/websockets/__pycache__/__init__.cpython-312.pyc,,
|
| 63 |
+
httpx2/websockets/__pycache__/_api.cpython-312.pyc,,
|
| 64 |
+
httpx2/websockets/__pycache__/_exceptions.cpython-312.pyc,,
|
| 65 |
+
httpx2/websockets/__pycache__/_ping.cpython-312.pyc,,
|
| 66 |
+
httpx2/websockets/__pycache__/_transport.cpython-312.pyc,,
|
| 67 |
+
httpx2/websockets/_api.py,sha256=pNdsYOmhqeq5k8_DrtXK-w5QDAcKEq1FNwgBWF3j0Gc,63283
|
| 68 |
+
httpx2/websockets/_exceptions.py,sha256=bpnBX9fDdkWh9jKWqqXAcQ_jsIPDRSuzUexxz_RPzqA,1290
|
| 69 |
+
httpx2/websockets/_ping.py,sha256=M6Xmbap01-ldl1ejejmkjJbBgK1ulBj-_oaUoKciqrM,1258
|
| 70 |
+
httpx2/websockets/_transport.py,sha256=pGtJcgy58Vi9246bj56yr-80qqpfUk6FapMFd8zw2Ls,11029
|
hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: hatchling 1.32.4
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/entry_points.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[console_scripts]
|
| 2 |
+
httpx2 = httpx2._main:main
|
hug/lib/python3.12/site-packages/httpx2-2.16.0.dist-info/licenses/LICENSE.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Copyright © 2026 to present Pydantic Services Inc. and individual contributors.
|
| 2 |
+
Copyright © 2019, [Encode OSS Ltd](https://www.encode.io/).
|
| 3 |
+
All rights reserved.
|
| 4 |
+
|
| 5 |
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
| 6 |
+
|
| 7 |
+
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
| 8 |
+
|
| 9 |
+
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
| 10 |
+
|
| 11 |
+
* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
| 12 |
+
|
| 13 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
hug/lib/python3.12/site-packages/httpx2/__init__.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .__version__ import __description__, __title__, __version__
|
| 2 |
+
from ._alias import *
|
| 3 |
+
from ._api import *
|
| 4 |
+
from ._auth import *
|
| 5 |
+
from ._client import *
|
| 6 |
+
from ._config import *
|
| 7 |
+
from ._content import *
|
| 8 |
+
from ._exceptions import *
|
| 9 |
+
from ._models import *
|
| 10 |
+
from ._sse import *
|
| 11 |
+
from ._status_codes import *
|
| 12 |
+
from ._transports import *
|
| 13 |
+
from ._types import *
|
| 14 |
+
from ._urls import *
|
| 15 |
+
|
| 16 |
+
__all__ = [
|
| 17 |
+
# __version__.py
|
| 18 |
+
"__description__",
|
| 19 |
+
"__title__",
|
| 20 |
+
"__version__",
|
| 21 |
+
# _alias.py
|
| 22 |
+
"alias_httpx",
|
| 23 |
+
# _api.py
|
| 24 |
+
"delete",
|
| 25 |
+
"get",
|
| 26 |
+
"head",
|
| 27 |
+
"options",
|
| 28 |
+
"patch",
|
| 29 |
+
"post",
|
| 30 |
+
"put",
|
| 31 |
+
"query",
|
| 32 |
+
"request",
|
| 33 |
+
"stream",
|
| 34 |
+
"websocket",
|
| 35 |
+
# _auth.py
|
| 36 |
+
"Auth",
|
| 37 |
+
"BasicAuth",
|
| 38 |
+
"DigestAuth",
|
| 39 |
+
"FunctionAuth",
|
| 40 |
+
"NetRCAuth",
|
| 41 |
+
# _client.py
|
| 42 |
+
"AsyncClient",
|
| 43 |
+
"Client",
|
| 44 |
+
"USE_CLIENT_DEFAULT",
|
| 45 |
+
# _config.py
|
| 46 |
+
"create_ssl_context",
|
| 47 |
+
"Limits",
|
| 48 |
+
"Proxy",
|
| 49 |
+
"Timeout",
|
| 50 |
+
# _content.py
|
| 51 |
+
"ByteStream",
|
| 52 |
+
# _exceptions.py
|
| 53 |
+
"CloseError",
|
| 54 |
+
"ConnectError",
|
| 55 |
+
"ConnectTimeout",
|
| 56 |
+
"CookieConflict",
|
| 57 |
+
"DecodingError",
|
| 58 |
+
"HTTPError",
|
| 59 |
+
"HTTPStatusError",
|
| 60 |
+
"InvalidURL",
|
| 61 |
+
"LocalProtocolError",
|
| 62 |
+
"NetworkError",
|
| 63 |
+
"PoolTimeout",
|
| 64 |
+
"ProtocolError",
|
| 65 |
+
"ProxyError",
|
| 66 |
+
"ReadError",
|
| 67 |
+
"ReadTimeout",
|
| 68 |
+
"RemoteProtocolError",
|
| 69 |
+
"RequestError",
|
| 70 |
+
"RequestNotRead",
|
| 71 |
+
"ResponseNotRead",
|
| 72 |
+
"StreamClosed",
|
| 73 |
+
"StreamConsumed",
|
| 74 |
+
"StreamError",
|
| 75 |
+
"TimeoutException",
|
| 76 |
+
"TooManyRedirects",
|
| 77 |
+
"TransportError",
|
| 78 |
+
"UnsupportedProtocol",
|
| 79 |
+
"WriteError",
|
| 80 |
+
"WriteTimeout",
|
| 81 |
+
# _models.py
|
| 82 |
+
"Cookies",
|
| 83 |
+
"Headers",
|
| 84 |
+
"Request",
|
| 85 |
+
"Response",
|
| 86 |
+
# _sse.py
|
| 87 |
+
"EventSource",
|
| 88 |
+
"ServerSentEvent",
|
| 89 |
+
"SSEError",
|
| 90 |
+
# _status_codes.py
|
| 91 |
+
"codes",
|
| 92 |
+
# _transports
|
| 93 |
+
"ASGITransport",
|
| 94 |
+
"AsyncBaseTransport",
|
| 95 |
+
"AsyncHTTPTransport",
|
| 96 |
+
"BaseTransport",
|
| 97 |
+
"HTTPTransport",
|
| 98 |
+
"MockTransport",
|
| 99 |
+
"WSGITransport",
|
| 100 |
+
# _types.py
|
| 101 |
+
"AsyncByteStream",
|
| 102 |
+
"SyncByteStream",
|
| 103 |
+
# _urls.py
|
| 104 |
+
"Origin",
|
| 105 |
+
"QueryParams",
|
| 106 |
+
"URL",
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
__locals = locals()
|
| 111 |
+
for __name in __all__:
|
| 112 |
+
if not __name.startswith("__"):
|
| 113 |
+
setattr(__locals[__name], "__module__", "httpx2") # noqa
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def __getattr__(name: str) -> object: # pragma: no cover
|
| 117 |
+
if name == "main":
|
| 118 |
+
import warnings
|
| 119 |
+
|
| 120 |
+
warnings.warn(
|
| 121 |
+
"`httpx2.main` is deprecated and will be removed in a future release. "
|
| 122 |
+
"Use the `httpx2` CLI entry point instead.",
|
| 123 |
+
DeprecationWarning,
|
| 124 |
+
stacklevel=2,
|
| 125 |
+
)
|
| 126 |
+
from ._main import main
|
| 127 |
+
|
| 128 |
+
return main
|
| 129 |
+
|
| 130 |
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (2.36 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/__version__.cpython-312.pyc
ADDED
|
Binary file (368 Bytes). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_alias.cpython-312.pyc
ADDED
|
Binary file (4.55 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_api.cpython-312.pyc
ADDED
|
Binary file (12.7 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_auth.cpython-312.pyc
ADDED
|
Binary file (15.5 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_client.cpython-312.pyc
ADDED
|
Binary file (71.2 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_config.cpython-312.pyc
ADDED
|
Binary file (9.7 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_content.cpython-312.pyc
ADDED
|
Binary file (10.8 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_cookies.cpython-312.pyc
ADDED
|
Binary file (4.86 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_decoders.cpython-312.pyc
ADDED
|
Binary file (20.8 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_exceptions.cpython-312.pyc
ADDED
|
Binary file (12.6 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_main.cpython-312.pyc
ADDED
|
Binary file (20.6 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_models.cpython-312.pyc
ADDED
|
Binary file (61.6 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_multipart.cpython-312.pyc
ADDED
|
Binary file (14 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_sse.cpython-312.pyc
ADDED
|
Binary file (12.4 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_status_codes.cpython-312.pyc
ADDED
|
Binary file (8.14 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_types.cpython-312.pyc
ADDED
|
Binary file (3.75 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_urlparse.cpython-312.pyc
ADDED
|
Binary file (17.4 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_urls.cpython-312.pyc
ADDED
|
Binary file (30.3 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__pycache__/_utils.cpython-312.pyc
ADDED
|
Binary file (11.8 kB). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/__version__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from importlib.metadata import version
|
| 2 |
+
|
| 3 |
+
__title__ = "httpx2"
|
| 4 |
+
__description__ = "A next generation HTTP client, for Python 3."
|
| 5 |
+
__version__ = version("httpx2")
|
hug/lib/python3.12/site-packages/httpx2/_alias.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import importlib
|
| 4 |
+
import importlib.abc
|
| 5 |
+
import importlib.machinery
|
| 6 |
+
import importlib.util
|
| 7 |
+
import sys
|
| 8 |
+
from collections.abc import Sequence
|
| 9 |
+
from types import ModuleType
|
| 10 |
+
|
| 11 |
+
__all__ = ["alias_httpx"]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class _AliasLoader(importlib.abc.Loader):
|
| 15 |
+
_original_spec: importlib.machinery.ModuleSpec | None
|
| 16 |
+
|
| 17 |
+
def __init__(self, real_name: str) -> None:
|
| 18 |
+
self._real_name = real_name
|
| 19 |
+
|
| 20 |
+
def create_module(self, spec: importlib.machinery.ModuleSpec) -> ModuleType:
|
| 21 |
+
module = importlib.import_module(self._real_name)
|
| 22 |
+
self._original_spec = module.__spec__
|
| 23 |
+
return module
|
| 24 |
+
|
| 25 |
+
def exec_module(self, module: ModuleType) -> None:
|
| 26 |
+
module.__spec__ = self._original_spec
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class _AliasFinder(importlib.abc.MetaPathFinder):
|
| 30 |
+
def __init__(self, alias: str, real: str) -> None:
|
| 31 |
+
self._alias = alias
|
| 32 |
+
self._real = real
|
| 33 |
+
|
| 34 |
+
def find_spec(
|
| 35 |
+
self,
|
| 36 |
+
fullname: str,
|
| 37 |
+
path: Sequence[str] | None = None,
|
| 38 |
+
target: ModuleType | None = None,
|
| 39 |
+
) -> importlib.machinery.ModuleSpec | None:
|
| 40 |
+
if fullname != self._alias and not fullname.startswith(self._alias + "."):
|
| 41 |
+
return None
|
| 42 |
+
real_name = self._real + fullname.removeprefix(self._alias)
|
| 43 |
+
if real_name not in sys.modules and importlib.util.find_spec(real_name) is None:
|
| 44 |
+
return None
|
| 45 |
+
return importlib.machinery.ModuleSpec(fullname, _AliasLoader(real_name))
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def _alias(alias: str, module: ModuleType) -> None:
|
| 49 |
+
existing = sys.modules.get(alias)
|
| 50 |
+
if existing is not None and existing is not module:
|
| 51 |
+
raise RuntimeError(f"{alias} was already imported; call `alias_httpx()` before any `import {alias}`.")
|
| 52 |
+
|
| 53 |
+
if not any(isinstance(finder, _AliasFinder) and finder._alias == alias for finder in sys.meta_path):
|
| 54 |
+
sys.meta_path.insert(0, _AliasFinder(alias, module.__name__))
|
| 55 |
+
sys.modules[alias] = module
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def alias_httpx() -> None:
|
| 59 |
+
"""
|
| 60 |
+
Make `import httpx` resolve to `httpx2`, and `import httpcore` to `httpcore2`, process-wide.
|
| 61 |
+
|
| 62 |
+
Intended for applications migrating from `httpx`, so that dependencies still
|
| 63 |
+
importing `httpx` or `httpcore` share the `httpx2` classes. Libraries should never call this.
|
| 64 |
+
|
| 65 |
+
Must be called before anything imports `httpx` or `httpcore`. Calling it again is a no-op.
|
| 66 |
+
"""
|
| 67 |
+
import httpcore2
|
| 68 |
+
import httpx2
|
| 69 |
+
|
| 70 |
+
_alias("httpx", httpx2)
|
| 71 |
+
_alias("httpcore", httpcore2)
|
hug/lib/python3.12/site-packages/httpx2/_api.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import typing
|
| 4 |
+
from collections.abc import Generator
|
| 5 |
+
from contextlib import contextmanager
|
| 6 |
+
|
| 7 |
+
from ._client import Client
|
| 8 |
+
from ._config import (
|
| 9 |
+
DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
|
| 10 |
+
DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
|
| 11 |
+
DEFAULT_MAX_MESSAGE_SIZE_BYTES,
|
| 12 |
+
DEFAULT_QUEUE_SIZE,
|
| 13 |
+
DEFAULT_TIMEOUT_CONFIG,
|
| 14 |
+
)
|
| 15 |
+
from ._models import Response
|
| 16 |
+
from ._types import (
|
| 17 |
+
AuthTypes,
|
| 18 |
+
CookieTypes,
|
| 19 |
+
HeaderTypes,
|
| 20 |
+
ProxyTypes,
|
| 21 |
+
QueryParamTypes,
|
| 22 |
+
RequestContent,
|
| 23 |
+
RequestData,
|
| 24 |
+
RequestFiles,
|
| 25 |
+
TimeoutTypes,
|
| 26 |
+
)
|
| 27 |
+
from ._urls import URL
|
| 28 |
+
|
| 29 |
+
if typing.TYPE_CHECKING:
|
| 30 |
+
import ssl # pragma: no cover
|
| 31 |
+
|
| 32 |
+
from .websockets._api import WebSocketSession
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
__all__ = [
|
| 36 |
+
"delete",
|
| 37 |
+
"get",
|
| 38 |
+
"head",
|
| 39 |
+
"options",
|
| 40 |
+
"patch",
|
| 41 |
+
"post",
|
| 42 |
+
"put",
|
| 43 |
+
"query",
|
| 44 |
+
"request",
|
| 45 |
+
"stream",
|
| 46 |
+
"websocket",
|
| 47 |
+
]
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def request(
|
| 51 |
+
method: str,
|
| 52 |
+
url: URL | str,
|
| 53 |
+
*,
|
| 54 |
+
params: QueryParamTypes | None = None,
|
| 55 |
+
content: RequestContent | None = None,
|
| 56 |
+
data: RequestData | None = None,
|
| 57 |
+
files: RequestFiles | None = None,
|
| 58 |
+
json: typing.Any | None = None,
|
| 59 |
+
headers: HeaderTypes | None = None,
|
| 60 |
+
cookies: CookieTypes | None = None,
|
| 61 |
+
auth: AuthTypes | None = None,
|
| 62 |
+
proxy: ProxyTypes | None = None,
|
| 63 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 64 |
+
follow_redirects: bool = False,
|
| 65 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 66 |
+
trust_env: bool = True,
|
| 67 |
+
) -> Response:
|
| 68 |
+
"""Sends an HTTP request.
|
| 69 |
+
|
| 70 |
+
Parameters:
|
| 71 |
+
method: HTTP method for the new `Request` object: `GET`, `OPTIONS`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`,
|
| 72 |
+
or `QUERY`.
|
| 73 |
+
url: URL for the new `Request` object.
|
| 74 |
+
params: *(optional)* Query parameters to include in the URL, as a string, dictionary, or sequence of two-tuples.
|
| 75 |
+
content: *(optional)* Binary content to include in the body of the request, as bytes or a byte iterator.
|
| 76 |
+
data: *(optional)* Form data to include in the body of the request, as a dictionary.
|
| 77 |
+
files: *(optional)* A dictionary of upload files to include in the body of the request.
|
| 78 |
+
json: *(optional)* A JSON serializable object to include in the body of the request.
|
| 79 |
+
headers: *(optional)* Dictionary of HTTP headers to include in the request.
|
| 80 |
+
cookies: *(optional)* Dictionary of Cookie items to include in the request.
|
| 81 |
+
auth: *(optional)* An authentication class to use when sending the request.
|
| 82 |
+
proxy: *(optional)* A proxy URL where all the traffic should be routed.
|
| 83 |
+
timeout: *(optional)* The timeout configuration to use when sending the request.
|
| 84 |
+
follow_redirects: *(optional)* Enables or disables HTTP redirects.
|
| 85 |
+
verify: *(optional)* Either `True` to use an SSL context with the default CA bundle, `False` to disable
|
| 86 |
+
verification, or an instance of `ssl.SSLContext` to use a custom context.
|
| 87 |
+
trust_env: *(optional)* Enables or disables usage of environment variables for configuration.
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
The `Response` object.
|
| 91 |
+
|
| 92 |
+
Usage:
|
| 93 |
+
|
| 94 |
+
```
|
| 95 |
+
>>> import httpx2
|
| 96 |
+
>>> response = httpx2.request('GET', 'https://httpbin.org/get')
|
| 97 |
+
>>> response
|
| 98 |
+
<Response [200 OK]>
|
| 99 |
+
```
|
| 100 |
+
"""
|
| 101 |
+
with Client(
|
| 102 |
+
cookies=cookies,
|
| 103 |
+
proxy=proxy,
|
| 104 |
+
verify=verify,
|
| 105 |
+
timeout=timeout,
|
| 106 |
+
trust_env=trust_env,
|
| 107 |
+
) as client:
|
| 108 |
+
return client.request(
|
| 109 |
+
method=method,
|
| 110 |
+
url=url,
|
| 111 |
+
content=content,
|
| 112 |
+
data=data,
|
| 113 |
+
files=files,
|
| 114 |
+
json=json,
|
| 115 |
+
params=params,
|
| 116 |
+
headers=headers,
|
| 117 |
+
auth=auth,
|
| 118 |
+
follow_redirects=follow_redirects,
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
@contextmanager
|
| 123 |
+
def stream(
|
| 124 |
+
method: str,
|
| 125 |
+
url: URL | str,
|
| 126 |
+
*,
|
| 127 |
+
params: QueryParamTypes | None = None,
|
| 128 |
+
content: RequestContent | None = None,
|
| 129 |
+
data: RequestData | None = None,
|
| 130 |
+
files: RequestFiles | None = None,
|
| 131 |
+
json: typing.Any | None = None,
|
| 132 |
+
headers: HeaderTypes | None = None,
|
| 133 |
+
cookies: CookieTypes | None = None,
|
| 134 |
+
auth: AuthTypes | None = None,
|
| 135 |
+
proxy: ProxyTypes | None = None,
|
| 136 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 137 |
+
follow_redirects: bool = False,
|
| 138 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 139 |
+
trust_env: bool = True,
|
| 140 |
+
) -> Generator[Response]:
|
| 141 |
+
"""
|
| 142 |
+
Alternative to `httpx2.request()` that streams the response body
|
| 143 |
+
instead of loading it into memory at once.
|
| 144 |
+
|
| 145 |
+
**Parameters**: See `httpx2.request`.
|
| 146 |
+
|
| 147 |
+
See also: [Streaming Responses][0]
|
| 148 |
+
|
| 149 |
+
[0]: quickstart.md#streaming-responses
|
| 150 |
+
"""
|
| 151 |
+
with Client(
|
| 152 |
+
cookies=cookies,
|
| 153 |
+
proxy=proxy,
|
| 154 |
+
verify=verify,
|
| 155 |
+
timeout=timeout,
|
| 156 |
+
trust_env=trust_env,
|
| 157 |
+
) as client:
|
| 158 |
+
with client.stream(
|
| 159 |
+
method=method,
|
| 160 |
+
url=url,
|
| 161 |
+
content=content,
|
| 162 |
+
data=data,
|
| 163 |
+
files=files,
|
| 164 |
+
json=json,
|
| 165 |
+
params=params,
|
| 166 |
+
headers=headers,
|
| 167 |
+
auth=auth,
|
| 168 |
+
follow_redirects=follow_redirects,
|
| 169 |
+
) as response:
|
| 170 |
+
yield response
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def get(
|
| 174 |
+
url: URL | str,
|
| 175 |
+
*,
|
| 176 |
+
params: QueryParamTypes | None = None,
|
| 177 |
+
headers: HeaderTypes | None = None,
|
| 178 |
+
cookies: CookieTypes | None = None,
|
| 179 |
+
auth: AuthTypes | None = None,
|
| 180 |
+
proxy: ProxyTypes | None = None,
|
| 181 |
+
follow_redirects: bool = False,
|
| 182 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 183 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 184 |
+
trust_env: bool = True,
|
| 185 |
+
) -> Response:
|
| 186 |
+
"""
|
| 187 |
+
Sends a `GET` request.
|
| 188 |
+
|
| 189 |
+
**Parameters**: See `httpx2.request`.
|
| 190 |
+
|
| 191 |
+
Note that the `data`, `files`, `json` and `content` parameters are not available
|
| 192 |
+
on this function, as `GET` requests should not include a request body.
|
| 193 |
+
"""
|
| 194 |
+
return request(
|
| 195 |
+
"GET",
|
| 196 |
+
url,
|
| 197 |
+
params=params,
|
| 198 |
+
headers=headers,
|
| 199 |
+
cookies=cookies,
|
| 200 |
+
auth=auth,
|
| 201 |
+
proxy=proxy,
|
| 202 |
+
follow_redirects=follow_redirects,
|
| 203 |
+
verify=verify,
|
| 204 |
+
timeout=timeout,
|
| 205 |
+
trust_env=trust_env,
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def options(
|
| 210 |
+
url: URL | str,
|
| 211 |
+
*,
|
| 212 |
+
params: QueryParamTypes | None = None,
|
| 213 |
+
headers: HeaderTypes | None = None,
|
| 214 |
+
cookies: CookieTypes | None = None,
|
| 215 |
+
auth: AuthTypes | None = None,
|
| 216 |
+
proxy: ProxyTypes | None = None,
|
| 217 |
+
follow_redirects: bool = False,
|
| 218 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 219 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 220 |
+
trust_env: bool = True,
|
| 221 |
+
) -> Response:
|
| 222 |
+
"""
|
| 223 |
+
Sends an `OPTIONS` request.
|
| 224 |
+
|
| 225 |
+
**Parameters**: See `httpx2.request`.
|
| 226 |
+
|
| 227 |
+
Note that the `data`, `files`, `json` and `content` parameters are not available
|
| 228 |
+
on this function, as `OPTIONS` requests should not include a request body.
|
| 229 |
+
"""
|
| 230 |
+
return request(
|
| 231 |
+
"OPTIONS",
|
| 232 |
+
url,
|
| 233 |
+
params=params,
|
| 234 |
+
headers=headers,
|
| 235 |
+
cookies=cookies,
|
| 236 |
+
auth=auth,
|
| 237 |
+
proxy=proxy,
|
| 238 |
+
follow_redirects=follow_redirects,
|
| 239 |
+
verify=verify,
|
| 240 |
+
timeout=timeout,
|
| 241 |
+
trust_env=trust_env,
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def head(
|
| 246 |
+
url: URL | str,
|
| 247 |
+
*,
|
| 248 |
+
params: QueryParamTypes | None = None,
|
| 249 |
+
headers: HeaderTypes | None = None,
|
| 250 |
+
cookies: CookieTypes | None = None,
|
| 251 |
+
auth: AuthTypes | None = None,
|
| 252 |
+
proxy: ProxyTypes | None = None,
|
| 253 |
+
follow_redirects: bool = False,
|
| 254 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 255 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 256 |
+
trust_env: bool = True,
|
| 257 |
+
) -> Response:
|
| 258 |
+
"""
|
| 259 |
+
Sends a `HEAD` request.
|
| 260 |
+
|
| 261 |
+
**Parameters**: See `httpx2.request`.
|
| 262 |
+
|
| 263 |
+
Note that the `data`, `files`, `json` and `content` parameters are not available
|
| 264 |
+
on this function, as `HEAD` requests should not include a request body.
|
| 265 |
+
"""
|
| 266 |
+
return request(
|
| 267 |
+
"HEAD",
|
| 268 |
+
url,
|
| 269 |
+
params=params,
|
| 270 |
+
headers=headers,
|
| 271 |
+
cookies=cookies,
|
| 272 |
+
auth=auth,
|
| 273 |
+
proxy=proxy,
|
| 274 |
+
follow_redirects=follow_redirects,
|
| 275 |
+
verify=verify,
|
| 276 |
+
timeout=timeout,
|
| 277 |
+
trust_env=trust_env,
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def post(
|
| 282 |
+
url: URL | str,
|
| 283 |
+
*,
|
| 284 |
+
content: RequestContent | None = None,
|
| 285 |
+
data: RequestData | None = None,
|
| 286 |
+
files: RequestFiles | None = None,
|
| 287 |
+
json: typing.Any | None = None,
|
| 288 |
+
params: QueryParamTypes | None = None,
|
| 289 |
+
headers: HeaderTypes | None = None,
|
| 290 |
+
cookies: CookieTypes | None = None,
|
| 291 |
+
auth: AuthTypes | None = None,
|
| 292 |
+
proxy: ProxyTypes | None = None,
|
| 293 |
+
follow_redirects: bool = False,
|
| 294 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 295 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 296 |
+
trust_env: bool = True,
|
| 297 |
+
) -> Response:
|
| 298 |
+
"""
|
| 299 |
+
Sends a `POST` request.
|
| 300 |
+
|
| 301 |
+
**Parameters**: See `httpx2.request`.
|
| 302 |
+
"""
|
| 303 |
+
return request(
|
| 304 |
+
"POST",
|
| 305 |
+
url,
|
| 306 |
+
content=content,
|
| 307 |
+
data=data,
|
| 308 |
+
files=files,
|
| 309 |
+
json=json,
|
| 310 |
+
params=params,
|
| 311 |
+
headers=headers,
|
| 312 |
+
cookies=cookies,
|
| 313 |
+
auth=auth,
|
| 314 |
+
proxy=proxy,
|
| 315 |
+
follow_redirects=follow_redirects,
|
| 316 |
+
verify=verify,
|
| 317 |
+
timeout=timeout,
|
| 318 |
+
trust_env=trust_env,
|
| 319 |
+
)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def put(
|
| 323 |
+
url: URL | str,
|
| 324 |
+
*,
|
| 325 |
+
content: RequestContent | None = None,
|
| 326 |
+
data: RequestData | None = None,
|
| 327 |
+
files: RequestFiles | None = None,
|
| 328 |
+
json: typing.Any | None = None,
|
| 329 |
+
params: QueryParamTypes | None = None,
|
| 330 |
+
headers: HeaderTypes | None = None,
|
| 331 |
+
cookies: CookieTypes | None = None,
|
| 332 |
+
auth: AuthTypes | None = None,
|
| 333 |
+
proxy: ProxyTypes | None = None,
|
| 334 |
+
follow_redirects: bool = False,
|
| 335 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 336 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 337 |
+
trust_env: bool = True,
|
| 338 |
+
) -> Response:
|
| 339 |
+
"""
|
| 340 |
+
Sends a `PUT` request.
|
| 341 |
+
|
| 342 |
+
**Parameters**: See `httpx2.request`.
|
| 343 |
+
"""
|
| 344 |
+
return request(
|
| 345 |
+
"PUT",
|
| 346 |
+
url,
|
| 347 |
+
content=content,
|
| 348 |
+
data=data,
|
| 349 |
+
files=files,
|
| 350 |
+
json=json,
|
| 351 |
+
params=params,
|
| 352 |
+
headers=headers,
|
| 353 |
+
cookies=cookies,
|
| 354 |
+
auth=auth,
|
| 355 |
+
proxy=proxy,
|
| 356 |
+
follow_redirects=follow_redirects,
|
| 357 |
+
verify=verify,
|
| 358 |
+
timeout=timeout,
|
| 359 |
+
trust_env=trust_env,
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
def patch(
|
| 364 |
+
url: URL | str,
|
| 365 |
+
*,
|
| 366 |
+
content: RequestContent | None = None,
|
| 367 |
+
data: RequestData | None = None,
|
| 368 |
+
files: RequestFiles | None = None,
|
| 369 |
+
json: typing.Any | None = None,
|
| 370 |
+
params: QueryParamTypes | None = None,
|
| 371 |
+
headers: HeaderTypes | None = None,
|
| 372 |
+
cookies: CookieTypes | None = None,
|
| 373 |
+
auth: AuthTypes | None = None,
|
| 374 |
+
proxy: ProxyTypes | None = None,
|
| 375 |
+
follow_redirects: bool = False,
|
| 376 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 377 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 378 |
+
trust_env: bool = True,
|
| 379 |
+
) -> Response:
|
| 380 |
+
"""
|
| 381 |
+
Sends a `PATCH` request.
|
| 382 |
+
|
| 383 |
+
**Parameters**: See `httpx2.request`.
|
| 384 |
+
"""
|
| 385 |
+
return request(
|
| 386 |
+
"PATCH",
|
| 387 |
+
url,
|
| 388 |
+
content=content,
|
| 389 |
+
data=data,
|
| 390 |
+
files=files,
|
| 391 |
+
json=json,
|
| 392 |
+
params=params,
|
| 393 |
+
headers=headers,
|
| 394 |
+
cookies=cookies,
|
| 395 |
+
auth=auth,
|
| 396 |
+
proxy=proxy,
|
| 397 |
+
follow_redirects=follow_redirects,
|
| 398 |
+
verify=verify,
|
| 399 |
+
timeout=timeout,
|
| 400 |
+
trust_env=trust_env,
|
| 401 |
+
)
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def delete(
|
| 405 |
+
url: URL | str,
|
| 406 |
+
*,
|
| 407 |
+
params: QueryParamTypes | None = None,
|
| 408 |
+
headers: HeaderTypes | None = None,
|
| 409 |
+
cookies: CookieTypes | None = None,
|
| 410 |
+
auth: AuthTypes | None = None,
|
| 411 |
+
proxy: ProxyTypes | None = None,
|
| 412 |
+
follow_redirects: bool = False,
|
| 413 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 414 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 415 |
+
trust_env: bool = True,
|
| 416 |
+
) -> Response:
|
| 417 |
+
"""
|
| 418 |
+
Sends a `DELETE` request.
|
| 419 |
+
|
| 420 |
+
**Parameters**: See `httpx2.request`.
|
| 421 |
+
|
| 422 |
+
Note that the `data`, `files`, `json` and `content` parameters are not available
|
| 423 |
+
on this function, as `DELETE` requests should not include a request body.
|
| 424 |
+
"""
|
| 425 |
+
return request(
|
| 426 |
+
"DELETE",
|
| 427 |
+
url,
|
| 428 |
+
params=params,
|
| 429 |
+
headers=headers,
|
| 430 |
+
cookies=cookies,
|
| 431 |
+
auth=auth,
|
| 432 |
+
proxy=proxy,
|
| 433 |
+
follow_redirects=follow_redirects,
|
| 434 |
+
verify=verify,
|
| 435 |
+
timeout=timeout,
|
| 436 |
+
trust_env=trust_env,
|
| 437 |
+
)
|
| 438 |
+
|
| 439 |
+
|
| 440 |
+
def query(
|
| 441 |
+
url: URL | str,
|
| 442 |
+
*,
|
| 443 |
+
content: RequestContent | None = None,
|
| 444 |
+
data: RequestData | None = None,
|
| 445 |
+
files: RequestFiles | None = None,
|
| 446 |
+
json: typing.Any | None = None,
|
| 447 |
+
params: QueryParamTypes | None = None,
|
| 448 |
+
headers: HeaderTypes | None = None,
|
| 449 |
+
cookies: CookieTypes | None = None,
|
| 450 |
+
auth: AuthTypes | None = None,
|
| 451 |
+
proxy: ProxyTypes | None = None,
|
| 452 |
+
follow_redirects: bool = False,
|
| 453 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 454 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 455 |
+
trust_env: bool = True,
|
| 456 |
+
) -> Response:
|
| 457 |
+
"""
|
| 458 |
+
Sends a `QUERY` request.
|
| 459 |
+
|
| 460 |
+
**Parameters**: See `httpx2.request`.
|
| 461 |
+
"""
|
| 462 |
+
return request(
|
| 463 |
+
"QUERY",
|
| 464 |
+
url,
|
| 465 |
+
content=content,
|
| 466 |
+
data=data,
|
| 467 |
+
files=files,
|
| 468 |
+
json=json,
|
| 469 |
+
params=params,
|
| 470 |
+
headers=headers,
|
| 471 |
+
cookies=cookies,
|
| 472 |
+
auth=auth,
|
| 473 |
+
proxy=proxy,
|
| 474 |
+
follow_redirects=follow_redirects,
|
| 475 |
+
verify=verify,
|
| 476 |
+
timeout=timeout,
|
| 477 |
+
trust_env=trust_env,
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
|
| 481 |
+
@contextmanager
|
| 482 |
+
def websocket(
|
| 483 |
+
url: URL | str,
|
| 484 |
+
*,
|
| 485 |
+
params: QueryParamTypes | None = None,
|
| 486 |
+
headers: HeaderTypes | None = None,
|
| 487 |
+
cookies: CookieTypes | None = None,
|
| 488 |
+
auth: AuthTypes | None = None,
|
| 489 |
+
proxy: ProxyTypes | None = None,
|
| 490 |
+
follow_redirects: bool = False,
|
| 491 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 492 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 493 |
+
trust_env: bool = True,
|
| 494 |
+
subprotocols: list[str] | None = None,
|
| 495 |
+
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
|
| 496 |
+
queue_size: int = DEFAULT_QUEUE_SIZE,
|
| 497 |
+
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
|
| 498 |
+
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
|
| 499 |
+
) -> Generator[WebSocketSession]:
|
| 500 |
+
"""
|
| 501 |
+
Open a WebSocket session.
|
| 502 |
+
|
| 503 |
+
The session is closed automatically when exiting the context manager.
|
| 504 |
+
|
| 505 |
+
```python
|
| 506 |
+
with httpx2.websocket("ws://localhost:8000/ws") as ws:
|
| 507 |
+
ws.send_text("Hello!")
|
| 508 |
+
message = ws.receive_text()
|
| 509 |
+
```
|
| 510 |
+
|
| 511 |
+
**Parameters**: See `httpx2.request` and `httpx2.Client.websocket`.
|
| 512 |
+
"""
|
| 513 |
+
with Client(
|
| 514 |
+
cookies=cookies,
|
| 515 |
+
proxy=proxy,
|
| 516 |
+
verify=verify,
|
| 517 |
+
timeout=timeout,
|
| 518 |
+
trust_env=trust_env,
|
| 519 |
+
) as client:
|
| 520 |
+
with client.websocket(
|
| 521 |
+
url,
|
| 522 |
+
params=params,
|
| 523 |
+
headers=headers,
|
| 524 |
+
auth=auth,
|
| 525 |
+
follow_redirects=follow_redirects,
|
| 526 |
+
subprotocols=subprotocols,
|
| 527 |
+
max_message_size_bytes=max_message_size_bytes,
|
| 528 |
+
queue_size=queue_size,
|
| 529 |
+
keepalive_ping_interval_seconds=keepalive_ping_interval_seconds,
|
| 530 |
+
keepalive_ping_timeout_seconds=keepalive_ping_timeout_seconds,
|
| 531 |
+
) as session:
|
| 532 |
+
yield session
|
hug/lib/python3.12/site-packages/httpx2/_auth.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 14 |
+
|
| 15 |
+
if typing.TYPE_CHECKING:
|
| 16 |
+
from collections.abc import Callable
|
| 17 |
+
from hashlib import _Hash
|
| 18 |
+
|
| 19 |
+
__all__ = ["Auth", "BasicAuth", "DigestAuth", "FunctionAuth", "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(self, request: Request) -> typing.Generator[Request, Response, None]:
|
| 63 |
+
"""
|
| 64 |
+
Execute the authentication flow synchronously.
|
| 65 |
+
|
| 66 |
+
By default, this defers to `.auth_flow()`. You should override this method
|
| 67 |
+
when the authentication scheme does I/O and/or uses concurrency primitives.
|
| 68 |
+
"""
|
| 69 |
+
if self.requires_request_body:
|
| 70 |
+
request.read()
|
| 71 |
+
|
| 72 |
+
flow = self.auth_flow(request)
|
| 73 |
+
request = next(flow)
|
| 74 |
+
|
| 75 |
+
while True:
|
| 76 |
+
response = yield request
|
| 77 |
+
if self.requires_response_body:
|
| 78 |
+
response.read()
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
request = flow.send(response)
|
| 82 |
+
except StopIteration:
|
| 83 |
+
break
|
| 84 |
+
|
| 85 |
+
async def async_auth_flow(self, request: Request) -> typing.AsyncGenerator[Request, Response]:
|
| 86 |
+
"""
|
| 87 |
+
Execute the authentication flow asynchronously.
|
| 88 |
+
|
| 89 |
+
By default, this defers to `.auth_flow()`. You should override this method
|
| 90 |
+
when the authentication scheme does I/O and/or uses concurrency primitives.
|
| 91 |
+
"""
|
| 92 |
+
if self.requires_request_body:
|
| 93 |
+
await request.aread()
|
| 94 |
+
|
| 95 |
+
flow = self.auth_flow(request)
|
| 96 |
+
request = next(flow)
|
| 97 |
+
|
| 98 |
+
while True:
|
| 99 |
+
response = yield request
|
| 100 |
+
if self.requires_response_body:
|
| 101 |
+
await response.aread()
|
| 102 |
+
|
| 103 |
+
try:
|
| 104 |
+
request = flow.send(response)
|
| 105 |
+
except StopIteration:
|
| 106 |
+
break
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class FunctionAuth(Auth):
|
| 110 |
+
"""
|
| 111 |
+
Allows the 'auth' argument to be passed as a simple callable function,
|
| 112 |
+
that takes the request, and returns a new, modified request.
|
| 113 |
+
"""
|
| 114 |
+
|
| 115 |
+
def __init__(self, func: typing.Callable[[Request], Request]) -> None:
|
| 116 |
+
self._func = func
|
| 117 |
+
|
| 118 |
+
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
|
| 119 |
+
yield self._func(request)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class BasicAuth(Auth):
|
| 123 |
+
"""
|
| 124 |
+
Allows the 'auth' argument to be passed as a (username, password) pair,
|
| 125 |
+
and uses HTTP Basic authentication.
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
def __init__(self, username: str | bytes, password: str | bytes) -> None:
|
| 129 |
+
self._auth_header = self._build_auth_header(username, password)
|
| 130 |
+
|
| 131 |
+
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
|
| 132 |
+
request.headers["Authorization"] = self._auth_header
|
| 133 |
+
yield request
|
| 134 |
+
|
| 135 |
+
def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str:
|
| 136 |
+
userpass = b":".join((to_bytes(username), to_bytes(password)))
|
| 137 |
+
token = b64encode(userpass).decode()
|
| 138 |
+
return f"Basic {token}"
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
class NetRCAuth(Auth):
|
| 142 |
+
"""
|
| 143 |
+
Use a 'netrc' file to lookup basic auth credentials based on the url host.
|
| 144 |
+
"""
|
| 145 |
+
|
| 146 |
+
def __init__(self, file: str | None = None) -> None:
|
| 147 |
+
# Lazily import 'netrc'.
|
| 148 |
+
# There's no need for us to load this module unless 'NetRCAuth' is being used.
|
| 149 |
+
import netrc
|
| 150 |
+
|
| 151 |
+
self._netrc_info = netrc.netrc(file)
|
| 152 |
+
|
| 153 |
+
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
|
| 154 |
+
auth_info = self._netrc_info.authenticators(request.url.host)
|
| 155 |
+
if auth_info is None or not auth_info[2]:
|
| 156 |
+
# The netrc file did not have authentication credentials for this host.
|
| 157 |
+
yield request
|
| 158 |
+
else:
|
| 159 |
+
# Build a basic auth header with credentials from the netrc file.
|
| 160 |
+
request.headers["Authorization"] = self._build_auth_header(username=auth_info[0], password=auth_info[2])
|
| 161 |
+
yield request
|
| 162 |
+
|
| 163 |
+
def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str:
|
| 164 |
+
userpass = b":".join((to_bytes(username), to_bytes(password)))
|
| 165 |
+
token = b64encode(userpass).decode()
|
| 166 |
+
return f"Basic {token}"
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class DigestAuth(Auth):
|
| 170 |
+
# Hashes are resolved by name at use time, since FIPS builds may remove or block MD5.
|
| 171 |
+
_ALGORITHM_TO_HASH_NAME = {
|
| 172 |
+
"MD5": "md5",
|
| 173 |
+
"MD5-SESS": "md5",
|
| 174 |
+
"SHA": "sha1",
|
| 175 |
+
"SHA-SESS": "sha1",
|
| 176 |
+
"SHA-256": "sha256",
|
| 177 |
+
"SHA-256-SESS": "sha256",
|
| 178 |
+
"SHA-512": "sha512",
|
| 179 |
+
"SHA-512-SESS": "sha512",
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
def __init__(self, username: str | bytes, password: str | bytes) -> None:
|
| 183 |
+
self._username = to_bytes(username)
|
| 184 |
+
self._password = to_bytes(password)
|
| 185 |
+
self._last_challenge: _DigestAuthChallenge | None = None
|
| 186 |
+
self._nonce_count = 1
|
| 187 |
+
|
| 188 |
+
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
|
| 189 |
+
if self._last_challenge:
|
| 190 |
+
request.headers["Authorization"] = self._build_auth_header(request, self._last_challenge)
|
| 191 |
+
|
| 192 |
+
response = yield request
|
| 193 |
+
|
| 194 |
+
if response.status_code != 401 or "www-authenticate" not in response.headers:
|
| 195 |
+
# If the response is not a 401 then we don't
|
| 196 |
+
# need to build an authenticated request.
|
| 197 |
+
return
|
| 198 |
+
|
| 199 |
+
for auth_header in response.headers.get_list("www-authenticate"):
|
| 200 |
+
if auth_header.lower().startswith("digest "):
|
| 201 |
+
break
|
| 202 |
+
else:
|
| 203 |
+
# If the response does not include a 'WWW-Authenticate: Digest ...'
|
| 204 |
+
# header, then we don't need to build an authenticated request.
|
| 205 |
+
return
|
| 206 |
+
|
| 207 |
+
self._last_challenge = self._parse_challenge(request, response, auth_header)
|
| 208 |
+
self._nonce_count = 1
|
| 209 |
+
|
| 210 |
+
request.headers["Authorization"] = self._build_auth_header(request, self._last_challenge)
|
| 211 |
+
if response.cookies:
|
| 212 |
+
Cookies(response.cookies).set_cookie_header(request=request)
|
| 213 |
+
yield request
|
| 214 |
+
|
| 215 |
+
def _parse_challenge(self, request: Request, response: Response, auth_header: str) -> _DigestAuthChallenge:
|
| 216 |
+
"""
|
| 217 |
+
Returns a challenge from a Digest WWW-Authenticate header.
|
| 218 |
+
These take the form of:
|
| 219 |
+
`Digest realm="realm@host.com",qop="auth,auth-int",nonce="abc",opaque="xyz"`
|
| 220 |
+
"""
|
| 221 |
+
scheme, _, fields = auth_header.partition(" ")
|
| 222 |
+
|
| 223 |
+
# This method should only ever have been called with a Digest auth header.
|
| 224 |
+
assert scheme.lower() == "digest"
|
| 225 |
+
|
| 226 |
+
header_dict: dict[str, str] = {}
|
| 227 |
+
for field in parse_http_list(fields):
|
| 228 |
+
key, value = field.strip().split("=", 1)
|
| 229 |
+
header_dict[key] = value.strip('"')
|
| 230 |
+
|
| 231 |
+
try:
|
| 232 |
+
realm = header_dict["realm"].encode()
|
| 233 |
+
nonce = header_dict["nonce"].encode()
|
| 234 |
+
algorithm = header_dict.get("algorithm", "MD5")
|
| 235 |
+
opaque = header_dict["opaque"].encode() if "opaque" in header_dict else None
|
| 236 |
+
qop = header_dict["qop"].encode() if "qop" in header_dict else None
|
| 237 |
+
return _DigestAuthChallenge(realm=realm, nonce=nonce, algorithm=algorithm, opaque=opaque, qop=qop)
|
| 238 |
+
except KeyError as exc:
|
| 239 |
+
message = "Malformed Digest WWW-Authenticate header"
|
| 240 |
+
raise ProtocolError(message, request=request) from exc
|
| 241 |
+
|
| 242 |
+
def _build_auth_header(self, request: Request, challenge: _DigestAuthChallenge) -> str:
|
| 243 |
+
try:
|
| 244 |
+
hash_func: Callable[[bytes], _Hash] = getattr(
|
| 245 |
+
hashlib, self._ALGORITHM_TO_HASH_NAME[challenge.algorithm.upper()]
|
| 246 |
+
)
|
| 247 |
+
except (KeyError, AttributeError) as exc:
|
| 248 |
+
message = f"Unsupported digest auth algorithm {challenge.algorithm!r}"
|
| 249 |
+
raise ProtocolError(message, request=request) from exc
|
| 250 |
+
|
| 251 |
+
def digest(data: bytes) -> bytes:
|
| 252 |
+
return hash_func(data).hexdigest().encode()
|
| 253 |
+
|
| 254 |
+
A1 = b":".join((self._username, challenge.realm, self._password))
|
| 255 |
+
|
| 256 |
+
path = request.url.raw_path
|
| 257 |
+
A2 = b":".join((request.method.encode(), path))
|
| 258 |
+
# TODO: implement auth-int
|
| 259 |
+
HA2 = digest(A2)
|
| 260 |
+
|
| 261 |
+
nc_value = b"%08x" % self._nonce_count
|
| 262 |
+
cnonce = self._get_client_nonce(self._nonce_count, challenge.nonce)
|
| 263 |
+
self._nonce_count += 1
|
| 264 |
+
|
| 265 |
+
HA1 = digest(A1)
|
| 266 |
+
if challenge.algorithm.lower().endswith("-sess"):
|
| 267 |
+
HA1 = digest(b":".join((HA1, challenge.nonce, cnonce)))
|
| 268 |
+
|
| 269 |
+
qop = self._resolve_qop(challenge.qop, request=request)
|
| 270 |
+
if qop is None:
|
| 271 |
+
# Following RFC 2069
|
| 272 |
+
digest_data = [HA1, challenge.nonce, HA2]
|
| 273 |
+
else:
|
| 274 |
+
# Following RFC 2617/7616
|
| 275 |
+
digest_data = [HA1, challenge.nonce, nc_value, cnonce, qop, HA2]
|
| 276 |
+
|
| 277 |
+
format_args = {
|
| 278 |
+
"username": self._username,
|
| 279 |
+
"realm": challenge.realm,
|
| 280 |
+
"nonce": challenge.nonce,
|
| 281 |
+
"uri": path,
|
| 282 |
+
"response": digest(b":".join(digest_data)),
|
| 283 |
+
"algorithm": challenge.algorithm.encode(),
|
| 284 |
+
}
|
| 285 |
+
if challenge.opaque:
|
| 286 |
+
format_args["opaque"] = challenge.opaque
|
| 287 |
+
if qop:
|
| 288 |
+
format_args["qop"] = b"auth"
|
| 289 |
+
format_args["nc"] = nc_value
|
| 290 |
+
format_args["cnonce"] = cnonce
|
| 291 |
+
|
| 292 |
+
return "Digest " + self._get_header_value(format_args)
|
| 293 |
+
|
| 294 |
+
def _get_client_nonce(self, nonce_count: int, nonce: bytes) -> bytes:
|
| 295 |
+
s = str(nonce_count).encode()
|
| 296 |
+
s += nonce
|
| 297 |
+
s += time.ctime().encode()
|
| 298 |
+
s += os.urandom(8)
|
| 299 |
+
|
| 300 |
+
return hashlib.sha1(s).hexdigest()[:16].encode()
|
| 301 |
+
|
| 302 |
+
def _get_header_value(self, header_fields: dict[str, bytes]) -> str:
|
| 303 |
+
NON_QUOTED_FIELDS = ("algorithm", "qop", "nc")
|
| 304 |
+
QUOTED_TEMPLATE = '{}="{}"'
|
| 305 |
+
NON_QUOTED_TEMPLATE = "{}={}"
|
| 306 |
+
|
| 307 |
+
header_value = ""
|
| 308 |
+
for i, (field, value) in enumerate(header_fields.items()):
|
| 309 |
+
if i > 0:
|
| 310 |
+
header_value += ", "
|
| 311 |
+
template = QUOTED_TEMPLATE if field not in NON_QUOTED_FIELDS else NON_QUOTED_TEMPLATE
|
| 312 |
+
header_value += template.format(field, to_str(value))
|
| 313 |
+
|
| 314 |
+
return header_value
|
| 315 |
+
|
| 316 |
+
def _resolve_qop(self, qop: bytes | None, request: Request) -> bytes | None:
|
| 317 |
+
if qop is None:
|
| 318 |
+
return None
|
| 319 |
+
qops = re.split(b", ?", qop)
|
| 320 |
+
if b"auth" in qops:
|
| 321 |
+
return b"auth"
|
| 322 |
+
|
| 323 |
+
if qops == [b"auth-int"]:
|
| 324 |
+
raise NotImplementedError("Digest auth-int support is not yet implemented")
|
| 325 |
+
|
| 326 |
+
message = f'Unexpected qop value "{qop!r}" in digest auth'
|
| 327 |
+
raise ProtocolError(message, request=request)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
class _DigestAuthChallenge(typing.NamedTuple):
|
| 331 |
+
realm: bytes
|
| 332 |
+
nonce: bytes
|
| 333 |
+
algorithm: str
|
| 334 |
+
opaque: bytes | None
|
| 335 |
+
qop: bytes | None
|
hug/lib/python3.12/site-packages/httpx2/_client.py
ADDED
|
@@ -0,0 +1,2236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 collections.abc import AsyncGenerator, Generator
|
| 10 |
+
from contextlib import asynccontextmanager, contextmanager
|
| 11 |
+
from types import TracebackType
|
| 12 |
+
|
| 13 |
+
from .__version__ import __version__
|
| 14 |
+
from ._auth import Auth, BasicAuth, FunctionAuth
|
| 15 |
+
from ._config import (
|
| 16 |
+
DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
|
| 17 |
+
DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
|
| 18 |
+
DEFAULT_LIMITS,
|
| 19 |
+
DEFAULT_MAX_EVENT_SIZE_BYTES,
|
| 20 |
+
DEFAULT_MAX_MESSAGE_SIZE_BYTES,
|
| 21 |
+
DEFAULT_MAX_REDIRECTS,
|
| 22 |
+
DEFAULT_QUEUE_SIZE,
|
| 23 |
+
DEFAULT_TIMEOUT_CONFIG,
|
| 24 |
+
Limits,
|
| 25 |
+
Proxy,
|
| 26 |
+
Timeout,
|
| 27 |
+
)
|
| 28 |
+
from ._decoders import SUPPORTED_DECODERS
|
| 29 |
+
from ._exceptions import (
|
| 30 |
+
InvalidURL,
|
| 31 |
+
RemoteProtocolError,
|
| 32 |
+
TooManyRedirects,
|
| 33 |
+
request_context,
|
| 34 |
+
)
|
| 35 |
+
from ._models import Cookies, Headers, Request, Response
|
| 36 |
+
from ._sse import EventSource
|
| 37 |
+
from ._status_codes import codes
|
| 38 |
+
from ._transports import AsyncHTTPTransport, HTTPTransport
|
| 39 |
+
from ._transports.base import AsyncBaseTransport, BaseTransport
|
| 40 |
+
from ._types import (
|
| 41 |
+
AsyncByteStream,
|
| 42 |
+
AuthTypes,
|
| 43 |
+
CertTypes,
|
| 44 |
+
CookieTypes,
|
| 45 |
+
HeaderTypes,
|
| 46 |
+
ProxyTypes,
|
| 47 |
+
QueryParamTypes,
|
| 48 |
+
RequestContent,
|
| 49 |
+
RequestData,
|
| 50 |
+
RequestExtensions,
|
| 51 |
+
RequestFiles,
|
| 52 |
+
SyncByteStream,
|
| 53 |
+
TimeoutTypes,
|
| 54 |
+
)
|
| 55 |
+
from ._urls import URL, QueryParams
|
| 56 |
+
from ._utils import URLPattern, get_environment_proxies
|
| 57 |
+
|
| 58 |
+
if typing.TYPE_CHECKING:
|
| 59 |
+
import ssl # pragma: no cover
|
| 60 |
+
|
| 61 |
+
from .websockets._api import AsyncWebSocketSession, WebSocketSession
|
| 62 |
+
|
| 63 |
+
__all__ = ["USE_CLIENT_DEFAULT", "AsyncClient", "Client"]
|
| 64 |
+
|
| 65 |
+
# The type annotation for @classmethod and context managers here follows PEP 484
|
| 66 |
+
# https://www.python.org/dev/peps/pep-0484/#annotating-instance-and-class-methods
|
| 67 |
+
T = typing.TypeVar("T", bound="Client")
|
| 68 |
+
U = typing.TypeVar("U", bound="AsyncClient")
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _is_https_redirect(url: URL, location: URL) -> bool:
|
| 72 |
+
"""
|
| 73 |
+
Return 'True' if 'location' is a HTTPS upgrade of 'url'
|
| 74 |
+
"""
|
| 75 |
+
origin = url.origin
|
| 76 |
+
location_origin = location.origin
|
| 77 |
+
|
| 78 |
+
return (
|
| 79 |
+
origin.host == location_origin.host
|
| 80 |
+
and origin.scheme == "http"
|
| 81 |
+
and origin.port == 80
|
| 82 |
+
and location_origin.scheme == "https"
|
| 83 |
+
and location_origin.port == 443
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
class UseClientDefault:
|
| 88 |
+
"""
|
| 89 |
+
For some parameters such as `auth=...` and `timeout=...` we need to be able
|
| 90 |
+
to indicate the default "unset" state, in a way that is distinctly different
|
| 91 |
+
to using `None`.
|
| 92 |
+
|
| 93 |
+
The default "unset" state indicates that whatever default is set on the
|
| 94 |
+
client should be used. This is different to setting `None`, which
|
| 95 |
+
explicitly disables the parameter, possibly overriding a client default.
|
| 96 |
+
|
| 97 |
+
For example we use `timeout=USE_CLIENT_DEFAULT` in the `request()` signature.
|
| 98 |
+
Omitting the `timeout` parameter will send a request using whatever default
|
| 99 |
+
timeout has been configured on the client. Including `timeout=None` will
|
| 100 |
+
ensure no timeout is used.
|
| 101 |
+
|
| 102 |
+
Note that user code shouldn't need to use the `USE_CLIENT_DEFAULT` constant,
|
| 103 |
+
but it is used internally when a parameter is not included.
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
USE_CLIENT_DEFAULT = UseClientDefault()
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
logger = logging.getLogger("httpx2")
|
| 111 |
+
|
| 112 |
+
USER_AGENT = f"python-httpx2/{__version__}"
|
| 113 |
+
ACCEPT_ENCODING = ", ".join([key for key in SUPPORTED_DECODERS.keys() if key != "identity"])
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class ClientState(enum.Enum):
|
| 117 |
+
# UNOPENED:
|
| 118 |
+
# The client has been instantiated, but has not been used to send a request,
|
| 119 |
+
# or been opened by entering the context of a `with` block.
|
| 120 |
+
UNOPENED = 1
|
| 121 |
+
# OPENED:
|
| 122 |
+
# The client has either sent a request, or is within a `with` block.
|
| 123 |
+
OPENED = 2
|
| 124 |
+
# CLOSED:
|
| 125 |
+
# The client has either exited the `with` block, or `close()` has
|
| 126 |
+
# been called explicitly.
|
| 127 |
+
CLOSED = 3
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class BoundSyncStream(SyncByteStream):
|
| 131 |
+
"""
|
| 132 |
+
A byte stream that tracks elapsed time for a response. Once closed, the
|
| 133 |
+
elapsed time is available via the `elapsed` attribute, and the response
|
| 134 |
+
can read it back from `response.stream.elapsed`.
|
| 135 |
+
"""
|
| 136 |
+
|
| 137 |
+
def __init__(self, stream: SyncByteStream, start: float) -> None:
|
| 138 |
+
self._stream = stream
|
| 139 |
+
self._start = start
|
| 140 |
+
self.elapsed: datetime.timedelta | None = None
|
| 141 |
+
|
| 142 |
+
def __iter__(self) -> typing.Iterator[bytes]:
|
| 143 |
+
yield from self._stream
|
| 144 |
+
|
| 145 |
+
def close(self) -> None:
|
| 146 |
+
self.elapsed = datetime.timedelta(seconds=time.perf_counter() - self._start)
|
| 147 |
+
self._stream.close()
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
class BoundAsyncStream(AsyncByteStream):
|
| 151 |
+
"""
|
| 152 |
+
An async byte stream that tracks elapsed time for a response. Once closed,
|
| 153 |
+
the elapsed time is available via the `elapsed` attribute, and the response
|
| 154 |
+
can read it back from `response.stream.elapsed`.
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
def __init__(self, stream: AsyncByteStream, start: float) -> None:
|
| 158 |
+
self._stream = stream
|
| 159 |
+
self._start = start
|
| 160 |
+
self.elapsed: datetime.timedelta | None = None
|
| 161 |
+
|
| 162 |
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
| 163 |
+
stream = self._stream.__aiter__()
|
| 164 |
+
try:
|
| 165 |
+
async for chunk in stream:
|
| 166 |
+
yield chunk
|
| 167 |
+
finally:
|
| 168 |
+
if isinstance(stream, AsyncGenerator):
|
| 169 |
+
await stream.aclose()
|
| 170 |
+
|
| 171 |
+
async def aclose(self) -> None:
|
| 172 |
+
self.elapsed = datetime.timedelta(seconds=time.perf_counter() - self._start)
|
| 173 |
+
await self._stream.aclose()
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
EventHook = typing.Callable[..., typing.Any]
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class BaseClient:
|
| 180 |
+
def __init__(
|
| 181 |
+
self,
|
| 182 |
+
*,
|
| 183 |
+
auth: AuthTypes | None = None,
|
| 184 |
+
params: QueryParamTypes | None = None,
|
| 185 |
+
headers: HeaderTypes | None = None,
|
| 186 |
+
cookies: CookieTypes | None = None,
|
| 187 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 188 |
+
follow_redirects: bool = False,
|
| 189 |
+
max_redirects: int = DEFAULT_MAX_REDIRECTS,
|
| 190 |
+
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
|
| 191 |
+
base_url: URL | str = "",
|
| 192 |
+
trust_env: bool = True,
|
| 193 |
+
default_encoding: str | typing.Callable[[bytes], str | None] = "utf-8",
|
| 194 |
+
) -> None:
|
| 195 |
+
event_hooks = {} if event_hooks is None else event_hooks
|
| 196 |
+
|
| 197 |
+
self._base_url = self._enforce_trailing_slash(URL(base_url))
|
| 198 |
+
|
| 199 |
+
self._auth = self._build_auth(auth)
|
| 200 |
+
self._params = QueryParams(params)
|
| 201 |
+
self.headers = Headers(headers)
|
| 202 |
+
self._cookies = Cookies(cookies)
|
| 203 |
+
self._timeout = Timeout(timeout)
|
| 204 |
+
self.follow_redirects = follow_redirects
|
| 205 |
+
self.max_redirects = max_redirects
|
| 206 |
+
self._event_hooks = {
|
| 207 |
+
"request": list(event_hooks.get("request", [])),
|
| 208 |
+
"response": list(event_hooks.get("response", [])),
|
| 209 |
+
}
|
| 210 |
+
self._trust_env = trust_env
|
| 211 |
+
self._default_encoding = default_encoding
|
| 212 |
+
self._state = ClientState.UNOPENED
|
| 213 |
+
|
| 214 |
+
@property
|
| 215 |
+
def is_closed(self) -> bool:
|
| 216 |
+
"""
|
| 217 |
+
Check if the client being closed
|
| 218 |
+
"""
|
| 219 |
+
return self._state == ClientState.CLOSED
|
| 220 |
+
|
| 221 |
+
@property
|
| 222 |
+
def trust_env(self) -> bool:
|
| 223 |
+
return self._trust_env
|
| 224 |
+
|
| 225 |
+
def _enforce_trailing_slash(self, url: URL) -> URL:
|
| 226 |
+
if url.raw_path.endswith(b"/"):
|
| 227 |
+
return url
|
| 228 |
+
return url.copy_with(raw_path=url.raw_path + b"/")
|
| 229 |
+
|
| 230 |
+
def _get_proxy_map(self, proxy: ProxyTypes | None, allow_env_proxies: bool) -> dict[str, Proxy | None]:
|
| 231 |
+
if proxy is None:
|
| 232 |
+
if allow_env_proxies:
|
| 233 |
+
return {key: None if url is None else Proxy(url=url) for key, url in get_environment_proxies().items()}
|
| 234 |
+
return {}
|
| 235 |
+
else:
|
| 236 |
+
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
|
| 237 |
+
return {"all://": proxy}
|
| 238 |
+
|
| 239 |
+
@property
|
| 240 |
+
def timeout(self) -> Timeout:
|
| 241 |
+
return self._timeout
|
| 242 |
+
|
| 243 |
+
@timeout.setter
|
| 244 |
+
def timeout(self, timeout: TimeoutTypes) -> None:
|
| 245 |
+
self._timeout = Timeout(timeout)
|
| 246 |
+
|
| 247 |
+
@property
|
| 248 |
+
def event_hooks(self) -> dict[str, list[EventHook]]:
|
| 249 |
+
return self._event_hooks
|
| 250 |
+
|
| 251 |
+
@event_hooks.setter
|
| 252 |
+
def event_hooks(self, event_hooks: dict[str, list[EventHook]]) -> None:
|
| 253 |
+
self._event_hooks = {
|
| 254 |
+
"request": list(event_hooks.get("request", [])),
|
| 255 |
+
"response": list(event_hooks.get("response", [])),
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
@property
|
| 259 |
+
def auth(self) -> Auth | None:
|
| 260 |
+
"""
|
| 261 |
+
Authentication class used when none is passed at the request-level.
|
| 262 |
+
|
| 263 |
+
See also [Authentication][0].
|
| 264 |
+
|
| 265 |
+
[0]: quickstart.md#authentication
|
| 266 |
+
"""
|
| 267 |
+
return self._auth
|
| 268 |
+
|
| 269 |
+
@auth.setter
|
| 270 |
+
def auth(self, auth: AuthTypes) -> None:
|
| 271 |
+
self._auth = self._build_auth(auth)
|
| 272 |
+
|
| 273 |
+
@property
|
| 274 |
+
def base_url(self) -> URL:
|
| 275 |
+
"""
|
| 276 |
+
Base URL to use when sending requests with relative URLs.
|
| 277 |
+
"""
|
| 278 |
+
return self._base_url
|
| 279 |
+
|
| 280 |
+
@base_url.setter
|
| 281 |
+
def base_url(self, url: URL | str) -> None:
|
| 282 |
+
self._base_url = self._enforce_trailing_slash(URL(url))
|
| 283 |
+
|
| 284 |
+
@property
|
| 285 |
+
def headers(self) -> Headers:
|
| 286 |
+
"""
|
| 287 |
+
HTTP headers to include when sending requests.
|
| 288 |
+
"""
|
| 289 |
+
return self._headers
|
| 290 |
+
|
| 291 |
+
@headers.setter
|
| 292 |
+
def headers(self, headers: HeaderTypes) -> None:
|
| 293 |
+
client_headers = Headers(
|
| 294 |
+
{
|
| 295 |
+
b"Accept": b"*/*",
|
| 296 |
+
b"Accept-Encoding": ACCEPT_ENCODING.encode("ascii"),
|
| 297 |
+
b"Connection": b"keep-alive",
|
| 298 |
+
b"User-Agent": USER_AGENT.encode("ascii"),
|
| 299 |
+
}
|
| 300 |
+
)
|
| 301 |
+
client_headers.update(headers)
|
| 302 |
+
self._headers = client_headers
|
| 303 |
+
|
| 304 |
+
@property
|
| 305 |
+
def cookies(self) -> Cookies:
|
| 306 |
+
"""
|
| 307 |
+
Cookie values to include when sending requests.
|
| 308 |
+
"""
|
| 309 |
+
return self._cookies
|
| 310 |
+
|
| 311 |
+
@cookies.setter
|
| 312 |
+
def cookies(self, cookies: CookieTypes) -> None:
|
| 313 |
+
self._cookies = Cookies(cookies)
|
| 314 |
+
|
| 315 |
+
@property
|
| 316 |
+
def params(self) -> QueryParams:
|
| 317 |
+
"""
|
| 318 |
+
Query parameters to include in the URL when sending requests.
|
| 319 |
+
"""
|
| 320 |
+
return self._params
|
| 321 |
+
|
| 322 |
+
@params.setter
|
| 323 |
+
def params(self, params: QueryParamTypes) -> None:
|
| 324 |
+
self._params = QueryParams(params)
|
| 325 |
+
|
| 326 |
+
def build_request(
|
| 327 |
+
self,
|
| 328 |
+
method: str,
|
| 329 |
+
url: URL | str,
|
| 330 |
+
*,
|
| 331 |
+
content: RequestContent | None = None,
|
| 332 |
+
data: RequestData | None = None,
|
| 333 |
+
files: RequestFiles | None = None,
|
| 334 |
+
json: typing.Any | None = None,
|
| 335 |
+
params: QueryParamTypes | None = None,
|
| 336 |
+
headers: HeaderTypes | None = None,
|
| 337 |
+
cookies: CookieTypes | None = None,
|
| 338 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 339 |
+
extensions: RequestExtensions | None = None,
|
| 340 |
+
) -> Request:
|
| 341 |
+
"""
|
| 342 |
+
Build and return a request instance.
|
| 343 |
+
|
| 344 |
+
* The `params`, `headers` and `cookies` arguments
|
| 345 |
+
are merged with any values set on the client.
|
| 346 |
+
* The `url` argument is merged with any `base_url` set on the client.
|
| 347 |
+
|
| 348 |
+
See also: [Request instances][0]
|
| 349 |
+
|
| 350 |
+
[0]: advanced/clients.md#request-instances
|
| 351 |
+
"""
|
| 352 |
+
url = self._merge_url(url)
|
| 353 |
+
headers = self._merge_headers(headers)
|
| 354 |
+
cookies = self._merge_cookies(cookies)
|
| 355 |
+
params = self._merge_queryparams(params)
|
| 356 |
+
extensions = {} if extensions is None else extensions
|
| 357 |
+
if "timeout" not in extensions:
|
| 358 |
+
timeout = self.timeout if isinstance(timeout, UseClientDefault) else Timeout(timeout)
|
| 359 |
+
extensions = dict(**extensions, timeout=timeout.as_dict())
|
| 360 |
+
return Request(
|
| 361 |
+
method,
|
| 362 |
+
url,
|
| 363 |
+
content=content,
|
| 364 |
+
data=data,
|
| 365 |
+
files=files,
|
| 366 |
+
json=json,
|
| 367 |
+
params=params,
|
| 368 |
+
headers=headers,
|
| 369 |
+
cookies=cookies,
|
| 370 |
+
extensions=extensions,
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
def _merge_url(self, url: URL | str) -> URL:
|
| 374 |
+
"""
|
| 375 |
+
Merge a URL argument together with any 'base_url' on the client,
|
| 376 |
+
to create the URL used for the outgoing request.
|
| 377 |
+
"""
|
| 378 |
+
merge_url = URL(url)
|
| 379 |
+
if merge_url.is_relative_url:
|
| 380 |
+
# To merge URLs we always append to the base URL. To get this
|
| 381 |
+
# behaviour correct we always ensure the base URL ends in a '/'
|
| 382 |
+
# separator, and strip any leading '/' from the merge URL.
|
| 383 |
+
#
|
| 384 |
+
# So, eg...
|
| 385 |
+
#
|
| 386 |
+
# >>> client = Client(base_url="https://www.example.com/subpath")
|
| 387 |
+
# >>> client.base_url
|
| 388 |
+
# URL('https://www.example.com/subpath/')
|
| 389 |
+
# >>> client.build_request("GET", "/path").url
|
| 390 |
+
# URL('https://www.example.com/subpath/path')
|
| 391 |
+
merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/")
|
| 392 |
+
return self.base_url.copy_with(raw_path=merge_raw_path)
|
| 393 |
+
return merge_url
|
| 394 |
+
|
| 395 |
+
def _merge_cookies(self, cookies: CookieTypes | None = None) -> Cookies:
|
| 396 |
+
"""
|
| 397 |
+
Merge a cookies argument together with any cookies on the client,
|
| 398 |
+
to create the cookies used for the outgoing request.
|
| 399 |
+
"""
|
| 400 |
+
if not cookies:
|
| 401 |
+
return self.cookies
|
| 402 |
+
merged_cookies = Cookies(self.cookies)
|
| 403 |
+
merged_cookies.update(cookies)
|
| 404 |
+
return merged_cookies
|
| 405 |
+
|
| 406 |
+
def _merge_headers(self, headers: HeaderTypes | None = None) -> HeaderTypes | None:
|
| 407 |
+
"""
|
| 408 |
+
Merge a headers argument together with any headers on the client,
|
| 409 |
+
to create the headers used for the outgoing request.
|
| 410 |
+
"""
|
| 411 |
+
merged_headers = Headers(self.headers)
|
| 412 |
+
merged_headers.update(headers)
|
| 413 |
+
return merged_headers
|
| 414 |
+
|
| 415 |
+
def _merge_queryparams(self, params: QueryParamTypes | None = None) -> QueryParamTypes | None:
|
| 416 |
+
"""
|
| 417 |
+
Merge a queryparams argument together with any queryparams on the client,
|
| 418 |
+
to create the queryparams used for the outgoing request.
|
| 419 |
+
"""
|
| 420 |
+
if params or self.params:
|
| 421 |
+
merged_queryparams = QueryParams(self.params)
|
| 422 |
+
return merged_queryparams.merge(params)
|
| 423 |
+
return params
|
| 424 |
+
|
| 425 |
+
def _build_auth(self, auth: AuthTypes | None) -> Auth | None:
|
| 426 |
+
if auth is None:
|
| 427 |
+
return None
|
| 428 |
+
elif isinstance(auth, tuple):
|
| 429 |
+
return BasicAuth(username=auth[0], password=auth[1])
|
| 430 |
+
elif isinstance(auth, Auth):
|
| 431 |
+
return auth
|
| 432 |
+
elif callable(auth):
|
| 433 |
+
return FunctionAuth(func=auth)
|
| 434 |
+
else:
|
| 435 |
+
raise TypeError(f'Invalid "auth" argument: {auth!r}')
|
| 436 |
+
|
| 437 |
+
def _build_request_auth(
|
| 438 |
+
self,
|
| 439 |
+
request: Request,
|
| 440 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 441 |
+
) -> Auth:
|
| 442 |
+
auth = self._auth if isinstance(auth, UseClientDefault) else self._build_auth(auth)
|
| 443 |
+
|
| 444 |
+
if auth is not None:
|
| 445 |
+
return auth
|
| 446 |
+
|
| 447 |
+
username, password = request.url.username, request.url.password
|
| 448 |
+
if username or password:
|
| 449 |
+
return BasicAuth(username=username, password=password)
|
| 450 |
+
|
| 451 |
+
return Auth()
|
| 452 |
+
|
| 453 |
+
def _build_redirect_request(self, request: Request, response: Response) -> Request:
|
| 454 |
+
"""
|
| 455 |
+
Given a request and a redirect response, return a new request that
|
| 456 |
+
should be used to effect the redirect.
|
| 457 |
+
"""
|
| 458 |
+
method = self._redirect_method(request, response)
|
| 459 |
+
url = self._redirect_url(request, response)
|
| 460 |
+
headers = self._redirect_headers(request, url, method)
|
| 461 |
+
stream = self._redirect_stream(request, method)
|
| 462 |
+
return Request(
|
| 463 |
+
method=method,
|
| 464 |
+
url=url,
|
| 465 |
+
headers=headers,
|
| 466 |
+
cookies=self.cookies,
|
| 467 |
+
stream=stream,
|
| 468 |
+
extensions=request.extensions,
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
def _redirect_method(self, request: Request, response: Response) -> str:
|
| 472 |
+
"""
|
| 473 |
+
When being redirected we may want to change the method of the request
|
| 474 |
+
based on certain specs or browser behavior.
|
| 475 |
+
"""
|
| 476 |
+
method = request.method
|
| 477 |
+
|
| 478 |
+
# https://tools.ietf.org/html/rfc7231#section-6.4.4
|
| 479 |
+
if response.status_code == codes.SEE_OTHER and method != "HEAD":
|
| 480 |
+
method = "GET"
|
| 481 |
+
|
| 482 |
+
# Do what the browsers do, despite standards...
|
| 483 |
+
# Turn 302s into GETs.
|
| 484 |
+
# QUERY is excluded, per RFC 10008 Section 2.5.
|
| 485 |
+
# https://datatracker.ietf.org/doc/html/rfc10008#section-2.5
|
| 486 |
+
if response.status_code == codes.FOUND and method not in ("HEAD", "QUERY"):
|
| 487 |
+
method = "GET"
|
| 488 |
+
|
| 489 |
+
# If a POST is responded to with a 301, turn it into a GET.
|
| 490 |
+
# This bizarre behaviour is explained in 'requests' issue 1704.
|
| 491 |
+
if response.status_code == codes.MOVED_PERMANENTLY and method == "POST":
|
| 492 |
+
method = "GET"
|
| 493 |
+
|
| 494 |
+
return method
|
| 495 |
+
|
| 496 |
+
def _redirect_url(self, request: Request, response: Response) -> URL:
|
| 497 |
+
"""
|
| 498 |
+
Return the URL for the redirect to follow.
|
| 499 |
+
"""
|
| 500 |
+
location = response.headers["Location"]
|
| 501 |
+
|
| 502 |
+
try:
|
| 503 |
+
url = URL(location)
|
| 504 |
+
except InvalidURL as exc:
|
| 505 |
+
raise RemoteProtocolError(f"Invalid URL in location header: {exc}.", request=request) from None
|
| 506 |
+
|
| 507 |
+
# Handle malformed 'Location' headers that are "absolute" form, have no host.
|
| 508 |
+
# See: https://github.com/encode/httpx/issues/771
|
| 509 |
+
if url.scheme and not url.host:
|
| 510 |
+
url = url.copy_with(host=request.url.host)
|
| 511 |
+
|
| 512 |
+
# Facilitate relative 'Location' headers, as allowed by RFC 7231.
|
| 513 |
+
# (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
|
| 514 |
+
if url.is_relative_url:
|
| 515 |
+
url = request.url.join(url)
|
| 516 |
+
|
| 517 |
+
# Attach previous fragment if needed (RFC 7231 7.1.2)
|
| 518 |
+
if request.url.fragment and not url.fragment:
|
| 519 |
+
url = url.copy_with(fragment=request.url.fragment)
|
| 520 |
+
|
| 521 |
+
return url
|
| 522 |
+
|
| 523 |
+
def _redirect_headers(self, request: Request, url: URL, method: str) -> Headers:
|
| 524 |
+
"""
|
| 525 |
+
Return the headers that should be used for the redirect request.
|
| 526 |
+
"""
|
| 527 |
+
headers = Headers(request.headers)
|
| 528 |
+
|
| 529 |
+
if url.origin != request.url.origin:
|
| 530 |
+
if not _is_https_redirect(request.url, url):
|
| 531 |
+
# Strip Authorization headers when responses are redirected
|
| 532 |
+
# away from the origin. (Except for direct HTTP to HTTPS redirects.)
|
| 533 |
+
headers.pop("Authorization", None)
|
| 534 |
+
|
| 535 |
+
# Update the Host header.
|
| 536 |
+
headers["Host"] = url.netloc.decode("ascii")
|
| 537 |
+
|
| 538 |
+
if method != request.method and method == "GET":
|
| 539 |
+
# If we've switch to a 'GET' request, then strip any headers which
|
| 540 |
+
# are only relevant to the request body.
|
| 541 |
+
headers.pop("Content-Length", None)
|
| 542 |
+
headers.pop("Transfer-Encoding", None)
|
| 543 |
+
|
| 544 |
+
# We should use the client cookie store to determine any cookie header,
|
| 545 |
+
# rather than whatever was on the original outgoing request.
|
| 546 |
+
headers.pop("Cookie", None)
|
| 547 |
+
|
| 548 |
+
return headers
|
| 549 |
+
|
| 550 |
+
def _redirect_stream(self, request: Request, method: str) -> SyncByteStream | AsyncByteStream | None:
|
| 551 |
+
"""
|
| 552 |
+
Return the body that should be used for the redirect request.
|
| 553 |
+
"""
|
| 554 |
+
if method != request.method and method == "GET":
|
| 555 |
+
return None
|
| 556 |
+
|
| 557 |
+
return request.stream
|
| 558 |
+
|
| 559 |
+
def _set_timeout(self, request: Request) -> None:
|
| 560 |
+
if "timeout" not in request.extensions:
|
| 561 |
+
timeout = self.timeout if isinstance(self.timeout, UseClientDefault) else Timeout(self.timeout)
|
| 562 |
+
request.extensions = dict(**request.extensions, timeout=timeout.as_dict())
|
| 563 |
+
|
| 564 |
+
|
| 565 |
+
class Client(BaseClient):
|
| 566 |
+
"""
|
| 567 |
+
An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc.
|
| 568 |
+
|
| 569 |
+
It can be shared between threads.
|
| 570 |
+
|
| 571 |
+
Usage:
|
| 572 |
+
|
| 573 |
+
```python
|
| 574 |
+
>>> client = httpx2.Client()
|
| 575 |
+
>>> response = client.get('https://example.org')
|
| 576 |
+
```
|
| 577 |
+
|
| 578 |
+
**Parameters:**
|
| 579 |
+
|
| 580 |
+
* **auth** - *(optional)* An authentication class to use when sending
|
| 581 |
+
requests.
|
| 582 |
+
* **params** - *(optional)* Query parameters to include in request URLs, as
|
| 583 |
+
a string, dictionary, or sequence of two-tuples.
|
| 584 |
+
* **headers** - *(optional)* Dictionary of HTTP headers to include when
|
| 585 |
+
sending requests.
|
| 586 |
+
* **cookies** - *(optional)* Dictionary of Cookie items to include when
|
| 587 |
+
sending requests.
|
| 588 |
+
* **verify** - *(optional)* Either `True` to use an SSL context with the
|
| 589 |
+
default CA bundle, `False` to disable verification, or an instance of
|
| 590 |
+
`ssl.SSLContext` to use a custom context.
|
| 591 |
+
* **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
|
| 592 |
+
enabled. Defaults to `False`.
|
| 593 |
+
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
|
| 594 |
+
* **mounts** - *(optional)* A dictionary mapping URL patterns to transports,
|
| 595 |
+
used to route requests through specific transports based on the URL.
|
| 596 |
+
* **timeout** - *(optional)* The timeout configuration to use when sending
|
| 597 |
+
requests.
|
| 598 |
+
* **limits** - *(optional)* The limits configuration to use.
|
| 599 |
+
* **max_redirects** - *(optional)* The maximum number of redirect responses
|
| 600 |
+
that should be followed.
|
| 601 |
+
* **base_url** - *(optional)* A URL to use as the base when building
|
| 602 |
+
request URLs.
|
| 603 |
+
* **transport** - *(optional)* A transport class to use for sending requests
|
| 604 |
+
over the network.
|
| 605 |
+
* **trust_env** - *(optional)* Enables or disables usage of environment
|
| 606 |
+
variables for configuration.
|
| 607 |
+
* **default_encoding** - *(optional)* The default encoding to use for decoding
|
| 608 |
+
response text, if no charset information is included in a response Content-Type
|
| 609 |
+
header. Set to a callable for automatic character set detection. Default: "utf-8".
|
| 610 |
+
"""
|
| 611 |
+
|
| 612 |
+
def __init__(
|
| 613 |
+
self,
|
| 614 |
+
*,
|
| 615 |
+
auth: AuthTypes | None = None,
|
| 616 |
+
params: QueryParamTypes | None = None,
|
| 617 |
+
headers: HeaderTypes | None = None,
|
| 618 |
+
cookies: CookieTypes | None = None,
|
| 619 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 620 |
+
cert: CertTypes | None = None,
|
| 621 |
+
trust_env: bool = True,
|
| 622 |
+
http1: bool = True,
|
| 623 |
+
http2: bool = False,
|
| 624 |
+
proxy: ProxyTypes | None = None,
|
| 625 |
+
mounts: None | (typing.Mapping[str, BaseTransport | None]) = None,
|
| 626 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 627 |
+
follow_redirects: bool = False,
|
| 628 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 629 |
+
max_redirects: int = DEFAULT_MAX_REDIRECTS,
|
| 630 |
+
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
|
| 631 |
+
base_url: URL | str = "",
|
| 632 |
+
transport: BaseTransport | None = None,
|
| 633 |
+
default_encoding: str | typing.Callable[[bytes], str | None] = "utf-8",
|
| 634 |
+
) -> None:
|
| 635 |
+
super().__init__(
|
| 636 |
+
auth=auth,
|
| 637 |
+
params=params,
|
| 638 |
+
headers=headers,
|
| 639 |
+
cookies=cookies,
|
| 640 |
+
timeout=timeout,
|
| 641 |
+
follow_redirects=follow_redirects,
|
| 642 |
+
max_redirects=max_redirects,
|
| 643 |
+
event_hooks=event_hooks,
|
| 644 |
+
base_url=base_url,
|
| 645 |
+
trust_env=trust_env,
|
| 646 |
+
default_encoding=default_encoding,
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
if http2:
|
| 650 |
+
try:
|
| 651 |
+
import h2 # noqa
|
| 652 |
+
except ImportError: # pragma: no cover
|
| 653 |
+
raise ImportError(
|
| 654 |
+
"Using http2=True, but the 'h2' package is not installed. "
|
| 655 |
+
"Make sure to install httpx using `pip install httpx[http2]`."
|
| 656 |
+
) from None
|
| 657 |
+
|
| 658 |
+
allow_env_proxies = trust_env and transport is None
|
| 659 |
+
proxy_map = self._get_proxy_map(proxy, allow_env_proxies)
|
| 660 |
+
|
| 661 |
+
self._transport = self._init_transport(
|
| 662 |
+
verify=verify,
|
| 663 |
+
cert=cert,
|
| 664 |
+
trust_env=trust_env,
|
| 665 |
+
http1=http1,
|
| 666 |
+
http2=http2,
|
| 667 |
+
limits=limits,
|
| 668 |
+
transport=transport,
|
| 669 |
+
)
|
| 670 |
+
self._mounts: dict[URLPattern, BaseTransport | None] = {
|
| 671 |
+
URLPattern(key): None
|
| 672 |
+
if proxy is None
|
| 673 |
+
else self._init_proxy_transport(
|
| 674 |
+
proxy,
|
| 675 |
+
verify=verify,
|
| 676 |
+
cert=cert,
|
| 677 |
+
trust_env=trust_env,
|
| 678 |
+
http1=http1,
|
| 679 |
+
http2=http2,
|
| 680 |
+
limits=limits,
|
| 681 |
+
)
|
| 682 |
+
for key, proxy in proxy_map.items()
|
| 683 |
+
}
|
| 684 |
+
if mounts is not None:
|
| 685 |
+
self._mounts.update({URLPattern(key): transport for key, transport in mounts.items()})
|
| 686 |
+
|
| 687 |
+
self._mounts = dict(sorted(self._mounts.items()))
|
| 688 |
+
|
| 689 |
+
def _init_transport(
|
| 690 |
+
self,
|
| 691 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 692 |
+
cert: CertTypes | None = None,
|
| 693 |
+
trust_env: bool = True,
|
| 694 |
+
http1: bool = True,
|
| 695 |
+
http2: bool = False,
|
| 696 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 697 |
+
transport: BaseTransport | None = None,
|
| 698 |
+
) -> BaseTransport:
|
| 699 |
+
if transport is not None:
|
| 700 |
+
return transport
|
| 701 |
+
|
| 702 |
+
return HTTPTransport(
|
| 703 |
+
verify=verify,
|
| 704 |
+
cert=cert,
|
| 705 |
+
trust_env=trust_env,
|
| 706 |
+
http1=http1,
|
| 707 |
+
http2=http2,
|
| 708 |
+
limits=limits,
|
| 709 |
+
)
|
| 710 |
+
|
| 711 |
+
def _init_proxy_transport(
|
| 712 |
+
self,
|
| 713 |
+
proxy: Proxy,
|
| 714 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 715 |
+
cert: CertTypes | None = None,
|
| 716 |
+
trust_env: bool = True,
|
| 717 |
+
http1: bool = True,
|
| 718 |
+
http2: bool = False,
|
| 719 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 720 |
+
) -> BaseTransport:
|
| 721 |
+
return HTTPTransport(
|
| 722 |
+
verify=verify,
|
| 723 |
+
cert=cert,
|
| 724 |
+
trust_env=trust_env,
|
| 725 |
+
http1=http1,
|
| 726 |
+
http2=http2,
|
| 727 |
+
limits=limits,
|
| 728 |
+
proxy=proxy,
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
def _transport_for_url(self, url: URL) -> BaseTransport:
|
| 732 |
+
"""
|
| 733 |
+
Returns the transport instance that should be used for a given URL.
|
| 734 |
+
This will either be the standard connection pool, or a proxy.
|
| 735 |
+
"""
|
| 736 |
+
for pattern, transport in self._mounts.items():
|
| 737 |
+
if pattern.matches(url):
|
| 738 |
+
return self._transport if transport is None else transport
|
| 739 |
+
|
| 740 |
+
return self._transport
|
| 741 |
+
|
| 742 |
+
def request(
|
| 743 |
+
self,
|
| 744 |
+
method: str,
|
| 745 |
+
url: URL | str,
|
| 746 |
+
*,
|
| 747 |
+
content: RequestContent | None = None,
|
| 748 |
+
data: RequestData | None = None,
|
| 749 |
+
files: RequestFiles | None = None,
|
| 750 |
+
json: typing.Any | None = None,
|
| 751 |
+
params: QueryParamTypes | None = None,
|
| 752 |
+
headers: HeaderTypes | None = None,
|
| 753 |
+
cookies: CookieTypes | None = None,
|
| 754 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 755 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 756 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 757 |
+
extensions: RequestExtensions | None = None,
|
| 758 |
+
) -> Response:
|
| 759 |
+
"""
|
| 760 |
+
Build and send a request.
|
| 761 |
+
|
| 762 |
+
Equivalent to:
|
| 763 |
+
|
| 764 |
+
```python
|
| 765 |
+
request = client.build_request(...)
|
| 766 |
+
response = client.send(request, ...)
|
| 767 |
+
```
|
| 768 |
+
|
| 769 |
+
See `Client.build_request()`, `Client.send()` and
|
| 770 |
+
[Merging of configuration][0] for how the various parameters
|
| 771 |
+
are merged with client-level configuration.
|
| 772 |
+
|
| 773 |
+
[0]: advanced/clients.md#merging-of-configuration
|
| 774 |
+
"""
|
| 775 |
+
if cookies is not None:
|
| 776 |
+
message = (
|
| 777 |
+
"Setting per-request cookies=<...> is being deprecated, because "
|
| 778 |
+
"the expected behaviour on cookie persistence is ambiguous. Set "
|
| 779 |
+
"cookies directly on the client instance instead."
|
| 780 |
+
)
|
| 781 |
+
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
| 782 |
+
|
| 783 |
+
request = self.build_request(
|
| 784 |
+
method=method,
|
| 785 |
+
url=url,
|
| 786 |
+
content=content,
|
| 787 |
+
data=data,
|
| 788 |
+
files=files,
|
| 789 |
+
json=json,
|
| 790 |
+
params=params,
|
| 791 |
+
headers=headers,
|
| 792 |
+
cookies=cookies,
|
| 793 |
+
timeout=timeout,
|
| 794 |
+
extensions=extensions,
|
| 795 |
+
)
|
| 796 |
+
return self.send(request, auth=auth, follow_redirects=follow_redirects)
|
| 797 |
+
|
| 798 |
+
@contextmanager
|
| 799 |
+
def stream(
|
| 800 |
+
self,
|
| 801 |
+
method: str,
|
| 802 |
+
url: URL | str,
|
| 803 |
+
*,
|
| 804 |
+
content: RequestContent | None = None,
|
| 805 |
+
data: RequestData | None = None,
|
| 806 |
+
files: RequestFiles | None = None,
|
| 807 |
+
json: typing.Any | None = None,
|
| 808 |
+
params: QueryParamTypes | None = None,
|
| 809 |
+
headers: HeaderTypes | None = None,
|
| 810 |
+
cookies: CookieTypes | None = None,
|
| 811 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 812 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 813 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 814 |
+
extensions: RequestExtensions | None = None,
|
| 815 |
+
) -> Generator[Response]:
|
| 816 |
+
"""
|
| 817 |
+
Alternative to `httpx2.request()` that streams the response body
|
| 818 |
+
instead of loading it into memory at once.
|
| 819 |
+
|
| 820 |
+
**Parameters**: See `httpx2.request`.
|
| 821 |
+
|
| 822 |
+
See also: [Streaming Responses][0]
|
| 823 |
+
|
| 824 |
+
[0]: quickstart.md#streaming-responses
|
| 825 |
+
"""
|
| 826 |
+
request = self.build_request(
|
| 827 |
+
method=method,
|
| 828 |
+
url=url,
|
| 829 |
+
content=content,
|
| 830 |
+
data=data,
|
| 831 |
+
files=files,
|
| 832 |
+
json=json,
|
| 833 |
+
params=params,
|
| 834 |
+
headers=headers,
|
| 835 |
+
cookies=cookies,
|
| 836 |
+
timeout=timeout,
|
| 837 |
+
extensions=extensions,
|
| 838 |
+
)
|
| 839 |
+
response = self.send(
|
| 840 |
+
request=request,
|
| 841 |
+
auth=auth,
|
| 842 |
+
follow_redirects=follow_redirects,
|
| 843 |
+
stream=True,
|
| 844 |
+
)
|
| 845 |
+
try:
|
| 846 |
+
yield response
|
| 847 |
+
finally:
|
| 848 |
+
response.close()
|
| 849 |
+
|
| 850 |
+
@contextmanager
|
| 851 |
+
def sse(
|
| 852 |
+
self,
|
| 853 |
+
url: URL | str,
|
| 854 |
+
*,
|
| 855 |
+
method: str = "GET",
|
| 856 |
+
content: RequestContent | None = None,
|
| 857 |
+
data: RequestData | None = None,
|
| 858 |
+
files: RequestFiles | None = None,
|
| 859 |
+
json: typing.Any | None = None,
|
| 860 |
+
params: QueryParamTypes | None = None,
|
| 861 |
+
headers: HeaderTypes | None = None,
|
| 862 |
+
cookies: CookieTypes | None = None,
|
| 863 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 864 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 865 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 866 |
+
extensions: RequestExtensions | None = None,
|
| 867 |
+
max_event_size: int | None = DEFAULT_MAX_EVENT_SIZE_BYTES,
|
| 868 |
+
) -> Generator[EventSource]:
|
| 869 |
+
"""
|
| 870 |
+
Connect to a server-sent events endpoint and yield an `EventSource`.
|
| 871 |
+
|
| 872 |
+
Iterating the `EventSource` yields `ServerSentEvent` instances.
|
| 873 |
+
|
| 874 |
+
**Parameters**: See `httpx2.request`.
|
| 875 |
+
"""
|
| 876 |
+
with self.stream(
|
| 877 |
+
method,
|
| 878 |
+
url,
|
| 879 |
+
content=content,
|
| 880 |
+
data=data,
|
| 881 |
+
files=files,
|
| 882 |
+
json=json,
|
| 883 |
+
params=params,
|
| 884 |
+
headers={"Accept": "text/event-stream", "Cache-Control": "no-store"} | Headers(headers),
|
| 885 |
+
cookies=cookies,
|
| 886 |
+
auth=auth,
|
| 887 |
+
follow_redirects=follow_redirects,
|
| 888 |
+
timeout=timeout,
|
| 889 |
+
extensions=extensions,
|
| 890 |
+
) as response:
|
| 891 |
+
yield EventSource(response, max_event_size=max_event_size)
|
| 892 |
+
|
| 893 |
+
@contextmanager
|
| 894 |
+
def websocket(
|
| 895 |
+
self,
|
| 896 |
+
url: URL | str,
|
| 897 |
+
*,
|
| 898 |
+
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
|
| 899 |
+
queue_size: int = DEFAULT_QUEUE_SIZE,
|
| 900 |
+
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
|
| 901 |
+
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
|
| 902 |
+
subprotocols: list[str] | None = None,
|
| 903 |
+
params: QueryParamTypes | None = None,
|
| 904 |
+
headers: HeaderTypes | None = None,
|
| 905 |
+
cookies: CookieTypes | None = None,
|
| 906 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 907 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 908 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 909 |
+
extensions: RequestExtensions | None = None,
|
| 910 |
+
) -> Generator[WebSocketSession]:
|
| 911 |
+
"""
|
| 912 |
+
Open a WebSocket session, using this client's configuration.
|
| 913 |
+
|
| 914 |
+
The session is closed automatically when exiting the context manager.
|
| 915 |
+
|
| 916 |
+
```python
|
| 917 |
+
with httpx2.Client() as client:
|
| 918 |
+
with client.websocket("ws://localhost:8000/ws") as ws:
|
| 919 |
+
ws.send_text("Hello!")
|
| 920 |
+
message = ws.receive_text()
|
| 921 |
+
```
|
| 922 |
+
"""
|
| 923 |
+
try:
|
| 924 |
+
from .websockets._api import connect_ws
|
| 925 |
+
except ImportError: # pragma: no cover
|
| 926 |
+
raise ImportError(
|
| 927 |
+
"WebSocket support requires the `wsproto` package. Install it with `pip install httpx2[ws]`."
|
| 928 |
+
)
|
| 929 |
+
|
| 930 |
+
with connect_ws(
|
| 931 |
+
str(url),
|
| 932 |
+
self,
|
| 933 |
+
max_message_size_bytes=max_message_size_bytes,
|
| 934 |
+
queue_size=queue_size,
|
| 935 |
+
keepalive_ping_interval_seconds=keepalive_ping_interval_seconds,
|
| 936 |
+
keepalive_ping_timeout_seconds=keepalive_ping_timeout_seconds,
|
| 937 |
+
subprotocols=subprotocols,
|
| 938 |
+
params=params,
|
| 939 |
+
headers=headers,
|
| 940 |
+
cookies=cookies,
|
| 941 |
+
auth=auth,
|
| 942 |
+
follow_redirects=follow_redirects,
|
| 943 |
+
timeout=timeout,
|
| 944 |
+
extensions=extensions,
|
| 945 |
+
) as session:
|
| 946 |
+
yield session
|
| 947 |
+
|
| 948 |
+
def send(
|
| 949 |
+
self,
|
| 950 |
+
request: Request,
|
| 951 |
+
*,
|
| 952 |
+
stream: bool = False,
|
| 953 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 954 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 955 |
+
) -> Response:
|
| 956 |
+
"""
|
| 957 |
+
Send a request.
|
| 958 |
+
|
| 959 |
+
The request is sent as-is, unmodified.
|
| 960 |
+
|
| 961 |
+
Typically you'll want to build one with `Client.build_request()`
|
| 962 |
+
so that any client-level configuration is merged into the request,
|
| 963 |
+
but passing an explicit `httpx2.Request()` is supported as well.
|
| 964 |
+
|
| 965 |
+
See also: [Request instances][0]
|
| 966 |
+
|
| 967 |
+
[0]: advanced/clients.md#request-instances
|
| 968 |
+
"""
|
| 969 |
+
if self._state == ClientState.CLOSED:
|
| 970 |
+
raise RuntimeError("Cannot send a request, as the client has been closed.")
|
| 971 |
+
|
| 972 |
+
self._state = ClientState.OPENED
|
| 973 |
+
follow_redirects = self.follow_redirects if isinstance(follow_redirects, UseClientDefault) else follow_redirects
|
| 974 |
+
|
| 975 |
+
self._set_timeout(request)
|
| 976 |
+
|
| 977 |
+
auth = self._build_request_auth(request, auth)
|
| 978 |
+
|
| 979 |
+
response = self._send_handling_auth(
|
| 980 |
+
request,
|
| 981 |
+
auth=auth,
|
| 982 |
+
follow_redirects=follow_redirects,
|
| 983 |
+
history=[],
|
| 984 |
+
)
|
| 985 |
+
try:
|
| 986 |
+
if not stream:
|
| 987 |
+
response.read()
|
| 988 |
+
|
| 989 |
+
return response
|
| 990 |
+
|
| 991 |
+
except BaseException as exc:
|
| 992 |
+
response.close()
|
| 993 |
+
raise exc
|
| 994 |
+
|
| 995 |
+
def _send_handling_auth(
|
| 996 |
+
self,
|
| 997 |
+
request: Request,
|
| 998 |
+
auth: Auth,
|
| 999 |
+
follow_redirects: bool,
|
| 1000 |
+
history: list[Response],
|
| 1001 |
+
) -> Response:
|
| 1002 |
+
auth_flow = auth.sync_auth_flow(request)
|
| 1003 |
+
try:
|
| 1004 |
+
request = next(auth_flow)
|
| 1005 |
+
|
| 1006 |
+
while True:
|
| 1007 |
+
response = self._send_handling_redirects(
|
| 1008 |
+
request,
|
| 1009 |
+
follow_redirects=follow_redirects,
|
| 1010 |
+
history=history,
|
| 1011 |
+
)
|
| 1012 |
+
try:
|
| 1013 |
+
try:
|
| 1014 |
+
next_request = auth_flow.send(response)
|
| 1015 |
+
except StopIteration:
|
| 1016 |
+
return response
|
| 1017 |
+
|
| 1018 |
+
response.history = list(history)
|
| 1019 |
+
response.read()
|
| 1020 |
+
request = next_request
|
| 1021 |
+
history.append(response)
|
| 1022 |
+
|
| 1023 |
+
except BaseException as exc:
|
| 1024 |
+
response.close()
|
| 1025 |
+
raise exc
|
| 1026 |
+
finally:
|
| 1027 |
+
auth_flow.close()
|
| 1028 |
+
|
| 1029 |
+
def _send_handling_redirects(
|
| 1030 |
+
self,
|
| 1031 |
+
request: Request,
|
| 1032 |
+
follow_redirects: bool,
|
| 1033 |
+
history: list[Response],
|
| 1034 |
+
) -> Response:
|
| 1035 |
+
while True:
|
| 1036 |
+
if len(history) > self.max_redirects:
|
| 1037 |
+
raise TooManyRedirects("Exceeded maximum allowed redirects.", request=request)
|
| 1038 |
+
|
| 1039 |
+
for hook in self._event_hooks["request"]:
|
| 1040 |
+
hook(request)
|
| 1041 |
+
|
| 1042 |
+
response = self._send_single_request(request)
|
| 1043 |
+
try:
|
| 1044 |
+
for hook in self._event_hooks["response"]:
|
| 1045 |
+
hook(response)
|
| 1046 |
+
response.history = list(history)
|
| 1047 |
+
|
| 1048 |
+
if not response.has_redirect_location:
|
| 1049 |
+
return response
|
| 1050 |
+
|
| 1051 |
+
request = self._build_redirect_request(request, response)
|
| 1052 |
+
history = history + [response]
|
| 1053 |
+
|
| 1054 |
+
if follow_redirects:
|
| 1055 |
+
response.read()
|
| 1056 |
+
else:
|
| 1057 |
+
response.next_request = request
|
| 1058 |
+
return response
|
| 1059 |
+
|
| 1060 |
+
except BaseException as exc:
|
| 1061 |
+
response.close()
|
| 1062 |
+
raise exc
|
| 1063 |
+
|
| 1064 |
+
def _send_single_request(self, request: Request) -> Response:
|
| 1065 |
+
"""
|
| 1066 |
+
Sends a single request, without handling any redirections.
|
| 1067 |
+
"""
|
| 1068 |
+
transport = self._transport_for_url(request.url)
|
| 1069 |
+
start = time.perf_counter()
|
| 1070 |
+
|
| 1071 |
+
if not isinstance(request.stream, SyncByteStream):
|
| 1072 |
+
raise RuntimeError("Attempted to send an async request with a sync Client instance.")
|
| 1073 |
+
|
| 1074 |
+
with request_context(request=request):
|
| 1075 |
+
response = transport.handle_request(request)
|
| 1076 |
+
|
| 1077 |
+
assert isinstance(response.stream, SyncByteStream)
|
| 1078 |
+
|
| 1079 |
+
response.request = request
|
| 1080 |
+
response.stream = BoundSyncStream(response.stream, start=start)
|
| 1081 |
+
self.cookies.extract_cookies(response)
|
| 1082 |
+
response.default_encoding = self._default_encoding
|
| 1083 |
+
|
| 1084 |
+
logger.info(
|
| 1085 |
+
'HTTP Request: %s %s "%s %d %s"',
|
| 1086 |
+
request.method,
|
| 1087 |
+
request.url,
|
| 1088 |
+
response.http_version,
|
| 1089 |
+
response.status_code,
|
| 1090 |
+
response.reason_phrase,
|
| 1091 |
+
)
|
| 1092 |
+
|
| 1093 |
+
return response
|
| 1094 |
+
|
| 1095 |
+
def get(
|
| 1096 |
+
self,
|
| 1097 |
+
url: URL | str,
|
| 1098 |
+
*,
|
| 1099 |
+
params: QueryParamTypes | None = None,
|
| 1100 |
+
headers: HeaderTypes | None = None,
|
| 1101 |
+
cookies: CookieTypes | None = None,
|
| 1102 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1103 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1104 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1105 |
+
extensions: RequestExtensions | None = None,
|
| 1106 |
+
) -> Response:
|
| 1107 |
+
"""
|
| 1108 |
+
Send a `GET` request.
|
| 1109 |
+
|
| 1110 |
+
**Parameters**: See `httpx2.request`.
|
| 1111 |
+
"""
|
| 1112 |
+
return self.request(
|
| 1113 |
+
"GET",
|
| 1114 |
+
url,
|
| 1115 |
+
params=params,
|
| 1116 |
+
headers=headers,
|
| 1117 |
+
cookies=cookies,
|
| 1118 |
+
auth=auth,
|
| 1119 |
+
follow_redirects=follow_redirects,
|
| 1120 |
+
timeout=timeout,
|
| 1121 |
+
extensions=extensions,
|
| 1122 |
+
)
|
| 1123 |
+
|
| 1124 |
+
def options(
|
| 1125 |
+
self,
|
| 1126 |
+
url: URL | str,
|
| 1127 |
+
*,
|
| 1128 |
+
params: QueryParamTypes | None = None,
|
| 1129 |
+
headers: HeaderTypes | None = None,
|
| 1130 |
+
cookies: CookieTypes | None = None,
|
| 1131 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1132 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1133 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1134 |
+
extensions: RequestExtensions | None = None,
|
| 1135 |
+
) -> Response:
|
| 1136 |
+
"""
|
| 1137 |
+
Send an `OPTIONS` request.
|
| 1138 |
+
|
| 1139 |
+
**Parameters**: See `httpx2.request`.
|
| 1140 |
+
"""
|
| 1141 |
+
return self.request(
|
| 1142 |
+
"OPTIONS",
|
| 1143 |
+
url,
|
| 1144 |
+
params=params,
|
| 1145 |
+
headers=headers,
|
| 1146 |
+
cookies=cookies,
|
| 1147 |
+
auth=auth,
|
| 1148 |
+
follow_redirects=follow_redirects,
|
| 1149 |
+
timeout=timeout,
|
| 1150 |
+
extensions=extensions,
|
| 1151 |
+
)
|
| 1152 |
+
|
| 1153 |
+
def head(
|
| 1154 |
+
self,
|
| 1155 |
+
url: URL | str,
|
| 1156 |
+
*,
|
| 1157 |
+
params: QueryParamTypes | None = None,
|
| 1158 |
+
headers: HeaderTypes | None = None,
|
| 1159 |
+
cookies: CookieTypes | None = None,
|
| 1160 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1161 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1162 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1163 |
+
extensions: RequestExtensions | None = None,
|
| 1164 |
+
) -> Response:
|
| 1165 |
+
"""
|
| 1166 |
+
Send a `HEAD` request.
|
| 1167 |
+
|
| 1168 |
+
**Parameters**: See `httpx2.request`.
|
| 1169 |
+
"""
|
| 1170 |
+
return self.request(
|
| 1171 |
+
"HEAD",
|
| 1172 |
+
url,
|
| 1173 |
+
params=params,
|
| 1174 |
+
headers=headers,
|
| 1175 |
+
cookies=cookies,
|
| 1176 |
+
auth=auth,
|
| 1177 |
+
follow_redirects=follow_redirects,
|
| 1178 |
+
timeout=timeout,
|
| 1179 |
+
extensions=extensions,
|
| 1180 |
+
)
|
| 1181 |
+
|
| 1182 |
+
def post(
|
| 1183 |
+
self,
|
| 1184 |
+
url: URL | str,
|
| 1185 |
+
*,
|
| 1186 |
+
content: RequestContent | None = None,
|
| 1187 |
+
data: RequestData | None = None,
|
| 1188 |
+
files: RequestFiles | None = None,
|
| 1189 |
+
json: typing.Any | None = None,
|
| 1190 |
+
params: QueryParamTypes | None = None,
|
| 1191 |
+
headers: HeaderTypes | None = None,
|
| 1192 |
+
cookies: CookieTypes | None = None,
|
| 1193 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1194 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1195 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1196 |
+
extensions: RequestExtensions | None = None,
|
| 1197 |
+
) -> Response:
|
| 1198 |
+
"""
|
| 1199 |
+
Send a `POST` request.
|
| 1200 |
+
|
| 1201 |
+
**Parameters**: See `httpx2.request`.
|
| 1202 |
+
"""
|
| 1203 |
+
return self.request(
|
| 1204 |
+
"POST",
|
| 1205 |
+
url,
|
| 1206 |
+
content=content,
|
| 1207 |
+
data=data,
|
| 1208 |
+
files=files,
|
| 1209 |
+
json=json,
|
| 1210 |
+
params=params,
|
| 1211 |
+
headers=headers,
|
| 1212 |
+
cookies=cookies,
|
| 1213 |
+
auth=auth,
|
| 1214 |
+
follow_redirects=follow_redirects,
|
| 1215 |
+
timeout=timeout,
|
| 1216 |
+
extensions=extensions,
|
| 1217 |
+
)
|
| 1218 |
+
|
| 1219 |
+
def put(
|
| 1220 |
+
self,
|
| 1221 |
+
url: URL | str,
|
| 1222 |
+
*,
|
| 1223 |
+
content: RequestContent | None = None,
|
| 1224 |
+
data: RequestData | None = None,
|
| 1225 |
+
files: RequestFiles | None = None,
|
| 1226 |
+
json: typing.Any | None = None,
|
| 1227 |
+
params: QueryParamTypes | None = None,
|
| 1228 |
+
headers: HeaderTypes | None = None,
|
| 1229 |
+
cookies: CookieTypes | None = None,
|
| 1230 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1231 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1232 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1233 |
+
extensions: RequestExtensions | None = None,
|
| 1234 |
+
) -> Response:
|
| 1235 |
+
"""
|
| 1236 |
+
Send a `PUT` request.
|
| 1237 |
+
|
| 1238 |
+
**Parameters**: See `httpx2.request`.
|
| 1239 |
+
"""
|
| 1240 |
+
return self.request(
|
| 1241 |
+
"PUT",
|
| 1242 |
+
url,
|
| 1243 |
+
content=content,
|
| 1244 |
+
data=data,
|
| 1245 |
+
files=files,
|
| 1246 |
+
json=json,
|
| 1247 |
+
params=params,
|
| 1248 |
+
headers=headers,
|
| 1249 |
+
cookies=cookies,
|
| 1250 |
+
auth=auth,
|
| 1251 |
+
follow_redirects=follow_redirects,
|
| 1252 |
+
timeout=timeout,
|
| 1253 |
+
extensions=extensions,
|
| 1254 |
+
)
|
| 1255 |
+
|
| 1256 |
+
def patch(
|
| 1257 |
+
self,
|
| 1258 |
+
url: URL | str,
|
| 1259 |
+
*,
|
| 1260 |
+
content: RequestContent | None = None,
|
| 1261 |
+
data: RequestData | None = None,
|
| 1262 |
+
files: RequestFiles | None = None,
|
| 1263 |
+
json: typing.Any | None = None,
|
| 1264 |
+
params: QueryParamTypes | None = None,
|
| 1265 |
+
headers: HeaderTypes | None = None,
|
| 1266 |
+
cookies: CookieTypes | None = None,
|
| 1267 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1268 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1269 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1270 |
+
extensions: RequestExtensions | None = None,
|
| 1271 |
+
) -> Response:
|
| 1272 |
+
"""
|
| 1273 |
+
Send a `PATCH` request.
|
| 1274 |
+
|
| 1275 |
+
**Parameters**: See `httpx2.request`.
|
| 1276 |
+
"""
|
| 1277 |
+
return self.request(
|
| 1278 |
+
"PATCH",
|
| 1279 |
+
url,
|
| 1280 |
+
content=content,
|
| 1281 |
+
data=data,
|
| 1282 |
+
files=files,
|
| 1283 |
+
json=json,
|
| 1284 |
+
params=params,
|
| 1285 |
+
headers=headers,
|
| 1286 |
+
cookies=cookies,
|
| 1287 |
+
auth=auth,
|
| 1288 |
+
follow_redirects=follow_redirects,
|
| 1289 |
+
timeout=timeout,
|
| 1290 |
+
extensions=extensions,
|
| 1291 |
+
)
|
| 1292 |
+
|
| 1293 |
+
def delete(
|
| 1294 |
+
self,
|
| 1295 |
+
url: URL | str,
|
| 1296 |
+
*,
|
| 1297 |
+
params: QueryParamTypes | None = None,
|
| 1298 |
+
headers: HeaderTypes | None = None,
|
| 1299 |
+
cookies: CookieTypes | None = None,
|
| 1300 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1301 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1302 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1303 |
+
extensions: RequestExtensions | None = None,
|
| 1304 |
+
) -> Response:
|
| 1305 |
+
"""
|
| 1306 |
+
Send a `DELETE` request.
|
| 1307 |
+
|
| 1308 |
+
**Parameters**: See `httpx2.request`.
|
| 1309 |
+
"""
|
| 1310 |
+
return self.request(
|
| 1311 |
+
"DELETE",
|
| 1312 |
+
url,
|
| 1313 |
+
params=params,
|
| 1314 |
+
headers=headers,
|
| 1315 |
+
cookies=cookies,
|
| 1316 |
+
auth=auth,
|
| 1317 |
+
follow_redirects=follow_redirects,
|
| 1318 |
+
timeout=timeout,
|
| 1319 |
+
extensions=extensions,
|
| 1320 |
+
)
|
| 1321 |
+
|
| 1322 |
+
def query(
|
| 1323 |
+
self,
|
| 1324 |
+
url: URL | str,
|
| 1325 |
+
*,
|
| 1326 |
+
content: RequestContent | None = None,
|
| 1327 |
+
data: RequestData | None = None,
|
| 1328 |
+
files: RequestFiles | None = None,
|
| 1329 |
+
json: typing.Any | None = None,
|
| 1330 |
+
params: QueryParamTypes | None = None,
|
| 1331 |
+
headers: HeaderTypes | None = None,
|
| 1332 |
+
cookies: CookieTypes | None = None,
|
| 1333 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1334 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1335 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1336 |
+
extensions: RequestExtensions | None = None,
|
| 1337 |
+
) -> Response:
|
| 1338 |
+
"""
|
| 1339 |
+
Send a `QUERY` request.
|
| 1340 |
+
|
| 1341 |
+
**Parameters**: See `httpx2.request`.
|
| 1342 |
+
"""
|
| 1343 |
+
return self.request(
|
| 1344 |
+
"QUERY",
|
| 1345 |
+
url,
|
| 1346 |
+
content=content,
|
| 1347 |
+
data=data,
|
| 1348 |
+
files=files,
|
| 1349 |
+
json=json,
|
| 1350 |
+
params=params,
|
| 1351 |
+
headers=headers,
|
| 1352 |
+
cookies=cookies,
|
| 1353 |
+
auth=auth,
|
| 1354 |
+
follow_redirects=follow_redirects,
|
| 1355 |
+
timeout=timeout,
|
| 1356 |
+
extensions=extensions,
|
| 1357 |
+
)
|
| 1358 |
+
|
| 1359 |
+
def close(self) -> None:
|
| 1360 |
+
"""
|
| 1361 |
+
Close transport and proxies.
|
| 1362 |
+
"""
|
| 1363 |
+
if self._state != ClientState.CLOSED:
|
| 1364 |
+
self._state = ClientState.CLOSED
|
| 1365 |
+
|
| 1366 |
+
self._transport.close()
|
| 1367 |
+
for transport in self._mounts.values():
|
| 1368 |
+
if transport is not None:
|
| 1369 |
+
transport.close()
|
| 1370 |
+
|
| 1371 |
+
def __enter__(self: T) -> T:
|
| 1372 |
+
if self._state != ClientState.UNOPENED:
|
| 1373 |
+
msg = {
|
| 1374 |
+
ClientState.OPENED: "Cannot open a client instance more than once.",
|
| 1375 |
+
ClientState.CLOSED: ("Cannot reopen a client instance, once it has been closed."),
|
| 1376 |
+
}[self._state]
|
| 1377 |
+
raise RuntimeError(msg)
|
| 1378 |
+
|
| 1379 |
+
self._state = ClientState.OPENED
|
| 1380 |
+
|
| 1381 |
+
self._transport.__enter__()
|
| 1382 |
+
for transport in self._mounts.values():
|
| 1383 |
+
if transport is not None:
|
| 1384 |
+
transport.__enter__()
|
| 1385 |
+
return self
|
| 1386 |
+
|
| 1387 |
+
def __exit__(
|
| 1388 |
+
self,
|
| 1389 |
+
exc_type: type[BaseException] | None = None,
|
| 1390 |
+
exc_value: BaseException | None = None,
|
| 1391 |
+
traceback: TracebackType | None = None,
|
| 1392 |
+
) -> None:
|
| 1393 |
+
self._state = ClientState.CLOSED
|
| 1394 |
+
|
| 1395 |
+
self._transport.__exit__(exc_type, exc_value, traceback)
|
| 1396 |
+
for transport in self._mounts.values():
|
| 1397 |
+
if transport is not None:
|
| 1398 |
+
transport.__exit__(exc_type, exc_value, traceback)
|
| 1399 |
+
|
| 1400 |
+
|
| 1401 |
+
class AsyncClient(BaseClient):
|
| 1402 |
+
"""
|
| 1403 |
+
An asynchronous HTTP client, with connection pooling, HTTP/2, redirects,
|
| 1404 |
+
cookie persistence, etc.
|
| 1405 |
+
|
| 1406 |
+
It can be shared between tasks.
|
| 1407 |
+
|
| 1408 |
+
Usage:
|
| 1409 |
+
|
| 1410 |
+
```python
|
| 1411 |
+
>>> async with httpx2.AsyncClient() as client:
|
| 1412 |
+
>>> response = await client.get('https://example.org')
|
| 1413 |
+
```
|
| 1414 |
+
|
| 1415 |
+
**Parameters:**
|
| 1416 |
+
|
| 1417 |
+
* **auth** - *(optional)* An authentication class to use when sending
|
| 1418 |
+
requests.
|
| 1419 |
+
* **params** - *(optional)* Query parameters to include in request URLs, as
|
| 1420 |
+
a string, dictionary, or sequence of two-tuples.
|
| 1421 |
+
* **headers** - *(optional)* Dictionary of HTTP headers to include when
|
| 1422 |
+
sending requests.
|
| 1423 |
+
* **cookies** - *(optional)* Dictionary of Cookie items to include when
|
| 1424 |
+
sending requests.
|
| 1425 |
+
* **verify** - *(optional)* Either `True` to use an SSL context with the
|
| 1426 |
+
default CA bundle, `False` to disable verification, or an instance of
|
| 1427 |
+
`ssl.SSLContext` to use a custom context.
|
| 1428 |
+
* **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
|
| 1429 |
+
enabled. Defaults to `False`.
|
| 1430 |
+
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
|
| 1431 |
+
* **mounts** - *(optional)* A dictionary mapping URL patterns to transports,
|
| 1432 |
+
used to route requests through specific transports based on the URL.
|
| 1433 |
+
* **timeout** - *(optional)* The timeout configuration to use when sending
|
| 1434 |
+
requests.
|
| 1435 |
+
* **limits** - *(optional)* The limits configuration to use.
|
| 1436 |
+
* **max_redirects** - *(optional)* The maximum number of redirect responses
|
| 1437 |
+
that should be followed.
|
| 1438 |
+
* **base_url** - *(optional)* A URL to use as the base when building
|
| 1439 |
+
request URLs.
|
| 1440 |
+
* **transport** - *(optional)* A transport class to use for sending requests
|
| 1441 |
+
over the network.
|
| 1442 |
+
* **trust_env** - *(optional)* Enables or disables usage of environment
|
| 1443 |
+
variables for configuration.
|
| 1444 |
+
* **default_encoding** - *(optional)* The default encoding to use for decoding
|
| 1445 |
+
response text, if no charset information is included in a response Content-Type
|
| 1446 |
+
header. Set to a callable for automatic character set detection. Default: "utf-8".
|
| 1447 |
+
"""
|
| 1448 |
+
|
| 1449 |
+
def __init__(
|
| 1450 |
+
self,
|
| 1451 |
+
*,
|
| 1452 |
+
auth: AuthTypes | None = None,
|
| 1453 |
+
params: QueryParamTypes | None = None,
|
| 1454 |
+
headers: HeaderTypes | None = None,
|
| 1455 |
+
cookies: CookieTypes | None = None,
|
| 1456 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 1457 |
+
cert: CertTypes | None = None,
|
| 1458 |
+
http1: bool = True,
|
| 1459 |
+
http2: bool = False,
|
| 1460 |
+
proxy: ProxyTypes | None = None,
|
| 1461 |
+
mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None,
|
| 1462 |
+
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
|
| 1463 |
+
follow_redirects: bool = False,
|
| 1464 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 1465 |
+
max_redirects: int = DEFAULT_MAX_REDIRECTS,
|
| 1466 |
+
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
|
| 1467 |
+
base_url: URL | str = "",
|
| 1468 |
+
transport: AsyncBaseTransport | None = None,
|
| 1469 |
+
trust_env: bool = True,
|
| 1470 |
+
default_encoding: str | typing.Callable[[bytes], str | None] = "utf-8",
|
| 1471 |
+
) -> None:
|
| 1472 |
+
super().__init__(
|
| 1473 |
+
auth=auth,
|
| 1474 |
+
params=params,
|
| 1475 |
+
headers=headers,
|
| 1476 |
+
cookies=cookies,
|
| 1477 |
+
timeout=timeout,
|
| 1478 |
+
follow_redirects=follow_redirects,
|
| 1479 |
+
max_redirects=max_redirects,
|
| 1480 |
+
event_hooks=event_hooks,
|
| 1481 |
+
base_url=base_url,
|
| 1482 |
+
trust_env=trust_env,
|
| 1483 |
+
default_encoding=default_encoding,
|
| 1484 |
+
)
|
| 1485 |
+
|
| 1486 |
+
if http2:
|
| 1487 |
+
try:
|
| 1488 |
+
import h2 # noqa
|
| 1489 |
+
except ImportError: # pragma: no cover
|
| 1490 |
+
raise ImportError(
|
| 1491 |
+
"Using http2=True, but the 'h2' package is not installed. "
|
| 1492 |
+
"Make sure to install httpx using `pip install httpx[http2]`."
|
| 1493 |
+
) from None
|
| 1494 |
+
|
| 1495 |
+
allow_env_proxies = trust_env and transport is None
|
| 1496 |
+
proxy_map = self._get_proxy_map(proxy, allow_env_proxies)
|
| 1497 |
+
|
| 1498 |
+
self._transport = self._init_transport(
|
| 1499 |
+
verify=verify,
|
| 1500 |
+
cert=cert,
|
| 1501 |
+
trust_env=trust_env,
|
| 1502 |
+
http1=http1,
|
| 1503 |
+
http2=http2,
|
| 1504 |
+
limits=limits,
|
| 1505 |
+
transport=transport,
|
| 1506 |
+
)
|
| 1507 |
+
|
| 1508 |
+
self._mounts: dict[URLPattern, AsyncBaseTransport | None] = {
|
| 1509 |
+
URLPattern(key): None
|
| 1510 |
+
if proxy is None
|
| 1511 |
+
else self._init_proxy_transport(
|
| 1512 |
+
proxy,
|
| 1513 |
+
verify=verify,
|
| 1514 |
+
cert=cert,
|
| 1515 |
+
trust_env=trust_env,
|
| 1516 |
+
http1=http1,
|
| 1517 |
+
http2=http2,
|
| 1518 |
+
limits=limits,
|
| 1519 |
+
)
|
| 1520 |
+
for key, proxy in proxy_map.items()
|
| 1521 |
+
}
|
| 1522 |
+
if mounts is not None:
|
| 1523 |
+
self._mounts.update({URLPattern(key): transport for key, transport in mounts.items()})
|
| 1524 |
+
self._mounts = dict(sorted(self._mounts.items()))
|
| 1525 |
+
|
| 1526 |
+
def _init_transport(
|
| 1527 |
+
self,
|
| 1528 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 1529 |
+
cert: CertTypes | None = None,
|
| 1530 |
+
trust_env: bool = True,
|
| 1531 |
+
http1: bool = True,
|
| 1532 |
+
http2: bool = False,
|
| 1533 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 1534 |
+
transport: AsyncBaseTransport | None = None,
|
| 1535 |
+
) -> AsyncBaseTransport:
|
| 1536 |
+
if transport is not None:
|
| 1537 |
+
return transport
|
| 1538 |
+
|
| 1539 |
+
return AsyncHTTPTransport(
|
| 1540 |
+
verify=verify,
|
| 1541 |
+
cert=cert,
|
| 1542 |
+
trust_env=trust_env,
|
| 1543 |
+
http1=http1,
|
| 1544 |
+
http2=http2,
|
| 1545 |
+
limits=limits,
|
| 1546 |
+
)
|
| 1547 |
+
|
| 1548 |
+
def _init_proxy_transport(
|
| 1549 |
+
self,
|
| 1550 |
+
proxy: Proxy,
|
| 1551 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 1552 |
+
cert: CertTypes | None = None,
|
| 1553 |
+
trust_env: bool = True,
|
| 1554 |
+
http1: bool = True,
|
| 1555 |
+
http2: bool = False,
|
| 1556 |
+
limits: Limits = DEFAULT_LIMITS,
|
| 1557 |
+
) -> AsyncBaseTransport:
|
| 1558 |
+
return AsyncHTTPTransport(
|
| 1559 |
+
verify=verify,
|
| 1560 |
+
cert=cert,
|
| 1561 |
+
trust_env=trust_env,
|
| 1562 |
+
http1=http1,
|
| 1563 |
+
http2=http2,
|
| 1564 |
+
limits=limits,
|
| 1565 |
+
proxy=proxy,
|
| 1566 |
+
)
|
| 1567 |
+
|
| 1568 |
+
def _transport_for_url(self, url: URL) -> AsyncBaseTransport:
|
| 1569 |
+
"""
|
| 1570 |
+
Returns the transport instance that should be used for a given URL.
|
| 1571 |
+
This will either be the standard connection pool, or a proxy.
|
| 1572 |
+
"""
|
| 1573 |
+
for pattern, transport in self._mounts.items():
|
| 1574 |
+
if pattern.matches(url):
|
| 1575 |
+
return self._transport if transport is None else transport
|
| 1576 |
+
|
| 1577 |
+
return self._transport
|
| 1578 |
+
|
| 1579 |
+
async def request(
|
| 1580 |
+
self,
|
| 1581 |
+
method: str,
|
| 1582 |
+
url: URL | str,
|
| 1583 |
+
*,
|
| 1584 |
+
content: RequestContent | None = None,
|
| 1585 |
+
data: RequestData | None = None,
|
| 1586 |
+
files: RequestFiles | None = None,
|
| 1587 |
+
json: typing.Any | None = None,
|
| 1588 |
+
params: QueryParamTypes | None = None,
|
| 1589 |
+
headers: HeaderTypes | None = None,
|
| 1590 |
+
cookies: CookieTypes | None = None,
|
| 1591 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1592 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1593 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1594 |
+
extensions: RequestExtensions | None = None,
|
| 1595 |
+
) -> Response:
|
| 1596 |
+
"""
|
| 1597 |
+
Build and send a request.
|
| 1598 |
+
|
| 1599 |
+
Equivalent to:
|
| 1600 |
+
|
| 1601 |
+
```python
|
| 1602 |
+
request = client.build_request(...)
|
| 1603 |
+
response = await client.send(request, ...)
|
| 1604 |
+
```
|
| 1605 |
+
|
| 1606 |
+
See `AsyncClient.build_request()`, `AsyncClient.send()`
|
| 1607 |
+
and [Merging of configuration][0] for how the various parameters
|
| 1608 |
+
are merged with client-level configuration.
|
| 1609 |
+
|
| 1610 |
+
[0]: advanced/clients.md#merging-of-configuration
|
| 1611 |
+
"""
|
| 1612 |
+
|
| 1613 |
+
if cookies is not None: # pragma: no cover
|
| 1614 |
+
message = (
|
| 1615 |
+
"Setting per-request cookies=<...> is being deprecated, because "
|
| 1616 |
+
"the expected behaviour on cookie persistence is ambiguous. Set "
|
| 1617 |
+
"cookies directly on the client instance instead."
|
| 1618 |
+
)
|
| 1619 |
+
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
| 1620 |
+
|
| 1621 |
+
request = self.build_request(
|
| 1622 |
+
method=method,
|
| 1623 |
+
url=url,
|
| 1624 |
+
content=content,
|
| 1625 |
+
data=data,
|
| 1626 |
+
files=files,
|
| 1627 |
+
json=json,
|
| 1628 |
+
params=params,
|
| 1629 |
+
headers=headers,
|
| 1630 |
+
cookies=cookies,
|
| 1631 |
+
timeout=timeout,
|
| 1632 |
+
extensions=extensions,
|
| 1633 |
+
)
|
| 1634 |
+
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
|
| 1635 |
+
|
| 1636 |
+
@asynccontextmanager
|
| 1637 |
+
async def stream(
|
| 1638 |
+
self,
|
| 1639 |
+
method: str,
|
| 1640 |
+
url: URL | str,
|
| 1641 |
+
*,
|
| 1642 |
+
content: RequestContent | None = None,
|
| 1643 |
+
data: RequestData | None = None,
|
| 1644 |
+
files: RequestFiles | None = None,
|
| 1645 |
+
json: typing.Any | None = None,
|
| 1646 |
+
params: QueryParamTypes | None = None,
|
| 1647 |
+
headers: HeaderTypes | None = None,
|
| 1648 |
+
cookies: CookieTypes | None = None,
|
| 1649 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1650 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1651 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1652 |
+
extensions: RequestExtensions | None = None,
|
| 1653 |
+
) -> AsyncGenerator[Response]:
|
| 1654 |
+
"""
|
| 1655 |
+
Alternative to `httpx2.request()` that streams the response body
|
| 1656 |
+
instead of loading it into memory at once.
|
| 1657 |
+
|
| 1658 |
+
**Parameters**: See `httpx2.request`.
|
| 1659 |
+
|
| 1660 |
+
See also: [Streaming Responses][0]
|
| 1661 |
+
|
| 1662 |
+
[0]: quickstart.md#streaming-responses
|
| 1663 |
+
"""
|
| 1664 |
+
request = self.build_request(
|
| 1665 |
+
method=method,
|
| 1666 |
+
url=url,
|
| 1667 |
+
content=content,
|
| 1668 |
+
data=data,
|
| 1669 |
+
files=files,
|
| 1670 |
+
json=json,
|
| 1671 |
+
params=params,
|
| 1672 |
+
headers=headers,
|
| 1673 |
+
cookies=cookies,
|
| 1674 |
+
timeout=timeout,
|
| 1675 |
+
extensions=extensions,
|
| 1676 |
+
)
|
| 1677 |
+
response = await self.send(
|
| 1678 |
+
request=request,
|
| 1679 |
+
auth=auth,
|
| 1680 |
+
follow_redirects=follow_redirects,
|
| 1681 |
+
stream=True,
|
| 1682 |
+
)
|
| 1683 |
+
try:
|
| 1684 |
+
yield response
|
| 1685 |
+
finally:
|
| 1686 |
+
await response.aclose()
|
| 1687 |
+
|
| 1688 |
+
@asynccontextmanager
|
| 1689 |
+
async def sse(
|
| 1690 |
+
self,
|
| 1691 |
+
url: URL | str,
|
| 1692 |
+
*,
|
| 1693 |
+
method: str = "GET",
|
| 1694 |
+
content: RequestContent | None = None,
|
| 1695 |
+
data: RequestData | None = None,
|
| 1696 |
+
files: RequestFiles | None = None,
|
| 1697 |
+
json: typing.Any | None = None,
|
| 1698 |
+
params: QueryParamTypes | None = None,
|
| 1699 |
+
headers: HeaderTypes | None = None,
|
| 1700 |
+
cookies: CookieTypes | None = None,
|
| 1701 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1702 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1703 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1704 |
+
extensions: RequestExtensions | None = None,
|
| 1705 |
+
max_event_size: int | None = DEFAULT_MAX_EVENT_SIZE_BYTES,
|
| 1706 |
+
) -> AsyncGenerator[EventSource]:
|
| 1707 |
+
"""
|
| 1708 |
+
Connect to a server-sent events endpoint and yield an `EventSource`.
|
| 1709 |
+
|
| 1710 |
+
Iterating the `EventSource` yields `ServerSentEvent` instances.
|
| 1711 |
+
|
| 1712 |
+
**Parameters**: See `httpx2.request`.
|
| 1713 |
+
"""
|
| 1714 |
+
async with self.stream(
|
| 1715 |
+
method,
|
| 1716 |
+
url,
|
| 1717 |
+
content=content,
|
| 1718 |
+
data=data,
|
| 1719 |
+
files=files,
|
| 1720 |
+
json=json,
|
| 1721 |
+
params=params,
|
| 1722 |
+
headers={"Accept": "text/event-stream", "Cache-Control": "no-store"} | Headers(headers),
|
| 1723 |
+
cookies=cookies,
|
| 1724 |
+
auth=auth,
|
| 1725 |
+
follow_redirects=follow_redirects,
|
| 1726 |
+
timeout=timeout,
|
| 1727 |
+
extensions=extensions,
|
| 1728 |
+
) as response:
|
| 1729 |
+
yield EventSource(response, max_event_size=max_event_size)
|
| 1730 |
+
|
| 1731 |
+
@asynccontextmanager
|
| 1732 |
+
async def websocket(
|
| 1733 |
+
self,
|
| 1734 |
+
url: URL | str,
|
| 1735 |
+
*,
|
| 1736 |
+
max_message_size_bytes: int = DEFAULT_MAX_MESSAGE_SIZE_BYTES,
|
| 1737 |
+
queue_size: int = DEFAULT_QUEUE_SIZE,
|
| 1738 |
+
keepalive_ping_interval_seconds: float | None = DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS,
|
| 1739 |
+
keepalive_ping_timeout_seconds: float | None = DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS,
|
| 1740 |
+
subprotocols: list[str] | None = None,
|
| 1741 |
+
params: QueryParamTypes | None = None,
|
| 1742 |
+
headers: HeaderTypes | None = None,
|
| 1743 |
+
cookies: CookieTypes | None = None,
|
| 1744 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1745 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1746 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1747 |
+
extensions: RequestExtensions | None = None,
|
| 1748 |
+
) -> AsyncGenerator[AsyncWebSocketSession]:
|
| 1749 |
+
"""
|
| 1750 |
+
Open a WebSocket session, using this client's configuration.
|
| 1751 |
+
|
| 1752 |
+
The session is closed automatically when exiting the context manager.
|
| 1753 |
+
|
| 1754 |
+
```python
|
| 1755 |
+
async with httpx2.AsyncClient() as client:
|
| 1756 |
+
async with client.websocket("ws://localhost:8000/ws") as ws:
|
| 1757 |
+
await ws.send_text("Hello!")
|
| 1758 |
+
message = await ws.receive_text()
|
| 1759 |
+
```
|
| 1760 |
+
"""
|
| 1761 |
+
try:
|
| 1762 |
+
from .websockets._api import aconnect_ws
|
| 1763 |
+
except ImportError: # pragma: no cover
|
| 1764 |
+
raise ImportError(
|
| 1765 |
+
"WebSocket support requires the `wsproto` package. Install it with `pip install httpx2[ws]`."
|
| 1766 |
+
)
|
| 1767 |
+
|
| 1768 |
+
async with aconnect_ws(
|
| 1769 |
+
str(url),
|
| 1770 |
+
self,
|
| 1771 |
+
max_message_size_bytes=max_message_size_bytes,
|
| 1772 |
+
queue_size=queue_size,
|
| 1773 |
+
keepalive_ping_interval_seconds=keepalive_ping_interval_seconds,
|
| 1774 |
+
keepalive_ping_timeout_seconds=keepalive_ping_timeout_seconds,
|
| 1775 |
+
subprotocols=subprotocols,
|
| 1776 |
+
params=params,
|
| 1777 |
+
headers=headers,
|
| 1778 |
+
cookies=cookies,
|
| 1779 |
+
auth=auth,
|
| 1780 |
+
follow_redirects=follow_redirects,
|
| 1781 |
+
timeout=timeout,
|
| 1782 |
+
extensions=extensions,
|
| 1783 |
+
) as session:
|
| 1784 |
+
yield session
|
| 1785 |
+
|
| 1786 |
+
async def send(
|
| 1787 |
+
self,
|
| 1788 |
+
request: Request,
|
| 1789 |
+
*,
|
| 1790 |
+
stream: bool = False,
|
| 1791 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1792 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1793 |
+
) -> Response:
|
| 1794 |
+
"""
|
| 1795 |
+
Send a request.
|
| 1796 |
+
|
| 1797 |
+
The request is sent as-is, unmodified.
|
| 1798 |
+
|
| 1799 |
+
Typically you'll want to build one with `AsyncClient.build_request()`
|
| 1800 |
+
so that any client-level configuration is merged into the request,
|
| 1801 |
+
but passing an explicit `httpx2.Request()` is supported as well.
|
| 1802 |
+
|
| 1803 |
+
See also: [Request instances][0]
|
| 1804 |
+
|
| 1805 |
+
[0]: advanced/clients.md#request-instances
|
| 1806 |
+
"""
|
| 1807 |
+
if self._state == ClientState.CLOSED:
|
| 1808 |
+
raise RuntimeError("Cannot send a request, as the client has been closed.")
|
| 1809 |
+
|
| 1810 |
+
self._state = ClientState.OPENED
|
| 1811 |
+
follow_redirects = self.follow_redirects if isinstance(follow_redirects, UseClientDefault) else follow_redirects
|
| 1812 |
+
|
| 1813 |
+
self._set_timeout(request)
|
| 1814 |
+
|
| 1815 |
+
auth = self._build_request_auth(request, auth)
|
| 1816 |
+
|
| 1817 |
+
response = await self._send_handling_auth(
|
| 1818 |
+
request,
|
| 1819 |
+
auth=auth,
|
| 1820 |
+
follow_redirects=follow_redirects,
|
| 1821 |
+
history=[],
|
| 1822 |
+
)
|
| 1823 |
+
try:
|
| 1824 |
+
if not stream:
|
| 1825 |
+
await response.aread()
|
| 1826 |
+
|
| 1827 |
+
return response
|
| 1828 |
+
|
| 1829 |
+
except BaseException as exc:
|
| 1830 |
+
await response.aclose()
|
| 1831 |
+
raise exc
|
| 1832 |
+
|
| 1833 |
+
async def _send_handling_auth(
|
| 1834 |
+
self,
|
| 1835 |
+
request: Request,
|
| 1836 |
+
auth: Auth,
|
| 1837 |
+
follow_redirects: bool,
|
| 1838 |
+
history: list[Response],
|
| 1839 |
+
) -> Response:
|
| 1840 |
+
auth_flow = auth.async_auth_flow(request)
|
| 1841 |
+
try:
|
| 1842 |
+
request = await auth_flow.__anext__()
|
| 1843 |
+
|
| 1844 |
+
while True:
|
| 1845 |
+
response = await self._send_handling_redirects(
|
| 1846 |
+
request,
|
| 1847 |
+
follow_redirects=follow_redirects,
|
| 1848 |
+
history=history,
|
| 1849 |
+
)
|
| 1850 |
+
try:
|
| 1851 |
+
try:
|
| 1852 |
+
next_request = await auth_flow.asend(response)
|
| 1853 |
+
except StopAsyncIteration:
|
| 1854 |
+
return response
|
| 1855 |
+
|
| 1856 |
+
response.history = list(history)
|
| 1857 |
+
await response.aread()
|
| 1858 |
+
request = next_request
|
| 1859 |
+
history.append(response)
|
| 1860 |
+
|
| 1861 |
+
except BaseException as exc:
|
| 1862 |
+
await response.aclose()
|
| 1863 |
+
raise exc
|
| 1864 |
+
finally:
|
| 1865 |
+
await auth_flow.aclose()
|
| 1866 |
+
|
| 1867 |
+
async def _send_handling_redirects(
|
| 1868 |
+
self,
|
| 1869 |
+
request: Request,
|
| 1870 |
+
follow_redirects: bool,
|
| 1871 |
+
history: list[Response],
|
| 1872 |
+
) -> Response:
|
| 1873 |
+
while True:
|
| 1874 |
+
if len(history) > self.max_redirects:
|
| 1875 |
+
raise TooManyRedirects("Exceeded maximum allowed redirects.", request=request)
|
| 1876 |
+
|
| 1877 |
+
for hook in self._event_hooks["request"]:
|
| 1878 |
+
await hook(request)
|
| 1879 |
+
|
| 1880 |
+
response = await self._send_single_request(request)
|
| 1881 |
+
try:
|
| 1882 |
+
for hook in self._event_hooks["response"]:
|
| 1883 |
+
await hook(response)
|
| 1884 |
+
|
| 1885 |
+
response.history = list(history)
|
| 1886 |
+
|
| 1887 |
+
if not response.has_redirect_location:
|
| 1888 |
+
return response
|
| 1889 |
+
|
| 1890 |
+
request = self._build_redirect_request(request, response)
|
| 1891 |
+
history = history + [response]
|
| 1892 |
+
|
| 1893 |
+
if follow_redirects:
|
| 1894 |
+
await response.aread()
|
| 1895 |
+
else:
|
| 1896 |
+
response.next_request = request
|
| 1897 |
+
return response
|
| 1898 |
+
|
| 1899 |
+
except BaseException as exc:
|
| 1900 |
+
await response.aclose()
|
| 1901 |
+
raise exc
|
| 1902 |
+
|
| 1903 |
+
async def _send_single_request(self, request: Request) -> Response:
|
| 1904 |
+
"""
|
| 1905 |
+
Sends a single request, without handling any redirections.
|
| 1906 |
+
"""
|
| 1907 |
+
transport = self._transport_for_url(request.url)
|
| 1908 |
+
start = time.perf_counter()
|
| 1909 |
+
|
| 1910 |
+
if not isinstance(request.stream, AsyncByteStream):
|
| 1911 |
+
raise RuntimeError("Attempted to send a sync request with an AsyncClient instance.")
|
| 1912 |
+
|
| 1913 |
+
with request_context(request=request):
|
| 1914 |
+
response = await transport.handle_async_request(request)
|
| 1915 |
+
|
| 1916 |
+
assert isinstance(response.stream, AsyncByteStream)
|
| 1917 |
+
response.request = request
|
| 1918 |
+
response.stream = BoundAsyncStream(response.stream, start=start)
|
| 1919 |
+
self.cookies.extract_cookies(response)
|
| 1920 |
+
response.default_encoding = self._default_encoding
|
| 1921 |
+
|
| 1922 |
+
logger.info(
|
| 1923 |
+
'HTTP Request: %s %s "%s %d %s"',
|
| 1924 |
+
request.method,
|
| 1925 |
+
request.url,
|
| 1926 |
+
response.http_version,
|
| 1927 |
+
response.status_code,
|
| 1928 |
+
response.reason_phrase,
|
| 1929 |
+
)
|
| 1930 |
+
|
| 1931 |
+
return response
|
| 1932 |
+
|
| 1933 |
+
async def get(
|
| 1934 |
+
self,
|
| 1935 |
+
url: URL | str,
|
| 1936 |
+
*,
|
| 1937 |
+
params: QueryParamTypes | None = None,
|
| 1938 |
+
headers: HeaderTypes | None = None,
|
| 1939 |
+
cookies: CookieTypes | None = None,
|
| 1940 |
+
auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT,
|
| 1941 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1942 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1943 |
+
extensions: RequestExtensions | None = None,
|
| 1944 |
+
) -> Response:
|
| 1945 |
+
"""
|
| 1946 |
+
Send a `GET` request.
|
| 1947 |
+
|
| 1948 |
+
**Parameters**: See `httpx2.request`.
|
| 1949 |
+
"""
|
| 1950 |
+
return await self.request(
|
| 1951 |
+
"GET",
|
| 1952 |
+
url,
|
| 1953 |
+
params=params,
|
| 1954 |
+
headers=headers,
|
| 1955 |
+
cookies=cookies,
|
| 1956 |
+
auth=auth,
|
| 1957 |
+
follow_redirects=follow_redirects,
|
| 1958 |
+
timeout=timeout,
|
| 1959 |
+
extensions=extensions,
|
| 1960 |
+
)
|
| 1961 |
+
|
| 1962 |
+
async def options(
|
| 1963 |
+
self,
|
| 1964 |
+
url: URL | str,
|
| 1965 |
+
*,
|
| 1966 |
+
params: QueryParamTypes | None = None,
|
| 1967 |
+
headers: HeaderTypes | None = None,
|
| 1968 |
+
cookies: CookieTypes | None = None,
|
| 1969 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1970 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1971 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1972 |
+
extensions: RequestExtensions | None = None,
|
| 1973 |
+
) -> Response:
|
| 1974 |
+
"""
|
| 1975 |
+
Send an `OPTIONS` request.
|
| 1976 |
+
|
| 1977 |
+
**Parameters**: See `httpx2.request`.
|
| 1978 |
+
"""
|
| 1979 |
+
return await self.request(
|
| 1980 |
+
"OPTIONS",
|
| 1981 |
+
url,
|
| 1982 |
+
params=params,
|
| 1983 |
+
headers=headers,
|
| 1984 |
+
cookies=cookies,
|
| 1985 |
+
auth=auth,
|
| 1986 |
+
follow_redirects=follow_redirects,
|
| 1987 |
+
timeout=timeout,
|
| 1988 |
+
extensions=extensions,
|
| 1989 |
+
)
|
| 1990 |
+
|
| 1991 |
+
async def head(
|
| 1992 |
+
self,
|
| 1993 |
+
url: URL | str,
|
| 1994 |
+
*,
|
| 1995 |
+
params: QueryParamTypes | None = None,
|
| 1996 |
+
headers: HeaderTypes | None = None,
|
| 1997 |
+
cookies: CookieTypes | None = None,
|
| 1998 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 1999 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2000 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2001 |
+
extensions: RequestExtensions | None = None,
|
| 2002 |
+
) -> Response:
|
| 2003 |
+
"""
|
| 2004 |
+
Send a `HEAD` request.
|
| 2005 |
+
|
| 2006 |
+
**Parameters**: See `httpx2.request`.
|
| 2007 |
+
"""
|
| 2008 |
+
return await self.request(
|
| 2009 |
+
"HEAD",
|
| 2010 |
+
url,
|
| 2011 |
+
params=params,
|
| 2012 |
+
headers=headers,
|
| 2013 |
+
cookies=cookies,
|
| 2014 |
+
auth=auth,
|
| 2015 |
+
follow_redirects=follow_redirects,
|
| 2016 |
+
timeout=timeout,
|
| 2017 |
+
extensions=extensions,
|
| 2018 |
+
)
|
| 2019 |
+
|
| 2020 |
+
async def post(
|
| 2021 |
+
self,
|
| 2022 |
+
url: URL | str,
|
| 2023 |
+
*,
|
| 2024 |
+
content: RequestContent | None = None,
|
| 2025 |
+
data: RequestData | None = None,
|
| 2026 |
+
files: RequestFiles | None = None,
|
| 2027 |
+
json: typing.Any | None = None,
|
| 2028 |
+
params: QueryParamTypes | None = None,
|
| 2029 |
+
headers: HeaderTypes | None = None,
|
| 2030 |
+
cookies: CookieTypes | None = None,
|
| 2031 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2032 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2033 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2034 |
+
extensions: RequestExtensions | None = None,
|
| 2035 |
+
) -> Response:
|
| 2036 |
+
"""
|
| 2037 |
+
Send a `POST` request.
|
| 2038 |
+
|
| 2039 |
+
**Parameters**: See `httpx2.request`.
|
| 2040 |
+
"""
|
| 2041 |
+
return await self.request(
|
| 2042 |
+
"POST",
|
| 2043 |
+
url,
|
| 2044 |
+
content=content,
|
| 2045 |
+
data=data,
|
| 2046 |
+
files=files,
|
| 2047 |
+
json=json,
|
| 2048 |
+
params=params,
|
| 2049 |
+
headers=headers,
|
| 2050 |
+
cookies=cookies,
|
| 2051 |
+
auth=auth,
|
| 2052 |
+
follow_redirects=follow_redirects,
|
| 2053 |
+
timeout=timeout,
|
| 2054 |
+
extensions=extensions,
|
| 2055 |
+
)
|
| 2056 |
+
|
| 2057 |
+
async def put(
|
| 2058 |
+
self,
|
| 2059 |
+
url: URL | str,
|
| 2060 |
+
*,
|
| 2061 |
+
content: RequestContent | None = None,
|
| 2062 |
+
data: RequestData | None = None,
|
| 2063 |
+
files: RequestFiles | None = None,
|
| 2064 |
+
json: typing.Any | None = None,
|
| 2065 |
+
params: QueryParamTypes | None = None,
|
| 2066 |
+
headers: HeaderTypes | None = None,
|
| 2067 |
+
cookies: CookieTypes | None = None,
|
| 2068 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2069 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2070 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2071 |
+
extensions: RequestExtensions | None = None,
|
| 2072 |
+
) -> Response:
|
| 2073 |
+
"""
|
| 2074 |
+
Send a `PUT` request.
|
| 2075 |
+
|
| 2076 |
+
**Parameters**: See `httpx2.request`.
|
| 2077 |
+
"""
|
| 2078 |
+
return await self.request(
|
| 2079 |
+
"PUT",
|
| 2080 |
+
url,
|
| 2081 |
+
content=content,
|
| 2082 |
+
data=data,
|
| 2083 |
+
files=files,
|
| 2084 |
+
json=json,
|
| 2085 |
+
params=params,
|
| 2086 |
+
headers=headers,
|
| 2087 |
+
cookies=cookies,
|
| 2088 |
+
auth=auth,
|
| 2089 |
+
follow_redirects=follow_redirects,
|
| 2090 |
+
timeout=timeout,
|
| 2091 |
+
extensions=extensions,
|
| 2092 |
+
)
|
| 2093 |
+
|
| 2094 |
+
async def patch(
|
| 2095 |
+
self,
|
| 2096 |
+
url: URL | str,
|
| 2097 |
+
*,
|
| 2098 |
+
content: RequestContent | None = None,
|
| 2099 |
+
data: RequestData | None = None,
|
| 2100 |
+
files: RequestFiles | None = None,
|
| 2101 |
+
json: typing.Any | None = None,
|
| 2102 |
+
params: QueryParamTypes | None = None,
|
| 2103 |
+
headers: HeaderTypes | None = None,
|
| 2104 |
+
cookies: CookieTypes | None = None,
|
| 2105 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2106 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2107 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2108 |
+
extensions: RequestExtensions | None = None,
|
| 2109 |
+
) -> Response:
|
| 2110 |
+
"""
|
| 2111 |
+
Send a `PATCH` request.
|
| 2112 |
+
|
| 2113 |
+
**Parameters**: See `httpx2.request`.
|
| 2114 |
+
"""
|
| 2115 |
+
return await self.request(
|
| 2116 |
+
"PATCH",
|
| 2117 |
+
url,
|
| 2118 |
+
content=content,
|
| 2119 |
+
data=data,
|
| 2120 |
+
files=files,
|
| 2121 |
+
json=json,
|
| 2122 |
+
params=params,
|
| 2123 |
+
headers=headers,
|
| 2124 |
+
cookies=cookies,
|
| 2125 |
+
auth=auth,
|
| 2126 |
+
follow_redirects=follow_redirects,
|
| 2127 |
+
timeout=timeout,
|
| 2128 |
+
extensions=extensions,
|
| 2129 |
+
)
|
| 2130 |
+
|
| 2131 |
+
async def delete(
|
| 2132 |
+
self,
|
| 2133 |
+
url: URL | str,
|
| 2134 |
+
*,
|
| 2135 |
+
params: QueryParamTypes | None = None,
|
| 2136 |
+
headers: HeaderTypes | None = None,
|
| 2137 |
+
cookies: CookieTypes | None = None,
|
| 2138 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2139 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2140 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2141 |
+
extensions: RequestExtensions | None = None,
|
| 2142 |
+
) -> Response:
|
| 2143 |
+
"""
|
| 2144 |
+
Send a `DELETE` request.
|
| 2145 |
+
|
| 2146 |
+
**Parameters**: See `httpx2.request`.
|
| 2147 |
+
"""
|
| 2148 |
+
return await self.request(
|
| 2149 |
+
"DELETE",
|
| 2150 |
+
url,
|
| 2151 |
+
params=params,
|
| 2152 |
+
headers=headers,
|
| 2153 |
+
cookies=cookies,
|
| 2154 |
+
auth=auth,
|
| 2155 |
+
follow_redirects=follow_redirects,
|
| 2156 |
+
timeout=timeout,
|
| 2157 |
+
extensions=extensions,
|
| 2158 |
+
)
|
| 2159 |
+
|
| 2160 |
+
async def query(
|
| 2161 |
+
self,
|
| 2162 |
+
url: URL | str,
|
| 2163 |
+
*,
|
| 2164 |
+
content: RequestContent | None = None,
|
| 2165 |
+
data: RequestData | None = None,
|
| 2166 |
+
files: RequestFiles | None = None,
|
| 2167 |
+
json: typing.Any | None = None,
|
| 2168 |
+
params: QueryParamTypes | None = None,
|
| 2169 |
+
headers: HeaderTypes | None = None,
|
| 2170 |
+
cookies: CookieTypes | None = None,
|
| 2171 |
+
auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2172 |
+
follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2173 |
+
timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT,
|
| 2174 |
+
extensions: RequestExtensions | None = None,
|
| 2175 |
+
) -> Response:
|
| 2176 |
+
"""
|
| 2177 |
+
Send a `QUERY` request.
|
| 2178 |
+
|
| 2179 |
+
**Parameters**: See `httpx2.request`.
|
| 2180 |
+
"""
|
| 2181 |
+
return await self.request(
|
| 2182 |
+
"QUERY",
|
| 2183 |
+
url,
|
| 2184 |
+
content=content,
|
| 2185 |
+
data=data,
|
| 2186 |
+
files=files,
|
| 2187 |
+
json=json,
|
| 2188 |
+
params=params,
|
| 2189 |
+
headers=headers,
|
| 2190 |
+
cookies=cookies,
|
| 2191 |
+
auth=auth,
|
| 2192 |
+
follow_redirects=follow_redirects,
|
| 2193 |
+
timeout=timeout,
|
| 2194 |
+
extensions=extensions,
|
| 2195 |
+
)
|
| 2196 |
+
|
| 2197 |
+
async def aclose(self) -> None:
|
| 2198 |
+
"""
|
| 2199 |
+
Close transport and proxies.
|
| 2200 |
+
"""
|
| 2201 |
+
if self._state != ClientState.CLOSED:
|
| 2202 |
+
self._state = ClientState.CLOSED
|
| 2203 |
+
|
| 2204 |
+
await self._transport.aclose()
|
| 2205 |
+
for proxy in self._mounts.values():
|
| 2206 |
+
if proxy is not None:
|
| 2207 |
+
await proxy.aclose()
|
| 2208 |
+
|
| 2209 |
+
async def __aenter__(self: U) -> U:
|
| 2210 |
+
if self._state != ClientState.UNOPENED:
|
| 2211 |
+
msg = {
|
| 2212 |
+
ClientState.OPENED: "Cannot open a client instance more than once.",
|
| 2213 |
+
ClientState.CLOSED: ("Cannot reopen a client instance, once it has been closed."),
|
| 2214 |
+
}[self._state]
|
| 2215 |
+
raise RuntimeError(msg)
|
| 2216 |
+
|
| 2217 |
+
self._state = ClientState.OPENED
|
| 2218 |
+
|
| 2219 |
+
await self._transport.__aenter__()
|
| 2220 |
+
for proxy in self._mounts.values():
|
| 2221 |
+
if proxy is not None:
|
| 2222 |
+
await proxy.__aenter__()
|
| 2223 |
+
return self
|
| 2224 |
+
|
| 2225 |
+
async def __aexit__(
|
| 2226 |
+
self,
|
| 2227 |
+
exc_type: type[BaseException] | None = None,
|
| 2228 |
+
exc_value: BaseException | None = None,
|
| 2229 |
+
traceback: TracebackType | None = None,
|
| 2230 |
+
) -> None:
|
| 2231 |
+
self._state = ClientState.CLOSED
|
| 2232 |
+
|
| 2233 |
+
await self._transport.__aexit__(exc_type, exc_value, traceback)
|
| 2234 |
+
for proxy in self._mounts.values():
|
| 2235 |
+
if proxy is not None:
|
| 2236 |
+
await proxy.__aexit__(exc_type, exc_value, traceback)
|
hug/lib/python3.12/site-packages/httpx2/_config.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import dataclasses
|
| 4 |
+
import os
|
| 5 |
+
import typing
|
| 6 |
+
|
| 7 |
+
from ._models import Headers
|
| 8 |
+
from ._types import CertTypes, HeaderTypes, TimeoutTypes
|
| 9 |
+
from ._urls import URL
|
| 10 |
+
|
| 11 |
+
if typing.TYPE_CHECKING:
|
| 12 |
+
import ssl # pragma: no cover
|
| 13 |
+
|
| 14 |
+
__all__ = ["Limits", "Proxy", "Timeout", "create_ssl_context"]
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class UnsetType:
|
| 18 |
+
pass # pragma: no cover
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
UNSET = UnsetType()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def create_ssl_context(
|
| 25 |
+
verify: ssl.SSLContext | str | bool = True,
|
| 26 |
+
cert: CertTypes | None = None,
|
| 27 |
+
trust_env: bool = True,
|
| 28 |
+
) -> ssl.SSLContext:
|
| 29 |
+
import ssl
|
| 30 |
+
import warnings
|
| 31 |
+
|
| 32 |
+
import httpcore2
|
| 33 |
+
|
| 34 |
+
if verify is True:
|
| 35 |
+
ctx = httpcore2.default_ssl_context(_trust_env=trust_env)
|
| 36 |
+
elif verify is False:
|
| 37 |
+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
| 38 |
+
ctx.check_hostname = False
|
| 39 |
+
ctx.verify_mode = ssl.CERT_NONE
|
| 40 |
+
elif isinstance(verify, str):
|
| 41 |
+
if cert:
|
| 42 |
+
raise TypeError(
|
| 43 |
+
"`verify=<str>` cannot be combined with `cert=...`. "
|
| 44 |
+
"Build an `ssl.SSLContext` and pass it as `verify=<ctx>`, "
|
| 45 |
+
"using `.load_cert_chain()` to configure the certificate chain."
|
| 46 |
+
)
|
| 47 |
+
message = (
|
| 48 |
+
"`verify=<str>` is deprecated. "
|
| 49 |
+
"Use `verify=ssl.create_default_context(cafile=...)` "
|
| 50 |
+
"or `verify=ssl.create_default_context(capath=...)` instead."
|
| 51 |
+
)
|
| 52 |
+
warnings.warn(message, DeprecationWarning)
|
| 53 |
+
if os.path.isdir(verify): # pragma: no cover
|
| 54 |
+
return ssl.create_default_context(capath=verify)
|
| 55 |
+
return ssl.create_default_context(cafile=verify)
|
| 56 |
+
else:
|
| 57 |
+
ctx = verify
|
| 58 |
+
|
| 59 |
+
if cert: # pragma: no cover
|
| 60 |
+
message = (
|
| 61 |
+
"`cert=...` is deprecated. Use `verify=<ssl_context>` instead,"
|
| 62 |
+
"with `.load_cert_chain()` to configure the certificate chain."
|
| 63 |
+
)
|
| 64 |
+
warnings.warn(message, DeprecationWarning)
|
| 65 |
+
if isinstance(cert, str):
|
| 66 |
+
ctx.load_cert_chain(cert)
|
| 67 |
+
else:
|
| 68 |
+
ctx.load_cert_chain(*cert)
|
| 69 |
+
|
| 70 |
+
return ctx
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
@dataclasses.dataclass(init=False, repr=False)
|
| 74 |
+
class Timeout:
|
| 75 |
+
"""
|
| 76 |
+
Timeout configuration.
|
| 77 |
+
|
| 78 |
+
**Usage**:
|
| 79 |
+
|
| 80 |
+
Timeout(None) # No timeouts.
|
| 81 |
+
Timeout(5.0) # 5s timeout on all operations.
|
| 82 |
+
Timeout(None, connect=5.0) # 5s timeout on connect, no other timeouts.
|
| 83 |
+
Timeout(5.0, connect=10.0) # 10s timeout on connect. 5s timeout elsewhere.
|
| 84 |
+
Timeout(5.0, pool=None) # No timeout on acquiring connection from pool.
|
| 85 |
+
# 5s timeout elsewhere.
|
| 86 |
+
"""
|
| 87 |
+
|
| 88 |
+
connect: float | None
|
| 89 |
+
read: float | None
|
| 90 |
+
write: float | None
|
| 91 |
+
pool: float | None
|
| 92 |
+
|
| 93 |
+
def __init__(
|
| 94 |
+
self,
|
| 95 |
+
timeout: TimeoutTypes | UnsetType = UNSET,
|
| 96 |
+
*,
|
| 97 |
+
connect: None | float | UnsetType = UNSET,
|
| 98 |
+
read: None | float | UnsetType = UNSET,
|
| 99 |
+
write: None | float | UnsetType = UNSET,
|
| 100 |
+
pool: None | float | UnsetType = UNSET,
|
| 101 |
+
) -> None:
|
| 102 |
+
if isinstance(timeout, Timeout):
|
| 103 |
+
# Passed as a single explicit Timeout.
|
| 104 |
+
assert connect is UNSET
|
| 105 |
+
assert read is UNSET
|
| 106 |
+
assert write is UNSET
|
| 107 |
+
assert pool is UNSET
|
| 108 |
+
self.connect = timeout.connect
|
| 109 |
+
self.read = timeout.read
|
| 110 |
+
self.write = timeout.write
|
| 111 |
+
self.pool = timeout.pool
|
| 112 |
+
elif isinstance(timeout, tuple):
|
| 113 |
+
# Passed as a tuple.
|
| 114 |
+
self.connect = timeout[0]
|
| 115 |
+
self.read = timeout[1]
|
| 116 |
+
self.write = None if len(timeout) < 3 else timeout[2]
|
| 117 |
+
self.pool = None if len(timeout) < 4 else timeout[3]
|
| 118 |
+
elif not (
|
| 119 |
+
isinstance(connect, UnsetType)
|
| 120 |
+
or isinstance(read, UnsetType)
|
| 121 |
+
or isinstance(write, UnsetType)
|
| 122 |
+
or isinstance(pool, UnsetType)
|
| 123 |
+
):
|
| 124 |
+
self.connect = connect
|
| 125 |
+
self.read = read
|
| 126 |
+
self.write = write
|
| 127 |
+
self.pool = pool
|
| 128 |
+
else:
|
| 129 |
+
if isinstance(timeout, UnsetType):
|
| 130 |
+
raise ValueError("httpx2.Timeout must either include a default, or set all four parameters explicitly.")
|
| 131 |
+
self.connect = timeout if isinstance(connect, UnsetType) else connect
|
| 132 |
+
self.read = timeout if isinstance(read, UnsetType) else read
|
| 133 |
+
self.write = timeout if isinstance(write, UnsetType) else write
|
| 134 |
+
self.pool = timeout if isinstance(pool, UnsetType) else pool
|
| 135 |
+
|
| 136 |
+
def as_dict(self) -> dict[str, float | None]:
|
| 137 |
+
return {
|
| 138 |
+
"connect": self.connect,
|
| 139 |
+
"read": self.read,
|
| 140 |
+
"write": self.write,
|
| 141 |
+
"pool": self.pool,
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
def __repr__(self) -> str:
|
| 145 |
+
class_name = self.__class__.__name__
|
| 146 |
+
if len({self.connect, self.read, self.write, self.pool}) == 1:
|
| 147 |
+
return f"{class_name}(timeout={self.connect})"
|
| 148 |
+
return f"{class_name}(connect={self.connect}, read={self.read}, write={self.write}, pool={self.pool})"
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
@dataclasses.dataclass(kw_only=True)
|
| 152 |
+
class Limits:
|
| 153 |
+
"""
|
| 154 |
+
Configuration for limits to various client behaviors.
|
| 155 |
+
|
| 156 |
+
**Parameters:**
|
| 157 |
+
|
| 158 |
+
* **max_connections** - The maximum number of concurrent connections that may be
|
| 159 |
+
established.
|
| 160 |
+
* **max_keepalive_connections** - Allow the connection pool to maintain
|
| 161 |
+
keep-alive connections below this point. Should be less than or equal
|
| 162 |
+
to `max_connections`.
|
| 163 |
+
* **keepalive_expiry** - Time limit on idle keep-alive connections in seconds.
|
| 164 |
+
"""
|
| 165 |
+
|
| 166 |
+
max_connections: int | None = None
|
| 167 |
+
max_keepalive_connections: int | None = None
|
| 168 |
+
keepalive_expiry: float | None = 5.0
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
class Proxy:
|
| 172 |
+
def __init__(
|
| 173 |
+
self,
|
| 174 |
+
url: URL | str,
|
| 175 |
+
*,
|
| 176 |
+
ssl_context: ssl.SSLContext | None = None,
|
| 177 |
+
auth: tuple[str, str] | None = None,
|
| 178 |
+
headers: HeaderTypes | None = None,
|
| 179 |
+
) -> None:
|
| 180 |
+
url = URL(url)
|
| 181 |
+
headers = Headers(headers)
|
| 182 |
+
|
| 183 |
+
if url.scheme not in ("http", "https", "socks5", "socks5h"):
|
| 184 |
+
raise ValueError(f"Unknown scheme for proxy URL {url!r}")
|
| 185 |
+
|
| 186 |
+
if url.username or url.password:
|
| 187 |
+
# Remove any auth credentials from the URL.
|
| 188 |
+
auth = (url.username, url.password)
|
| 189 |
+
url = url.copy_with(username=None, password=None)
|
| 190 |
+
|
| 191 |
+
self.url = url
|
| 192 |
+
self.auth = auth
|
| 193 |
+
self.headers = headers
|
| 194 |
+
self.ssl_context = ssl_context
|
| 195 |
+
|
| 196 |
+
@property
|
| 197 |
+
def raw_auth(self) -> tuple[bytes, bytes] | None:
|
| 198 |
+
# The proxy authentication as raw bytes.
|
| 199 |
+
return None if self.auth is None else (self.auth[0].encode("utf-8"), self.auth[1].encode("utf-8"))
|
| 200 |
+
|
| 201 |
+
def __repr__(self) -> str:
|
| 202 |
+
# The authentication is represented with the password component masked.
|
| 203 |
+
auth = (self.auth[0], "********") if self.auth else None
|
| 204 |
+
|
| 205 |
+
# Build a nice concise representation.
|
| 206 |
+
url_str = f"{str(self.url)!r}"
|
| 207 |
+
auth_str = f", auth={auth!r}" if auth else ""
|
| 208 |
+
headers_str = f", headers={dict(self.headers)!r}" if self.headers else ""
|
| 209 |
+
return f"Proxy({url_str}{auth_str}{headers_str})"
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
DEFAULT_TIMEOUT_CONFIG = Timeout(timeout=5.0)
|
| 213 |
+
DEFAULT_LIMITS = Limits(max_connections=100, max_keepalive_connections=20)
|
| 214 |
+
DEFAULT_MAX_REDIRECTS = 20
|
| 215 |
+
|
| 216 |
+
DEFAULT_MAX_EVENT_SIZE_BYTES = 1024 * 1024
|
| 217 |
+
|
| 218 |
+
DEFAULT_MAX_MESSAGE_SIZE_BYTES = 65_536
|
| 219 |
+
DEFAULT_QUEUE_SIZE = 512
|
| 220 |
+
DEFAULT_KEEPALIVE_PING_INTERVAL_SECONDS = 20.0
|
| 221 |
+
DEFAULT_KEEPALIVE_PING_TIMEOUT_SECONDS = 20.0
|
hug/lib/python3.12/site-packages/httpx2/_content.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import inspect
|
| 4 |
+
import warnings
|
| 5 |
+
from collections.abc import AsyncGenerator, AsyncIterable, AsyncIterator, Iterable, Iterator, Mapping
|
| 6 |
+
from json import dumps as json_dumps
|
| 7 |
+
from typing import (
|
| 8 |
+
Any,
|
| 9 |
+
)
|
| 10 |
+
from urllib.parse import urlencode
|
| 11 |
+
|
| 12 |
+
from ._exceptions import StreamClosed, StreamConsumed
|
| 13 |
+
from ._multipart import MultipartStream
|
| 14 |
+
from ._types import (
|
| 15 |
+
AsyncByteStream,
|
| 16 |
+
RequestContent,
|
| 17 |
+
RequestData,
|
| 18 |
+
RequestFiles,
|
| 19 |
+
ResponseContent,
|
| 20 |
+
SyncByteStream,
|
| 21 |
+
)
|
| 22 |
+
from ._utils import peek_filelike_remaining_length, primitive_value_to_str
|
| 23 |
+
|
| 24 |
+
__all__ = ["ByteStream"]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class ByteStream(AsyncByteStream, SyncByteStream):
|
| 28 |
+
def __init__(self, stream: bytes) -> None:
|
| 29 |
+
self._stream = stream
|
| 30 |
+
|
| 31 |
+
def __iter__(self) -> Iterator[bytes]:
|
| 32 |
+
yield self._stream
|
| 33 |
+
|
| 34 |
+
async def __aiter__(self) -> AsyncIterator[bytes]:
|
| 35 |
+
yield self._stream
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class IteratorByteStream(SyncByteStream):
|
| 39 |
+
CHUNK_SIZE = 65_536
|
| 40 |
+
|
| 41 |
+
def __init__(self, stream: Iterable[bytes]) -> None:
|
| 42 |
+
self._stream = stream
|
| 43 |
+
self._is_stream_consumed = False
|
| 44 |
+
self._is_generator = inspect.isgenerator(stream)
|
| 45 |
+
|
| 46 |
+
def __iter__(self) -> Iterator[bytes]:
|
| 47 |
+
if self._is_stream_consumed and self._is_generator:
|
| 48 |
+
raise StreamConsumed()
|
| 49 |
+
|
| 50 |
+
self._is_stream_consumed = True
|
| 51 |
+
if hasattr(self._stream, "read"):
|
| 52 |
+
# File-like interfaces should use 'read' directly.
|
| 53 |
+
chunk = self._stream.read(self.CHUNK_SIZE)
|
| 54 |
+
while chunk:
|
| 55 |
+
yield chunk
|
| 56 |
+
chunk = self._stream.read(self.CHUNK_SIZE)
|
| 57 |
+
else:
|
| 58 |
+
# Otherwise iterate.
|
| 59 |
+
yield from self._stream
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class AsyncIteratorByteStream(AsyncByteStream):
|
| 63 |
+
CHUNK_SIZE = 65_536
|
| 64 |
+
|
| 65 |
+
def __init__(self, stream: AsyncIterable[bytes]) -> None:
|
| 66 |
+
self._stream = stream
|
| 67 |
+
self._is_stream_consumed = False
|
| 68 |
+
self._is_generator = inspect.isasyncgen(stream)
|
| 69 |
+
|
| 70 |
+
async def __aiter__(self) -> AsyncIterator[bytes]:
|
| 71 |
+
if self._is_stream_consumed and self._is_generator:
|
| 72 |
+
raise StreamConsumed()
|
| 73 |
+
|
| 74 |
+
self._is_stream_consumed = True
|
| 75 |
+
if hasattr(self._stream, "aread"):
|
| 76 |
+
# File-like interfaces should use 'aread' directly.
|
| 77 |
+
chunk = await self._stream.aread(self.CHUNK_SIZE)
|
| 78 |
+
while chunk:
|
| 79 |
+
yield chunk
|
| 80 |
+
chunk = await self._stream.aread(self.CHUNK_SIZE)
|
| 81 |
+
else:
|
| 82 |
+
# Otherwise iterate, making sure the wrapped stream is closed even if the
|
| 83 |
+
# consumer stops early (e.g. an exception is raised part-way through decoding).
|
| 84 |
+
stream = self._stream.__aiter__()
|
| 85 |
+
try:
|
| 86 |
+
async for part in stream:
|
| 87 |
+
yield part
|
| 88 |
+
finally:
|
| 89 |
+
if isinstance(stream, AsyncGenerator):
|
| 90 |
+
await stream.aclose()
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class UnattachedStream(AsyncByteStream, SyncByteStream):
|
| 94 |
+
"""
|
| 95 |
+
If a request or response is serialized using pickle, then it is no longer
|
| 96 |
+
attached to a stream for I/O purposes. Any stream operations should result
|
| 97 |
+
in `httpx2.StreamClosed`.
|
| 98 |
+
"""
|
| 99 |
+
|
| 100 |
+
def __iter__(self) -> Iterator[bytes]:
|
| 101 |
+
raise StreamClosed()
|
| 102 |
+
|
| 103 |
+
async def __aiter__(self) -> AsyncIterator[bytes]:
|
| 104 |
+
raise StreamClosed()
|
| 105 |
+
yield b"" # pragma: no cover
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def encode_content(
|
| 109 |
+
content: str | bytes | Iterable[bytes] | AsyncIterable[bytes],
|
| 110 |
+
) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
|
| 111 |
+
if isinstance(content, (bytes, str)):
|
| 112 |
+
body = content.encode("utf-8") if isinstance(content, str) else content
|
| 113 |
+
content_length = len(body)
|
| 114 |
+
headers = {"Content-Length": str(content_length)} if body else {}
|
| 115 |
+
return headers, ByteStream(body)
|
| 116 |
+
|
| 117 |
+
elif isinstance(content, Iterable) and not isinstance(content, dict):
|
| 118 |
+
# `not isinstance(content, dict)` is a bit oddly specific, but it
|
| 119 |
+
# catches a case that's easy for users to make in error, and would
|
| 120 |
+
# otherwise pass through here, like any other bytes-iterable,
|
| 121 |
+
# because `dict` happens to be iterable. See issue #2491.
|
| 122 |
+
content_length_or_none = peek_filelike_remaining_length(content)
|
| 123 |
+
|
| 124 |
+
if content_length_or_none is None:
|
| 125 |
+
headers = {"Transfer-Encoding": "chunked"}
|
| 126 |
+
else:
|
| 127 |
+
headers = {"Content-Length": str(content_length_or_none)}
|
| 128 |
+
return headers, IteratorByteStream(content)
|
| 129 |
+
|
| 130 |
+
elif isinstance(content, AsyncIterable):
|
| 131 |
+
headers = {"Transfer-Encoding": "chunked"}
|
| 132 |
+
return headers, AsyncIteratorByteStream(content)
|
| 133 |
+
|
| 134 |
+
raise TypeError(f"Unexpected type for 'content', {type(content)!r}")
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def encode_urlencoded_data(data: RequestData) -> tuple[dict[str, str], ByteStream]:
|
| 138 |
+
plain_data: list[tuple[str, str]] = []
|
| 139 |
+
for key, value in data.items():
|
| 140 |
+
if isinstance(value, (list, tuple)):
|
| 141 |
+
plain_data.extend([(key, primitive_value_to_str(item)) for item in value])
|
| 142 |
+
else:
|
| 143 |
+
plain_data.append((key, primitive_value_to_str(value)))
|
| 144 |
+
body = urlencode(plain_data, doseq=True).encode("utf-8")
|
| 145 |
+
content_length = str(len(body))
|
| 146 |
+
content_type = "application/x-www-form-urlencoded"
|
| 147 |
+
headers = {"Content-Length": content_length, "Content-Type": content_type}
|
| 148 |
+
return headers, ByteStream(body)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def encode_multipart_data(
|
| 152 |
+
data: RequestData, files: RequestFiles, boundary: bytes | None
|
| 153 |
+
) -> tuple[dict[str, str], MultipartStream]:
|
| 154 |
+
multipart = MultipartStream(data=data, files=files, boundary=boundary)
|
| 155 |
+
headers = multipart.get_headers()
|
| 156 |
+
return headers, multipart
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def encode_text(text: str) -> tuple[dict[str, str], ByteStream]:
|
| 160 |
+
body = text.encode("utf-8")
|
| 161 |
+
content_length = str(len(body))
|
| 162 |
+
content_type = "text/plain; charset=utf-8"
|
| 163 |
+
headers = {"Content-Length": content_length, "Content-Type": content_type}
|
| 164 |
+
return headers, ByteStream(body)
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def encode_html(html: str) -> tuple[dict[str, str], ByteStream]:
|
| 168 |
+
body = html.encode("utf-8")
|
| 169 |
+
content_length = str(len(body))
|
| 170 |
+
content_type = "text/html; charset=utf-8"
|
| 171 |
+
headers = {"Content-Length": content_length, "Content-Type": content_type}
|
| 172 |
+
return headers, ByteStream(body)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def encode_json(json: Any) -> tuple[dict[str, str], ByteStream]:
|
| 176 |
+
body = json_dumps(json, ensure_ascii=False, separators=(",", ":"), allow_nan=False).encode("utf-8")
|
| 177 |
+
content_length = str(len(body))
|
| 178 |
+
content_type = "application/json"
|
| 179 |
+
headers = {"Content-Length": content_length, "Content-Type": content_type}
|
| 180 |
+
return headers, ByteStream(body)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def encode_request(
|
| 184 |
+
content: RequestContent | None = None,
|
| 185 |
+
data: RequestData | None = None,
|
| 186 |
+
files: RequestFiles | None = None,
|
| 187 |
+
json: Any | None = None,
|
| 188 |
+
boundary: bytes | None = None,
|
| 189 |
+
) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
|
| 190 |
+
"""
|
| 191 |
+
Handles encoding the given `content`, `data`, `files`, and `json`,
|
| 192 |
+
returning a two-tuple of (<headers>, <stream>).
|
| 193 |
+
"""
|
| 194 |
+
if data is not None and not isinstance(data, Mapping):
|
| 195 |
+
# We prefer to separate `content=<bytes|str|byte iterator|bytes aiterator>`
|
| 196 |
+
# for raw request content, and `data=<form data>` for url encoded or
|
| 197 |
+
# multipart form content.
|
| 198 |
+
#
|
| 199 |
+
# However for compat with requests, we *do* still support
|
| 200 |
+
# `data=<bytes...>` usages. We deal with that case here, treating it
|
| 201 |
+
# as if `content=<...>` had been supplied instead.
|
| 202 |
+
message = "Use 'content=<...>' to upload raw bytes/text content."
|
| 203 |
+
warnings.warn(message, DeprecationWarning, stacklevel=2)
|
| 204 |
+
return encode_content(data)
|
| 205 |
+
|
| 206 |
+
if content is not None:
|
| 207 |
+
return encode_content(content)
|
| 208 |
+
elif files:
|
| 209 |
+
return encode_multipart_data(data or {}, files, boundary)
|
| 210 |
+
elif data:
|
| 211 |
+
return encode_urlencoded_data(data)
|
| 212 |
+
elif json is not None:
|
| 213 |
+
return encode_json(json)
|
| 214 |
+
|
| 215 |
+
return {}, ByteStream(b"")
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def encode_response(
|
| 219 |
+
content: ResponseContent | None = None,
|
| 220 |
+
text: str | None = None,
|
| 221 |
+
html: str | None = None,
|
| 222 |
+
json: Any | None = None,
|
| 223 |
+
) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
|
| 224 |
+
"""
|
| 225 |
+
Handles encoding the given `content`, returning a two-tuple of
|
| 226 |
+
(<headers>, <stream>).
|
| 227 |
+
"""
|
| 228 |
+
if content is not None:
|
| 229 |
+
return encode_content(content)
|
| 230 |
+
elif text is not None:
|
| 231 |
+
return encode_text(text)
|
| 232 |
+
elif html is not None:
|
| 233 |
+
return encode_html(html)
|
| 234 |
+
elif json is not None:
|
| 235 |
+
return encode_json(json)
|
| 236 |
+
|
| 237 |
+
return {}, ByteStream(b"")
|
hug/lib/python3.12/site-packages/httpx2/_cookies.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
import threading
|
| 3 |
+
import time
|
| 4 |
+
import urllib.request
|
| 5 |
+
from http import cookiejar
|
| 6 |
+
from http.cookiejar import Cookie, CookieJar, CookiePolicy, DefaultCookiePolicy
|
| 7 |
+
from typing import TYPE_CHECKING
|
| 8 |
+
|
| 9 |
+
__all__ = ["IndexedCookieJar"]
|
| 10 |
+
|
| 11 |
+
# [RFC 1035](https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4) caps DNS names at 255 octets;
|
| 12 |
+
# longer hosts fall back to scanning every stored domain.
|
| 13 |
+
MAX_HOST_LENGTH = 255
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class IndexedCookieJar(CookieJar):
|
| 17 |
+
"""
|
| 18 |
+
A `CookieJar` whose per-request cost does not grow with the number of stored cookies.
|
| 19 |
+
|
| 20 |
+
Compared to `CookieJar`, on each request it:
|
| 21 |
+
|
| 22 |
+
1. Looks up only the domains that can match the request host, instead of checking every stored domain.
|
| 23 |
+
2. Skips the expiry sweep until the earliest known expiry has passed, instead of checking every stored cookie.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
_cookies: dict[str, dict[str, dict[str, Cookie]]]
|
| 27 |
+
_cookies_lock: threading.RLock
|
| 28 |
+
_policy: CookiePolicy
|
| 29 |
+
_next_expiry: float = math.inf
|
| 30 |
+
|
| 31 |
+
if TYPE_CHECKING:
|
| 32 |
+
|
| 33 |
+
def _cookies_for_domain(self, domain: str, request: urllib.request.Request) -> list[Cookie]: ...
|
| 34 |
+
|
| 35 |
+
def set_cookie(self, cookie: Cookie) -> None:
|
| 36 |
+
with self._cookies_lock:
|
| 37 |
+
super().set_cookie(cookie)
|
| 38 |
+
if cookie.expires is not None:
|
| 39 |
+
self._next_expiry = min(self._next_expiry, cookie.expires)
|
| 40 |
+
|
| 41 |
+
def clear_expired_cookies(self) -> None:
|
| 42 |
+
if time.time() < self._next_expiry:
|
| 43 |
+
return
|
| 44 |
+
with self._cookies_lock:
|
| 45 |
+
super().clear_expired_cookies()
|
| 46 |
+
self._next_expiry = min((c.expires for c in self if c.expires is not None), default=math.inf)
|
| 47 |
+
|
| 48 |
+
def _cookies_for_request(self, request: urllib.request.Request) -> list[Cookie]:
|
| 49 |
+
hosts: tuple[str, str] = cookiejar.eff_request_host(request) # type: ignore[attr-defined]
|
| 50 |
+
uses_default_matching = type(self._policy).domain_return_ok is DefaultCookiePolicy.domain_return_ok
|
| 51 |
+
if not uses_default_matching or max(map(len, hosts)) > MAX_HOST_LENGTH:
|
| 52 |
+
return super()._cookies_for_request(request) # type: ignore[misc, no-any-return]
|
| 53 |
+
|
| 54 |
+
cookies: list[Cookie] = []
|
| 55 |
+
for domain in _matching_domains(hosts):
|
| 56 |
+
if domain in self._cookies:
|
| 57 |
+
cookies.extend(self._cookies_for_domain(domain, request))
|
| 58 |
+
return cookies
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def _matching_domains(hosts: tuple[str, str]) -> dict[str, None]:
|
| 62 |
+
"""
|
| 63 |
+
Every cookie domain `DefaultCookiePolicy.domain_return_ok` can accept for these hosts, least specific first.
|
| 64 |
+
|
| 65 |
+
For `a.example.org` that is `""`, `.org`, `org`, `.example.org`, `example.org`, `.a.example.org`, `a.example.org`.
|
| 66 |
+
"""
|
| 67 |
+
domains = {"": None}
|
| 68 |
+
for host in hosts:
|
| 69 |
+
dotted = host if host.startswith(".") else f".{host}"
|
| 70 |
+
for index in range(len(dotted) - 1, -1, -1):
|
| 71 |
+
if dotted[index] == ".":
|
| 72 |
+
domains[dotted[index:]] = None
|
| 73 |
+
domains[dotted[index + 1 :]] = None
|
| 74 |
+
return domains
|
hug/lib/python3.12/site-packages/httpx2/_decoders.py
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 itertools
|
| 12 |
+
import sys
|
| 13 |
+
import typing
|
| 14 |
+
import zlib
|
| 15 |
+
|
| 16 |
+
from ._exceptions import DecodingError
|
| 17 |
+
|
| 18 |
+
# Brotli support is optional
|
| 19 |
+
try:
|
| 20 |
+
# The C bindings in `brotli` are recommended for CPython.
|
| 21 |
+
import brotli
|
| 22 |
+
except ImportError: # pragma: no cover
|
| 23 |
+
try:
|
| 24 |
+
# The CFFI bindings in `brotlicffi` are recommended for PyPy
|
| 25 |
+
# and other environments.
|
| 26 |
+
import brotlicffi as brotli
|
| 27 |
+
except ImportError:
|
| 28 |
+
brotli = None
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# Zstandard support is optional on Python <= 3.13.
|
| 32 |
+
# On Python 3.14+, the stdlib includes an optional built-in zstd implementation.
|
| 33 |
+
if typing.TYPE_CHECKING:
|
| 34 |
+
if sys.version_info >= (3, 14):
|
| 35 |
+
from compression.zstd import ZstdDecompressor, ZstdError
|
| 36 |
+
else:
|
| 37 |
+
from backports.zstd import ZstdDecompressor, ZstdError
|
| 38 |
+
|
| 39 |
+
_zstandard_installed: bool
|
| 40 |
+
else: # pragma: no cover
|
| 41 |
+
try:
|
| 42 |
+
if sys.version_info >= (3, 14):
|
| 43 |
+
from compression.zstd import ZstdDecompressor, ZstdError
|
| 44 |
+
else:
|
| 45 |
+
from backports.zstd import ZstdDecompressor, ZstdError
|
| 46 |
+
|
| 47 |
+
_zstandard_installed = True
|
| 48 |
+
except ImportError:
|
| 49 |
+
_zstandard_installed = False
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
MAX_DECODE_CHUNK_SIZE = 2**20 # 1 MiB
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class Decompressor(typing.Protocol):
|
| 56 |
+
@property
|
| 57 |
+
def unconsumed_tail(self) -> bytes: ...
|
| 58 |
+
|
| 59 |
+
def decompress(self, data: bytes, max_length: int) -> bytes: ...
|
| 60 |
+
|
| 61 |
+
def flush(self) -> bytes: ...
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class ZlibDecompressor:
|
| 65 |
+
"""
|
| 66 |
+
Drain a `zlib`/`gzip` decompressor in bounded pieces so a small compressed
|
| 67 |
+
input cannot inflate to an unbounded buffer in a single call.
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
def __init__(self, decompressor: Decompressor) -> None:
|
| 71 |
+
self.decompressor = decompressor
|
| 72 |
+
|
| 73 |
+
def decompress(self, data: bytes) -> typing.Iterator[bytes]:
|
| 74 |
+
decompressed = self.decompressor.decompress(data, MAX_DECODE_CHUNK_SIZE)
|
| 75 |
+
while decompressed:
|
| 76 |
+
yield decompressed
|
| 77 |
+
decompressed = self.decompressor.decompress(self.decompressor.unconsumed_tail, MAX_DECODE_CHUNK_SIZE)
|
| 78 |
+
|
| 79 |
+
def flush(self) -> bytes:
|
| 80 |
+
return self.decompressor.flush()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
class ContentDecoder:
|
| 84 |
+
def decode(self, data: bytes) -> typing.Iterator[bytes]:
|
| 85 |
+
raise NotImplementedError() # pragma: no cover
|
| 86 |
+
|
| 87 |
+
def flush(self) -> typing.Iterator[bytes]:
|
| 88 |
+
raise NotImplementedError() # pragma: no cover
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class IdentityDecoder(ContentDecoder):
|
| 92 |
+
"""
|
| 93 |
+
Handle unencoded data.
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
def decode(self, data: bytes) -> typing.Iterator[bytes]:
|
| 97 |
+
yield data
|
| 98 |
+
|
| 99 |
+
def flush(self) -> typing.Iterator[bytes]:
|
| 100 |
+
yield from ()
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class DeflateDecoder(ContentDecoder):
|
| 104 |
+
"""
|
| 105 |
+
Handle 'deflate' decoding.
|
| 106 |
+
|
| 107 |
+
See: https://stackoverflow.com/questions/1838699
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
def __init__(self) -> None:
|
| 111 |
+
self.first_attempt = True
|
| 112 |
+
self.decompressor = ZlibDecompressor(zlib.decompressobj())
|
| 113 |
+
|
| 114 |
+
def decode(self, data: bytes) -> typing.Iterator[bytes]:
|
| 115 |
+
was_first_attempt = self.first_attempt
|
| 116 |
+
self.first_attempt = False
|
| 117 |
+
try:
|
| 118 |
+
yield from self.decompressor.decompress(data)
|
| 119 |
+
except zlib.error as exc:
|
| 120 |
+
if was_first_attempt:
|
| 121 |
+
self.decompressor = ZlibDecompressor(zlib.decompressobj(-zlib.MAX_WBITS))
|
| 122 |
+
yield from self.decode(data)
|
| 123 |
+
else:
|
| 124 |
+
raise DecodingError(str(exc)) from exc
|
| 125 |
+
|
| 126 |
+
def flush(self) -> typing.Iterator[bytes]:
|
| 127 |
+
try:
|
| 128 |
+
yield self.decompressor.flush()
|
| 129 |
+
except zlib.error as exc: # pragma: no cover
|
| 130 |
+
raise DecodingError(str(exc)) from exc
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
class GZipDecoder(ContentDecoder):
|
| 134 |
+
"""
|
| 135 |
+
Handle 'gzip' decoding.
|
| 136 |
+
|
| 137 |
+
See: https://stackoverflow.com/questions/1838699
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
def __init__(self) -> None:
|
| 141 |
+
self.decompressor = ZlibDecompressor(zlib.decompressobj(zlib.MAX_WBITS | 16))
|
| 142 |
+
|
| 143 |
+
def decode(self, data: bytes) -> typing.Iterator[bytes]:
|
| 144 |
+
try:
|
| 145 |
+
yield from self.decompressor.decompress(data)
|
| 146 |
+
except zlib.error as exc:
|
| 147 |
+
raise DecodingError(str(exc)) from exc
|
| 148 |
+
|
| 149 |
+
def flush(self) -> typing.Iterator[bytes]:
|
| 150 |
+
try:
|
| 151 |
+
yield self.decompressor.flush()
|
| 152 |
+
except zlib.error as exc: # pragma: no cover
|
| 153 |
+
raise DecodingError(str(exc)) from exc
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class BrotliDecoder(ContentDecoder):
|
| 157 |
+
"""
|
| 158 |
+
Handle 'brotli' decoding.
|
| 159 |
+
|
| 160 |
+
Requires `pip install brotlipy`. See: https://brotlipy.readthedocs.io/
|
| 161 |
+
or `pip install brotli`. See https://github.com/google/brotli
|
| 162 |
+
Supports both 'brotlipy' and 'Brotli' packages since they share an import
|
| 163 |
+
name. The top branches are for 'brotlipy' and bottom branches for 'Brotli'
|
| 164 |
+
"""
|
| 165 |
+
|
| 166 |
+
def __init__(self) -> None:
|
| 167 |
+
if brotli is None: # pragma: no cover
|
| 168 |
+
raise ImportError(
|
| 169 |
+
"Using 'BrotliDecoder', but neither of the 'brotlicffi' or 'brotli' "
|
| 170 |
+
"packages have been installed. "
|
| 171 |
+
"Make sure to install httpx using `pip install httpx[brotli]`."
|
| 172 |
+
) from None
|
| 173 |
+
|
| 174 |
+
self.decompressor = brotli.Decompressor()
|
| 175 |
+
self.seen_data = False
|
| 176 |
+
self._decompress: typing.Callable[..., bytes]
|
| 177 |
+
if hasattr(self.decompressor, "decompress"):
|
| 178 |
+
# The 'brotlicffi' package.
|
| 179 |
+
self._decompress = self.decompressor.decompress # pragma: no cover
|
| 180 |
+
else:
|
| 181 |
+
# The 'brotli' package.
|
| 182 |
+
self._decompress = self.decompressor.process
|
| 183 |
+
|
| 184 |
+
def decode(self, data: bytes) -> typing.Iterator[bytes]:
|
| 185 |
+
if not data:
|
| 186 |
+
return
|
| 187 |
+
self.seen_data = True
|
| 188 |
+
try:
|
| 189 |
+
# The C backend may allocate nearly twice the requested threshold.
|
| 190 |
+
output_buffer_limit = MAX_DECODE_CHUNK_SIZE // 2
|
| 191 |
+
decompressed = self._decompress(data, output_buffer_limit=output_buffer_limit)
|
| 192 |
+
while decompressed:
|
| 193 |
+
yield decompressed
|
| 194 |
+
decompressed = self._decompress(b"", output_buffer_limit=output_buffer_limit)
|
| 195 |
+
except brotli.error as exc:
|
| 196 |
+
raise DecodingError(str(exc)) from exc
|
| 197 |
+
|
| 198 |
+
def flush(self) -> typing.Iterator[bytes]:
|
| 199 |
+
if not self.seen_data:
|
| 200 |
+
return
|
| 201 |
+
try:
|
| 202 |
+
if hasattr(self.decompressor, "finish"):
|
| 203 |
+
# Only available in the 'brotlicffi' package.
|
| 204 |
+
|
| 205 |
+
# As the decompressor decompresses eagerly, this
|
| 206 |
+
# will never actually emit any data. However, it will potentially throw
|
| 207 |
+
# errors if a truncated or damaged data stream has been used.
|
| 208 |
+
self.decompressor.finish() # pragma: no cover
|
| 209 |
+
except brotli.error as exc: # pragma: no cover
|
| 210 |
+
raise DecodingError(str(exc)) from exc
|
| 211 |
+
yield from ()
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
class ZStandardDecoder(ContentDecoder):
|
| 215 |
+
"""Handle 'zstd' RFC 8878 decoding.
|
| 216 |
+
|
| 217 |
+
On Python 3.13 and below, this requires `pip install httpx2[zstd]`.
|
| 218 |
+
"""
|
| 219 |
+
|
| 220 |
+
# inspired by the ZstdDecoder implementation in urllib3
|
| 221 |
+
def __init__(self) -> None:
|
| 222 |
+
if not _zstandard_installed: # pragma: no cover
|
| 223 |
+
raise ImportError(
|
| 224 |
+
"Using 'ZStandardDecoder', ...Make sure to install httpx using `pip install httpx[zstd]`."
|
| 225 |
+
) from None
|
| 226 |
+
|
| 227 |
+
self.decompressor = ZstdDecompressor()
|
| 228 |
+
self.seen_data = False
|
| 229 |
+
|
| 230 |
+
def decode(self, data: bytes) -> typing.Iterator[bytes]:
|
| 231 |
+
if not data:
|
| 232 |
+
return
|
| 233 |
+
self.seen_data = True
|
| 234 |
+
try:
|
| 235 |
+
if self.decompressor.eof:
|
| 236 |
+
data = self.decompressor.unused_data + data
|
| 237 |
+
self.decompressor = ZstdDecompressor()
|
| 238 |
+
while True:
|
| 239 |
+
decompressed = self.decompressor.decompress(data, MAX_DECODE_CHUNK_SIZE)
|
| 240 |
+
while decompressed:
|
| 241 |
+
yield decompressed
|
| 242 |
+
if self.decompressor.needs_input or self.decompressor.eof:
|
| 243 |
+
break
|
| 244 |
+
decompressed = self.decompressor.decompress(b"", MAX_DECODE_CHUNK_SIZE)
|
| 245 |
+
if not (self.decompressor.eof and self.decompressor.unused_data):
|
| 246 |
+
break
|
| 247 |
+
data = self.decompressor.unused_data
|
| 248 |
+
self.decompressor = ZstdDecompressor()
|
| 249 |
+
except ZstdError as exc:
|
| 250 |
+
raise DecodingError(str(exc)) from exc
|
| 251 |
+
|
| 252 |
+
def flush(self) -> typing.Iterator[bytes]:
|
| 253 |
+
if not self.seen_data:
|
| 254 |
+
return
|
| 255 |
+
if not self.decompressor.eof:
|
| 256 |
+
raise DecodingError("Zstandard data is incomplete") # pragma: no cover
|
| 257 |
+
yield from ()
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
class MultiDecoder(ContentDecoder):
|
| 261 |
+
"""
|
| 262 |
+
Handle the case where multiple encodings have been applied.
|
| 263 |
+
"""
|
| 264 |
+
|
| 265 |
+
max_decode_links: typing.ClassVar[int] = 5
|
| 266 |
+
|
| 267 |
+
def __init__(self, encodings: typing.Sequence[str]) -> None:
|
| 268 |
+
"""
|
| 269 |
+
'encodings' should be the content codings in the order in which
|
| 270 |
+
each was applied.
|
| 271 |
+
"""
|
| 272 |
+
codings = [encoding for encoding in encodings if encoding in SUPPORTED_DECODERS]
|
| 273 |
+
if len(codings) > self.max_decode_links:
|
| 274 |
+
raise DecodingError(f"Cannot apply more than {self.max_decode_links} content encodings.")
|
| 275 |
+
# Note that we reverse the order for decoding.
|
| 276 |
+
self.children: list[ContentDecoder] = [SUPPORTED_DECODERS[coding]() for coding in reversed(codings)]
|
| 277 |
+
|
| 278 |
+
def decode(self, data: bytes) -> typing.Iterator[bytes]:
|
| 279 |
+
streams: typing.Iterator[bytes] = iter((data,))
|
| 280 |
+
for child in self.children:
|
| 281 |
+
streams = self._pipe(child.decode, streams)
|
| 282 |
+
yield from streams
|
| 283 |
+
|
| 284 |
+
def flush(self) -> typing.Iterator[bytes]:
|
| 285 |
+
streams: typing.Iterator[bytes] = iter(())
|
| 286 |
+
for child in self.children:
|
| 287 |
+
streams = itertools.chain(self._pipe(child.decode, streams), child.flush())
|
| 288 |
+
yield from streams
|
| 289 |
+
|
| 290 |
+
@staticmethod
|
| 291 |
+
def _pipe(
|
| 292 |
+
decode: typing.Callable[[bytes], typing.Iterator[bytes]],
|
| 293 |
+
upstream: typing.Iterator[bytes],
|
| 294 |
+
) -> typing.Iterator[bytes]:
|
| 295 |
+
for chunk in upstream:
|
| 296 |
+
yield from decode(chunk)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
class ByteChunker:
|
| 300 |
+
"""
|
| 301 |
+
Handles returning byte content in fixed-size chunks.
|
| 302 |
+
"""
|
| 303 |
+
|
| 304 |
+
def __init__(self, chunk_size: int | None = None) -> None:
|
| 305 |
+
self._buffer = io.BytesIO()
|
| 306 |
+
self._chunk_size = chunk_size
|
| 307 |
+
|
| 308 |
+
def decode(self, content: bytes) -> list[bytes]:
|
| 309 |
+
if self._chunk_size is None:
|
| 310 |
+
return [content] if content else []
|
| 311 |
+
|
| 312 |
+
self._buffer.write(content)
|
| 313 |
+
if self._buffer.tell() >= self._chunk_size:
|
| 314 |
+
value = self._buffer.getvalue()
|
| 315 |
+
chunks = [value[i : i + self._chunk_size] for i in range(0, len(value), self._chunk_size)]
|
| 316 |
+
if len(chunks[-1]) == self._chunk_size:
|
| 317 |
+
self._buffer.seek(0)
|
| 318 |
+
self._buffer.truncate()
|
| 319 |
+
return chunks
|
| 320 |
+
else:
|
| 321 |
+
self._buffer.seek(0)
|
| 322 |
+
self._buffer.write(chunks[-1])
|
| 323 |
+
self._buffer.truncate()
|
| 324 |
+
return chunks[:-1]
|
| 325 |
+
else:
|
| 326 |
+
return []
|
| 327 |
+
|
| 328 |
+
def flush(self) -> list[bytes]:
|
| 329 |
+
value = self._buffer.getvalue()
|
| 330 |
+
self._buffer.seek(0)
|
| 331 |
+
self._buffer.truncate()
|
| 332 |
+
return [value] if value else []
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
class TextChunker:
|
| 336 |
+
"""
|
| 337 |
+
Handles returning text content in fixed-size chunks.
|
| 338 |
+
"""
|
| 339 |
+
|
| 340 |
+
def __init__(self, chunk_size: int | None = None) -> None:
|
| 341 |
+
self._buffer = io.StringIO()
|
| 342 |
+
self._chunk_size = chunk_size
|
| 343 |
+
|
| 344 |
+
def decode(self, content: str) -> list[str]:
|
| 345 |
+
if self._chunk_size is None:
|
| 346 |
+
return [content] if content else []
|
| 347 |
+
|
| 348 |
+
self._buffer.write(content)
|
| 349 |
+
if self._buffer.tell() >= self._chunk_size:
|
| 350 |
+
value = self._buffer.getvalue()
|
| 351 |
+
chunks = [value[i : i + self._chunk_size] for i in range(0, len(value), self._chunk_size)]
|
| 352 |
+
if len(chunks[-1]) == self._chunk_size:
|
| 353 |
+
self._buffer.seek(0)
|
| 354 |
+
self._buffer.truncate()
|
| 355 |
+
return chunks
|
| 356 |
+
else:
|
| 357 |
+
self._buffer.seek(0)
|
| 358 |
+
self._buffer.write(chunks[-1])
|
| 359 |
+
self._buffer.truncate()
|
| 360 |
+
return chunks[:-1]
|
| 361 |
+
else:
|
| 362 |
+
return []
|
| 363 |
+
|
| 364 |
+
def flush(self) -> list[str]:
|
| 365 |
+
value = self._buffer.getvalue()
|
| 366 |
+
self._buffer.seek(0)
|
| 367 |
+
self._buffer.truncate()
|
| 368 |
+
return [value] if value else []
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
class TextDecoder:
|
| 372 |
+
"""
|
| 373 |
+
Handles incrementally decoding bytes into text
|
| 374 |
+
"""
|
| 375 |
+
|
| 376 |
+
def __init__(self, encoding: str = "utf-8") -> None:
|
| 377 |
+
self.decoder = codecs.getincrementaldecoder(encoding)(errors="replace")
|
| 378 |
+
|
| 379 |
+
def decode(self, data: bytes) -> str:
|
| 380 |
+
return self.decoder.decode(data)
|
| 381 |
+
|
| 382 |
+
def flush(self) -> str:
|
| 383 |
+
return self.decoder.decode(b"", True)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
class LineDecoder:
|
| 387 |
+
"""
|
| 388 |
+
Handles incrementally reading lines from text.
|
| 389 |
+
|
| 390 |
+
Has the same behaviour as the stdllib splitlines,
|
| 391 |
+
but handling the input iteratively.
|
| 392 |
+
"""
|
| 393 |
+
|
| 394 |
+
def __init__(self) -> None:
|
| 395 |
+
self.buffer: list[str] = []
|
| 396 |
+
self.trailing_cr: bool = False
|
| 397 |
+
|
| 398 |
+
def decode(self, text: str) -> list[str]:
|
| 399 |
+
# See https://docs.python.org/3/library/stdtypes.html#str.splitlines
|
| 400 |
+
NEWLINE_CHARS = "\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029"
|
| 401 |
+
|
| 402 |
+
# We always push a trailing `\r` into the next decode iteration.
|
| 403 |
+
if self.trailing_cr:
|
| 404 |
+
text = "\r" + text
|
| 405 |
+
self.trailing_cr = False
|
| 406 |
+
if text.endswith("\r"):
|
| 407 |
+
self.trailing_cr = True
|
| 408 |
+
text = text[:-1]
|
| 409 |
+
|
| 410 |
+
if not text:
|
| 411 |
+
# NOTE: the edge case input of empty text doesn't occur in practice,
|
| 412 |
+
# because other httpx internals filter out this value
|
| 413 |
+
return [] # pragma: no cover
|
| 414 |
+
|
| 415 |
+
trailing_newline = text[-1] in NEWLINE_CHARS
|
| 416 |
+
lines = text.splitlines()
|
| 417 |
+
|
| 418 |
+
if len(lines) == 1 and not trailing_newline:
|
| 419 |
+
# No new lines, buffer the input and continue.
|
| 420 |
+
self.buffer.append(lines[0])
|
| 421 |
+
return []
|
| 422 |
+
|
| 423 |
+
if self.buffer:
|
| 424 |
+
# Include any existing buffer in the first portion of the
|
| 425 |
+
# splitlines result.
|
| 426 |
+
lines = ["".join(self.buffer) + lines[0]] + lines[1:]
|
| 427 |
+
self.buffer = []
|
| 428 |
+
|
| 429 |
+
if not trailing_newline:
|
| 430 |
+
# If the last segment of splitlines is not newline terminated,
|
| 431 |
+
# then drop it from our output and start a new buffer.
|
| 432 |
+
self.buffer = [lines.pop()]
|
| 433 |
+
|
| 434 |
+
return lines
|
| 435 |
+
|
| 436 |
+
def flush(self) -> list[str]:
|
| 437 |
+
if not self.buffer and not self.trailing_cr:
|
| 438 |
+
return []
|
| 439 |
+
|
| 440 |
+
lines = ["".join(self.buffer)]
|
| 441 |
+
self.buffer = []
|
| 442 |
+
self.trailing_cr = False
|
| 443 |
+
return lines
|
| 444 |
+
|
| 445 |
+
|
| 446 |
+
SUPPORTED_DECODERS: dict[str, type[ContentDecoder]] = {
|
| 447 |
+
"identity": IdentityDecoder,
|
| 448 |
+
"gzip": GZipDecoder,
|
| 449 |
+
"deflate": DeflateDecoder,
|
| 450 |
+
"br": BrotliDecoder,
|
| 451 |
+
"zstd": ZStandardDecoder,
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
if brotli is None:
|
| 456 |
+
SUPPORTED_DECODERS.pop("br") # pragma: no cover
|
| 457 |
+
if not _zstandard_installed:
|
| 458 |
+
SUPPORTED_DECODERS.pop("zstd") # pragma: no cover
|
hug/lib/python3.12/site-packages/httpx2/_exceptions.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
from collections.abc import Generator
|
| 39 |
+
|
| 40 |
+
if typing.TYPE_CHECKING:
|
| 41 |
+
from ._models import Request, Response # pragma: no cover
|
| 42 |
+
|
| 43 |
+
__all__ = [
|
| 44 |
+
"CloseError",
|
| 45 |
+
"ConnectError",
|
| 46 |
+
"ConnectTimeout",
|
| 47 |
+
"CookieConflict",
|
| 48 |
+
"DecodingError",
|
| 49 |
+
"HTTPError",
|
| 50 |
+
"HTTPStatusError",
|
| 51 |
+
"InvalidURL",
|
| 52 |
+
"LocalProtocolError",
|
| 53 |
+
"NetworkError",
|
| 54 |
+
"PoolTimeout",
|
| 55 |
+
"ProtocolError",
|
| 56 |
+
"ProxyError",
|
| 57 |
+
"ReadError",
|
| 58 |
+
"ReadTimeout",
|
| 59 |
+
"RemoteProtocolError",
|
| 60 |
+
"RequestError",
|
| 61 |
+
"RequestNotRead",
|
| 62 |
+
"ResponseNotRead",
|
| 63 |
+
"StreamClosed",
|
| 64 |
+
"StreamConsumed",
|
| 65 |
+
"StreamError",
|
| 66 |
+
"TimeoutException",
|
| 67 |
+
"TooManyRedirects",
|
| 68 |
+
"TransportError",
|
| 69 |
+
"UnsupportedProtocol",
|
| 70 |
+
"WriteError",
|
| 71 |
+
"WriteTimeout",
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class HTTPXDeprecationWarning(UserWarning):
|
| 76 |
+
"""A custom deprecation warning for HTTPX.
|
| 77 |
+
|
| 78 |
+
Unlike the built-in `DeprecationWarning`, this inherits from `UserWarning` to ensure it is visible by default,
|
| 79 |
+
helping users discover deprecated features without needing to enable warnings explicitly.
|
| 80 |
+
|
| 81 |
+
Reference: https://sethmlarson.dev/deprecations-via-warnings-dont-work-for-python-libraries
|
| 82 |
+
"""
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class HTTPError(Exception):
|
| 86 |
+
"""
|
| 87 |
+
Base class for `RequestError` and `HTTPStatusError`.
|
| 88 |
+
|
| 89 |
+
Useful for `try...except` blocks when issuing a request,
|
| 90 |
+
and then calling `.raise_for_status()`.
|
| 91 |
+
|
| 92 |
+
For example:
|
| 93 |
+
|
| 94 |
+
```
|
| 95 |
+
try:
|
| 96 |
+
response = httpx2.get("https://www.example.com")
|
| 97 |
+
response.raise_for_status()
|
| 98 |
+
except httpx2.HTTPError as exc:
|
| 99 |
+
print(f"HTTP Exception for {exc.request.url} - {exc}")
|
| 100 |
+
```
|
| 101 |
+
"""
|
| 102 |
+
|
| 103 |
+
def __init__(self, message: str) -> None:
|
| 104 |
+
super().__init__(message)
|
| 105 |
+
self._request: Request | None = None
|
| 106 |
+
|
| 107 |
+
@property
|
| 108 |
+
def request(self) -> Request:
|
| 109 |
+
if self._request is None:
|
| 110 |
+
raise RuntimeError("The .request property has not been set.")
|
| 111 |
+
return self._request
|
| 112 |
+
|
| 113 |
+
@request.setter
|
| 114 |
+
def request(self, request: Request) -> None:
|
| 115 |
+
self._request = request
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class RequestError(HTTPError):
|
| 119 |
+
"""
|
| 120 |
+
Base class for all exceptions that may occur when issuing a `.request()`.
|
| 121 |
+
"""
|
| 122 |
+
|
| 123 |
+
def __init__(self, message: str, *, request: Request | None = None) -> None:
|
| 124 |
+
super().__init__(message)
|
| 125 |
+
# At the point an exception is raised we won't typically have a request
|
| 126 |
+
# instance to associate it with.
|
| 127 |
+
#
|
| 128 |
+
# The 'request_context' context manager is used within the Client and
|
| 129 |
+
# Response methods in order to ensure that any raised exceptions
|
| 130 |
+
# have a `.request` property set on them.
|
| 131 |
+
self._request = request
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
class TransportError(RequestError):
|
| 135 |
+
"""
|
| 136 |
+
Base class for all exceptions that occur at the level of the Transport API.
|
| 137 |
+
"""
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# Timeout exceptions...
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
class TimeoutException(TransportError):
|
| 144 |
+
"""
|
| 145 |
+
The base class for timeout errors.
|
| 146 |
+
|
| 147 |
+
An operation has timed out.
|
| 148 |
+
"""
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
class ConnectTimeout(TimeoutException):
|
| 152 |
+
"""
|
| 153 |
+
Timed out while connecting to the host.
|
| 154 |
+
"""
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
class ReadTimeout(TimeoutException):
|
| 158 |
+
"""
|
| 159 |
+
Timed out while receiving data from the host.
|
| 160 |
+
"""
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
class WriteTimeout(TimeoutException):
|
| 164 |
+
"""
|
| 165 |
+
Timed out while sending data to the host.
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
class PoolTimeout(TimeoutException):
|
| 170 |
+
"""
|
| 171 |
+
Timed out waiting to acquire a connection from the pool.
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
# Core networking exceptions...
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
class NetworkError(TransportError):
|
| 179 |
+
"""
|
| 180 |
+
The base class for network-related errors.
|
| 181 |
+
|
| 182 |
+
An error occurred while interacting with the network.
|
| 183 |
+
"""
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
class ReadError(NetworkError):
|
| 187 |
+
"""
|
| 188 |
+
Failed to receive data from the network.
|
| 189 |
+
"""
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
class WriteError(NetworkError):
|
| 193 |
+
"""
|
| 194 |
+
Failed to send data through the network.
|
| 195 |
+
"""
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
class ConnectError(NetworkError):
|
| 199 |
+
"""
|
| 200 |
+
Failed to establish a connection.
|
| 201 |
+
"""
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
class CloseError(NetworkError):
|
| 205 |
+
"""
|
| 206 |
+
Failed to close a connection.
|
| 207 |
+
"""
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
# Other transport exceptions...
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
class ProxyError(TransportError):
|
| 214 |
+
"""
|
| 215 |
+
An error occurred while establishing a proxy connection.
|
| 216 |
+
"""
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class UnsupportedProtocol(TransportError):
|
| 220 |
+
"""
|
| 221 |
+
Attempted to make a request to an unsupported protocol.
|
| 222 |
+
|
| 223 |
+
For example issuing a request to `ftp://www.example.com`.
|
| 224 |
+
"""
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
class ProtocolError(TransportError):
|
| 228 |
+
"""
|
| 229 |
+
The protocol was violated.
|
| 230 |
+
"""
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class LocalProtocolError(ProtocolError):
|
| 234 |
+
"""
|
| 235 |
+
A protocol was violated by the client.
|
| 236 |
+
|
| 237 |
+
For example if the user instantiated a `Request` instance explicitly,
|
| 238 |
+
failed to include the mandatory `Host:` header, and then issued it directly
|
| 239 |
+
using `client.send()`.
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
class RemoteProtocolError(ProtocolError):
|
| 244 |
+
"""
|
| 245 |
+
The protocol was violated by the server.
|
| 246 |
+
|
| 247 |
+
For example, returning malformed HTTP.
|
| 248 |
+
"""
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
# Other request exceptions...
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
class DecodingError(RequestError):
|
| 255 |
+
"""
|
| 256 |
+
Decoding of the response failed, due to a malformed encoding.
|
| 257 |
+
"""
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
class TooManyRedirects(RequestError):
|
| 261 |
+
"""
|
| 262 |
+
Too many redirects.
|
| 263 |
+
"""
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
# Client errors
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
class HTTPStatusError(HTTPError):
|
| 270 |
+
"""
|
| 271 |
+
The response had an error HTTP status of 4xx or 5xx.
|
| 272 |
+
|
| 273 |
+
May be raised when calling `response.raise_for_status()`
|
| 274 |
+
"""
|
| 275 |
+
|
| 276 |
+
def __init__(self, message: str, *, request: Request, response: Response) -> None:
|
| 277 |
+
super().__init__(message)
|
| 278 |
+
self.request = request
|
| 279 |
+
self.response = response
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
class InvalidURL(Exception):
|
| 283 |
+
"""
|
| 284 |
+
URL is improperly formed or cannot be parsed.
|
| 285 |
+
"""
|
| 286 |
+
|
| 287 |
+
def __init__(self, message: str) -> None:
|
| 288 |
+
super().__init__(message)
|
| 289 |
+
|
| 290 |
+
|
| 291 |
+
class CookieConflict(Exception):
|
| 292 |
+
"""
|
| 293 |
+
Attempted to lookup a cookie by name, but multiple cookies existed.
|
| 294 |
+
|
| 295 |
+
Can occur when calling `response.cookies.get(...)`.
|
| 296 |
+
"""
|
| 297 |
+
|
| 298 |
+
def __init__(self, message: str) -> None:
|
| 299 |
+
super().__init__(message)
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
# Stream exceptions...
|
| 303 |
+
|
| 304 |
+
# These may occur as the result of a programming error, by accessing
|
| 305 |
+
# the request/response stream in an invalid manner.
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
class StreamError(RuntimeError):
|
| 309 |
+
"""
|
| 310 |
+
The base class for stream exceptions.
|
| 311 |
+
|
| 312 |
+
The developer made an error in accessing the request stream in
|
| 313 |
+
an invalid way.
|
| 314 |
+
"""
|
| 315 |
+
|
| 316 |
+
def __init__(self, message: str) -> None:
|
| 317 |
+
super().__init__(message)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
class StreamConsumed(StreamError):
|
| 321 |
+
"""
|
| 322 |
+
Attempted to read or stream content, but the content has already
|
| 323 |
+
been streamed.
|
| 324 |
+
"""
|
| 325 |
+
|
| 326 |
+
def __init__(self) -> None:
|
| 327 |
+
message = (
|
| 328 |
+
"Attempted to read or stream some content, but the content has "
|
| 329 |
+
"already been streamed. For requests, this could be due to passing "
|
| 330 |
+
"a generator as request content, and then receiving a redirect "
|
| 331 |
+
"response or a secondary request as part of an authentication flow."
|
| 332 |
+
"For responses, this could be due to attempting to stream the response "
|
| 333 |
+
"content more than once."
|
| 334 |
+
)
|
| 335 |
+
super().__init__(message)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
class StreamClosed(StreamError):
|
| 339 |
+
"""
|
| 340 |
+
Attempted to read or stream response content, but the request has been
|
| 341 |
+
closed.
|
| 342 |
+
"""
|
| 343 |
+
|
| 344 |
+
def __init__(self) -> None:
|
| 345 |
+
message = "Attempted to read or stream content, but the stream has been closed."
|
| 346 |
+
super().__init__(message)
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
class ResponseNotRead(StreamError):
|
| 350 |
+
"""
|
| 351 |
+
Attempted to access streaming response content, without having called `read()`.
|
| 352 |
+
"""
|
| 353 |
+
|
| 354 |
+
def __init__(self) -> None:
|
| 355 |
+
message = "Attempted to access streaming response content, without having called `read()`."
|
| 356 |
+
super().__init__(message)
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class RequestNotRead(StreamError):
|
| 360 |
+
"""
|
| 361 |
+
Attempted to access streaming request content, without having called `read()`.
|
| 362 |
+
"""
|
| 363 |
+
|
| 364 |
+
def __init__(self) -> None:
|
| 365 |
+
message = "Attempted to access streaming request content, without having called `read()`."
|
| 366 |
+
super().__init__(message)
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
@contextlib.contextmanager
|
| 370 |
+
def request_context(request: Request | None = None) -> Generator[None]:
|
| 371 |
+
"""
|
| 372 |
+
A context manager that can be used to attach the given request context
|
| 373 |
+
to any `RequestError` exceptions that are raised within the block.
|
| 374 |
+
"""
|
| 375 |
+
try:
|
| 376 |
+
yield
|
| 377 |
+
except RequestError as exc:
|
| 378 |
+
if request is not None:
|
| 379 |
+
exc.request = request
|
| 380 |
+
raise exc
|
hug/lib/python3.12/site-packages/httpx2/_main.py
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 httpcore2 # 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("Usage: [bold]httpx2[/bold] [cyan]<URL> [OPTIONS][/cyan] ", justify="left")
|
| 34 |
+
console.print()
|
| 35 |
+
|
| 36 |
+
table = rich.table.Table.grid(padding=1, pad_edge=True)
|
| 37 |
+
table.add_column("Parameter", no_wrap=True, justify="left", style="bold")
|
| 38 |
+
table.add_column("Description")
|
| 39 |
+
table.add_row(
|
| 40 |
+
"-m, --method [cyan]METHOD",
|
| 41 |
+
"Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD, QUERY.\n"
|
| 42 |
+
"[Default: GET, or POST if a request body is included]",
|
| 43 |
+
)
|
| 44 |
+
table.add_row(
|
| 45 |
+
"-p, --params [cyan]<NAME VALUE> ...",
|
| 46 |
+
"Query parameters to include in the request URL.",
|
| 47 |
+
)
|
| 48 |
+
table.add_row("-c, --content [cyan]TEXT", "Byte content to include in the request body.")
|
| 49 |
+
table.add_row("-d, --data [cyan]<NAME VALUE> ...", "Form data to include in the request body.")
|
| 50 |
+
table.add_row(
|
| 51 |
+
"-f, --files [cyan]<NAME FILENAME> ...",
|
| 52 |
+
"Form files to include in the request body.",
|
| 53 |
+
)
|
| 54 |
+
table.add_row("-j, --json [cyan]TEXT", "JSON data to include in the request body.")
|
| 55 |
+
table.add_row(
|
| 56 |
+
"-h, --headers [cyan]<NAME VALUE> ...",
|
| 57 |
+
"Include additional HTTP headers in the request.",
|
| 58 |
+
)
|
| 59 |
+
table.add_row("--cookies [cyan]<NAME VALUE> ...", "Cookies to include in the request.")
|
| 60 |
+
table.add_row(
|
| 61 |
+
"--auth [cyan]<USER PASS>",
|
| 62 |
+
"Username and password to include in the request. Specify '-' for the password"
|
| 63 |
+
" to use a password prompt. Note that using --verbose/-v will expose"
|
| 64 |
+
" the Authorization header, including the password encoding"
|
| 65 |
+
" in a trivially reversible format.",
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
table.add_row(
|
| 69 |
+
"--proxy [cyan]URL",
|
| 70 |
+
"Send the request via a proxy. Should be the URL giving the proxy address.",
|
| 71 |
+
)
|
| 72 |
+
|
| 73 |
+
table.add_row(
|
| 74 |
+
"--timeout [cyan]FLOAT",
|
| 75 |
+
"Timeout value to use for network operations, such as establishing the"
|
| 76 |
+
" connection, reading some data, etc... [Default: 5.0]",
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
table.add_row("--follow-redirects", "Automatically follow redirects.")
|
| 80 |
+
table.add_row("--verify / --no-verify", "Enable SSL verification.")
|
| 81 |
+
table.add_row("--http2", "Send the request using HTTP/2, if the remote server supports it.")
|
| 82 |
+
|
| 83 |
+
table.add_row(
|
| 84 |
+
"--download [cyan]FILE",
|
| 85 |
+
"Save the response content as a file, rather than displaying it.",
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
table.add_row("-v, --verbose", "Verbose output. Show request as well as response.")
|
| 89 |
+
table.add_row("--help", "Show this message and exit.")
|
| 90 |
+
console.print(table)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def get_lexer_for_response(response: Response) -> str:
|
| 94 |
+
content_type = response.headers.get("Content-Type")
|
| 95 |
+
if content_type is not None:
|
| 96 |
+
mime_type, _, _ = content_type.partition(";")
|
| 97 |
+
try:
|
| 98 |
+
return typing.cast(str, pygments.lexers.get_lexer_for_mimetype(mime_type.strip()).name)
|
| 99 |
+
except pygments.util.ClassNotFound: # pragma: no cover
|
| 100 |
+
pass
|
| 101 |
+
return "" # pragma: no cover
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def format_request_headers(request: httpcore2.Request, http2: bool = False) -> str:
|
| 105 |
+
version = "HTTP/2" if http2 else "HTTP/1.1"
|
| 106 |
+
headers = [(name.lower() if http2 else name, value) for name, value in request.headers]
|
| 107 |
+
method = request.method.decode("ascii")
|
| 108 |
+
target = request.url.target.decode("ascii")
|
| 109 |
+
lines = [f"{method} {target} {version}"] + [
|
| 110 |
+
f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers
|
| 111 |
+
]
|
| 112 |
+
return "\n".join(lines)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def format_response_headers(
|
| 116 |
+
http_version: bytes,
|
| 117 |
+
status: int,
|
| 118 |
+
reason_phrase: bytes | None,
|
| 119 |
+
headers: list[tuple[bytes, bytes]],
|
| 120 |
+
) -> str:
|
| 121 |
+
version = http_version.decode("ascii")
|
| 122 |
+
reason = codes.get_reason_phrase(status) if reason_phrase is None else reason_phrase.decode("ascii")
|
| 123 |
+
lines = [f"{version} {status} {reason}"] + [
|
| 124 |
+
f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers
|
| 125 |
+
]
|
| 126 |
+
return "\n".join(lines)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def print_request_headers(request: httpcore2.Request, http2: bool = False) -> None:
|
| 130 |
+
console = rich.console.Console()
|
| 131 |
+
http_text = format_request_headers(request, http2=http2)
|
| 132 |
+
syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True)
|
| 133 |
+
console.print(syntax)
|
| 134 |
+
syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True)
|
| 135 |
+
console.print(syntax)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def print_response_headers(
|
| 139 |
+
http_version: bytes,
|
| 140 |
+
status: int,
|
| 141 |
+
reason_phrase: bytes | None,
|
| 142 |
+
headers: list[tuple[bytes, bytes]],
|
| 143 |
+
) -> None:
|
| 144 |
+
console = rich.console.Console()
|
| 145 |
+
http_text = format_response_headers(http_version, status, reason_phrase, headers)
|
| 146 |
+
syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True)
|
| 147 |
+
console.print(syntax)
|
| 148 |
+
syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True)
|
| 149 |
+
console.print(syntax)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def print_response(response: Response) -> None:
|
| 153 |
+
console = rich.console.Console()
|
| 154 |
+
lexer_name = get_lexer_for_response(response)
|
| 155 |
+
if lexer_name:
|
| 156 |
+
if lexer_name.lower() == "json":
|
| 157 |
+
try:
|
| 158 |
+
data = response.json()
|
| 159 |
+
text = json.dumps(data, indent=4)
|
| 160 |
+
except ValueError: # pragma: no cover
|
| 161 |
+
text = response.text
|
| 162 |
+
else:
|
| 163 |
+
text = response.text
|
| 164 |
+
|
| 165 |
+
syntax = rich.syntax.Syntax(text, lexer_name, theme="ansi_dark", word_wrap=True)
|
| 166 |
+
console.print(syntax)
|
| 167 |
+
else:
|
| 168 |
+
console.print(f"<{len(response.content)} bytes of binary data>")
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
_PCTRTT = tuple[tuple[str, str], ...]
|
| 172 |
+
_PCTRTTT = tuple[_PCTRTT, ...]
|
| 173 |
+
_PeerCertRetDictType = dict[str, str | _PCTRTTT | _PCTRTT]
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def format_certificate(cert: _PeerCertRetDictType) -> str: # pragma: no cover
|
| 177 |
+
lines: list[str] = []
|
| 178 |
+
for key, value in cert.items():
|
| 179 |
+
if isinstance(value, (list, tuple)):
|
| 180 |
+
lines.append(f"* {key}:")
|
| 181 |
+
for item in value:
|
| 182 |
+
if key in ("subject", "issuer"):
|
| 183 |
+
lines.extend(f"* {sub_item[0]}: {sub_item[1]!r}" for sub_item in item)
|
| 184 |
+
elif isinstance(item, tuple) and len(item) == 2:
|
| 185 |
+
lines.append(f"* {item[0]}: {item[1]!r}")
|
| 186 |
+
else:
|
| 187 |
+
lines.append(f"* {item!r}")
|
| 188 |
+
else:
|
| 189 |
+
lines.append(f"* {key}: {value!r}")
|
| 190 |
+
return "\n".join(lines)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def trace(name: str, info: typing.Mapping[str, typing.Any], verbose: bool = False) -> None:
|
| 194 |
+
console = rich.console.Console()
|
| 195 |
+
if name == "connection.connect_tcp.started" and verbose:
|
| 196 |
+
host = info["host"]
|
| 197 |
+
console.print(f"* Connecting to {host!r}")
|
| 198 |
+
elif name == "connection.connect_tcp.complete" and verbose:
|
| 199 |
+
stream = info["return_value"]
|
| 200 |
+
server_addr = stream.get_extra_info("server_addr")
|
| 201 |
+
console.print(f"* Connected to {server_addr[0]!r} on port {server_addr[1]}")
|
| 202 |
+
elif name == "connection.start_tls.complete" and verbose: # pragma: no cover
|
| 203 |
+
stream = info["return_value"]
|
| 204 |
+
ssl_object = stream.get_extra_info("ssl_object")
|
| 205 |
+
version = ssl_object.version()
|
| 206 |
+
cipher = ssl_object.cipher()
|
| 207 |
+
server_cert = ssl_object.getpeercert()
|
| 208 |
+
alpn = ssl_object.selected_alpn_protocol()
|
| 209 |
+
console.print(f"* SSL established using {version!r} / {cipher[0]!r}")
|
| 210 |
+
console.print(f"* Selected ALPN protocol: {alpn!r}")
|
| 211 |
+
if server_cert:
|
| 212 |
+
console.print("* Server certificate:")
|
| 213 |
+
console.print(format_certificate(server_cert))
|
| 214 |
+
elif name == "http11.send_request_headers.started" and verbose:
|
| 215 |
+
request = info["request"]
|
| 216 |
+
print_request_headers(request, http2=False)
|
| 217 |
+
elif name == "http2.send_request_headers.started" and verbose: # pragma: no cover
|
| 218 |
+
request = info["request"]
|
| 219 |
+
print_request_headers(request, http2=True)
|
| 220 |
+
elif name == "http11.receive_response_headers.complete":
|
| 221 |
+
http_version, status, reason_phrase, headers = info["return_value"]
|
| 222 |
+
print_response_headers(http_version, status, reason_phrase, headers)
|
| 223 |
+
elif name == "http2.receive_response_headers.complete": # pragma: no cover
|
| 224 |
+
status, headers = info["return_value"]
|
| 225 |
+
http_version = b"HTTP/2"
|
| 226 |
+
reason_phrase = None
|
| 227 |
+
print_response_headers(http_version, status, reason_phrase, headers)
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def download_response(response: Response, download: typing.BinaryIO) -> None:
|
| 231 |
+
console = rich.console.Console()
|
| 232 |
+
console.print()
|
| 233 |
+
content_length = response.headers.get("Content-Length")
|
| 234 |
+
with rich.progress.Progress(
|
| 235 |
+
"[progress.description]{task.description}",
|
| 236 |
+
"[progress.percentage]{task.percentage:>3.0f}%",
|
| 237 |
+
rich.progress.BarColumn(bar_width=None),
|
| 238 |
+
rich.progress.DownloadColumn(),
|
| 239 |
+
rich.progress.TransferSpeedColumn(),
|
| 240 |
+
) as progress:
|
| 241 |
+
description = f"Downloading [bold]{rich.markup.escape(download.name)}"
|
| 242 |
+
download_task = progress.add_task(
|
| 243 |
+
description,
|
| 244 |
+
total=int(content_length or 0),
|
| 245 |
+
start=content_length is not None,
|
| 246 |
+
)
|
| 247 |
+
for chunk in response.iter_bytes():
|
| 248 |
+
download.write(chunk)
|
| 249 |
+
progress.update(download_task, completed=response.num_bytes_downloaded)
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
def validate_json(
|
| 253 |
+
ctx: click.Context,
|
| 254 |
+
param: click.Option | click.Parameter,
|
| 255 |
+
value: typing.Any,
|
| 256 |
+
) -> typing.Any:
|
| 257 |
+
if value is None:
|
| 258 |
+
return None
|
| 259 |
+
|
| 260 |
+
try:
|
| 261 |
+
return json.loads(value)
|
| 262 |
+
except json.JSONDecodeError: # pragma: no cover
|
| 263 |
+
raise click.BadParameter("Not valid JSON")
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def validate_auth(
|
| 267 |
+
ctx: click.Context,
|
| 268 |
+
param: click.Option | click.Parameter,
|
| 269 |
+
value: typing.Any,
|
| 270 |
+
) -> typing.Any:
|
| 271 |
+
if value == (None, None):
|
| 272 |
+
return None
|
| 273 |
+
|
| 274 |
+
username, password = value
|
| 275 |
+
if password == "-": # pragma: no cover
|
| 276 |
+
password = click.prompt("Password", hide_input=True)
|
| 277 |
+
return (username, password)
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def handle_help(
|
| 281 |
+
ctx: click.Context,
|
| 282 |
+
param: click.Option | click.Parameter,
|
| 283 |
+
value: typing.Any,
|
| 284 |
+
) -> None:
|
| 285 |
+
if not value or ctx.resilient_parsing:
|
| 286 |
+
return
|
| 287 |
+
|
| 288 |
+
print_help()
|
| 289 |
+
ctx.exit()
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
@click.command(add_help_option=False)
|
| 293 |
+
@click.argument("url", type=str)
|
| 294 |
+
@click.option(
|
| 295 |
+
"--method",
|
| 296 |
+
"-m",
|
| 297 |
+
"method",
|
| 298 |
+
type=str,
|
| 299 |
+
help=(
|
| 300 |
+
"Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD, QUERY. "
|
| 301 |
+
"[Default: GET, or POST if a request body is included]"
|
| 302 |
+
),
|
| 303 |
+
)
|
| 304 |
+
@click.option(
|
| 305 |
+
"--params",
|
| 306 |
+
"-p",
|
| 307 |
+
"params",
|
| 308 |
+
type=(str, str),
|
| 309 |
+
multiple=True,
|
| 310 |
+
help="Query parameters to include in the request URL.",
|
| 311 |
+
)
|
| 312 |
+
@click.option(
|
| 313 |
+
"--content",
|
| 314 |
+
"-c",
|
| 315 |
+
"content",
|
| 316 |
+
type=str,
|
| 317 |
+
help="Byte content to include in the request body.",
|
| 318 |
+
)
|
| 319 |
+
@click.option(
|
| 320 |
+
"--data",
|
| 321 |
+
"-d",
|
| 322 |
+
"data",
|
| 323 |
+
type=(str, str),
|
| 324 |
+
multiple=True,
|
| 325 |
+
help="Form data to include in the request body.",
|
| 326 |
+
)
|
| 327 |
+
@click.option(
|
| 328 |
+
"--files",
|
| 329 |
+
"-f",
|
| 330 |
+
"files",
|
| 331 |
+
type=(str, click.File(mode="rb")),
|
| 332 |
+
multiple=True,
|
| 333 |
+
help="Form files to include in the request body.",
|
| 334 |
+
)
|
| 335 |
+
@click.option(
|
| 336 |
+
"--json",
|
| 337 |
+
"-j",
|
| 338 |
+
"json",
|
| 339 |
+
type=str,
|
| 340 |
+
callback=validate_json,
|
| 341 |
+
help="JSON data to include in the request body.",
|
| 342 |
+
)
|
| 343 |
+
@click.option(
|
| 344 |
+
"--headers",
|
| 345 |
+
"-h",
|
| 346 |
+
"headers",
|
| 347 |
+
type=(str, str),
|
| 348 |
+
multiple=True,
|
| 349 |
+
help="Include additional HTTP headers in the request.",
|
| 350 |
+
)
|
| 351 |
+
@click.option(
|
| 352 |
+
"--cookies",
|
| 353 |
+
"cookies",
|
| 354 |
+
type=(str, str),
|
| 355 |
+
multiple=True,
|
| 356 |
+
help="Cookies to include in the request.",
|
| 357 |
+
)
|
| 358 |
+
@click.option(
|
| 359 |
+
"--auth",
|
| 360 |
+
"auth",
|
| 361 |
+
type=(str, str),
|
| 362 |
+
default=(None, None),
|
| 363 |
+
callback=validate_auth,
|
| 364 |
+
help=(
|
| 365 |
+
"Username and password to include in the request. "
|
| 366 |
+
"Specify '-' for the password to use a password prompt. "
|
| 367 |
+
"Note that using --verbose/-v will expose the Authorization header, "
|
| 368 |
+
"including the password encoding in a trivially reversible format."
|
| 369 |
+
),
|
| 370 |
+
)
|
| 371 |
+
@click.option(
|
| 372 |
+
"--proxy",
|
| 373 |
+
"proxy",
|
| 374 |
+
type=str,
|
| 375 |
+
default=None,
|
| 376 |
+
help="Send the request via a proxy. Should be the URL giving the proxy address.",
|
| 377 |
+
)
|
| 378 |
+
@click.option(
|
| 379 |
+
"--timeout",
|
| 380 |
+
"timeout",
|
| 381 |
+
type=float,
|
| 382 |
+
default=5.0,
|
| 383 |
+
help=(
|
| 384 |
+
"Timeout value to use for network operations, such as establishing the "
|
| 385 |
+
"connection, reading some data, etc... [Default: 5.0]"
|
| 386 |
+
),
|
| 387 |
+
)
|
| 388 |
+
@click.option(
|
| 389 |
+
"--follow-redirects",
|
| 390 |
+
"follow_redirects",
|
| 391 |
+
is_flag=True,
|
| 392 |
+
default=False,
|
| 393 |
+
help="Automatically follow redirects.",
|
| 394 |
+
)
|
| 395 |
+
@click.option(
|
| 396 |
+
"--verify/--no-verify",
|
| 397 |
+
default=True,
|
| 398 |
+
help="Enable SSL verification.",
|
| 399 |
+
)
|
| 400 |
+
@click.option(
|
| 401 |
+
"--http2",
|
| 402 |
+
"http2",
|
| 403 |
+
type=bool,
|
| 404 |
+
is_flag=True,
|
| 405 |
+
default=False,
|
| 406 |
+
help="Send the request using HTTP/2, if the remote server supports it.",
|
| 407 |
+
)
|
| 408 |
+
@click.option(
|
| 409 |
+
"--download",
|
| 410 |
+
type=click.File("wb"),
|
| 411 |
+
help="Save the response content as a file, rather than displaying it.",
|
| 412 |
+
)
|
| 413 |
+
@click.option(
|
| 414 |
+
"--verbose",
|
| 415 |
+
"-v",
|
| 416 |
+
type=bool,
|
| 417 |
+
is_flag=True,
|
| 418 |
+
default=False,
|
| 419 |
+
help="Verbose. Show request as well as response.",
|
| 420 |
+
)
|
| 421 |
+
@click.option(
|
| 422 |
+
"--help",
|
| 423 |
+
is_flag=True,
|
| 424 |
+
is_eager=True,
|
| 425 |
+
expose_value=False,
|
| 426 |
+
callback=handle_help,
|
| 427 |
+
help="Show this message and exit.",
|
| 428 |
+
)
|
| 429 |
+
def main(
|
| 430 |
+
url: str,
|
| 431 |
+
method: str,
|
| 432 |
+
params: list[tuple[str, str]],
|
| 433 |
+
content: str,
|
| 434 |
+
data: list[tuple[str, str]],
|
| 435 |
+
files: list[tuple[str, click.File]],
|
| 436 |
+
json: str,
|
| 437 |
+
headers: list[tuple[str, str]],
|
| 438 |
+
cookies: list[tuple[str, str]],
|
| 439 |
+
auth: tuple[str, str] | None,
|
| 440 |
+
proxy: str,
|
| 441 |
+
timeout: float,
|
| 442 |
+
follow_redirects: bool,
|
| 443 |
+
verify: bool,
|
| 444 |
+
http2: bool,
|
| 445 |
+
download: typing.BinaryIO | None,
|
| 446 |
+
verbose: bool,
|
| 447 |
+
) -> None:
|
| 448 |
+
"""
|
| 449 |
+
An HTTP command line client.
|
| 450 |
+
Sends a request and displays the response.
|
| 451 |
+
"""
|
| 452 |
+
if not method:
|
| 453 |
+
method = "POST" if content or data or files or json else "GET"
|
| 454 |
+
|
| 455 |
+
try:
|
| 456 |
+
with Client(proxy=proxy, timeout=timeout, http2=http2, verify=verify) as client:
|
| 457 |
+
with client.stream(
|
| 458 |
+
method,
|
| 459 |
+
url,
|
| 460 |
+
params=list(params),
|
| 461 |
+
content=content,
|
| 462 |
+
data=dict(data),
|
| 463 |
+
files=files, # type: ignore[arg-type]
|
| 464 |
+
json=json,
|
| 465 |
+
headers=headers,
|
| 466 |
+
cookies=dict(cookies),
|
| 467 |
+
auth=auth,
|
| 468 |
+
follow_redirects=follow_redirects,
|
| 469 |
+
extensions={"trace": functools.partial(trace, verbose=verbose)},
|
| 470 |
+
) as response:
|
| 471 |
+
if download is not None:
|
| 472 |
+
download_response(response, download)
|
| 473 |
+
else:
|
| 474 |
+
response.read()
|
| 475 |
+
if response.content:
|
| 476 |
+
print_response(response)
|
| 477 |
+
|
| 478 |
+
except RequestError as exc:
|
| 479 |
+
console = rich.console.Console()
|
| 480 |
+
console.print(f"[red]{type(exc).__name__}[/red]: {exc}")
|
| 481 |
+
sys.exit(1)
|
| 482 |
+
|
| 483 |
+
sys.exit(0 if response.is_success else 1)
|
hug/lib/python3.12/site-packages/httpx2/_models.py
ADDED
|
@@ -0,0 +1,1274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import codecs
|
| 4 |
+
import contextlib
|
| 5 |
+
import datetime
|
| 6 |
+
import email.message
|
| 7 |
+
import json as jsonlib
|
| 8 |
+
import re
|
| 9 |
+
import typing
|
| 10 |
+
import urllib.request
|
| 11 |
+
from collections.abc import AsyncGenerator, Mapping
|
| 12 |
+
from http.cookiejar import Cookie, CookieJar
|
| 13 |
+
|
| 14 |
+
from ._content import ByteStream, UnattachedStream, encode_request, encode_response
|
| 15 |
+
from ._cookies import IndexedCookieJar
|
| 16 |
+
from ._decoders import (
|
| 17 |
+
ByteChunker,
|
| 18 |
+
ContentDecoder,
|
| 19 |
+
IdentityDecoder,
|
| 20 |
+
LineDecoder,
|
| 21 |
+
MultiDecoder,
|
| 22 |
+
TextChunker,
|
| 23 |
+
TextDecoder,
|
| 24 |
+
)
|
| 25 |
+
from ._exceptions import (
|
| 26 |
+
CookieConflict,
|
| 27 |
+
HTTPStatusError,
|
| 28 |
+
RequestNotRead,
|
| 29 |
+
ResponseNotRead,
|
| 30 |
+
StreamClosed,
|
| 31 |
+
StreamConsumed,
|
| 32 |
+
request_context,
|
| 33 |
+
)
|
| 34 |
+
from ._multipart import get_multipart_boundary_from_content_type
|
| 35 |
+
from ._status_codes import codes
|
| 36 |
+
from ._types import (
|
| 37 |
+
AsyncByteStream,
|
| 38 |
+
CookieTypes,
|
| 39 |
+
HeaderTypes,
|
| 40 |
+
QueryParamTypes,
|
| 41 |
+
RequestContent,
|
| 42 |
+
RequestData,
|
| 43 |
+
RequestExtensions,
|
| 44 |
+
RequestFiles,
|
| 45 |
+
ResponseContent,
|
| 46 |
+
ResponseExtensions,
|
| 47 |
+
SyncByteStream,
|
| 48 |
+
)
|
| 49 |
+
from ._urls import URL
|
| 50 |
+
from ._utils import to_bytes_or_str, to_str
|
| 51 |
+
|
| 52 |
+
__all__ = ["Cookies", "Headers", "Request", "Response"]
|
| 53 |
+
|
| 54 |
+
SENSITIVE_HEADERS = {"authorization", "proxy-authorization"}
|
| 55 |
+
|
| 56 |
+
_T = typing.TypeVar("_T")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def _is_known_encoding(encoding: str) -> bool:
|
| 60 |
+
"""
|
| 61 |
+
Return `True` if `encoding` is a known codec.
|
| 62 |
+
"""
|
| 63 |
+
try:
|
| 64 |
+
codecs.lookup(encoding)
|
| 65 |
+
except LookupError:
|
| 66 |
+
return False
|
| 67 |
+
return True
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def _normalize_header_key(key: str | bytes, encoding: str | None = None) -> bytes:
|
| 71 |
+
"""
|
| 72 |
+
Coerce str/bytes into a strictly byte-wise HTTP header key.
|
| 73 |
+
"""
|
| 74 |
+
return key if isinstance(key, bytes) else key.encode(encoding or "ascii")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _normalize_header_value(value: str | bytes, encoding: str | None = None) -> bytes:
|
| 78 |
+
"""
|
| 79 |
+
Coerce str/bytes into a strictly byte-wise HTTP header value.
|
| 80 |
+
"""
|
| 81 |
+
if isinstance(value, bytes):
|
| 82 |
+
return value
|
| 83 |
+
if not isinstance(value, str):
|
| 84 |
+
raise TypeError(f"Header value must be str or bytes, not {type(value)}")
|
| 85 |
+
return value.encode(encoding or "ascii")
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _parse_content_type_charset(content_type: str) -> str | None:
|
| 89 |
+
# We used to use `cgi.parse_header()` here, but `cgi` became a dead battery.
|
| 90 |
+
# See: https://peps.python.org/pep-0594/#cgi
|
| 91 |
+
msg = email.message.Message()
|
| 92 |
+
msg["content-type"] = content_type
|
| 93 |
+
return msg.get_content_charset(failobj=None)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def _parse_header_links(value: str) -> list[dict[str, str]]:
|
| 97 |
+
"""
|
| 98 |
+
Returns a list of parsed link headers, for more info see:
|
| 99 |
+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
|
| 100 |
+
The generic syntax of those is:
|
| 101 |
+
Link: < uri-reference >; param1=value1; param2="value2"
|
| 102 |
+
So for instance:
|
| 103 |
+
Link; '<http:/.../front.jpeg>; type="image/jpeg",<http://.../back.jpeg>;'
|
| 104 |
+
would return
|
| 105 |
+
[
|
| 106 |
+
{"url": "http:/.../front.jpeg", "type": "image/jpeg"},
|
| 107 |
+
{"url": "http://.../back.jpeg"},
|
| 108 |
+
]
|
| 109 |
+
:param value: HTTP Link entity-header field
|
| 110 |
+
:return: list of parsed link headers
|
| 111 |
+
"""
|
| 112 |
+
links: list[dict[str, str]] = []
|
| 113 |
+
replace_chars = " '\""
|
| 114 |
+
value = value.strip(replace_chars)
|
| 115 |
+
if not value:
|
| 116 |
+
return links
|
| 117 |
+
for val in re.split(", *<", value):
|
| 118 |
+
try:
|
| 119 |
+
url, params = val.split(";", 1)
|
| 120 |
+
except ValueError:
|
| 121 |
+
url, params = val, ""
|
| 122 |
+
link = {"url": url.strip("<> '\"")}
|
| 123 |
+
for param in params.split(";"):
|
| 124 |
+
try:
|
| 125 |
+
key, value = param.split("=")
|
| 126 |
+
except ValueError:
|
| 127 |
+
break
|
| 128 |
+
link[key.strip(replace_chars)] = value.strip(replace_chars)
|
| 129 |
+
links.append(link)
|
| 130 |
+
return links
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _obfuscate_sensitive_headers(
|
| 134 |
+
items: typing.Iterable[tuple[typing.AnyStr, typing.AnyStr]],
|
| 135 |
+
) -> typing.Iterator[tuple[typing.AnyStr, typing.AnyStr]]:
|
| 136 |
+
for k, v in items:
|
| 137 |
+
if to_str(k.lower()) in SENSITIVE_HEADERS:
|
| 138 |
+
v = to_bytes_or_str("[secure]", match_type_of=v)
|
| 139 |
+
yield k, v
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class Headers(typing.MutableMapping[str, str]):
|
| 143 |
+
"""
|
| 144 |
+
HTTP headers, as a case-insensitive multi-dict.
|
| 145 |
+
"""
|
| 146 |
+
|
| 147 |
+
def __init__(
|
| 148 |
+
self,
|
| 149 |
+
headers: HeaderTypes | None = None,
|
| 150 |
+
encoding: str | None = None,
|
| 151 |
+
) -> None:
|
| 152 |
+
self._list: list[tuple[bytes, bytes, bytes]] = []
|
| 153 |
+
|
| 154 |
+
if isinstance(headers, Headers):
|
| 155 |
+
self._list = list(headers._list)
|
| 156 |
+
elif isinstance(headers, Mapping):
|
| 157 |
+
for k, v in headers.items():
|
| 158 |
+
bytes_key = _normalize_header_key(k, encoding)
|
| 159 |
+
bytes_value = _normalize_header_value(v, encoding)
|
| 160 |
+
self._list.append((bytes_key, bytes_key.lower(), bytes_value))
|
| 161 |
+
elif headers is not None:
|
| 162 |
+
for k, v in headers:
|
| 163 |
+
bytes_key = _normalize_header_key(k, encoding)
|
| 164 |
+
bytes_value = _normalize_header_value(v, encoding)
|
| 165 |
+
self._list.append((bytes_key, bytes_key.lower(), bytes_value))
|
| 166 |
+
|
| 167 |
+
self._encoding = encoding
|
| 168 |
+
|
| 169 |
+
@property
|
| 170 |
+
def encoding(self) -> str:
|
| 171 |
+
"""
|
| 172 |
+
Header encoding is mandated as ascii, but we allow fallbacks to utf-8
|
| 173 |
+
or iso-8859-1.
|
| 174 |
+
"""
|
| 175 |
+
if self._encoding is None:
|
| 176 |
+
for encoding in ["ascii", "utf-8"]:
|
| 177 |
+
for key, value in self.raw:
|
| 178 |
+
try:
|
| 179 |
+
key.decode(encoding)
|
| 180 |
+
value.decode(encoding)
|
| 181 |
+
except UnicodeDecodeError:
|
| 182 |
+
break
|
| 183 |
+
else:
|
| 184 |
+
# The else block runs if 'break' did not occur, meaning
|
| 185 |
+
# all values fitted the encoding.
|
| 186 |
+
self._encoding = encoding
|
| 187 |
+
break
|
| 188 |
+
else:
|
| 189 |
+
# The ISO-8859-1 encoding covers all 256 code points in a byte,
|
| 190 |
+
# so will never raise decode errors.
|
| 191 |
+
self._encoding = "iso-8859-1"
|
| 192 |
+
return self._encoding
|
| 193 |
+
|
| 194 |
+
@encoding.setter
|
| 195 |
+
def encoding(self, value: str) -> None:
|
| 196 |
+
self._encoding = value
|
| 197 |
+
|
| 198 |
+
@property
|
| 199 |
+
def raw(self) -> list[tuple[bytes, bytes]]:
|
| 200 |
+
"""
|
| 201 |
+
Returns a list of the raw header items, as byte pairs.
|
| 202 |
+
"""
|
| 203 |
+
return [(raw_key, value) for raw_key, _, value in self._list]
|
| 204 |
+
|
| 205 |
+
def keys(self) -> typing.KeysView[str]:
|
| 206 |
+
return {key.decode(self.encoding): None for _, key, _value in self._list}.keys()
|
| 207 |
+
|
| 208 |
+
def values(self) -> typing.ValuesView[str]:
|
| 209 |
+
values_dict: dict[str, str] = {}
|
| 210 |
+
for _, key, value in self._list:
|
| 211 |
+
str_key = key.decode(self.encoding)
|
| 212 |
+
str_value = value.decode(self.encoding)
|
| 213 |
+
if str_key in values_dict:
|
| 214 |
+
values_dict[str_key] += f", {str_value}"
|
| 215 |
+
else:
|
| 216 |
+
values_dict[str_key] = str_value
|
| 217 |
+
return values_dict.values()
|
| 218 |
+
|
| 219 |
+
def items(self) -> typing.ItemsView[str, str]:
|
| 220 |
+
"""
|
| 221 |
+
Return `(key, value)` items of headers. Concatenate headers
|
| 222 |
+
into a single comma separated value when a key occurs multiple times.
|
| 223 |
+
"""
|
| 224 |
+
values_dict: dict[str, str] = {}
|
| 225 |
+
for _, key, value in self._list:
|
| 226 |
+
str_key = key.decode(self.encoding)
|
| 227 |
+
str_value = value.decode(self.encoding)
|
| 228 |
+
if str_key in values_dict:
|
| 229 |
+
values_dict[str_key] += f", {str_value}"
|
| 230 |
+
else:
|
| 231 |
+
values_dict[str_key] = str_value
|
| 232 |
+
return values_dict.items()
|
| 233 |
+
|
| 234 |
+
def multi_items(self) -> list[tuple[str, str]]:
|
| 235 |
+
"""
|
| 236 |
+
Return a list of `(key, value)` pairs of headers. Allow multiple
|
| 237 |
+
occurrences of the same key without concatenating into a single
|
| 238 |
+
comma separated value.
|
| 239 |
+
"""
|
| 240 |
+
return [(key.decode(self.encoding), value.decode(self.encoding)) for _, key, value in self._list]
|
| 241 |
+
|
| 242 |
+
@typing.overload
|
| 243 |
+
def get(self, key: str, /) -> str | None: ...
|
| 244 |
+
|
| 245 |
+
@typing.overload
|
| 246 |
+
def get(self, key: str, default: _T) -> str | _T: ...
|
| 247 |
+
|
| 248 |
+
def get(self, key: str, default: _T | None = None) -> str | _T | None:
|
| 249 |
+
"""
|
| 250 |
+
Return a header value. If multiple occurrences of the header occur
|
| 251 |
+
then concatenate them together with commas.
|
| 252 |
+
"""
|
| 253 |
+
try:
|
| 254 |
+
return self[key]
|
| 255 |
+
except KeyError:
|
| 256 |
+
return default
|
| 257 |
+
|
| 258 |
+
def get_list(self, key: str, split_commas: bool = False) -> list[str]:
|
| 259 |
+
"""
|
| 260 |
+
Return a list of all header values for a given key.
|
| 261 |
+
If `split_commas=True` is passed, then any comma separated header
|
| 262 |
+
values are split into multiple return strings.
|
| 263 |
+
"""
|
| 264 |
+
get_header_key = key.lower().encode(self.encoding)
|
| 265 |
+
|
| 266 |
+
values = [
|
| 267 |
+
item_value.decode(self.encoding)
|
| 268 |
+
for _, item_key, item_value in self._list
|
| 269 |
+
if item_key.lower() == get_header_key
|
| 270 |
+
]
|
| 271 |
+
|
| 272 |
+
if not split_commas:
|
| 273 |
+
return values
|
| 274 |
+
|
| 275 |
+
split_values: list[str] = []
|
| 276 |
+
for value in values:
|
| 277 |
+
split_values.extend([item.strip() for item in value.split(",")])
|
| 278 |
+
return split_values
|
| 279 |
+
|
| 280 |
+
def update(self, headers: HeaderTypes | None = None) -> None: # type: ignore
|
| 281 |
+
headers = Headers(headers)
|
| 282 |
+
for key in headers.keys():
|
| 283 |
+
if key in self:
|
| 284 |
+
self.pop(key)
|
| 285 |
+
self._list.extend(headers._list)
|
| 286 |
+
|
| 287 |
+
def copy(self) -> Headers:
|
| 288 |
+
return Headers(self, encoding=self.encoding)
|
| 289 |
+
|
| 290 |
+
def __or__(self, other: Mapping[str, str]) -> Headers:
|
| 291 |
+
if not isinstance(other, Mapping):
|
| 292 |
+
return NotImplemented
|
| 293 |
+
merged = self.copy()
|
| 294 |
+
merged.update(other)
|
| 295 |
+
return merged
|
| 296 |
+
|
| 297 |
+
def __ror__(self, other: Mapping[str, str]) -> Headers:
|
| 298 |
+
if not isinstance(other, Mapping):
|
| 299 |
+
return NotImplemented
|
| 300 |
+
merged = Headers(other)
|
| 301 |
+
merged.update(self)
|
| 302 |
+
return merged
|
| 303 |
+
|
| 304 |
+
def __ior__(self, other: HeaderTypes) -> Headers:
|
| 305 |
+
self.update(other)
|
| 306 |
+
return self
|
| 307 |
+
|
| 308 |
+
def __getitem__(self, key: str) -> str:
|
| 309 |
+
"""
|
| 310 |
+
Return a single header value.
|
| 311 |
+
|
| 312 |
+
If there are multiple headers with the same key, then we concatenate
|
| 313 |
+
them with commas. See: https://tools.ietf.org/html/rfc7230#section-3.2.2
|
| 314 |
+
"""
|
| 315 |
+
normalized_key = key.lower().encode(self.encoding)
|
| 316 |
+
|
| 317 |
+
items = [
|
| 318 |
+
header_value.decode(self.encoding)
|
| 319 |
+
for _, header_key, header_value in self._list
|
| 320 |
+
if header_key == normalized_key
|
| 321 |
+
]
|
| 322 |
+
|
| 323 |
+
if items:
|
| 324 |
+
return ", ".join(items)
|
| 325 |
+
|
| 326 |
+
raise KeyError(key)
|
| 327 |
+
|
| 328 |
+
def __setitem__(self, key: str, value: str) -> None:
|
| 329 |
+
"""
|
| 330 |
+
Set the header `key` to `value`, removing any duplicate entries.
|
| 331 |
+
Retains insertion order.
|
| 332 |
+
"""
|
| 333 |
+
set_key = key.encode(self._encoding or "utf-8")
|
| 334 |
+
set_value = value.encode(self._encoding or "utf-8")
|
| 335 |
+
lookup_key = set_key.lower()
|
| 336 |
+
|
| 337 |
+
found_indexes = [idx for idx, (_, item_key, _) in enumerate(self._list) if item_key == lookup_key]
|
| 338 |
+
|
| 339 |
+
for idx in reversed(found_indexes[1:]):
|
| 340 |
+
del self._list[idx]
|
| 341 |
+
|
| 342 |
+
if found_indexes:
|
| 343 |
+
idx = found_indexes[0]
|
| 344 |
+
self._list[idx] = (set_key, lookup_key, set_value)
|
| 345 |
+
else:
|
| 346 |
+
self._list.append((set_key, lookup_key, set_value))
|
| 347 |
+
|
| 348 |
+
def __delitem__(self, key: str) -> None:
|
| 349 |
+
"""
|
| 350 |
+
Remove the header `key`.
|
| 351 |
+
"""
|
| 352 |
+
del_key = key.lower().encode(self.encoding)
|
| 353 |
+
|
| 354 |
+
pop_indexes = [idx for idx, (_, item_key, _) in enumerate(self._list) if item_key.lower() == del_key]
|
| 355 |
+
|
| 356 |
+
if not pop_indexes:
|
| 357 |
+
raise KeyError(key)
|
| 358 |
+
|
| 359 |
+
for idx in reversed(pop_indexes):
|
| 360 |
+
del self._list[idx]
|
| 361 |
+
|
| 362 |
+
def __contains__(self, key: typing.Any) -> bool:
|
| 363 |
+
header_key = key.lower().encode(self.encoding)
|
| 364 |
+
return header_key in [key for _, key, _ in self._list]
|
| 365 |
+
|
| 366 |
+
def __iter__(self) -> typing.Iterator[typing.Any]:
|
| 367 |
+
return iter(self.keys())
|
| 368 |
+
|
| 369 |
+
def __len__(self) -> int:
|
| 370 |
+
return len(self._list)
|
| 371 |
+
|
| 372 |
+
def __eq__(self, other: typing.Any) -> bool:
|
| 373 |
+
try:
|
| 374 |
+
other_headers = Headers(other)
|
| 375 |
+
except ValueError:
|
| 376 |
+
return False
|
| 377 |
+
|
| 378 |
+
self_list = [(key, value) for _, key, value in self._list]
|
| 379 |
+
other_list = [(key, value) for _, key, value in other_headers._list]
|
| 380 |
+
return sorted(self_list) == sorted(other_list)
|
| 381 |
+
|
| 382 |
+
def __repr__(self) -> str:
|
| 383 |
+
class_name = self.__class__.__name__
|
| 384 |
+
|
| 385 |
+
encoding_str = ""
|
| 386 |
+
if self.encoding != "ascii":
|
| 387 |
+
encoding_str = f", encoding={self.encoding!r}"
|
| 388 |
+
|
| 389 |
+
as_list = list(_obfuscate_sensitive_headers(self.multi_items()))
|
| 390 |
+
as_dict = dict(as_list)
|
| 391 |
+
|
| 392 |
+
no_duplicate_keys = len(as_dict) == len(as_list)
|
| 393 |
+
if no_duplicate_keys:
|
| 394 |
+
return f"{class_name}({as_dict!r}{encoding_str})"
|
| 395 |
+
return f"{class_name}({as_list!r}{encoding_str})"
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
class Request:
|
| 399 |
+
def __init__(
|
| 400 |
+
self,
|
| 401 |
+
method: str,
|
| 402 |
+
url: URL | str,
|
| 403 |
+
*,
|
| 404 |
+
params: QueryParamTypes | None = None,
|
| 405 |
+
headers: HeaderTypes | None = None,
|
| 406 |
+
cookies: CookieTypes | None = None,
|
| 407 |
+
content: RequestContent | None = None,
|
| 408 |
+
data: RequestData | None = None,
|
| 409 |
+
files: RequestFiles | None = None,
|
| 410 |
+
json: typing.Any | None = None,
|
| 411 |
+
stream: SyncByteStream | AsyncByteStream | None = None,
|
| 412 |
+
extensions: RequestExtensions | None = None,
|
| 413 |
+
) -> None:
|
| 414 |
+
self.method = method.upper()
|
| 415 |
+
self.url = URL(url) if params is None else URL(url, params=params)
|
| 416 |
+
self.headers = Headers(headers)
|
| 417 |
+
self.extensions = {} if extensions is None else dict(extensions)
|
| 418 |
+
|
| 419 |
+
if cookies:
|
| 420 |
+
(cookies if isinstance(cookies, Cookies) else Cookies(cookies)).set_cookie_header(self)
|
| 421 |
+
|
| 422 |
+
if stream is None:
|
| 423 |
+
content_type: str | None = self.headers.get("content-type")
|
| 424 |
+
headers, stream = encode_request(
|
| 425 |
+
content=content,
|
| 426 |
+
data=data,
|
| 427 |
+
files=files,
|
| 428 |
+
json=json,
|
| 429 |
+
boundary=get_multipart_boundary_from_content_type(
|
| 430 |
+
content_type=content_type.encode(self.headers.encoding) if content_type else None
|
| 431 |
+
),
|
| 432 |
+
)
|
| 433 |
+
self._prepare(headers)
|
| 434 |
+
self.stream = stream
|
| 435 |
+
# Load the request body, except for streaming content.
|
| 436 |
+
if isinstance(stream, ByteStream):
|
| 437 |
+
self.read()
|
| 438 |
+
else:
|
| 439 |
+
# There's an important distinction between `Request(content=...)`,
|
| 440 |
+
# and `Request(stream=...)`.
|
| 441 |
+
#
|
| 442 |
+
# Using `content=...` implies automatically populated `Host` and content
|
| 443 |
+
# headers, of either `Content-Length: ...` or `Transfer-Encoding: chunked`.
|
| 444 |
+
#
|
| 445 |
+
# Using `stream=...` will not automatically include *any*
|
| 446 |
+
# auto-populated headers.
|
| 447 |
+
#
|
| 448 |
+
# As an end-user you don't really need `stream=...`. It's only
|
| 449 |
+
# useful when:
|
| 450 |
+
#
|
| 451 |
+
# * Preserving the request stream when copying requests, eg for redirects.
|
| 452 |
+
# * Creating request instances on the *server-side* of the transport API.
|
| 453 |
+
self.stream = stream
|
| 454 |
+
|
| 455 |
+
def _prepare(self, default_headers: dict[str, str]) -> None:
|
| 456 |
+
for key, value in default_headers.items():
|
| 457 |
+
# Ignore Content-Length if Transfer-Encoding has been set explicitly.
|
| 458 |
+
if key.lower() == "content-length" and "Transfer-Encoding" in self.headers:
|
| 459 |
+
continue
|
| 460 |
+
# Ignore Transfer-Encoding if Content-Length has been set explicitly.
|
| 461 |
+
if key.lower() == "transfer-encoding" and "Content-Length" in self.headers:
|
| 462 |
+
continue
|
| 463 |
+
self.headers.setdefault(key, value)
|
| 464 |
+
|
| 465 |
+
auto_headers: list[tuple[bytes, bytes]] = []
|
| 466 |
+
|
| 467 |
+
has_host = "Host" in self.headers
|
| 468 |
+
has_content_length = "Content-Length" in self.headers or "Transfer-Encoding" in self.headers
|
| 469 |
+
|
| 470 |
+
if not has_host and self.url.host:
|
| 471 |
+
auto_headers.append((b"Host", self.url.netloc))
|
| 472 |
+
if not has_content_length and self.method in ("POST", "PUT", "PATCH", "QUERY"):
|
| 473 |
+
auto_headers.append((b"Content-Length", b"0"))
|
| 474 |
+
|
| 475 |
+
self.headers = Headers(auto_headers + self.headers.raw)
|
| 476 |
+
|
| 477 |
+
@property
|
| 478 |
+
def content(self) -> bytes:
|
| 479 |
+
if not hasattr(self, "_content"):
|
| 480 |
+
raise RequestNotRead()
|
| 481 |
+
return self._content
|
| 482 |
+
|
| 483 |
+
def read(self) -> bytes:
|
| 484 |
+
"""
|
| 485 |
+
Read and return the request content.
|
| 486 |
+
"""
|
| 487 |
+
if not hasattr(self, "_content"):
|
| 488 |
+
assert isinstance(self.stream, typing.Iterable)
|
| 489 |
+
self._content = b"".join(self.stream)
|
| 490 |
+
if not isinstance(self.stream, ByteStream):
|
| 491 |
+
# If a streaming request has been read entirely into memory, then
|
| 492 |
+
# we can replace the stream with a raw bytes implementation,
|
| 493 |
+
# to ensure that any non-replayable streams can still be used.
|
| 494 |
+
self.stream = ByteStream(self._content)
|
| 495 |
+
return self._content
|
| 496 |
+
|
| 497 |
+
async def aread(self) -> bytes:
|
| 498 |
+
"""
|
| 499 |
+
Read and return the request content.
|
| 500 |
+
"""
|
| 501 |
+
if not hasattr(self, "_content"):
|
| 502 |
+
assert isinstance(self.stream, typing.AsyncIterable)
|
| 503 |
+
self._content = b"".join([part async for part in self.stream])
|
| 504 |
+
if not isinstance(self.stream, ByteStream):
|
| 505 |
+
# If a streaming request has been read entirely into memory, then
|
| 506 |
+
# we can replace the stream with a raw bytes implementation,
|
| 507 |
+
# to ensure that any non-replayable streams can still be used.
|
| 508 |
+
self.stream = ByteStream(self._content)
|
| 509 |
+
return self._content
|
| 510 |
+
|
| 511 |
+
def __repr__(self) -> str:
|
| 512 |
+
class_name = self.__class__.__name__
|
| 513 |
+
url = str(self.url)
|
| 514 |
+
return f"<{class_name}({self.method!r}, {url!r})>"
|
| 515 |
+
|
| 516 |
+
def __getstate__(self) -> dict[str, typing.Any]:
|
| 517 |
+
return {name: value for name, value in self.__dict__.items() if name not in ["extensions", "stream"]}
|
| 518 |
+
|
| 519 |
+
def __setstate__(self, state: dict[str, typing.Any]) -> None:
|
| 520 |
+
for name, value in state.items():
|
| 521 |
+
setattr(self, name, value)
|
| 522 |
+
self.extensions = {}
|
| 523 |
+
self.stream = UnattachedStream()
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
class Response:
|
| 527 |
+
def __init__(
|
| 528 |
+
self,
|
| 529 |
+
status_code: int,
|
| 530 |
+
*,
|
| 531 |
+
headers: HeaderTypes | None = None,
|
| 532 |
+
content: ResponseContent | None = None,
|
| 533 |
+
text: str | None = None,
|
| 534 |
+
html: str | None = None,
|
| 535 |
+
json: typing.Any = None,
|
| 536 |
+
stream: SyncByteStream | AsyncByteStream | None = None,
|
| 537 |
+
request: Request | None = None,
|
| 538 |
+
extensions: ResponseExtensions | None = None,
|
| 539 |
+
history: list[Response] | None = None,
|
| 540 |
+
default_encoding: str | typing.Callable[[bytes], str | None] = "utf-8",
|
| 541 |
+
) -> None:
|
| 542 |
+
self.status_code = status_code
|
| 543 |
+
self.headers = Headers(headers)
|
| 544 |
+
|
| 545 |
+
self._request: Request | None = request
|
| 546 |
+
|
| 547 |
+
# When follow_redirects=False and a redirect is received,
|
| 548 |
+
# the client will set `response.next_request`.
|
| 549 |
+
self.next_request: Request | None = None
|
| 550 |
+
|
| 551 |
+
self.extensions = {} if extensions is None else dict(extensions)
|
| 552 |
+
self.history = [] if history is None else list(history)
|
| 553 |
+
|
| 554 |
+
self.is_closed = False
|
| 555 |
+
self.is_stream_consumed = False
|
| 556 |
+
|
| 557 |
+
self.default_encoding = default_encoding
|
| 558 |
+
|
| 559 |
+
if stream is None:
|
| 560 |
+
headers, stream = encode_response(content, text, html, json)
|
| 561 |
+
self._prepare(headers)
|
| 562 |
+
self.stream = stream
|
| 563 |
+
if isinstance(stream, ByteStream):
|
| 564 |
+
# Load the response body, except for streaming content.
|
| 565 |
+
self.read()
|
| 566 |
+
else:
|
| 567 |
+
# There's an important distinction between `Response(content=...)`,
|
| 568 |
+
# and `Response(stream=...)`.
|
| 569 |
+
#
|
| 570 |
+
# Using `content=...` implies automatically populated content headers,
|
| 571 |
+
# of either `Content-Length: ...` or `Transfer-Encoding: chunked`.
|
| 572 |
+
#
|
| 573 |
+
# Using `stream=...` will not automatically include any content headers.
|
| 574 |
+
#
|
| 575 |
+
# As an end-user you don't really need `stream=...`. It's only
|
| 576 |
+
# useful when creating response instances having received a stream
|
| 577 |
+
# from the transport API.
|
| 578 |
+
self.stream = stream
|
| 579 |
+
|
| 580 |
+
self._num_bytes_downloaded = 0
|
| 581 |
+
|
| 582 |
+
def _prepare(self, default_headers: dict[str, str]) -> None:
|
| 583 |
+
for key, value in default_headers.items():
|
| 584 |
+
# Ignore Transfer-Encoding if the Content-Length has been set explicitly.
|
| 585 |
+
if key.lower() == "transfer-encoding" and "content-length" in self.headers:
|
| 586 |
+
continue
|
| 587 |
+
self.headers.setdefault(key, value)
|
| 588 |
+
|
| 589 |
+
@property
|
| 590 |
+
def elapsed(self) -> datetime.timedelta:
|
| 591 |
+
"""
|
| 592 |
+
Returns the time taken for the complete request/response
|
| 593 |
+
cycle to complete.
|
| 594 |
+
"""
|
| 595 |
+
if not hasattr(self, "_elapsed"):
|
| 596 |
+
stream_elapsed: datetime.timedelta | None = getattr(self.stream, "elapsed", None)
|
| 597 |
+
if stream_elapsed is not None:
|
| 598 |
+
return stream_elapsed
|
| 599 |
+
raise RuntimeError("'.elapsed' may only be accessed after the response has been read or closed.")
|
| 600 |
+
return self._elapsed
|
| 601 |
+
|
| 602 |
+
@elapsed.setter
|
| 603 |
+
def elapsed(self, elapsed: datetime.timedelta) -> None:
|
| 604 |
+
self._elapsed = elapsed
|
| 605 |
+
|
| 606 |
+
@property
|
| 607 |
+
def request(self) -> Request:
|
| 608 |
+
"""
|
| 609 |
+
Returns the request instance associated to the current response.
|
| 610 |
+
"""
|
| 611 |
+
if self._request is None:
|
| 612 |
+
raise RuntimeError("The request instance has not been set on this response.")
|
| 613 |
+
return self._request
|
| 614 |
+
|
| 615 |
+
@request.setter
|
| 616 |
+
def request(self, value: Request) -> None:
|
| 617 |
+
self._request = value
|
| 618 |
+
|
| 619 |
+
@property
|
| 620 |
+
def http_version(self) -> str:
|
| 621 |
+
try:
|
| 622 |
+
http_version: bytes = self.extensions["http_version"]
|
| 623 |
+
except KeyError:
|
| 624 |
+
return "HTTP/1.1"
|
| 625 |
+
else:
|
| 626 |
+
return http_version.decode("ascii", errors="ignore")
|
| 627 |
+
|
| 628 |
+
@property
|
| 629 |
+
def reason_phrase(self) -> str:
|
| 630 |
+
try:
|
| 631 |
+
reason_phrase: bytes = self.extensions["reason_phrase"]
|
| 632 |
+
except KeyError:
|
| 633 |
+
return codes.get_reason_phrase(self.status_code)
|
| 634 |
+
else:
|
| 635 |
+
return reason_phrase.decode("ascii", errors="ignore")
|
| 636 |
+
|
| 637 |
+
@property
|
| 638 |
+
def url(self) -> URL:
|
| 639 |
+
"""
|
| 640 |
+
Returns the URL for which the request was made.
|
| 641 |
+
"""
|
| 642 |
+
return self.request.url
|
| 643 |
+
|
| 644 |
+
@property
|
| 645 |
+
def content(self) -> bytes:
|
| 646 |
+
if not hasattr(self, "_content"):
|
| 647 |
+
raise ResponseNotRead()
|
| 648 |
+
return self._content
|
| 649 |
+
|
| 650 |
+
@property
|
| 651 |
+
def text(self) -> str:
|
| 652 |
+
if not hasattr(self, "_text"):
|
| 653 |
+
content = self.content
|
| 654 |
+
if not content:
|
| 655 |
+
self._text = ""
|
| 656 |
+
else:
|
| 657 |
+
decoder = TextDecoder(encoding=self.encoding or "utf-8")
|
| 658 |
+
self._text = "".join([decoder.decode(self.content), decoder.flush()])
|
| 659 |
+
return self._text
|
| 660 |
+
|
| 661 |
+
@property
|
| 662 |
+
def encoding(self) -> str | None:
|
| 663 |
+
"""
|
| 664 |
+
Return an encoding to use for decoding the byte content into text.
|
| 665 |
+
The priority for determining this is given by...
|
| 666 |
+
|
| 667 |
+
* `.encoding = <>` has been set explicitly.
|
| 668 |
+
* The encoding as specified by the charset parameter in the Content-Type header.
|
| 669 |
+
* The encoding as determined by `default_encoding`, which may either be
|
| 670 |
+
a string like "utf-8" indicating the encoding to use, or may be a callable
|
| 671 |
+
which enables charset autodetection.
|
| 672 |
+
"""
|
| 673 |
+
if not hasattr(self, "_encoding"):
|
| 674 |
+
encoding = self.charset_encoding
|
| 675 |
+
if encoding is None or not _is_known_encoding(encoding):
|
| 676 |
+
if isinstance(self.default_encoding, str):
|
| 677 |
+
encoding = self.default_encoding
|
| 678 |
+
elif hasattr(self, "_content"):
|
| 679 |
+
encoding = self.default_encoding(self._content)
|
| 680 |
+
self._encoding = encoding or "utf-8"
|
| 681 |
+
return self._encoding
|
| 682 |
+
|
| 683 |
+
@encoding.setter
|
| 684 |
+
def encoding(self, value: str) -> None:
|
| 685 |
+
"""
|
| 686 |
+
Set the encoding to use for decoding the byte content into text.
|
| 687 |
+
|
| 688 |
+
If the `text` attribute has been accessed, attempting to set the
|
| 689 |
+
encoding will throw a ValueError.
|
| 690 |
+
"""
|
| 691 |
+
if hasattr(self, "_text"):
|
| 692 |
+
raise ValueError("Setting encoding after `text` has been accessed is not allowed.")
|
| 693 |
+
self._encoding = value
|
| 694 |
+
|
| 695 |
+
@property
|
| 696 |
+
def charset_encoding(self) -> str | None:
|
| 697 |
+
"""
|
| 698 |
+
Return the encoding, as specified by the Content-Type header.
|
| 699 |
+
"""
|
| 700 |
+
content_type = self.headers.get("Content-Type")
|
| 701 |
+
if content_type is None:
|
| 702 |
+
return None
|
| 703 |
+
|
| 704 |
+
return _parse_content_type_charset(content_type)
|
| 705 |
+
|
| 706 |
+
def _get_content_decoder(self) -> ContentDecoder:
|
| 707 |
+
"""
|
| 708 |
+
Returns a decoder instance which can be used to decode the raw byte
|
| 709 |
+
content, depending on the Content-Encoding used in the response.
|
| 710 |
+
"""
|
| 711 |
+
if not hasattr(self, "_decoder"):
|
| 712 |
+
values = self.headers.get_list("content-encoding", split_commas=True)
|
| 713 |
+
encodings = [value.strip().lower() for value in values]
|
| 714 |
+
decoder = MultiDecoder([encoding for encoding in encodings if encoding != "identity"])
|
| 715 |
+
if len(decoder.children) == 1:
|
| 716 |
+
self._decoder = decoder.children[0]
|
| 717 |
+
elif decoder.children:
|
| 718 |
+
self._decoder = decoder
|
| 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 instance has not been set on this response."
|
| 802 |
+
)
|
| 803 |
+
|
| 804 |
+
if self.is_success:
|
| 805 |
+
return self
|
| 806 |
+
|
| 807 |
+
if self.has_redirect_location:
|
| 808 |
+
message = (
|
| 809 |
+
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
|
| 810 |
+
"Redirect location: '{0.headers[location]}'\n"
|
| 811 |
+
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
|
| 812 |
+
)
|
| 813 |
+
else:
|
| 814 |
+
message = (
|
| 815 |
+
"{error_type} '{0.status_code} {0.reason_phrase}' for url '{0.url}'\n"
|
| 816 |
+
"For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/{0.status_code}"
|
| 817 |
+
)
|
| 818 |
+
|
| 819 |
+
status_class = self.status_code // 100
|
| 820 |
+
error_types = {
|
| 821 |
+
1: "Informational response",
|
| 822 |
+
3: "Redirect response",
|
| 823 |
+
4: "Client error",
|
| 824 |
+
5: "Server error",
|
| 825 |
+
}
|
| 826 |
+
error_type = error_types.get(status_class, "Invalid status code")
|
| 827 |
+
message = message.format(self, error_type=error_type)
|
| 828 |
+
raise HTTPStatusError(message, request=request, response=self)
|
| 829 |
+
|
| 830 |
+
def json(self, **kwargs: typing.Any) -> typing.Any:
|
| 831 |
+
return jsonlib.loads(self.content, **kwargs)
|
| 832 |
+
|
| 833 |
+
@property
|
| 834 |
+
def cookies(self) -> Cookies:
|
| 835 |
+
if not hasattr(self, "_cookies"):
|
| 836 |
+
self._cookies = Cookies()
|
| 837 |
+
self._cookies.extract_cookies(self)
|
| 838 |
+
return self._cookies
|
| 839 |
+
|
| 840 |
+
@property
|
| 841 |
+
def links(self) -> dict[str | None, dict[str, str]]:
|
| 842 |
+
"""
|
| 843 |
+
Returns the parsed header links of the response, if any
|
| 844 |
+
"""
|
| 845 |
+
header = self.headers.get("link")
|
| 846 |
+
if header is None:
|
| 847 |
+
return {}
|
| 848 |
+
|
| 849 |
+
return {(link.get("rel") or link.get("url")): link for link in _parse_header_links(header)}
|
| 850 |
+
|
| 851 |
+
@property
|
| 852 |
+
def num_bytes_downloaded(self) -> int:
|
| 853 |
+
return self._num_bytes_downloaded
|
| 854 |
+
|
| 855 |
+
def __repr__(self) -> str:
|
| 856 |
+
return f"<Response [{self.status_code} {self.reason_phrase}]>"
|
| 857 |
+
|
| 858 |
+
def __getstate__(self) -> dict[str, typing.Any]:
|
| 859 |
+
return {
|
| 860 |
+
name: value
|
| 861 |
+
for name, value in self.__dict__.items()
|
| 862 |
+
if name not in ["extensions", "stream", "is_closed", "_decoder"]
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
def __setstate__(self, state: dict[str, typing.Any]) -> None:
|
| 866 |
+
for name, value in state.items():
|
| 867 |
+
setattr(self, name, value)
|
| 868 |
+
self.is_closed = True
|
| 869 |
+
self.extensions = {}
|
| 870 |
+
self.stream = UnattachedStream()
|
| 871 |
+
|
| 872 |
+
def read(self) -> bytes:
|
| 873 |
+
"""
|
| 874 |
+
Read and return the response content.
|
| 875 |
+
"""
|
| 876 |
+
if not hasattr(self, "_content"):
|
| 877 |
+
self._content = b"".join(self.iter_bytes())
|
| 878 |
+
return self._content
|
| 879 |
+
|
| 880 |
+
def iter_bytes(self, chunk_size: int | None = None) -> typing.Iterator[bytes]:
|
| 881 |
+
"""
|
| 882 |
+
A byte-iterator over the decoded response content.
|
| 883 |
+
This allows us to handle gzip, deflate, brotli, and zstd encoded responses.
|
| 884 |
+
"""
|
| 885 |
+
if hasattr(self, "_content"):
|
| 886 |
+
chunk_size = len(self._content) if chunk_size is None else chunk_size
|
| 887 |
+
for i in range(0, len(self._content), max(chunk_size, 1)):
|
| 888 |
+
yield self._content[i : i + chunk_size]
|
| 889 |
+
else:
|
| 890 |
+
decoder = self._get_content_decoder()
|
| 891 |
+
chunker = ByteChunker(chunk_size=chunk_size)
|
| 892 |
+
with request_context(request=self._request):
|
| 893 |
+
for raw_bytes in self.iter_raw():
|
| 894 |
+
for decoded in decoder.decode(raw_bytes):
|
| 895 |
+
for chunk in chunker.decode(decoded):
|
| 896 |
+
yield chunk
|
| 897 |
+
for decoded in decoder.flush():
|
| 898 |
+
for chunk in chunker.decode(decoded):
|
| 899 |
+
yield chunk # pragma: no cover
|
| 900 |
+
for chunk in chunker.flush():
|
| 901 |
+
yield chunk
|
| 902 |
+
|
| 903 |
+
def iter_text(self, chunk_size: int | None = None) -> typing.Iterator[str]:
|
| 904 |
+
"""
|
| 905 |
+
A str-iterator over the decoded response content
|
| 906 |
+
that handles both gzip, deflate, etc but also detects the content's
|
| 907 |
+
string encoding.
|
| 908 |
+
"""
|
| 909 |
+
decoder = TextDecoder(encoding=self.encoding or "utf-8")
|
| 910 |
+
chunker = TextChunker(chunk_size=chunk_size)
|
| 911 |
+
with request_context(request=self._request):
|
| 912 |
+
for byte_content in self.iter_bytes():
|
| 913 |
+
text_content = decoder.decode(byte_content)
|
| 914 |
+
for chunk in chunker.decode(text_content):
|
| 915 |
+
yield chunk
|
| 916 |
+
text_content = decoder.flush()
|
| 917 |
+
for chunk in chunker.decode(text_content):
|
| 918 |
+
yield chunk # pragma: no cover
|
| 919 |
+
for chunk in chunker.flush():
|
| 920 |
+
yield chunk
|
| 921 |
+
|
| 922 |
+
def iter_lines(self) -> typing.Iterator[str]:
|
| 923 |
+
decoder = LineDecoder()
|
| 924 |
+
with request_context(request=self._request):
|
| 925 |
+
for text in self.iter_text():
|
| 926 |
+
for line in decoder.decode(text):
|
| 927 |
+
yield line
|
| 928 |
+
for line in decoder.flush():
|
| 929 |
+
yield line
|
| 930 |
+
|
| 931 |
+
def iter_raw(self, chunk_size: int | None = None) -> typing.Iterator[bytes]:
|
| 932 |
+
"""
|
| 933 |
+
A byte-iterator over the raw response content.
|
| 934 |
+
"""
|
| 935 |
+
if self.is_stream_consumed:
|
| 936 |
+
raise StreamConsumed()
|
| 937 |
+
if self.is_closed:
|
| 938 |
+
raise StreamClosed()
|
| 939 |
+
if not isinstance(self.stream, SyncByteStream):
|
| 940 |
+
raise RuntimeError("Attempted to call a sync iterator on an async stream.")
|
| 941 |
+
|
| 942 |
+
self.is_stream_consumed = True
|
| 943 |
+
self._num_bytes_downloaded = 0
|
| 944 |
+
chunker = ByteChunker(chunk_size=chunk_size)
|
| 945 |
+
|
| 946 |
+
try:
|
| 947 |
+
with request_context(request=self._request):
|
| 948 |
+
for raw_stream_bytes in self.stream:
|
| 949 |
+
self._num_bytes_downloaded += len(raw_stream_bytes)
|
| 950 |
+
for chunk in chunker.decode(raw_stream_bytes):
|
| 951 |
+
yield chunk
|
| 952 |
+
|
| 953 |
+
for chunk in chunker.flush():
|
| 954 |
+
yield chunk
|
| 955 |
+
finally:
|
| 956 |
+
self.close()
|
| 957 |
+
|
| 958 |
+
def close(self) -> None:
|
| 959 |
+
"""
|
| 960 |
+
Close the response and release the connection.
|
| 961 |
+
Automatically called if the response body is read to completion.
|
| 962 |
+
"""
|
| 963 |
+
if not isinstance(self.stream, SyncByteStream):
|
| 964 |
+
raise RuntimeError("Attempted to call a sync close on an async stream.")
|
| 965 |
+
|
| 966 |
+
if not self.is_closed:
|
| 967 |
+
self.is_closed = True
|
| 968 |
+
with request_context(request=self._request):
|
| 969 |
+
self.stream.close()
|
| 970 |
+
|
| 971 |
+
async def aread(self) -> bytes:
|
| 972 |
+
"""
|
| 973 |
+
Read and return the response content.
|
| 974 |
+
"""
|
| 975 |
+
if not hasattr(self, "_content"):
|
| 976 |
+
async with contextlib.aclosing(self.aiter_bytes()) as parts:
|
| 977 |
+
self._content = b"".join([part async for part in parts])
|
| 978 |
+
return self._content
|
| 979 |
+
|
| 980 |
+
async def aiter_bytes(self, chunk_size: int | None = None) -> typing.AsyncGenerator[bytes, None]:
|
| 981 |
+
"""
|
| 982 |
+
A byte-iterator over the decoded response content.
|
| 983 |
+
This allows us to handle gzip, deflate, brotli, and zstd encoded responses.
|
| 984 |
+
"""
|
| 985 |
+
if hasattr(self, "_content"):
|
| 986 |
+
chunk_size = len(self._content) if chunk_size is None else chunk_size
|
| 987 |
+
for i in range(0, len(self._content), max(chunk_size, 1)):
|
| 988 |
+
yield self._content[i : i + chunk_size]
|
| 989 |
+
else:
|
| 990 |
+
decoder = self._get_content_decoder()
|
| 991 |
+
chunker = ByteChunker(chunk_size=chunk_size)
|
| 992 |
+
with request_context(request=self._request):
|
| 993 |
+
async with contextlib.aclosing(self.aiter_raw()) as raw_stream:
|
| 994 |
+
async for raw_bytes in raw_stream:
|
| 995 |
+
for decoded in decoder.decode(raw_bytes):
|
| 996 |
+
for chunk in chunker.decode(decoded):
|
| 997 |
+
yield chunk
|
| 998 |
+
for decoded in decoder.flush():
|
| 999 |
+
for chunk in chunker.decode(decoded):
|
| 1000 |
+
yield chunk # pragma: no cover
|
| 1001 |
+
for chunk in chunker.flush():
|
| 1002 |
+
yield chunk
|
| 1003 |
+
|
| 1004 |
+
async def aiter_text(self, chunk_size: int | None = None) -> typing.AsyncIterator[str]:
|
| 1005 |
+
"""
|
| 1006 |
+
A str-iterator over the decoded response content
|
| 1007 |
+
that handles both gzip, deflate, etc but also detects the content's
|
| 1008 |
+
string encoding.
|
| 1009 |
+
"""
|
| 1010 |
+
decoder = TextDecoder(encoding=self.encoding or "utf-8")
|
| 1011 |
+
chunker = TextChunker(chunk_size=chunk_size)
|
| 1012 |
+
with request_context(request=self._request):
|
| 1013 |
+
async for byte_content in self.aiter_bytes():
|
| 1014 |
+
text_content = decoder.decode(byte_content)
|
| 1015 |
+
for chunk in chunker.decode(text_content):
|
| 1016 |
+
yield chunk
|
| 1017 |
+
text_content = decoder.flush()
|
| 1018 |
+
for chunk in chunker.decode(text_content):
|
| 1019 |
+
yield chunk # pragma: no cover
|
| 1020 |
+
for chunk in chunker.flush():
|
| 1021 |
+
yield chunk
|
| 1022 |
+
|
| 1023 |
+
async def aiter_lines(self) -> typing.AsyncIterator[str]:
|
| 1024 |
+
decoder = LineDecoder()
|
| 1025 |
+
with request_context(request=self._request):
|
| 1026 |
+
async for text in self.aiter_text():
|
| 1027 |
+
for line in decoder.decode(text):
|
| 1028 |
+
yield line
|
| 1029 |
+
for line in decoder.flush():
|
| 1030 |
+
yield line
|
| 1031 |
+
|
| 1032 |
+
async def aiter_raw(self, chunk_size: int | None = None) -> typing.AsyncGenerator[bytes, None]:
|
| 1033 |
+
"""
|
| 1034 |
+
A byte-iterator over the raw response content.
|
| 1035 |
+
"""
|
| 1036 |
+
if self.is_stream_consumed:
|
| 1037 |
+
raise StreamConsumed()
|
| 1038 |
+
if self.is_closed:
|
| 1039 |
+
raise StreamClosed()
|
| 1040 |
+
if not isinstance(self.stream, AsyncByteStream):
|
| 1041 |
+
raise RuntimeError("Attempted to call an async iterator on a sync stream.")
|
| 1042 |
+
|
| 1043 |
+
self.is_stream_consumed = True
|
| 1044 |
+
self._num_bytes_downloaded = 0
|
| 1045 |
+
chunker = ByteChunker(chunk_size=chunk_size)
|
| 1046 |
+
|
| 1047 |
+
stream = self.stream.__aiter__()
|
| 1048 |
+
try:
|
| 1049 |
+
with request_context(request=self._request):
|
| 1050 |
+
async for raw_stream_bytes in stream:
|
| 1051 |
+
self._num_bytes_downloaded += len(raw_stream_bytes)
|
| 1052 |
+
for chunk in chunker.decode(raw_stream_bytes):
|
| 1053 |
+
yield chunk
|
| 1054 |
+
|
| 1055 |
+
for chunk in chunker.flush():
|
| 1056 |
+
yield chunk
|
| 1057 |
+
finally:
|
| 1058 |
+
if isinstance(stream, AsyncGenerator):
|
| 1059 |
+
await stream.aclose()
|
| 1060 |
+
await self.aclose()
|
| 1061 |
+
|
| 1062 |
+
async def aclose(self) -> None:
|
| 1063 |
+
"""
|
| 1064 |
+
Close the response and release the connection.
|
| 1065 |
+
Automatically called if the response body is read to completion.
|
| 1066 |
+
"""
|
| 1067 |
+
if not isinstance(self.stream, AsyncByteStream):
|
| 1068 |
+
raise RuntimeError("Attempted to call an async close on a sync stream.")
|
| 1069 |
+
|
| 1070 |
+
if not self.is_closed:
|
| 1071 |
+
self.is_closed = True
|
| 1072 |
+
with request_context(request=self._request):
|
| 1073 |
+
await self.stream.aclose()
|
| 1074 |
+
|
| 1075 |
+
|
| 1076 |
+
class Cookies(typing.MutableMapping[str, str]):
|
| 1077 |
+
"""
|
| 1078 |
+
HTTP Cookies, as a mutable mapping.
|
| 1079 |
+
"""
|
| 1080 |
+
|
| 1081 |
+
def __init__(self, cookies: CookieTypes | None = None) -> None:
|
| 1082 |
+
if cookies is None or isinstance(cookies, dict):
|
| 1083 |
+
self.jar: CookieJar = IndexedCookieJar()
|
| 1084 |
+
if isinstance(cookies, dict):
|
| 1085 |
+
for key, value in cookies.items():
|
| 1086 |
+
self.set(key, value)
|
| 1087 |
+
elif isinstance(cookies, list):
|
| 1088 |
+
self.jar = IndexedCookieJar()
|
| 1089 |
+
for key, value in cookies:
|
| 1090 |
+
self.set(key, value)
|
| 1091 |
+
elif isinstance(cookies, Cookies):
|
| 1092 |
+
self.jar = IndexedCookieJar()
|
| 1093 |
+
for cookie in cookies.jar:
|
| 1094 |
+
self.jar.set_cookie(cookie)
|
| 1095 |
+
else:
|
| 1096 |
+
self.jar = cookies
|
| 1097 |
+
|
| 1098 |
+
def extract_cookies(self, response: Response) -> None:
|
| 1099 |
+
"""
|
| 1100 |
+
Loads any cookies based on the response `Set-Cookie` headers.
|
| 1101 |
+
"""
|
| 1102 |
+
if "set-cookie" not in response.headers and "set-cookie2" not in response.headers:
|
| 1103 |
+
return
|
| 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: list[str] = []
|
| 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 |
+
[f"<Cookie {cookie.name}={cookie.value} for {cookie.domain} />" for cookie in self.jar]
|
| 1236 |
+
)
|
| 1237 |
+
|
| 1238 |
+
return f"<Cookies[{cookies_repr}]>"
|
| 1239 |
+
|
| 1240 |
+
class _CookieCompatRequest(urllib.request.Request):
|
| 1241 |
+
"""
|
| 1242 |
+
Wraps a `Request` instance up in a compatibility interface suitable
|
| 1243 |
+
for use with `CookieJar` operations.
|
| 1244 |
+
"""
|
| 1245 |
+
|
| 1246 |
+
def __init__(self, request: Request) -> None:
|
| 1247 |
+
super().__init__(
|
| 1248 |
+
url=str(request.url),
|
| 1249 |
+
headers=dict(request.headers),
|
| 1250 |
+
method=request.method,
|
| 1251 |
+
)
|
| 1252 |
+
self.request = request
|
| 1253 |
+
|
| 1254 |
+
def add_unredirected_header(self, key: str, val: str) -> None:
|
| 1255 |
+
super().add_unredirected_header(key, val)
|
| 1256 |
+
self.request.headers[key] = val
|
| 1257 |
+
|
| 1258 |
+
class _CookieCompatResponse:
|
| 1259 |
+
"""
|
| 1260 |
+
Wraps a `Request` instance up in a compatibility interface suitable
|
| 1261 |
+
for use with `CookieJar` operations.
|
| 1262 |
+
"""
|
| 1263 |
+
|
| 1264 |
+
def __init__(self, response: Response) -> None:
|
| 1265 |
+
self.response = response
|
| 1266 |
+
|
| 1267 |
+
def info(self) -> email.message.Message:
|
| 1268 |
+
info = email.message.Message()
|
| 1269 |
+
for key, value in self.response.headers.multi_items():
|
| 1270 |
+
# Note that setting `info[key]` here is an "append" operation,
|
| 1271 |
+
# not a "replace" operation.
|
| 1272 |
+
# https://docs.python.org/3/library/email.compat32-message.html#email.message.Message.__setitem__
|
| 1273 |
+
info[key] = value
|
| 1274 |
+
return info
|
hug/lib/python3.12/site-packages/httpx2/_multipart.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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({chr(c): f"%{c:02X}" for c in range(0x1F + 1) if c != 0x1B})
|
| 26 |
+
_HTML5_FORM_ENCODING_RE = re.compile(r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()]))
|
| 27 |
+
_HEADER_NAME_RE = re.compile(r"[!#$%&'*+\-.^_`|~0-9A-Za-z]+")
|
| 28 |
+
_FORBIDDEN_HEADER_VALUE_CHARS_RE = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _format_form_param(name: str, value: str) -> bytes:
|
| 32 |
+
"""
|
| 33 |
+
Encode a name/value pair within a multipart form.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def replacer(match: typing.Match[str]) -> str:
|
| 37 |
+
return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)]
|
| 38 |
+
|
| 39 |
+
value = _HTML5_FORM_ENCODING_RE.sub(replacer, value)
|
| 40 |
+
return f'{name}="{value}"'.encode()
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _guess_content_type(filename: str | None) -> str | None:
|
| 44 |
+
"""
|
| 45 |
+
Guesses the mimetype based on a filename. Defaults to `application/octet-stream`.
|
| 46 |
+
|
| 47 |
+
Returns `None` if `filename` is `None` or empty.
|
| 48 |
+
"""
|
| 49 |
+
if filename:
|
| 50 |
+
return mimetypes.guess_type(filename)[0] or "application/octet-stream"
|
| 51 |
+
return None
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def get_multipart_boundary_from_content_type(
|
| 55 |
+
content_type: bytes | None,
|
| 56 |
+
) -> bytes | None:
|
| 57 |
+
if not content_type or not content_type.startswith(b"multipart/form-data"):
|
| 58 |
+
return None
|
| 59 |
+
# parse boundary according to
|
| 60 |
+
# https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1
|
| 61 |
+
if b";" in content_type:
|
| 62 |
+
for section in content_type.split(b";"):
|
| 63 |
+
if section.strip().lower().startswith(b"boundary="):
|
| 64 |
+
return section.strip()[len(b"boundary=") :].strip(b'"')
|
| 65 |
+
return None
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class DataField:
|
| 69 |
+
"""
|
| 70 |
+
A single form field item, within a multipart form field.
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
def __init__(self, name: str, value: str | bytes | int | float | None) -> None:
|
| 74 |
+
if not isinstance(name, str):
|
| 75 |
+
raise TypeError(f"Invalid type for name. Expected str, got {type(name)}: {name!r}")
|
| 76 |
+
if value is not None and not isinstance(value, (str, bytes, int, float)):
|
| 77 |
+
raise TypeError(f"Invalid type for value. Expected primitive type, got {type(value)}: {value!r}")
|
| 78 |
+
self.name = name
|
| 79 |
+
self.value: str | bytes = value if isinstance(value, bytes) else primitive_value_to_str(value)
|
| 80 |
+
|
| 81 |
+
def render_headers(self) -> bytes:
|
| 82 |
+
if not hasattr(self, "_headers"):
|
| 83 |
+
name = _format_form_param("name", self.name)
|
| 84 |
+
self._headers = b"".join([b"Content-Disposition: form-data; ", name, b"\r\n\r\n"])
|
| 85 |
+
|
| 86 |
+
return self._headers
|
| 87 |
+
|
| 88 |
+
def render_data(self) -> bytes:
|
| 89 |
+
if not hasattr(self, "_data"):
|
| 90 |
+
self._data = to_bytes(self.value)
|
| 91 |
+
|
| 92 |
+
return self._data
|
| 93 |
+
|
| 94 |
+
def get_length(self) -> int:
|
| 95 |
+
headers = self.render_headers()
|
| 96 |
+
data = self.render_data()
|
| 97 |
+
return len(headers) + len(data)
|
| 98 |
+
|
| 99 |
+
def render(self) -> typing.Iterator[bytes]:
|
| 100 |
+
yield self.render_headers()
|
| 101 |
+
yield self.render_data()
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
class FileField:
|
| 105 |
+
"""
|
| 106 |
+
A single file field item, within a multipart form field.
|
| 107 |
+
"""
|
| 108 |
+
|
| 109 |
+
CHUNK_SIZE = 64 * 1024
|
| 110 |
+
|
| 111 |
+
def __init__(self, name: str, value: FileTypes) -> None:
|
| 112 |
+
self.name = name
|
| 113 |
+
|
| 114 |
+
fileobj: FileContent
|
| 115 |
+
|
| 116 |
+
headers: dict[str, str] = {}
|
| 117 |
+
content_type: str | None = None
|
| 118 |
+
|
| 119 |
+
# This large tuple based API largely mirror's requests' API
|
| 120 |
+
# It would be good to think of better APIs for this that we could
|
| 121 |
+
# include in httpx 2.0 since variable length tuples(especially of 4 elements)
|
| 122 |
+
# are quite unwieldy
|
| 123 |
+
if isinstance(value, tuple):
|
| 124 |
+
if len(value) == 2:
|
| 125 |
+
# neither the 3rd parameter (content_type) nor the 4th (headers)
|
| 126 |
+
# was included
|
| 127 |
+
filename, fileobj = value
|
| 128 |
+
elif len(value) == 3:
|
| 129 |
+
filename, fileobj, content_type = value
|
| 130 |
+
else:
|
| 131 |
+
# all 4 parameters included
|
| 132 |
+
filename, fileobj, content_type, headers = value # type: ignore
|
| 133 |
+
else:
|
| 134 |
+
filename = Path(str(getattr(value, "name", "upload"))).name
|
| 135 |
+
fileobj = value
|
| 136 |
+
|
| 137 |
+
if content_type is None:
|
| 138 |
+
content_type = _guess_content_type(filename)
|
| 139 |
+
|
| 140 |
+
has_content_type_header = any("content-type" in key.lower() for key in headers)
|
| 141 |
+
if content_type is not None and not has_content_type_header:
|
| 142 |
+
# note that unlike requests, we ignore the content_type provided in the 3rd
|
| 143 |
+
# tuple element if it is also included in the headers requests does
|
| 144 |
+
# the opposite (it overwrites the headerwith the 3rd tuple element)
|
| 145 |
+
headers["Content-Type"] = content_type
|
| 146 |
+
|
| 147 |
+
if isinstance(fileobj, io.StringIO):
|
| 148 |
+
raise TypeError("Multipart file uploads require 'io.BytesIO', not 'io.StringIO'.")
|
| 149 |
+
if isinstance(fileobj, io.TextIOBase):
|
| 150 |
+
raise TypeError("Multipart file uploads must be opened in binary mode, not text mode.")
|
| 151 |
+
|
| 152 |
+
self.filename = filename
|
| 153 |
+
self.file = fileobj
|
| 154 |
+
self.headers = headers
|
| 155 |
+
|
| 156 |
+
def get_length(self) -> int | None:
|
| 157 |
+
headers = self.render_headers()
|
| 158 |
+
|
| 159 |
+
if isinstance(self.file, (str, bytes)):
|
| 160 |
+
return len(headers) + len(to_bytes(self.file))
|
| 161 |
+
|
| 162 |
+
file_length = peek_filelike_length(self.file)
|
| 163 |
+
|
| 164 |
+
# If we can't determine the filesize without reading it into memory,
|
| 165 |
+
# then return `None` here, to indicate an unknown file length.
|
| 166 |
+
if file_length is None:
|
| 167 |
+
return None
|
| 168 |
+
|
| 169 |
+
return len(headers) + file_length
|
| 170 |
+
|
| 171 |
+
def render_headers(self) -> bytes:
|
| 172 |
+
if not hasattr(self, "_headers"):
|
| 173 |
+
parts = [
|
| 174 |
+
b"Content-Disposition: form-data; ",
|
| 175 |
+
_format_form_param("name", self.name),
|
| 176 |
+
]
|
| 177 |
+
if self.filename:
|
| 178 |
+
filename = _format_form_param("filename", self.filename)
|
| 179 |
+
parts.extend([b"; ", filename])
|
| 180 |
+
for header_name, header_value in self.headers.items():
|
| 181 |
+
if _HEADER_NAME_RE.fullmatch(header_name) is None:
|
| 182 |
+
raise ValueError("Invalid multipart header name.")
|
| 183 |
+
if _FORBIDDEN_HEADER_VALUE_CHARS_RE.search(header_value) is not None:
|
| 184 |
+
raise ValueError("Invalid control character in multipart header value.")
|
| 185 |
+
key, val = f"\r\n{header_name}: ".encode(), header_value.encode()
|
| 186 |
+
parts.extend([key, val])
|
| 187 |
+
parts.append(b"\r\n\r\n")
|
| 188 |
+
self._headers = b"".join(parts)
|
| 189 |
+
|
| 190 |
+
return self._headers
|
| 191 |
+
|
| 192 |
+
def render_data(self) -> typing.Iterator[bytes]:
|
| 193 |
+
if isinstance(self.file, (str, bytes)):
|
| 194 |
+
yield to_bytes(self.file)
|
| 195 |
+
return
|
| 196 |
+
|
| 197 |
+
if hasattr(self.file, "seek"):
|
| 198 |
+
try:
|
| 199 |
+
self.file.seek(0)
|
| 200 |
+
except io.UnsupportedOperation:
|
| 201 |
+
pass
|
| 202 |
+
|
| 203 |
+
chunk = self.file.read(self.CHUNK_SIZE)
|
| 204 |
+
while chunk:
|
| 205 |
+
yield to_bytes(chunk)
|
| 206 |
+
chunk = self.file.read(self.CHUNK_SIZE)
|
| 207 |
+
|
| 208 |
+
def render(self) -> typing.Iterator[bytes]:
|
| 209 |
+
yield self.render_headers()
|
| 210 |
+
yield from self.render_data()
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
class MultipartStream(SyncByteStream, AsyncByteStream):
|
| 214 |
+
"""
|
| 215 |
+
Request content as streaming multipart encoded form data.
|
| 216 |
+
"""
|
| 217 |
+
|
| 218 |
+
def __init__(
|
| 219 |
+
self,
|
| 220 |
+
data: RequestData,
|
| 221 |
+
files: RequestFiles,
|
| 222 |
+
boundary: bytes | None = None,
|
| 223 |
+
) -> None:
|
| 224 |
+
if boundary is None:
|
| 225 |
+
boundary = os.urandom(16).hex().encode("ascii")
|
| 226 |
+
|
| 227 |
+
self.boundary = boundary
|
| 228 |
+
self.content_type = f"multipart/form-data; boundary={boundary.decode('ascii')}"
|
| 229 |
+
self.fields = list(self._iter_fields(data, files))
|
| 230 |
+
|
| 231 |
+
def _iter_fields(self, data: RequestData, files: RequestFiles) -> typing.Iterator[FileField | DataField]:
|
| 232 |
+
for name, value in data.items():
|
| 233 |
+
if isinstance(value, (tuple, list)):
|
| 234 |
+
for item in value:
|
| 235 |
+
yield DataField(name=name, value=item)
|
| 236 |
+
else:
|
| 237 |
+
yield DataField(name=name, value=value)
|
| 238 |
+
|
| 239 |
+
file_items = files.items() if isinstance(files, typing.Mapping) else files
|
| 240 |
+
for name, value in file_items:
|
| 241 |
+
yield FileField(name=name, value=value)
|
| 242 |
+
|
| 243 |
+
def iter_chunks(self) -> typing.Iterator[bytes]:
|
| 244 |
+
for field in self.fields:
|
| 245 |
+
yield b"--%s\r\n" % self.boundary
|
| 246 |
+
yield from field.render()
|
| 247 |
+
yield b"\r\n"
|
| 248 |
+
yield b"--%s--\r\n" % self.boundary
|
| 249 |
+
|
| 250 |
+
def get_content_length(self) -> int | None:
|
| 251 |
+
"""
|
| 252 |
+
Return the length of the multipart encoded content, or `None` if
|
| 253 |
+
any of the files have a length that cannot be determined upfront.
|
| 254 |
+
"""
|
| 255 |
+
boundary_length = len(self.boundary)
|
| 256 |
+
length = 0
|
| 257 |
+
|
| 258 |
+
for field in self.fields:
|
| 259 |
+
field_length = field.get_length()
|
| 260 |
+
if field_length is None:
|
| 261 |
+
return None
|
| 262 |
+
|
| 263 |
+
length += 2 + boundary_length + 2 # b"--{boundary}\r\n"
|
| 264 |
+
length += field_length
|
| 265 |
+
length += 2 # b"\r\n"
|
| 266 |
+
|
| 267 |
+
length += 2 + boundary_length + 4 # b"--{boundary}--\r\n"
|
| 268 |
+
return length
|
| 269 |
+
|
| 270 |
+
# Content stream interface.
|
| 271 |
+
|
| 272 |
+
def get_headers(self) -> dict[str, str]:
|
| 273 |
+
content_length = self.get_content_length()
|
| 274 |
+
content_type = self.content_type
|
| 275 |
+
if content_length is None:
|
| 276 |
+
return {"Transfer-Encoding": "chunked", "Content-Type": content_type}
|
| 277 |
+
return {"Content-Length": str(content_length), "Content-Type": content_type}
|
| 278 |
+
|
| 279 |
+
def __iter__(self) -> typing.Iterator[bytes]:
|
| 280 |
+
yield from self.iter_chunks()
|
| 281 |
+
|
| 282 |
+
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
|
| 283 |
+
for chunk in self.iter_chunks():
|
| 284 |
+
yield chunk
|
hug/lib/python3.12/site-packages/httpx2/_sse.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Server-sent events support, derived from httpx-sse (https://github.com/florimondmanca/httpx-sse).
|
| 3 |
+
|
| 4 |
+
```
|
| 5 |
+
MIT License
|
| 6 |
+
|
| 7 |
+
Copyright (c) 2022 Florimond Manca
|
| 8 |
+
|
| 9 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 10 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 11 |
+
in the Software without restriction, including without limitation the rights
|
| 12 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 13 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 14 |
+
furnished to do so, subject to the following conditions:
|
| 15 |
+
|
| 16 |
+
The above copyright notice and this permission notice shall be included in all
|
| 17 |
+
copies or substantial portions of the Software.
|
| 18 |
+
|
| 19 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 20 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 21 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 22 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 23 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 24 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 25 |
+
SOFTWARE.
|
| 26 |
+
```
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
from __future__ import annotations
|
| 30 |
+
|
| 31 |
+
import json as jsonlib
|
| 32 |
+
from collections.abc import AsyncIterator, Iterator
|
| 33 |
+
from dataclasses import dataclass
|
| 34 |
+
|
| 35 |
+
from ._config import DEFAULT_MAX_EVENT_SIZE_BYTES
|
| 36 |
+
from ._exceptions import TransportError, request_context
|
| 37 |
+
from ._models import Response
|
| 38 |
+
|
| 39 |
+
__all__ = ["EventSource", "SSEError", "ServerSentEvent"]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class SSEError(TransportError):
|
| 43 |
+
"""
|
| 44 |
+
An error that occurred while connecting to a server-sent events endpoint.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass(frozen=True)
|
| 49 |
+
class ServerSentEvent:
|
| 50 |
+
event: str = "message"
|
| 51 |
+
data: str = ""
|
| 52 |
+
id: str = ""
|
| 53 |
+
retry: int | None = None
|
| 54 |
+
|
| 55 |
+
def json(self) -> object:
|
| 56 |
+
return jsonlib.loads(self.data)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class _SSEEventDecoder:
|
| 60 |
+
def __init__(self, max_event_size: int | None = None) -> None:
|
| 61 |
+
self._max_event_size = max_event_size
|
| 62 |
+
self._event = ""
|
| 63 |
+
self._data: list[str] = []
|
| 64 |
+
self._event_size = 0
|
| 65 |
+
self._last_event_id = ""
|
| 66 |
+
self._retry: int | None = None
|
| 67 |
+
self._pending = False
|
| 68 |
+
|
| 69 |
+
def decode(self, line: str) -> ServerSentEvent | None:
|
| 70 |
+
if not line:
|
| 71 |
+
self._event_size = 0
|
| 72 |
+
if not self._pending:
|
| 73 |
+
return None
|
| 74 |
+
|
| 75 |
+
sse = ServerSentEvent(
|
| 76 |
+
event=self._event or "message",
|
| 77 |
+
data="\n".join(self._data),
|
| 78 |
+
id=self._last_event_id,
|
| 79 |
+
retry=self._retry,
|
| 80 |
+
)
|
| 81 |
+
self._event = ""
|
| 82 |
+
self._data = []
|
| 83 |
+
self._retry = None
|
| 84 |
+
self._pending = False
|
| 85 |
+
return sse
|
| 86 |
+
|
| 87 |
+
if line.startswith(":"):
|
| 88 |
+
return None
|
| 89 |
+
|
| 90 |
+
self._event_size += len(line.encode("utf-8"))
|
| 91 |
+
self._check_size()
|
| 92 |
+
|
| 93 |
+
fieldname, _, value = line.partition(":")
|
| 94 |
+
value = value[1:] if value.startswith(" ") else value
|
| 95 |
+
|
| 96 |
+
if fieldname == "event":
|
| 97 |
+
self._event = value
|
| 98 |
+
self._pending = True
|
| 99 |
+
elif fieldname == "data":
|
| 100 |
+
self._data.append(value)
|
| 101 |
+
self._pending = True
|
| 102 |
+
elif fieldname == "id":
|
| 103 |
+
if "\0" not in value:
|
| 104 |
+
self._last_event_id = value
|
| 105 |
+
self._pending = True
|
| 106 |
+
elif fieldname == "retry":
|
| 107 |
+
try:
|
| 108 |
+
self._retry = int(value)
|
| 109 |
+
self._pending = True
|
| 110 |
+
except ValueError:
|
| 111 |
+
pass
|
| 112 |
+
|
| 113 |
+
return None
|
| 114 |
+
|
| 115 |
+
def check_pending(self, pending_size: int) -> None:
|
| 116 |
+
"""
|
| 117 |
+
Bound the total bytes buffered for the in-progress event, including
|
| 118 |
+
a trailing line that has not been terminated by a newline yet.
|
| 119 |
+
"""
|
| 120 |
+
self._check_size(pending_size)
|
| 121 |
+
|
| 122 |
+
def _check_size(self, pending_size: int = 0) -> None:
|
| 123 |
+
if self._max_event_size is not None and self._event_size + pending_size > self._max_event_size:
|
| 124 |
+
raise SSEError(f"Server-sent event exceeded the {self._max_event_size} byte limit.")
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class _SSELineDecoder:
|
| 128 |
+
def __init__(self) -> None:
|
| 129 |
+
self._parts: list[str] = []
|
| 130 |
+
self._pending_size = 0
|
| 131 |
+
self._trailing_cr = False
|
| 132 |
+
|
| 133 |
+
@property
|
| 134 |
+
def pending_size(self) -> int:
|
| 135 |
+
return self._pending_size
|
| 136 |
+
|
| 137 |
+
def decode(self, text: str) -> list[str]:
|
| 138 |
+
if self._trailing_cr:
|
| 139 |
+
text = "\r" + text
|
| 140 |
+
self._trailing_cr = False
|
| 141 |
+
if text.endswith("\r"):
|
| 142 |
+
self._trailing_cr = True
|
| 143 |
+
text = text[:-1]
|
| 144 |
+
|
| 145 |
+
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
| 146 |
+
if "\n" not in text:
|
| 147 |
+
self._append(text)
|
| 148 |
+
return []
|
| 149 |
+
|
| 150 |
+
lines = text.split("\n")
|
| 151 |
+
self._append(lines[0])
|
| 152 |
+
lines[0] = self._consume_pending()
|
| 153 |
+
self._append(lines.pop())
|
| 154 |
+
return lines
|
| 155 |
+
|
| 156 |
+
def flush(self) -> list[str]:
|
| 157 |
+
if self._trailing_cr:
|
| 158 |
+
self._append("\n")
|
| 159 |
+
self._trailing_cr = False
|
| 160 |
+
buffer = self._consume_pending()
|
| 161 |
+
if not buffer:
|
| 162 |
+
return []
|
| 163 |
+
return buffer.split("\n")
|
| 164 |
+
|
| 165 |
+
def _append(self, text: str) -> None:
|
| 166 |
+
if text:
|
| 167 |
+
self._parts.append(text)
|
| 168 |
+
self._pending_size += len(text.encode("utf-8"))
|
| 169 |
+
|
| 170 |
+
def _consume_pending(self) -> str:
|
| 171 |
+
pending = "".join(self._parts)
|
| 172 |
+
self._parts = []
|
| 173 |
+
self._pending_size = 0
|
| 174 |
+
return pending
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
class _SSEParser:
|
| 178 |
+
def __init__(self, max_event_size: int | None = None) -> None:
|
| 179 |
+
self._event_decoder = _SSEEventDecoder(max_event_size)
|
| 180 |
+
self._line_decoder = _SSELineDecoder()
|
| 181 |
+
|
| 182 |
+
def decode(self, text: str) -> Iterator[ServerSentEvent]:
|
| 183 |
+
yield from self._decode_lines(self._line_decoder.decode(text))
|
| 184 |
+
self._event_decoder.check_pending(self._line_decoder.pending_size)
|
| 185 |
+
|
| 186 |
+
def flush(self) -> Iterator[ServerSentEvent]:
|
| 187 |
+
yield from self._decode_lines(self._line_decoder.flush())
|
| 188 |
+
|
| 189 |
+
def _decode_lines(self, lines: list[str]) -> Iterator[ServerSentEvent]:
|
| 190 |
+
for line in lines:
|
| 191 |
+
sse = self._event_decoder.decode(line)
|
| 192 |
+
if sse is not None:
|
| 193 |
+
yield sse
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
class EventSource:
|
| 197 |
+
def __init__(self, response: Response, max_event_size: int | None = DEFAULT_MAX_EVENT_SIZE_BYTES) -> None:
|
| 198 |
+
self._response = response
|
| 199 |
+
self._max_event_size = max_event_size
|
| 200 |
+
|
| 201 |
+
@property
|
| 202 |
+
def response(self) -> Response:
|
| 203 |
+
return self._response
|
| 204 |
+
|
| 205 |
+
def _check_content_type(self) -> None:
|
| 206 |
+
content_type, _, _ = self._response.headers.get("content-type", "").partition(";")
|
| 207 |
+
if content_type.strip().lower() != "text/event-stream":
|
| 208 |
+
raise SSEError(f"Expected response with content type 'text/event-stream', got {content_type.strip()!r}.")
|
| 209 |
+
|
| 210 |
+
def __iter__(self) -> Iterator[ServerSentEvent]:
|
| 211 |
+
with request_context(request=self._response.request):
|
| 212 |
+
self._check_content_type()
|
| 213 |
+
parser = _SSEParser(self._max_event_size)
|
| 214 |
+
for chunk in self._response.iter_text():
|
| 215 |
+
yield from parser.decode(chunk)
|
| 216 |
+
yield from parser.flush()
|
| 217 |
+
|
| 218 |
+
async def __aiter__(self) -> AsyncIterator[ServerSentEvent]:
|
| 219 |
+
with request_context(request=self._response.request):
|
| 220 |
+
self._check_content_type()
|
| 221 |
+
parser = _SSEParser(self._max_event_size)
|
| 222 |
+
async for chunk in self._response.aiter_text():
|
| 223 |
+
for sse in parser.decode(chunk):
|
| 224 |
+
yield sse
|
| 225 |
+
for sse in parser.flush():
|
| 226 |
+
yield sse
|
hug/lib/python3.12/site-packages/httpx2/_status_codes.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import warnings
|
| 4 |
+
from enum import EnumMeta, IntEnum
|
| 5 |
+
|
| 6 |
+
__all__ = ["codes"]
|
| 7 |
+
|
| 8 |
+
# Constant names that were renamed by RFC 9110, kept as deprecated aliases
|
| 9 |
+
# mapping each old name to its canonical replacement.
|
| 10 |
+
_DEPRECATED_CODES = {
|
| 11 |
+
"REQUEST_ENTITY_TOO_LARGE": "CONTENT_TOO_LARGE",
|
| 12 |
+
"REQUEST_URI_TOO_LONG": "URI_TOO_LONG",
|
| 13 |
+
"REQUESTED_RANGE_NOT_SATISFIABLE": "RANGE_NOT_SATISFIABLE",
|
| 14 |
+
"UNPROCESSABLE_ENTITY": "UNPROCESSABLE_CONTENT",
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class _CodesMeta(EnumMeta):
|
| 19 |
+
def __getattribute__(cls, name: str) -> object:
|
| 20 |
+
if name in _DEPRECATED_CODES:
|
| 21 |
+
new_name = _DEPRECATED_CODES[name]
|
| 22 |
+
warnings.warn(
|
| 23 |
+
f"'{name}' is deprecated. Use '{new_name}' instead.",
|
| 24 |
+
category=DeprecationWarning,
|
| 25 |
+
stacklevel=2,
|
| 26 |
+
)
|
| 27 |
+
return super().__getattribute__(name)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class codes(IntEnum, metaclass=_CodesMeta):
|
| 31 |
+
"""HTTP status codes and reason phrases
|
| 32 |
+
|
| 33 |
+
Status codes from the following RFCs are all observed:
|
| 34 |
+
|
| 35 |
+
* RFC 9110: HTTP Semantics, obsoletes 7231 & 7238
|
| 36 |
+
* RFC 6585: Additional HTTP Status Codes
|
| 37 |
+
* RFC 3229: Delta encoding in HTTP
|
| 38 |
+
* RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518
|
| 39 |
+
* RFC 5842: Binding Extensions to WebDAV
|
| 40 |
+
* RFC 2295: Transparent Content Negotiation in HTTP
|
| 41 |
+
* RFC 2774: An HTTP Extension Framework
|
| 42 |
+
* RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2)
|
| 43 |
+
* RFC 2324: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0)
|
| 44 |
+
* RFC 7725: An HTTP Status Code to Report Legal Obstacles
|
| 45 |
+
* RFC 8297: An HTTP Status Code for Indicating Hints
|
| 46 |
+
* RFC 8470: Using Early Data in HTTP
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __new__(cls, value: int, phrase: str = "") -> codes:
|
| 50 |
+
obj = int.__new__(cls, value)
|
| 51 |
+
obj._value_ = value
|
| 52 |
+
|
| 53 |
+
obj.phrase = phrase # type: ignore[attr-defined]
|
| 54 |
+
return obj
|
| 55 |
+
|
| 56 |
+
def __str__(self) -> str:
|
| 57 |
+
return str(self.value)
|
| 58 |
+
|
| 59 |
+
@classmethod
|
| 60 |
+
def get_reason_phrase(cls, value: int) -> str:
|
| 61 |
+
try:
|
| 62 |
+
return codes(value).phrase # type: ignore
|
| 63 |
+
except ValueError:
|
| 64 |
+
return ""
|
| 65 |
+
|
| 66 |
+
@classmethod
|
| 67 |
+
def is_informational(cls, value: int) -> bool:
|
| 68 |
+
"""
|
| 69 |
+
Returns `True` for 1xx status codes, `False` otherwise.
|
| 70 |
+
"""
|
| 71 |
+
return 100 <= value <= 199
|
| 72 |
+
|
| 73 |
+
@classmethod
|
| 74 |
+
def is_success(cls, value: int) -> bool:
|
| 75 |
+
"""
|
| 76 |
+
Returns `True` for 2xx status codes, `False` otherwise.
|
| 77 |
+
"""
|
| 78 |
+
return 200 <= value <= 299
|
| 79 |
+
|
| 80 |
+
@classmethod
|
| 81 |
+
def is_redirect(cls, value: int) -> bool:
|
| 82 |
+
"""
|
| 83 |
+
Returns `True` for 3xx status codes, `False` otherwise.
|
| 84 |
+
"""
|
| 85 |
+
return 300 <= value <= 399
|
| 86 |
+
|
| 87 |
+
@classmethod
|
| 88 |
+
def is_client_error(cls, value: int) -> bool:
|
| 89 |
+
"""
|
| 90 |
+
Returns `True` for 4xx status codes, `False` otherwise.
|
| 91 |
+
"""
|
| 92 |
+
return 400 <= value <= 499
|
| 93 |
+
|
| 94 |
+
@classmethod
|
| 95 |
+
def is_server_error(cls, value: int) -> bool:
|
| 96 |
+
"""
|
| 97 |
+
Returns `True` for 5xx status codes, `False` otherwise.
|
| 98 |
+
"""
|
| 99 |
+
return 500 <= value <= 599
|
| 100 |
+
|
| 101 |
+
@classmethod
|
| 102 |
+
def is_error(cls, value: int) -> bool:
|
| 103 |
+
"""
|
| 104 |
+
Returns `True` for 4xx or 5xx status codes, `False` otherwise.
|
| 105 |
+
"""
|
| 106 |
+
return 400 <= value <= 599
|
| 107 |
+
|
| 108 |
+
# informational
|
| 109 |
+
CONTINUE = 100, "Continue"
|
| 110 |
+
SWITCHING_PROTOCOLS = 101, "Switching Protocols"
|
| 111 |
+
PROCESSING = 102, "Processing"
|
| 112 |
+
EARLY_HINTS = 103, "Early Hints"
|
| 113 |
+
|
| 114 |
+
# success
|
| 115 |
+
OK = 200, "OK"
|
| 116 |
+
CREATED = 201, "Created"
|
| 117 |
+
ACCEPTED = 202, "Accepted"
|
| 118 |
+
NON_AUTHORITATIVE_INFORMATION = 203, "Non-Authoritative Information"
|
| 119 |
+
NO_CONTENT = 204, "No Content"
|
| 120 |
+
RESET_CONTENT = 205, "Reset Content"
|
| 121 |
+
PARTIAL_CONTENT = 206, "Partial Content"
|
| 122 |
+
MULTI_STATUS = 207, "Multi-Status"
|
| 123 |
+
ALREADY_REPORTED = 208, "Already Reported"
|
| 124 |
+
IM_USED = 226, "IM Used"
|
| 125 |
+
|
| 126 |
+
# redirection
|
| 127 |
+
MULTIPLE_CHOICES = 300, "Multiple Choices"
|
| 128 |
+
MOVED_PERMANENTLY = 301, "Moved Permanently"
|
| 129 |
+
FOUND = 302, "Found"
|
| 130 |
+
SEE_OTHER = 303, "See Other"
|
| 131 |
+
NOT_MODIFIED = 304, "Not Modified"
|
| 132 |
+
USE_PROXY = 305, "Use Proxy"
|
| 133 |
+
TEMPORARY_REDIRECT = 307, "Temporary Redirect"
|
| 134 |
+
PERMANENT_REDIRECT = 308, "Permanent Redirect"
|
| 135 |
+
|
| 136 |
+
# client error
|
| 137 |
+
BAD_REQUEST = 400, "Bad Request"
|
| 138 |
+
UNAUTHORIZED = 401, "Unauthorized"
|
| 139 |
+
PAYMENT_REQUIRED = 402, "Payment Required"
|
| 140 |
+
FORBIDDEN = 403, "Forbidden"
|
| 141 |
+
NOT_FOUND = 404, "Not Found"
|
| 142 |
+
METHOD_NOT_ALLOWED = 405, "Method Not Allowed"
|
| 143 |
+
NOT_ACCEPTABLE = 406, "Not Acceptable"
|
| 144 |
+
PROXY_AUTHENTICATION_REQUIRED = 407, "Proxy Authentication Required"
|
| 145 |
+
REQUEST_TIMEOUT = 408, "Request Timeout"
|
| 146 |
+
CONFLICT = 409, "Conflict"
|
| 147 |
+
GONE = 410, "Gone"
|
| 148 |
+
LENGTH_REQUIRED = 411, "Length Required"
|
| 149 |
+
PRECONDITION_FAILED = 412, "Precondition Failed"
|
| 150 |
+
CONTENT_TOO_LARGE = 413, "Content Too Large"
|
| 151 |
+
URI_TOO_LONG = 414, "URI Too Long"
|
| 152 |
+
UNSUPPORTED_MEDIA_TYPE = 415, "Unsupported Media Type"
|
| 153 |
+
RANGE_NOT_SATISFIABLE = 416, "Range Not Satisfiable"
|
| 154 |
+
EXPECTATION_FAILED = 417, "Expectation Failed"
|
| 155 |
+
IM_A_TEAPOT = 418, "I'm a teapot"
|
| 156 |
+
MISDIRECTED_REQUEST = 421, "Misdirected Request"
|
| 157 |
+
UNPROCESSABLE_CONTENT = 422, "Unprocessable Content"
|
| 158 |
+
LOCKED = 423, "Locked"
|
| 159 |
+
FAILED_DEPENDENCY = 424, "Failed Dependency"
|
| 160 |
+
TOO_EARLY = 425, "Too Early"
|
| 161 |
+
UPGRADE_REQUIRED = 426, "Upgrade Required"
|
| 162 |
+
PRECONDITION_REQUIRED = 428, "Precondition Required"
|
| 163 |
+
TOO_MANY_REQUESTS = 429, "Too Many Requests"
|
| 164 |
+
REQUEST_HEADER_FIELDS_TOO_LARGE = 431, "Request Header Fields Too Large"
|
| 165 |
+
UNAVAILABLE_FOR_LEGAL_REASONS = 451, "Unavailable For Legal Reasons"
|
| 166 |
+
|
| 167 |
+
# server errors
|
| 168 |
+
INTERNAL_SERVER_ERROR = 500, "Internal Server Error"
|
| 169 |
+
NOT_IMPLEMENTED = 501, "Not Implemented"
|
| 170 |
+
BAD_GATEWAY = 502, "Bad Gateway"
|
| 171 |
+
SERVICE_UNAVAILABLE = 503, "Service Unavailable"
|
| 172 |
+
GATEWAY_TIMEOUT = 504, "Gateway Timeout"
|
| 173 |
+
HTTP_VERSION_NOT_SUPPORTED = 505, "HTTP Version Not Supported"
|
| 174 |
+
VARIANT_ALSO_NEGOTIATES = 506, "Variant Also Negotiates"
|
| 175 |
+
INSUFFICIENT_STORAGE = 507, "Insufficient Storage"
|
| 176 |
+
LOOP_DETECTED = 508, "Loop Detected"
|
| 177 |
+
NOT_EXTENDED = 510, "Not Extended"
|
| 178 |
+
NETWORK_AUTHENTICATION_REQUIRED = 511, "Network Authentication Required"
|
| 179 |
+
|
| 180 |
+
# Deprecated aliases for names replaced by RFC 9110.
|
| 181 |
+
REQUEST_ENTITY_TOO_LARGE = CONTENT_TOO_LARGE
|
| 182 |
+
REQUEST_URI_TOO_LONG = URI_TOO_LONG
|
| 183 |
+
REQUESTED_RANGE_NOT_SATISFIABLE = RANGE_NOT_SATISFIABLE
|
| 184 |
+
UNPROCESSABLE_ENTITY = UNPROCESSABLE_CONTENT
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
# Include lower-case styles for `requests` compatibility.
|
| 188 |
+
for name, code in codes.__members__.items():
|
| 189 |
+
setattr(codes, name.lower(), int(code))
|
hug/lib/python3.12/site-packages/httpx2/_transports/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
|
| 3 |
+
from .asgi import ASGITransport
|
| 4 |
+
from .base import AsyncBaseTransport, BaseTransport
|
| 5 |
+
from .mock import MockTransport
|
| 6 |
+
from .wsgi import WSGITransport
|
| 7 |
+
|
| 8 |
+
if sys.platform == "emscripten": # pragma: nocover
|
| 9 |
+
if sys.version_info < (3, 12):
|
| 10 |
+
raise RuntimeError("httpx2 requires Python 3.12+ on Emscripten.")
|
| 11 |
+
|
| 12 |
+
from httpx2_jsfetch import (
|
| 13 |
+
AsyncJavascriptFetchTransport as AsyncHTTPTransport,
|
| 14 |
+
JavascriptFetchTransport as HTTPTransport,
|
| 15 |
+
)
|
| 16 |
+
else:
|
| 17 |
+
from .default import AsyncHTTPTransport, HTTPTransport
|
| 18 |
+
|
| 19 |
+
__all__ = [
|
| 20 |
+
"ASGITransport",
|
| 21 |
+
"AsyncBaseTransport",
|
| 22 |
+
"BaseTransport",
|
| 23 |
+
"AsyncHTTPTransport",
|
| 24 |
+
"HTTPTransport",
|
| 25 |
+
"MockTransport",
|
| 26 |
+
"WSGITransport",
|
| 27 |
+
]
|
hug/lib/python3.12/site-packages/httpx2/_transports/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (881 Bytes). View file
|
|
|
hug/lib/python3.12/site-packages/httpx2/_transports/__pycache__/asgi.cpython-312.pyc
ADDED
|
Binary file (7.49 kB). View file
|
|
|