Compare commits

...

1109 Commits

Author SHA1 Message Date
Ben Beasley
b5addb64f0
Adapt test_response_decode_text_using_autodetect for chardet 6.0 (#3773) 2026-02-23 10:40:42 +00:00
Josh Cannon
ae1b9f6623
Expose FunctionAuth in __all__ (#3699)
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
Co-authored-by: Kar Petrosyan <kar.petrosyanpy@gmail.com>
2025-12-10 18:58:48 +04:00
Riccardo Magliocchetti
ca097c96f9
docs/ssl: fix typo (#3703) 2025-12-10 18:47:31 +04:00
ZProger
def4778d62
Fixed a syntax error in the file upload example (#3692) 2025-10-16 10:04:38 +01:00
dependabot[bot]
435e1dac89
Bump actions/setup-python from 5 to 6 (#3677)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-04 18:38:23 +01:00
Kim Christie
4b23574cf8
Update dependencies (#3665) 2025-09-16 14:23:31 +01:00
Tobias Fischer
652f051fea
Documentation for SSL_CERT_FILE and SSL_CERT_DIR (#3579)
Co-authored-by: Kim Christie <tom@tomchristie.com>
2025-09-11 11:59:20 +01:00
nikkie
3fee27838e
[docs] Remove load_ssl_context & load_verify_locations DEBUG log (#3589)
Co-authored-by: Kim Christie <tom@tomchristie.com>
2025-09-05 15:30:31 +01:00
Glen Keane
bc00d2bd9f
Update compatibility.md with documentation of exceptions differences (#3649)
Co-authored-by: Kim Christie <tom@tomchristie.com>
2025-09-05 15:19:37 +01:00
dependabot[bot]
767cf6baa6
Bump the python-packages group across 1 directory with 10 updates (#3658)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-04 15:29:43 +01:00
Christian Clauss
b55d463570
Upgrade Python type checker mypy (#3654) 2025-09-04 08:48:49 -05:00
Kamil Monicz
15e9759e65
Add httpx-secure to third party packages (#3629)
Co-authored-by: Kim Christie <tom@tomchristie.com>
2025-09-04 09:52:37 +01:00
Christian Clauss
364697efca
Upgrade Python formatter ruff (#3651) 2025-09-03 06:17:26 -05:00
Chai Landau
89102021fc
chore: update sponsorship graphic (#3620) 2025-08-07 08:52:25 -05:00
Alex Grönholm
4fb9528c2f
Drop Python 3.8 support (#3592) 2025-06-27 12:45:12 +02:00
Emmanuel Ferdman
336204f012
Display proxy protocol scheme on error (#3571) 2025-06-02 20:29:52 +01:00
Will Ockmore
6c7af96773
Add httpx-retries to third party packages docs (#3552) 2025-05-02 12:24:26 +01:00
mv-python
9e8ab40369
Docs: Add httpx.Proxy to api.md (#3512) 2025-03-05 12:52:58 +00:00
T-256
ce7a6e91fb
Add httpdbg to third party packages. (#3327)
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2025-02-27 21:43:26 +04:00
Kar Petrosyan
4189b7f051
fix typo (#3519) 2025-02-27 20:38:39 +04:00
Tom Christie
e70d0b08c9
Sharper CHANGELOG entry. (#3448) 2025-02-14 14:52:54 +00:00
dependabot[bot]
b395e6626b
Bump cryptography from 44.0.0 to 44.0.1 (#3499) 2025-02-12 11:25:05 +00:00
Bazyli Cyran
10b7295922
docs: Use with to open files in multipart examples (#3478) 2025-01-17 10:56:46 +00:00
Hugo van Kemenade
c7c13f18a5
Add support for Python 3.13 (#3460) 2024-12-23 15:50:57 -06:00
Tom Christie
26d48e0634
Version 0.28.1 (#3445) 2024-12-06 15:35:41 +00:00
Tom Christie
89599a9541
Fix verify=False, cert=... case. (#3442) 2024-12-04 11:29:09 +00:00
Elaina
8ecb86f0d7
Add test for request params behavior changes (#3364) (#3440)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-12-03 16:12:27 +00:00
dependabot[bot]
0cb7e5a2e7
Bump the python-packages group with 11 updates (#3434)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2024-12-03 08:37:45 +01:00
Daniel Arvelini
15e21e9ea3
Updating deprecated docstring Client() class (#3426) 2024-11-29 11:15:56 +00:00
Tom Christie
80960fa319
Version 0.28.0. (#3419) 2024-11-28 14:50:04 +00:00
Tom Christie
a33c87852b
Fix extensions type annotation. (#3380) 2024-11-28 13:31:17 +00:00
Tom Christie
ce7e14da27
Error on verify as str. (#3418) 2024-11-28 11:46:59 +00:00
Tom Christie
47f4a96ffa
Handle empty zstd responses (#3412) 2024-11-22 11:42:51 +00:00
Bob Conan
189fc4bcbe
Update CHANGELOG.md, fix typo(s) (#3406) 2024-11-20 12:27:29 +00:00
RafaelWO
7b19cd5f4b
Move utility functions from _utils.py to _client.py (#3389) 2024-11-15 11:42:52 +00:00
Tom Christie
b47d94c904
Avoid private imports in test cases. (#3403) 2024-11-15 10:26:56 +00:00
Tom Christie
2ea2286db4
Import ssl on demand (#3401) 2024-11-15 10:17:42 +00:00
Tom Christie
1805ee0d22
Graceful upgrade path for 0.28. (#3394) 2024-11-12 11:31:42 +00:00
RafaelWO
41597adffa
Move remaining utility functions from _utils.py to _models.py (#3387) 2024-11-01 19:20:18 +00:00
RafaelWO
6212e8fa3b
Move utility functions from _utils.py to _multipart.py (#3388) 2024-11-01 12:54:13 +00:00
Mayank Sinha
83a85189c7
Move normalize header functions from _utils.py to _models.py (#3382) 2024-10-30 17:12:21 +00:00
Tom Christie
6622553979
Cleanup Request method parameter. (#3378) 2024-10-29 15:31:31 +00:00
Bin Liu
12be5c44ca
add socks5h proxy support (#3178)
Signed-off-by: bin liu <liubin0329@gmail.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-10-29 14:10:33 +00:00
Joe Marshall
e9cabc8e1d
made dependencies on certifi and httpcore only load when required (#3377)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-10-29 13:18:39 +00:00
Tom Christie
eeb5e3c2a3
Cleanup unneccessary test case (#3375) 2024-10-28 17:38:33 +00:00
Tom Christie
5dda2aa306
Just use default safe=... characters for urlescape (#3376) 2024-10-28 17:38:16 +00:00
Tom Christie
5440381553
Update CHANGELOG.md (#3374) 2024-10-28 16:23:45 +00:00
Tom Christie
ba2e51215e
Review urlescape percent-safe set, and use + behavior for form spaces. (#3373) 2024-10-28 16:19:59 +00:00
Tom Christie
d293374b66
Review URL percent escaping sets, from whatwg. (#3371) 2024-10-28 15:06:10 +00:00
Tom Christie
489fef48ba
Update CHANGELOG.md (#3372) 2024-10-28 14:43:24 +00:00
BERRADA-Omar
9fd6f0ca66
Ensure JSON representation is compact. #3363 (#3367)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-10-28 14:40:22 +00:00
Tom Christie
8e36f2bc68
Introduce new SSLContext API & escalate deprecations. (#3319)
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
2024-10-28 14:30:08 +00:00
Tom Christie
3f76571d34
Concise URL instantiation. (#3364) 2024-10-25 14:27:54 +01:00
Colin Bounouar
6f9b50990d
typo: Reading a response expose response text, not request text (#3359) 2024-10-23 20:06:45 +01:00
dependabot[bot]
1bf1fc0ea8
Bump the python-packages group with 5 updates (#3329)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-01 18:05:20 +01:00
T-256
95a9527ed6
Add httpx-ws to third party packages. (#3325)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-09-27 16:41:16 +01:00
T-256
3849e1518f
Add httpx-socks to third party packages. (#3326) 2024-09-27 16:36:34 +01:00
Polina Beskorovainaia
49d74a2e7f
Clarified error when header value is None (#3312)
Co-authored-by: Zanie Blue <contact@zanie.dev>
2024-09-26 18:01:47 +01:00
Tom Christie
2e01aa0075
Enable TestSuite for PRs to version branches. (#3318) 2024-09-24 17:21:56 +01:00
Tom Christie
f06171fd5a
Revert "Removed leading $ from cli code blocks" (#3192)
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2024-09-23 00:16:32 +04:00
Tom Christie
d4961b9f8e
Add speakeasy sponsorship (#3305) 2024-09-17 11:31:15 +01:00
dependabot[bot]
0aa20e449e
Bump cryptography from 43.0.0 to 43.0.1 (#3295)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-04 13:24:25 +01:00
dependabot[bot]
d46fa57a6a
Bump the python-packages group across 1 directory with 8 updates (#3292) 2024-09-01 18:45:24 +01:00
Tom Christie
609df7ecc0
Reintroduce URLTypes. (#3288) 2024-08-27 13:52:05 +01:00
Tom Christie
1d6b663433
Update CHANGELOG for 0.27.1 release date. (#3285) 2024-08-27 12:27:08 +01:00
Michiel W. Beijen
1bf1ba5124
Version 0.27.1 (#3275) 2024-08-22 16:03:23 +01:00
Tom Christie
7c0cda153d
Improve InvalidURL error message. (#3250) 2024-07-26 09:36:03 +01:00
dependabot[bot]
beb501fc28
Bump the python-packages group across 1 directory with 8 updates (#3247)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-07-23 15:54:34 +01:00
Tom Christie
359f77d4f6
Clean up URL signature. (#3245) 2024-07-23 15:46:47 +01:00
Tom Christie
b351a44fb6
Update requirements.txt (#3246) 2024-07-23 15:43:47 +01:00
Tom Christie
db9072f998
Add URL parsing tests from WHATWG (#3188)
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2024-06-13 14:46:36 +01:00
Michael Feil
92e9dfb399
Update asgi.py docstring (#3210)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-06-06 16:36:07 +01:00
dependabot[bot]
e186ecc9f8
Bump the python-packages group with 8 updates (#3213)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 11:38:14 +01:00
Tom Christie
37593c1952
Fast path returns for normalize_path cases (#3189)
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2024-05-17 18:25:38 +01:00
manav-a
88a81c5d31
[fix] Use proxy ssl context consistently (#3175)
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2024-05-10 06:42:50 -04:00
Shiny
fa6dac8383
Removed leading $ from cli code blocks (#3174)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-05-05 17:24:16 +01:00
Tom Christie
a7092af2fd
Resolve queryparam quoting (#3187) 2024-05-03 01:09:08 +01:00
Kien Dang
be56b74735
Fix doc links for making requests directly to WSGI/ASGI apps (#3186) 2024-05-02 11:07:09 +01:00
dependabot[bot]
2f5ae50726
Bump the python-packages group with 6 updates (#3185) 2024-05-01 17:56:17 +01:00
Michiel W. Beijen
4b85e6c389
Docs: fix small typos in Extensions doc (#3138)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-04-12 07:11:12 +01:00
dependabot[bot]
7354ed70ce
Bump the python-packages group with 8 updates (#3156) 2024-04-09 21:38:43 +01:00
Hugo Cachitas
5bb2ea0f4e
Update URL.__init__ signature (#3159) 2024-04-06 13:55:26 +02:00
Tom Christie
45bb65bba1
Document 'target' extension (#3160) 2024-04-06 08:30:16 +02:00
Michiel W. Beijen
392dbe45f0
Add support for zstd decoding (#3139)
This adds support for zstd decoding using the python package zstandard.
This is similar to how it is implemented in urllib3. I also chose the
optional installation option httpx[zstd] to mimic the same option in
urllib3.

zstd decoding is similar to brotli, but in benchmarks it is supposed to
be even faster. The zstd compression is described in RFC 8878.

See https://github.com/encode/httpx/discussions/1986

Co-authored-by: Kamil Monicz <kamil@monicz.dev>
2024-03-21 10:17:15 +00:00
dependabot[bot]
7df47ce4d9
Bump the python-packages group with 8 updates (#3129)
Bumps the python-packages group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [mkdocs-material](https://github.com/squidfunk/mkdocs-material) | `9.5.6` | `9.5.12` |
| [build](https://github.com/pypa/build) | `1.0.3` | `1.1.1` |
| [twine](https://github.com/pypa/twine) | `4.0.2` | `5.0.0` |
| [coverage[toml]](https://github.com/nedbat/coveragepy) | `7.4.1` | `7.4.3` |
| [cryptography](https://github.com/pyca/cryptography) | `42.0.4` | `42.0.5` |
| [pytest](https://github.com/pytest-dev/pytest) | `8.0.0` | `8.0.2` |
| [ruff](https://github.com/astral-sh/ruff) | `0.1.15` | `0.3.0` |
| [uvicorn](https://github.com/encode/uvicorn) | `0.27.0.post1` | `0.27.1` |


Updates `mkdocs-material` from 9.5.6 to 9.5.12
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.6...9.5.12)

Updates `build` from 1.0.3 to 1.1.1
- [Release notes](https://github.com/pypa/build/releases)
- [Changelog](https://github.com/pypa/build/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/build/compare/1.0.3...1.1.1)

Updates `twine` from 4.0.2 to 5.0.0
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/4.0.2...5.0.0)

Updates `coverage[toml]` from 7.4.1 to 7.4.3
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.4.1...7.4.3)

Updates `cryptography` from 42.0.4 to 42.0.5
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.4...42.0.5)

Updates `pytest` from 8.0.0 to 8.0.2
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.0.0...8.0.2)

Updates `ruff` from 0.1.15 to 0.3.0
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.15...v0.3.0)

Updates `uvicorn` from 0.27.0.post1 to 0.27.1
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.27.0.post1...0.27.1)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-packages
- dependency-name: build
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-packages
- dependency-name: twine
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: python-packages
- dependency-name: coverage[toml]
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-packages
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-packages
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-packages
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-packages
- dependency-name: uvicorn
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-packages
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-03-01 20:05:55 +00:00
T-256
0006ed0547
format (#3131)
Co-authored-by: T-256 <Tester@test.com>
2024-03-01 19:49:23 +00:00
Kar Petrosyan
f3eb3c90fd
Keep clients in sync (#3120)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-02-29 13:40:07 +00:00
Tom Christie
7e10342c2a
Delete README_chinese.md (#3122)
Discussed in https://github.com/encode/httpx/discussions/3024

Having translated versions for our users is friendly, but we're not doing this in a consistent way.
2024-02-29 04:42:17 -07:00
Nick Cameron
4941b40cbb
Fix broken links in docs/contributing.md and CHANGELOG.md (#3124) 2024-02-29 11:11:43 +00:00
Nick Cameron
6045186f7d
Update /advanced/#<anchor> links -> /advanced/clients/#<anchor> (#3123) 2024-02-28 18:13:23 +00:00
Alex
6d852d319a
Fix client.send() timeout new Request instance (#3116) 2024-02-26 16:36:58 +00:00
akgnah
df5345140e
fix docs basic authentication typo (#3112)
Signed-off-by: akgnah <1024@setq.me>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-02-23 14:33:15 +00:00
T-256
fc84f7f6eb
test same_origin via public api (#3062)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-02-23 14:16:03 +00:00
T-256
e745060c75
test is_https_redirect via public api (#3064)
* test `is_https_redirect` via public api

* Update tests/test_utils.py
2024-02-23 14:11:43 +00:00
Jon Finerty
4de13707ee
Use more permissible types in ASGIApp (#3109)
* Use the type.MutableMapping instead of Dict

MutableMapping is a slightly more permissible type (allowing the previous Dict type) but matches up to Starlettes tpyes

* Update CHANGELOG.md

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-02-23 13:36:45 +00:00
Kar Petrosyan
87713d2172
Define and expose the API from the same place (#3106)
* Tidy up imports

* Update tests/test_exported_members.py

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-02-23 12:30:05 +00:00
dependabot[bot]
77cb36f181
Bump cryptography from 42.0.2 to 42.0.4 (#3107)
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.2 to 42.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.2...42.0.4)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-22 14:52:20 +00:00
Tom Christie
326b9431c7
Version 0.27.0 (#3095)
* Version 0.27.0

* Update CHANGELOG.md (#3097)

wrong year I think? I'm new to github so idk if I'm doing this right

Co-authored-by: ReadyRainFor <119354484+ReadyRainFor@users.noreply.github.com>

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Rain <119354484+Rainkenstein@users.noreply.github.com>
Co-authored-by: ReadyRainFor <119354484+ReadyRainFor@users.noreply.github.com>
2024-02-21 13:06:19 +00:00
Tom Christie
3faa4a8f2e
Improve 'Custom transports' docs (#3081) 2024-02-14 11:14:02 +00:00
Tom Christie
c51af4ba52
Extensions docs (#3080)
* Deprecate app=... in favour of explicit WSGITransport/ASGITransport

* Linting

* Linting

* Update WSGITransport and ASGITransport docs

* Deprecate app

* Drop deprecation tests

* Add CHANGELOG

* Deprecate 'app=...' shortcut, rather than removing it.

* Update CHANGELOG

* Fix test_asgi.test_deprecated_shortcut

* Extensions docs

* Include 'extensions' in docs index

* Update docs/advanced/extensions.md

Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>

---------

Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2024-02-06 11:40:35 +01:00
Tom Christie
cabd1c095e
Deprecate app=... in favor of explicit WSGITransport/ASGITransport. (#3050)
* Deprecate app=... in favour of explicit WSGITransport/ASGITransport

* Linting

* Linting

* Update WSGITransport and ASGITransport docs

* Deprecate app

* Drop deprecation tests

* Add CHANGELOG

* Deprecate 'app=...' shortcut, rather than removing it.

* Update CHANGELOG

* Fix test_asgi.test_deprecated_shortcut
2024-02-02 13:29:41 +00:00
dependabot[bot]
6f461522a5
Bump the python-packages group with 6 updates (#3077)
Bumps the python-packages group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [mkdocs-material](https://github.com/squidfunk/mkdocs-material) | `9.5.3` | `9.5.6` |
| [coverage[toml]](https://github.com/nedbat/coveragepy) | `7.4.0` | `7.4.1` |
| [cryptography](https://github.com/pyca/cryptography) | `41.0.7` | `42.0.2` |
| [pytest](https://github.com/pytest-dev/pytest) | `7.4.4` | `8.0.0` |
| [ruff](https://github.com/astral-sh/ruff) | `0.1.13` | `0.1.15` |
| [uvicorn](https://github.com/encode/uvicorn) | `0.25.0` | `0.27.0.post1` |


Updates `mkdocs-material` from 9.5.3 to 9.5.6
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.3...9.5.6)

Updates `coverage[toml]` from 7.4.0 to 7.4.1
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.4.0...7.4.1)

Updates `cryptography` from 41.0.7 to 42.0.2
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.7...42.0.2)

Updates `pytest` from 7.4.4 to 8.0.0
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.4.4...8.0.0)

Updates `ruff` from 0.1.13 to 0.1.15
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.13...v0.1.15)

Updates `uvicorn` from 0.25.0 to 0.27.0.post1
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.25.0...0.27.0.post1)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-packages
- dependency-name: coverage[toml]
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-packages
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: python-packages
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: python-packages
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-packages
- dependency-name: uvicorn
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-packages
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-01 16:19:47 +00:00
Richie B2B
37a2901af3
Mention NO_PROXY environment variable on Advanced Usage page (#3066)
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2024-01-30 11:01:56 +04:00
Kar Petrosyan
371b6e946c
Use __future__.annotations (#3068)
* Switch to new typing style

* lint
2024-01-24 14:30:22 +00:00
T-256
4f6edf36e9
test parse_header_links via public api (#3061)
* test `parse_header_links` via public api

* add no-link test

* Update tests/test_utils.py

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-01-16 10:25:02 +00:00
T-256
c7cd6aa5bd
test obfuscate_sensitive_headers via public api (#3063) 2024-01-16 09:53:23 +00:00
Tom Christie
15f925336c
Drop outdated section (#3057) 2024-01-15 13:01:04 +00:00
Nyakku Shigure
d76607b112
Adding an indent to fix wrong rendering in warning block (#3056) 2024-01-15 12:30:09 +00:00
Kar Petrosyan
73e688875a
Fix sections references (#3058) 2024-01-15 11:15:31 +00:00
dependabot[bot]
419d3a9d80
Bump the python-packages group with 3 updates (#3055)
Bumps the python-packages group with 3 updates: [ruff](https://github.com/astral-sh/ruff), [trio](https://github.com/python-trio/trio) and [uvicorn](https://github.com/encode/uvicorn).


Updates `ruff` from 0.1.9 to 0.1.13
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.9...v0.1.13)

Updates `trio` from 0.22.2 to 0.24.0
- [Release notes](https://github.com/python-trio/trio/releases)
- [Commits](https://github.com/python-trio/trio/compare/v0.22.2...v0.24.0)

Updates `uvicorn` from 0.24.0.post1 to 0.25.0
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.24.0.post1...0.25.0)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: python-packages
- dependency-name: trio
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-packages
- dependency-name: uvicorn
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: python-packages
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-01-15 10:04:09 +00:00
Tom Christie
8cd952c88f
Docs restructuring. (#3049)
* Tweak docs layout

* Move client docs into folder

* Add clients/authentication section

* Client authentication docs

* Fix authentication example

* SSL Context

* Timeouts

* Event hooks

* Proxies, Transports

* Text encodings

* Resource limits

* 'Clients' -> 'Advanced'

* 'Clients' -> 'Advanced'

* Add client docs

---------

Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2024-01-15 09:48:56 +00:00
Marcelo Trylesinski
ab720d3258
Group dependencies on dependabot updates (#3054) 2024-01-12 11:48:03 -07:00
Tereza Tomcova
99cba6ac64
Fix RFC 2069 mode digest authentication (#3045)
* Fix RFC 2069 mode digest authentication

* Update CHANGELOG.md
2024-01-10 10:08:42 +00:00
Kar Petrosyan
ca51b4532a
Keep clients in sync (#3042)
* Keep clients in sync

* Update httpx/_client.py

* Update httpx/_client.py
2024-01-08 15:09:14 +04:00
Kar Petrosyan
c6907c2203
Remove unused type: ignore (#3038)
* Remove unused type: ignore

* Bump mypy version

* Revert "Bump mypy version"

This reverts commit 55b44b5d2f.

* Bump mypy

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2024-01-04 15:03:09 +00:00
dependabot[bot]
ebc1393c5c
Bump pytest from 7.4.3 to 7.4.4 (#3032)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.3 to 7.4.4.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.4.3...7.4.4)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2024-01-03 11:18:02 +04:00
dependabot[bot]
4ddff16bbe
Bump coverage[toml] from 7.3.0 to 7.4.0 (#3034)
Bumps [coverage[toml]](https://github.com/nedbat/coveragepy) from 7.3.0 to 7.4.0.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.3.0...7.4.0)

---
updated-dependencies:
- dependency-name: coverage[toml]
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2024-01-03 09:44:26 +04:00
dependabot[bot]
f1ed746308
Bump actions/setup-python from 4 to 5 (#3036)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2024-01-03 09:36:16 +04:00
dependabot[bot]
ea3071642d
Bump ruff from 0.1.6 to 0.1.9 (#3031)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.6 to 0.1.9.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.6...v0.1.9)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2024-01-03 09:14:26 +04:00
dependabot[bot]
b871b4b8b2
Bump mkdocs-material from 9.4.14 to 9.5.3 (#3035)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.4.14 to 9.5.3.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.4.14...9.5.3)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-03 09:11:45 +04:00
Tom Christie
dd5304d3eb
Tidy up import (#3020)
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2023-12-28 13:10:37 +00:00
Kar Petrosyan
1a660147ed
Add missing argument (#3023)
* Add missing argument

* chaneglog

* changelog
2023-12-28 16:50:43 +04:00
Marcel Telka
1d526a0180
types-certifi and types-chardet are no longer needed (#3015) 2023-12-21 10:51:11 +00:00
Kar Petrosyan
08eff926a6
Version 0.26.0 (#3009)
* Version 0.26.0

* Update changelog

* Update CHANGELOG.md

* Add `Deprecated` section
2023-12-20 14:52:22 +04:00
Kar Petrosyan
b4b27ff677
Remove unused curio check (#3010) 2023-12-19 08:53:30 +04:00
Tom Christie
a11fc3849b
Cleanup URL percent-encoding behavior. (#2990)
* Replace path_query_fragment encoding tests

* Remove replaced test cases

* Fix test case to use correct hex sequence for 'abc'

* Fix 'quote' behaviour so we don't double-escape.

* Add '/' to safe chars in query strings

* Update docstring

* Linting

* Update outdated comment.

* Revert unrelated change

---------

Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2023-12-15 11:35:16 +00:00
Kar Petrosyan
3b9060ee11
Fix environment proxies (#2741)
* Add red test

* Make the test pass

* Lint

* chanelog

---------

Co-authored-by: Karen Petrosyan <92274156+karosis88@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-12-14 14:04:04 +00:00
James Braza
2318fd822c
Enabling ruff C416 (#3001)
* Enabled C416 in ruff

* Ran ruff on all files

* Ran ruff format

* Update pyproject.toml

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-12-13 11:30:39 +00:00
Tom Christie
2c51edd0c0
Update CHANGELOG.md (#3000) 2023-12-12 13:44:26 +00:00
James Braza
1e11096473
Fixed iter_text adding an empty string (#2998) 2023-12-11 22:34:25 +00:00
Tom Christie
90538a3b46
Ensure that ASGI 'raw_path' does not include query component of URL. (#2999) 2023-12-11 15:45:20 +00:00
Kar Petrosyan
f8981f3d12
Add the 'proxy' parameter and deprecate 'proxies'. (#2879)
* Add the proxy parameter and deprecate proxies

* Make the Client.proxy and HTTPTransport.proxy types the same

* Update httpx/_transports/default.py

Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com>

* Update httpx/_transports/default.py

Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com>

* Drop unneeded noqa

* Changelog

* update documentation

* Allow None in mounts

* typos

* Update httpx/_types.py

* Changes proxies to proxy in CLI app

* Add proxy to request function

* Update CHANGELOG.md

Co-authored-by: Tom Christie <tom@tomchristie.com>

* Update docs/troubleshooting.md

Co-authored-by: Tom Christie <tom@tomchristie.com>

* Update docs/troubleshooting.md

Co-authored-by: Tom Christie <tom@tomchristie.com>

* Lint

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com>
2023-12-11 17:55:52 +04:00
Tom Christie
b471f01d66
Allow URLs where username or password contains unescaped '@'. (#2986)
* Add test cases for userinfo in URL

* Resolve failing test cases

* Update CHANGELOG.md

* Update CHANGELOG.md
2023-12-07 10:08:14 +00:00
Tom Christie
5b5f6d8e17
Moving test cases into 'test_url.py' (#2982)
* Moving test cases into 'test_url.py'

* Move test_url to test_basic_url

* Linting

* Move TypeError test case. Move basic httpx.URL cases.

* Linting

* Merge invalid URL cases

* Move percent encoding test cases

* Move remaining test cases

* Linting

* Add missing test cases
2023-12-05 15:36:05 +00:00
Tom Christie
724eced022
Reorganise tests in 'test_url.py' (#2981)
* Reorganise tests in 'test_url.py'

* Linting
2023-12-05 13:24:34 +00:00
dependabot[bot]
9ef08c7949
Bump uvicorn from 0.22.0 to 0.24.0.post1 (#2972)
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.22.0 to 0.24.0.post1.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.22.0...0.24.0.post1)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2023-12-02 22:20:07 -08:00
dependabot[bot]
266761d8d9
Bump mkdocs-material from 9.4.7 to 9.4.14 (#2973)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.4.7 to 9.4.14.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.4.7...9.4.14)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2023-12-02 22:17:24 -08:00
dependabot[bot]
fe5954c98f
Bump trio-typing from 0.9.0 to 0.10.0 (#2970)
Bumps [trio-typing](https://github.com/python-trio/trio-typing) from 0.9.0 to 0.10.0.
- [Commits](https://github.com/python-trio/trio-typing/compare/v0.9.0...v0.10.0)

---
updated-dependencies:
- dependency-name: trio-typing
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-02 09:42:50 +04:00
dependabot[bot]
0265d95faa
Bump cryptography from 41.0.6 to 41.0.7 (#2971)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.6 to 41.0.7.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.6...41.0.7)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 13:34:15 -06:00
dependabot[bot]
d4b70fe895
Bump ruff from 0.1.3 to 0.1.6 (#2974)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.3 to 0.1.6.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.1.3...v0.1.6)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-01 13:31:23 -06:00
T-256
fd60b1815c
Ruff linter: Use the default line-length (#2922)
Co-authored-by: Tester <Tester@test.com>
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2023-11-29 13:28:31 +04:00
dependabot[bot]
90d71e63e0
Bump cryptography from 41.0.4 to 41.0.6 (#2965)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.4 to 41.0.6.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.4...41.0.6)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 22:32:50 -06:00
Kar Petrosyan
cc206cf2da
Version 0.25.2 (#2957)
* Version 0.25.2

* Update CHANGELOG.md
2023-11-24 16:33:18 +04:00
Petr Belskiy
87f39f12c9
add missing type hints to __init__(...) (#2938)
* add missing type hints to __init__

https://peps.python.org/pep-0484/

* add info to changelog

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-11-17 17:58:51 +04:00
Kar Petrosyan
c51e0466be
Add missing changelog section (#2943) 2023-11-17 12:48:27 +00:00
Marcel Telka
497b315fc7
Add tests and requirements.txt to sdist (#2927)
* Add tests and requirements.txt to sdist

* Update pyproject.toml

Co-authored-by: Tom Christie <tom@tomchristie.com>

---------

Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-11-14 12:17:27 +04:00
Wenceslas Sanchez
89cbd3c942
📌 pin httpcore==1.* (#2937)
* 📌 set httpcore>=1.0.0

* 📌 set httpcore==1.*
2023-11-14 11:51:48 +04:00
Michał Górny
f653b2f0cf
Inline Brotli samples in tests (#2935)
Inline the compressed Brotli samples in tests to make them independent
of Brotli implementation.  This makes it possible to run the test suite
both against Brotli and brotlicffi.

Fixes #2906
2023-11-10 15:07:05 +00:00
Tom Christie
fbe35add82
Tidy up headers in CHANGELOG.md (#2925) 2023-11-03 14:28:37 +00:00
dependabot[bot]
c19728ca39
Bump build from 0.10.0 to 1.0.3 (#2913)
Bumps [build](https://github.com/pypa/build) from 0.10.0 to 1.0.3.
- [Release notes](https://github.com/pypa/build/releases)
- [Changelog](https://github.com/pypa/build/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/build/compare/0.10.0...1.0.3)

---
updated-dependencies:
- dependency-name: build
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-11-03 08:55:10 -05:00
Tom Christie
b07d4e8ce4
Version 0.25.1 (#2923) 2023-11-03 13:18:54 +00:00
Paul Schreiber
280a89a4d1
Support newer versions of httpcore (#2885)
* Support newer versions of httpcore

httpcore 1.0.0 was release October 6, 2023.

* Update pyproject.toml

* Update pyproject.toml

Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>

* Update pyproject.toml

Co-authored-by: Tom Christie <tom@tomchristie.com>

* Update CHANGELOG.md

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2023-11-02 14:41:25 +00:00
Kar Petrosyan
1b7f39eb44
Use ruff format (#2901) 2023-11-02 12:48:53 +01:00
dependabot[bot]
05937f4130
Bump trio-typing from 0.8.0 to 0.9.0 (#2914)
Bumps [trio-typing](https://github.com/python-trio/trio-typing) from 0.8.0 to 0.9.0.
- [Commits](https://github.com/python-trio/trio-typing/compare/v0.8.0...v0.9.0)

---
updated-dependencies:
- dependency-name: trio-typing
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-01 15:57:15 -05:00
dependabot[bot]
aea487059b
Bump mkdocs-material from 9.4.2 to 9.4.7 (#2915)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.4.2 to 9.4.7.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.4.2...9.4.7)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-01 20:27:23 +04:00
dependabot[bot]
2cb3252228
Bump pytest from 7.4.2 to 7.4.3 (#2917)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.2 to 7.4.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.4.2...7.4.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-01 11:21:17 -05:00
Tom Christie
1d73150c1f
Cleanup response.json() method (#2911)
Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2023-10-31 10:10:16 +00:00
Kar Petrosyan
5f2d62096a
Fix third party package documentation link (#2902)
* Fix doc link

* Update docs/third_party_packages.md
2023-10-31 09:57:34 +03:00
Tom Christie
ad06741d1e
Lazily import 'netrc' module (#2910) 2023-10-30 20:07:42 +00:00
Tom Christie
9751f76186
Drop unneccessary binascii import (#2909)
* Drop unneccessary binascii import

* Update httpx/_multipart.py

Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com>

* boundary is 'bytes' not 'str'

---------

Co-authored-by: T-256 <132141463+T-256@users.noreply.github.com>
2023-10-30 19:13:47 +00:00
Mahmoud
31a7bb381a
Delete js folder and remove extra_javascript (#2899)
* Delete js folder and remove extra_javascript

* Update mkdocs.yml

---------

Co-authored-by: Kar Petrosyan <92274156+karpetrosyan@users.noreply.github.com>
2023-10-25 10:29:20 +03:00
robinaly
e63b6594f2
Fix encode host (#2886)
* Fix requiring dot literal rather than any character in IPv4

* Add check to prevent future errors
2023-10-10 12:03:47 +01:00
dependabot[bot]
3ba5fe0d7a
Bump mkdocs-material from 9.2.6 to 9.4.2 (#2872)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.2.6 to 9.4.2.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.2.6...9.4.2)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-10-02 14:35:16 +01:00
dependabot[bot]
05b8e32844
Bump pytest from 7.4.0 to 7.4.2 (#2871)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.4.0 to 7.4.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.4.0...7.4.2)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zanie Blue <contact@zanie.dev>
2023-10-02 13:39:05 +01:00
dependabot[bot]
8dc2fb3e33
Bump mkdocs from 1.5.2 to 1.5.3 (#2869)
Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.5.2 to 1.5.3.
- [Release notes](https://github.com/mkdocs/mkdocs/releases)
- [Commits](https://github.com/mkdocs/mkdocs/compare/1.5.2...1.5.3)

---
updated-dependencies:
- dependency-name: mkdocs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-01 23:34:13 -05:00
dependabot[bot]
e63cec5492
Bump ruff from 0.0.286 to 0.0.291 (#2870)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.286 to 0.0.291.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.0.286...v0.0.291)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-01 22:26:46 -06:00
dependabot[bot]
47fe956f74
Bump black from 23.7.0 to 23.9.1 (#2873)
Bumps [black](https://github.com/psf/black) from 23.7.0 to 23.9.1.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/23.7.0...23.9.1)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-01 22:15:01 -06:00
Kar Petrosyan
c684e9f3aa
Hishel as an alternative for "cachecontrol" and "requests-cache". (#2866)
* Add Caching section in compatibility

* typo
2023-09-28 12:30:49 -04:00
dependabot[bot]
5d32e4c1bf
Bump cryptography from 41.0.3 to 41.0.4 (#2859)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.3 to 41.0.4.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.3...41.0.4)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-21 22:53:13 -06:00
Hugo van Kemenade
7c9db49f0c
Add support for Python 3.12 (#2854)
* Add support for Python 3.12

* Bump GitHub Actions

* Remove redundant version checks

* Add CHANGELOG entry
2023-09-21 15:35:56 +01:00
xzmeng
59df8190a4
Raise ValueError on Response.encoding being set after Response.text has been accessed (#2852)
* Raise ValueError on change encoding

* Always raise ValueError for simplicity

* update CHANGELOG.md
2023-09-19 08:54:32 +01:00
Y.D.X
e4241c6155
Drop private imports from test_proxies.py (#2850) 2023-09-16 21:58:56 +01:00
Musale Martin
88e8431437
Add cookies to the retried request when performing digest authentication. (#2846)
* Add cookies from the response to the retried request

* Conditionally add cookies from the response

* Fix failing auth module tests

* Fix linting error

* Add tests to check set cookies from server
2023-09-15 10:52:11 +01:00
Trim21
c3585a5ccf
Version 0.25.0 (#2801)
* bump

* Update CHANGELOG.md

* Update CHANGELOG.md

Co-authored-by: Kar Petrosyan <92274156+karosis88@users.noreply.github.com>

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Kar Petrosyan <92274156+karosis88@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-09-11 11:13:24 +01:00
Kar Petrosyan
a54ecccd5b
HTTPS proxies support (#2845)
* Add ssl_context argument to Proxy class

* Changelog
2023-09-11 10:56:01 +01:00
Kar Petrosyan
adbcd0e0e7
Change extensions type (#2803)
* Change extensions type

* Update changelog

* install httpcore from the git

* Revert "install httpcore from the git"

This reverts commit 1813c6aff1.

* bump httpcore version

* fix requirements

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-09-11 09:29:22 +03:00
Kalle Møller
e874351f04
Update _models.py (#2840)
To remove the unknown dict type info

(variable) extensions: ResponseExtensions | dict[Unknown, Unknown]

Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-09-07 10:24:49 +01:00
dependabot[bot]
7ecd828237
Bump coverage[toml] from 7.2.7 to 7.3.0 (#2839)
Bumps [coverage[toml]](https://github.com/nedbat/coveragepy) from 7.2.7 to 7.3.0.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.2.7...7.3.0)

---
updated-dependencies:
- dependency-name: coverage[toml]
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-09-07 10:19:54 +01:00
dependabot[bot]
ec4aa5e4ce
Bump mkdocs-material from 9.1.17 to 9.2.6 (#2835) 2023-09-02 08:10:53 +01:00
dependabot[bot]
1703da8706
Bump chardet from 5.1.0 to 5.2.0 (#2837)
Bumps [chardet](https://github.com/chardet/chardet) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/chardet/chardet/releases)
- [Commits](https://github.com/chardet/chardet/compare/5.1.0...5.2.0)

---
updated-dependencies:
- dependency-name: chardet
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-01 14:12:56 -05:00
dependabot[bot]
b95ef3e489
Bump mypy from 1.4.1 to 1.5.1 (#2838)
Bumps [mypy](https://github.com/python/mypy) from 1.4.1 to 1.5.1.
- [Commits](https://github.com/python/mypy/compare/v1.4.1...v1.5.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Zanie Blue <contact@zanie.dev>
2023-09-01 13:55:09 -05:00
dependabot[bot]
3a7f6d1a5d
Bump ruff from 0.0.275 to 0.0.286 (#2836)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.275 to 0.0.286.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.0.275...v0.0.286)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-01 13:48:11 -05:00
xzmeng
053bc57c37
fix a typo in docs/logging.md (#2830) 2023-08-29 11:27:23 +02:00
dependabot[bot]
0f61aa58d6
Bump mkdocs from 1.4.3 to 1.5.2 (#2818)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-20 08:44:32 +02:00
dependabot[bot]
c20bacbf76
Bump trio from 0.22.0 to 0.22.2 (#2807)
Bumps [trio](https://github.com/python-trio/trio) from 0.22.0 to 0.22.2.
- [Release notes](https://github.com/python-trio/trio/releases)
- [Commits](https://github.com/python-trio/trio/compare/v0.22.0...v0.22.2)

---
updated-dependencies:
- dependency-name: trio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-14 09:01:00 -05:00
dependabot[bot]
304433ebaa
Bump black from 23.3.0 to 23.7.0 (#2805)
Bumps [black](https://github.com/psf/black) from 23.3.0 to 23.7.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/23.3.0...23.7.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-14 08:52:17 -05:00
dependabot[bot]
534b47ebf2
Bump trustme from 1.0.0 to 1.1.0 (#2804)
Bumps [trustme](https://github.com/python-trio/trustme) from 1.0.0 to 1.1.0.
- [Commits](https://github.com/python-trio/trustme/compare/v1.0.0...v1.1.0)

---
updated-dependencies:
- dependency-name: trustme
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-14 14:36:38 +02:00
Iurii Pliner
b40c04dfa6
Drop support for Python 3.7 (#2813)
* Drop Python 3.7 support

* Fix lint

* Changelog
2023-08-09 10:02:28 +01:00
Trim21
76c9cb65f2
remove unnecessary black argument (#2817) 2023-08-07 18:47:48 +02:00
dependabot[bot]
e99e2948e6
Bump cryptography from 41.0.2 to 41.0.3 (#2809)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.2 to 41.0.3.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.2...41.0.3)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-02 06:48:15 +01:00
Trim21
9415af643f
Make raise_for_status chainable (#2776)
* merge upstream

* lint

* Update test_async_client.py

* update docs

* add example

* Update docs/quickstart.md

Co-authored-by: Tom Christie <tom@tomchristie.com>

* Update CHANGELOG.md

Co-authored-by: Tom Christie <tom@tomchristie.com>

* Update docs/quickstart.md

Co-authored-by: Tom Christie <tom@tomchristie.com>

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-08-01 10:22:58 +01:00
Kar Petrosyan
55b8669acb
Add Hishel into the Third Party Packages (#2799) 2023-07-31 16:40:10 +01:00
dependabot[bot]
6a1841b924
Bump cryptography from 41.0.0 to 41.0.2 (#2773)
Bumps [cryptography](https://github.com/pyca/cryptography) from 41.0.0 to 41.0.2.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/41.0.0...41.0.2)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-14 22:38:15 -05:00
Zanie
18d7721c38
Use Mozilla documentation instead of httpstatuses.com for HTTP error reference (#2768) 2023-07-13 15:17:07 -05:00
Marcelo Trylesinski
f6866ce388
Remove temporary click version pin (#2771) 2023-07-13 13:02:01 -05:00
Trim21
f115ce4e09
docs: upload progress (#2725)
* upload progress

* typo

* typo

* Update docs/advanced.md

* Update advanced.md

* Update docs/advanced.md

Co-authored-by: Kar Petrosyan <92274156+karosis88@users.noreply.github.com>

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Kar Petrosyan <92274156+karosis88@users.noreply.github.com>
2023-07-13 15:55:41 +03:00
Zanie
2c49a151d2
Pin CI version of click to resolve mypy error (#2769)
* Add upper bound to click version to fix mypy error

* Move pin to `requirements.txt`

* Restore `pyproject.toml`
2023-07-12 09:07:06 -05:00
dependabot[bot]
5b156dca7f
Bump coverage[toml] from 7.2.2 to 7.2.7 (#2752)
Bumps [coverage[toml]](https://github.com/nedbat/coveragepy) from 7.2.2 to 7.2.7.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.2.2...7.2.7)

---
updated-dependencies:
- dependency-name: coverage[toml]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-07-03 10:56:05 +01:00
dependabot[bot]
353fb358eb
Bump mkdocs-material from 9.1.15 to 9.1.17 (#2755)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.15 to 9.1.17.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.15...9.1.17)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 10:18:52 +01:00
dependabot[bot]
9d022c0a88
Bump ruff from 0.0.260 to 0.0.275 (#2753)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.260 to 0.0.275.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.0.260...v0.0.275)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 10:13:56 +01:00
dependabot[bot]
8a6ef6ed14
Bump mypy from 1.3.0 to 1.4.1 (#2754)
Bumps [mypy](https://github.com/python/mypy) from 1.3.0 to 1.4.1.
- [Commits](https://github.com/python/mypy/compare/v1.3.0...v1.4.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 09:56:42 +01:00
dependabot[bot]
354a6fc8dc
Bump pytest from 7.3.1 to 7.4.0 (#2751)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.3.1 to 7.4.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.3.1...7.4.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-03 09:48:29 +01:00
Johnny Lim
2e2949c8ea
Fix sample in quickstart.md (#2747) 2023-06-22 09:44:44 +01:00
Trond Hindenes
6d183a87e1
async recommendations (#2727)
* async recommendations

* better

* Update docs/async.md

Co-authored-by: Tom Christie <tom@tomchristie.com>

* added async recommendation tweak

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-06-15 12:20:28 +03:00
Zanie Adkins
920333ea98
Always encode forward slashes as %2F in query parameters (#2723)
* Always encode forward slashes as `%2F` in query parameters

* Revert inclusion of "%"

This is expected to fail tests due to double escaping

* Update `urlencode`

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-06-09 10:06:56 +01:00
Karen Petrosyan
301b8fb03a
Fix URLPattern examples (#2740)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-06-08 15:32:18 +03:00
dependabot[bot]
b4f66c2cd7
Bump mkdocs-material from 9.1.5 to 9.1.15 (#2729)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.1.5 to 9.1.15.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.1.5...9.1.15)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-06-06 10:24:53 +01:00
dependabot[bot]
1b8187ca67
Bump pytest from 7.2.2 to 7.3.1 (#2731)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.2.2 to 7.3.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.2.2...7.3.1)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-06-06 10:20:00 +01:00
dependabot[bot]
5eb00b77a3
Bump mypy from 1.0.1 to 1.3.0 (#2732)
Bumps [mypy](https://github.com/python/mypy) from 1.0.1 to 1.3.0.
- [Commits](https://github.com/python/mypy/compare/v1.0.1...v1.3.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-06 10:14:50 +01:00
dependabot[bot]
2073ea0046
Bump cryptography from 40.0.2 to 41.0.0 (#2735)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-02 14:33:26 -06:00
dependabot[bot]
dedd37b9ce
Bump mkdocs from 1.4.2 to 1.4.3 (#2730)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-01 10:59:45 -06:00
Karen Petrosyan
733595037a
Add socket_options argument to httpx.HTTPTransport class (#2716)
* Add `socket_options` argument to `httpx.HTTPTransport` and `httpx.AsyncHTTPTransport` classes

* Update changelog

* Fix changelog format

* Set httpcore's minimum version to 0.17.2

* Remove SOCKET_OPTIONS import
2023-05-24 11:42:39 +03:00
Nik
a682f6f1c7
Fix exception suppression in asgi transport (#2669) 2023-05-21 01:17:23 +01:00
Peter Lazorchak
f9abbbbb48
Update changelog with WSGITransport SERVER_PROTOCOL fix (#2711) 2023-05-19 18:18:00 +01:00
Peter Lazorchak
abb994c0c2
Ensure all WSGITransport environs have a SERVER_PROTOCOL (#2708) 2023-05-19 11:38:18 +01:00
epenet
fcf1bc73db
Version 0.24.1 (#2702)
* Version 0.24.1

* Update CHANGELOG.md

* Update CHANGELOG.md
2023-05-18 12:03:21 +01:00
Tom Christie
ee432c0d30
Fix for gen-delims escaping behaviour in path/query/fragment (#2701) 2023-05-09 14:20:12 +01:00
Bartosz Sokorski
df5dbc0558
Move configuration of tools to pyproject.toml (#2686)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-05-03 14:13:40 +01:00
dependabot[bot]
746eaef3b4
Bump trio-typing from 0.7.0 to 0.8.0 (#2688)
Bumps [trio-typing](https://github.com/python-trio/trio-typing) from 0.7.0 to 0.8.0.
- [Release notes](https://github.com/python-trio/trio-typing/releases)
- [Commits](https://github.com/python-trio/trio-typing/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: trio-typing
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 12:15:35 -06:00
dependabot[bot]
919da41dc3
Bump types-chardet from 5.0.4.2 to 5.0.4.5 (#2689)
Bumps [types-chardet](https://github.com/python/typeshed) from 5.0.4.2 to 5.0.4.5.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-chardet
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 12:00:42 -06:00
dependabot[bot]
028b9aa201
Bump trustme from 0.9.0 to 1.0.0 (#2690)
Bumps [trustme](https://github.com/python-trio/trustme) from 0.9.0 to 1.0.0.
- [Release notes](https://github.com/python-trio/trustme/releases)
- [Commits](https://github.com/python-trio/trustme/compare/v0.9.0...v1.0.0)

---
updated-dependencies:
- dependency-name: trustme
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 11:43:47 -06:00
dependabot[bot]
f98268a334
Bump cryptography from 39.0.1 to 40.0.2 (#2692)
Bumps [cryptography](https://github.com/pyca/cryptography) from 39.0.1 to 40.0.2.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/39.0.1...40.0.2)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 11:40:51 -06:00
dependabot[bot]
1250a6fd68
Bump uvicorn from 0.20.0 to 0.22.0 (#2691)
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.20.0 to 0.22.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.20.0...0.22.0)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-01 11:17:31 -06:00
Florimond Manca
859038a9e6
Add httpx-sse to Third Party Packages (#2683) 2023-04-28 08:52:16 +02:00
Amin Alaee
32e25497a3
Fix ruff error and script (#2680) 2023-04-26 09:34:33 +02:00
Leon Kuchenbecker
472597fb6b
More robust check for upload files in binary mode (#2630)
* Fix check for binary mode

* Change order of type checks

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-04-20 12:52:44 +01:00
Tom Christie
26dc39213a
Additional context in InvalidURL exceptions (#2675) 2023-04-20 12:17:44 +01:00
Florimond Manca
cca62060cb
Drop private imports from test_decoders.py (#2570)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-04-19 14:16:29 +01:00
dependabot[bot]
21e8a7d90c
Bump mkdocs-material from 9.0.15 to 9.1.5 (#2640)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.0.15 to 9.1.5.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.0.15...9.1.5)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-04-19 13:48:56 +01:00
Tom Christie
9ae170a936
Fix optional percent encoding behaviour. (#2671)
* Tests for failing optional percent encoding

* Linting

* Fix for optional percent escaping
2023-04-19 13:21:42 +01:00
Jiayun Shen
15d09a3bbc
fix: NO_PROXY should support IPv4, IPv6 and localhost (#2659)
* fix: NO_PROXY supports IPv4, IPv6 and localhost

* add more tests for test_get_environment_proxies

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-04-19 13:14:37 +01:00
Tom Christie
7d7c4f15b8
Update PULL_REQUEST_TEMPLATE.md (#2672) 2023-04-19 13:43:58 +02:00
Marcelo Trylesinski
7455c00327
Update PULL_REQUEST_TEMPLATE.md (#2668) 2023-04-19 10:29:40 +02:00
Alex Prengère
c1cc6b2462
Fixes #2666: None is the default value of file for httpx.NetRCAuth (#2667) 2023-04-18 10:03:01 +01:00
Piotr Staroszczyk
4b5a92e88e
set logging request lines to INFO level for async method also (#2656) 2023-04-12 15:53:11 +01:00
Tom Christie
579a3f2fb8
Version 0.24.0 (#2652)
* Version 0.24.0

* Typo

* Update CHANGELOG.md
2023-04-11 11:00:02 +01:00
Marcelo Trylesinski
daec2bdcdb
Use ruff instead of flake8, autoflake and isort (#2648)
* Use ruff instead of flake8, autoflake and isort

* Update pyproject.toml
2023-04-05 12:37:10 +02:00
dependabot[bot]
ab8177c53a
Bump coverage from 7.2.1 to 7.2.2 (#2641)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.2.1 to 7.2.2.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.2.1...7.2.2)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-05 11:17:42 +01:00
dependabot[bot]
46a5d38bd7
Update httpcore requirement from <0.17.0,>=0.15.0 to >=0.15.0,<0.18.0 (#2642)
Updates the requirements on [httpcore](https://github.com/encode/httpcore) to permit the latest version.
- [Release notes](https://github.com/encode/httpcore/releases)
- [Changelog](https://github.com/encode/httpcore/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/httpcore/compare/0.15.0...0.17.0)

---
updated-dependencies:
- dependency-name: httpcore
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-05 11:05:31 +01:00
dependabot[bot]
70f6ff8c3b
Bump pytest from 7.2.1 to 7.2.2 (#2643)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 11:00:39 +02:00
dependabot[bot]
35a00672dd
Bump black from 22.10.0 to 23.3.0 (#2644)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-02 10:50:53 +02:00
Tom Christie
f1157dbc41
Use standard logging style (#2547)
* Use standard logging style

* Add docs for logging

* Drop out-of-date HTTPX_LOG_LEVEL variable docs
2023-03-20 11:30:11 +00:00
Gianni Tedesco
85c5898d8e
Change LineDecoder to match stdlib splitlines, resulting in significant speed up (#2423)
* Replace quadratic algo in LineDecoder

Leading to enormous speedups when doing things such as
Response(...).iter_lines() as described on issue #2422

* Update httpx/_decoders.py

* Update _decoders.py

Handle text ending in `\r` more gracefully.
Return as much content as possible.

* Update test_decoders.py

* Update _decoders.py

* Update _decoders.py

* Update _decoders.py

* Update httpx/_decoders.py

Co-authored-by: cdeler <serj.krotov@gmail.com>

* Update _decoders.py

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: cdeler <serj.krotov@gmail.com>
2023-03-16 14:29:15 +00:00
Tom Christie
e486fbceea
Update _client.py 2023-03-15 10:11:01 +00:00
Tom Christie
0fc9009b90
Update _client.py 2023-03-15 10:07:09 +00:00
bpoirriez
c5e9c82c90
Add sslcontext to the asynclient docstring (#2609)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-03-13 12:06:21 +00:00
dependabot[bot]
ead8010a57
Bump mkdocs-material from 8.5.11 to 9.0.15 (#2610)
* Bump mkdocs-material from 8.5.11 to 9.0.15

Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.11 to 9.0.15.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Upgrade guide](https://github.com/squidfunk/mkdocs-material/blob/master/docs/upgrade.md)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.11...9.0.15)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump `mkdocs` to minimum version required by `mkdocs-material-9.0.15`

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Adkins <michael@prefect.io>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-03-06 11:32:02 +00:00
dependabot[bot]
978e5bbe9f
Bump types-chardet from 5.0.3 to 5.0.4.2 (#2613)
Bumps [types-chardet](https://github.com/python/typeshed) from 5.0.3 to 5.0.4.2.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-chardet
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-06 11:29:01 +00:00
dependabot[bot]
6779fae5ee
Bump mypy from 0.982 to 1.0.1 (#2611)
* Bump mypy from 0.982 to 1.0.1

Bumps [mypy](https://github.com/python/mypy) from 0.982 to 1.0.1.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.982...v1.0.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Remove unused ignores for mypy-1.0.1

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Adkins <michael@prefect.io>
2023-03-01 11:53:54 -06:00
dependabot[bot]
99a080ea78
Bump pytest from 7.2.0 to 7.2.1 (#2612)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-01 18:26:09 +01:00
dependabot[bot]
988809a9d6
Bump coverage from 6.5.0 to 7.2.1 (#2614)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.5.0 to 7.2.1.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/6.5.0...7.2.1)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-01 18:13:03 +01:00
Florimond Manca
a934c36a85
Drop private imports from test_urlparse.py (#2572)
* Drop private imports from test_urlparse.py

* Coverage

* Drop ._uri_reference
2023-02-15 14:43:19 +00:00
Adrian Garcia Badaracco
f0fd91925b
fix type annotation for MockTransport (#2581)
* fix type annotation for MockTransport

* add type ignore

* better type checks

* better type checks

* add pragma

---------

Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-02-09 16:05:07 +00:00
Florimond Manca
18e0ae45ca
Drop private imports from test_exported_members.py (#2573)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-02-09 16:00:27 +00:00
Florimond Manca
ef06f7d076
Drop private imports in tests/conftest.py (#2569)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-02-09 15:54:53 +00:00
Florimond Manca
78d381fc7d
Drop private imports from test_main.py (#2574)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-02-09 15:50:09 +00:00
Florimond Manca
7488b15226
Drop private imports from test_exceptions.py (#2571) 2023-02-08 19:35:52 -05:00
dependabot[bot]
2f0c291435
Bump cryptography from 38.0.4 to 39.0.1 (#2579)
Bumps [cryptography](https://github.com/pyca/cryptography) from 38.0.4 to 39.0.1.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/38.0.4...39.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-08 13:20:04 +01:00
Paul Schreiber
5747764104
Remove rfc3986 from Chinese readme (#2576)
fix https://github.com/encode/httpx/pull/2252
2023-02-06 18:42:35 +01:00
dependabot[bot]
562b4b40aa
Bump trio from 0.21.0 to 0.22.0 (#2562)
* Bump trio from 0.21.0 to 0.22.0

Bumps [trio](https://github.com/python-trio/trio) from 0.21.0 to 0.22.0.
- [Release notes](https://github.com/python-trio/trio/releases)
- [Commits](https://github.com/python-trio/trio/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: trio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Ignore MultiError deprecation warning, it will disappear with anyio 4

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: florimondmanca <florimond.manca@protonmail.com>
2023-02-04 23:18:36 +01:00
dependabot[bot]
bd00151b25
Bump isort from 5.11.4 to 5.12.0 (#2563)
* Bump isort from 5.11.4 to 5.12.0

Bumps [isort](https://github.com/pycqa/isort) from 5.11.4 to 5.12.0.
- [Release notes](https://github.com/pycqa/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pycqa/isort/compare/5.11.4...5.12.0)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update requirements.txt

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Florimond Manca <florimond.manca@protonmail.com>
2023-02-04 23:06:00 +01:00
dependabot[bot]
2880c0861e
Bump twine from 4.0.1 to 4.0.2 (#2565)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-01 18:27:53 +01:00
dependabot[bot]
64f4523253
Bump build from 0.8.0 to 0.10.0 (#2564)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-01 17:27:46 +01:00
dependabot[bot]
e071ca13a6
Bump flake8-bugbear from 22.7.1 to 23.1.20 (#2561)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-01 17:18:21 +01:00
Tom Christie
59914c7690
Add NetRCAuth() class. (#2535)
* NetRCAuth class

* Add docs for httpx.NetRCAuth()

* Drop failing cross-domain test for NetRCAuth()

* Update tests

* Update httpx/_auth.py

* Add tests for netrc file with no password
2023-01-12 11:27:46 +00:00
Tom Christie
7947b56076
Drop private import of 'encode_request' in test_multipart (#2525) 2023-01-10 11:23:14 +00:00
Tom Christie
a6af45edac
Use '%20' for encoding spaces in query parameters. (#2543)
* Add failing test

* Fix failing test case

* Add urlencode

* Update comment
2023-01-10 11:16:09 +00:00
Tom Christie
57daabf673
Drop rfc3986 requirement. (#2252)
* Drop RawURL

* First pass at adding urlparse

* Update urlparse

* Add urlparse

* Add urlparse

* Unicode non-printables can be valid in IDNA hostnames

* Update _urlparse.py docstring

* Linting

* Trim away ununsed codepaths

* Tweaks for path validation depending on scheme and authority presence

* Minor cleanups

* Minor cleanups

* full_path -> raw_path, forr internal consistency

* Linting fixes

* Drop rfc3986 dependency

* Add test for #1833

* Linting

* Drop 'rfc3986' dependancy from README and docs homepage

Co-authored-by: Thomas Grainger <tagrain@gmail.com>
2023-01-10 10:36:15 +00:00
Michał Górny
7c53d99da8
Bump rich pin to allow version 13 (#2546)
See https://github.com/encode/httpx/discussions/2544
2023-01-05 12:38:37 +00:00
Tom Christie
08a557e3e2
Version 0.23.3 (#2540) 2023-01-04 09:39:43 +00:00
Michael Adkins
bddd774ce0
Revert "Raise TypeError on invalid query params. (#2523)" (#2539)
This reverts commit 4cbf13ece2.
2023-01-04 09:23:32 +00:00
Thomas Grainger
e27d1b8333
replace pytest-asyncio and pytest-trio with anyio (#2512)
* replace pytest-asyncio with anyio

* remove pytest-trio also

* Update setup.cfg

* use anyio.Lock in test_auth

Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-01-02 12:53:30 +00:00
Hynek Schlawack
e4438a3a71
PyPI readme: fix screenshot links & trim changelog (#2522)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-01-02 12:41:45 +00:00
dependabot[bot]
6a98e188c4
Bump chardet from 5.0.0 to 5.1.0 (#2530)
Bumps [chardet](https://github.com/chardet/chardet) from 5.0.0 to 5.1.0.
- [Release notes](https://github.com/chardet/chardet/releases)
- [Commits](https://github.com/chardet/chardet/compare/5.0.0...5.1.0)

---
updated-dependencies:
- dependency-name: chardet
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-01-02 12:36:58 +00:00
dependabot[bot]
c6084c0f63
Bump mkdocs-material from 8.5.5 to 8.5.11 (#2529)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.5.5 to 8.5.11.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.5.5...8.5.11)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-01-02 12:33:30 +00:00
dependabot[bot]
8c9110740d
Bump isort from 5.10.1 to 5.11.4 (#2528)
Bumps [isort](https://github.com/pycqa/isort) from 5.10.1 to 5.11.4.
- [Release notes](https://github.com/pycqa/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pycqa/isort/compare/5.10.1...5.11.4)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2023-01-02 12:29:40 +00:00
Tom Christie
2ab735849a
Version 0.23.2 (#2510)
* Version 0.23.2

* Update CHANGELOG.md
2023-01-02 11:46:22 +00:00
Tom Christie
4cbf13ece2
Raise TypeError on invalid query params. (#2523)
* Raise TypeError on invalid query params

* Fix TypeError

* Update tests/models/test_queryparams.py

Co-authored-by: Michael Adkins <contact@zanie.dev>

* Linting

* Fix exception check

Co-authored-by: Michael Adkins <contact@zanie.dev>
2022-12-30 20:56:48 +00:00
Marcelo Trylesinski
10a3b68a71
Delete setup.py (#2516) 2022-12-29 12:47:24 +01:00
Tom Christie
e5bc1ea533
Update pytest-asyncio (#2511) 2022-12-20 13:46:16 +00:00
Tom Christie
b82fbe2c13
Drop private import of 'format_form_param' from tests (#2500)
* Drop private import of 'format_form_param' from tests

* Drop unused code path
2022-12-12 16:31:29 +00:00
Tom Christie
b97c0594a5
Streaming multipart support (#2382)
* Streaming multipart support

* Update tests for streaming multipary
2022-12-12 16:14:56 +00:00
Tom Christie
af56476a8c
Drop unneccessary private import in tests (#2498) 2022-12-12 10:43:46 +00:00
Tom Christie
a8dd079be7
Raise TypeError if content is passed a 'dict' instance. (#2495) 2022-12-11 19:12:08 +00:00
Tom Christie
563a1031f5
Remove some private imports from test_decoders (#2496) 2022-12-06 18:22:04 +00:00
Tom Christie
71a1589928
Use httpx public API for 'test_content' tests (#2494) 2022-12-06 13:27:05 +00:00
Tom Christie
7985f685ca
Use consistent import style (#2493) 2022-12-06 11:58:30 +00:00
Demetri
40a0da093b
Add back in URL.raw with NamedTuple (#2481)
* add back in URL.raw with NamedTuple

* Update _urls.py

* Update _urls.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-12-02 16:20:05 +00:00
Martijn Pieters
933551c519
Typing: enable disallow_untyped_calls (#2479)
* Typing: enable disallow_untyped_calls

Only the test suite needed adjusting to add type hints.

* Update setup.cfg

Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-12-02 10:47:42 +00:00
Martijn Pieters
884a69a902
Typing: enable strict_equality (#2480)
- ignore mypy when it comes to comparing an IntEnum member with an int
- Correct type hint for `test_wsgi_server_port`'s `expected_server_port`
  parameter.

Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-12-02 08:45:28 +00:00
dependabot[bot]
5098f47444
Bump cryptography from 38.0.3 to 38.0.4 (#2485) 2022-12-01 17:13:10 +00:00
dependabot[bot]
d809a24892
Bump uvicorn from 0.19.0 to 0.20.0 (#2484)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-01 18:04:23 +01:00
dependabot[bot]
192e8d828f
Bump pytest from 7.1.2 to 7.2.0 (#2483)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-01 17:45:12 +01:00
Martijn Pieters
57aa5b08a4
Enable no_implicit_reexport (#2475)
- Add explicit exports for names imported from `_compat`
- Correct imports of `URL` (from `._urls`, not via `._models`)
- Correct import of the uvicorn `Server` class

Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-11-30 13:11:22 +00:00
Martijn Pieters
2d37321842
Typing: enable warn_return_any (#2477) 2022-11-30 13:08:02 +00:00
Michael Oliver
0eb97fc988
Use wsgi types from typeshed (#2478) 2022-11-30 09:48:51 +00:00
Martijn Pieters
049afe5b25
Typing: enable disallow_incomplete_defs (#2476)
The only places mypy reports issues is in the test suite.
2022-11-30 09:04:54 +00:00
Adrian Garcia Badaracco
1ff67ea47c
Typing: use wsgiref.types to validate types and fix issues uncovered (#2467)
* Typing: use wsgiref.types to validate types and fix issues uncovered

- start_response() must return a write(bytes) function, even though this
  is now deprecated. It's fine to be a no-op here.
- sys.exc_info() can return (None, None, None), so make sure to handle that case.

* remove typing_extensions

Co-authored-by: Martijn Pieters <mj@zopatista.com>
2022-11-29 17:55:21 +00:00
rettier
8327e13454
reuse the digest auth state to avoid unnecessary requests (#2463)
* reuse the digest auth challenge to avoid sending twice as many requests

* fix for digest testcase

* ran testing/linting scripts

* codereview changes, removed tomchristie username from all authentication tests

Co-authored-by: Philipp Reitter <p.reitter@accessio.at>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-11-29 17:05:37 +00:00
Adrian Garcia Badaracco
69e13cbc39
Remove and dissallow unused type ignores (#2470)
* Remove and dissallow unused type ignores

* add pragmas

* fix ignore
2022-11-29 16:55:45 +00:00
Adrian Garcia Badaracco
1fc6a52546
Add mypy flags (#2472) 2022-11-29 16:46:00 +00:00
Adrian Garcia Badaracco
1b4e7fbb48
Typing: always fill in generic type parameters (#2468)
* Typing: always fill in generic type parameters

Being explicit about the parameters helps find bugs and makes the library
easier to use for users.

- Tell mypy to disallow generics without parameter values
- Give all generic types parameters values

* fix things that aren't coming in from other commits

* lint

Co-authored-by: Martijn Pieters <mj@zopatista.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-11-29 16:36:03 +00:00
Adrian Garcia Badaracco
16e2830624
use # pragma: no cover instead of # pragma: nocover (#2471)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-11-29 10:23:18 -06:00
Simon K
deb904dd15
remove chunk_size from api docs for iter_lines variants (#2464)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-11-29 15:55:38 +00:00
Adrian Garcia Badaracco
75a13a15aa
Remove typeshed iscoroutine workaround (#2466)
This was resolved in typeshed and newer mypy versions have inherited the fix.

Co-authored-by: Martijn Pieters <mj@zopatista.com>
2022-11-29 02:45:10 -06:00
Tom Christie
1ba3e2ad4c
Switch extensions from Dict to Mapping. (#2465)
* Ignore Mapping -> Dict type error

* Fix type of ResponseExtension

* Switch extensions from Dict to Mapping
2022-11-28 12:29:02 +00:00
Ben Falk
8e5e3b871b
update requests compatibility docs on query and form params (#2461) 2022-11-25 12:05:46 +00:00
Tom Christie
c0e9dba320
Update CHANGELOG.md (#2458) 2022-11-21 13:38:53 +00:00
Tom Christie
883cf8ca6f
Drop multipart requirement from tests (#2456) 2022-11-21 10:29:31 +00:00
dependabot[bot]
cd1d51d597
Bump cryptography from 38.0.1 to 38.0.3 (#2447)
Bumps [cryptography](https://github.com/pyca/cryptography) from 38.0.1 to 38.0.3.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/38.0.1...38.0.3)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-18 12:55:22 +00:00
Tom Christie
f11eff45b7
Version 0.23.1 (#2442)
* Version 0.23.1

* Update dependencies

* Update CHANGELOG

* Update CHANGELOG.md

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>

* Update CHANGELOG.md

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2022-11-18 12:48:32 +00:00
František Nesveda
a2a69e4bf7
Mention default timeout differences from Requests in compatibility docs (#2433)
I was recently migrating a project from Requests to HTTPX, and I stumbled a bit on the default socket timeouts being different between the two, which I haven't seen explicitly mentioned anywhere.

This adds a mention of those differences to the compatibility section, feel free to edit it or move it around as you choose.
2022-11-07 15:25:49 +01:00
Tom Christie
8752e2672c
Drop .read/.aread from SyncByteStream/AsyncByteStream (#2407) 2022-11-07 14:01:29 +00:00
Michael K
9f9deea944
Run tests against Python 3.11 stable (#2420)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-11-07 13:57:48 +00:00
Vincent Fazio
1aea9539bb
Drop cgi module from test_multipart (#2424)
* Use multipart instead of cgi for multipart tests

The cgi module has been deprecated as of python 3.11.

Signed-off-by: Vincent Fazio <vfazio@gmail.com>

* Update setup.cfg

All references to the cgi module have all been removed so there's no
longer a need to silence those deprecation warnings.

The deprecation warning for certifi is resolved as of version 2022.09.24.

Signed-off-by: Vincent Fazio <vfazio@gmail.com>

Signed-off-by: Vincent Fazio <vfazio@gmail.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-11-03 11:29:23 +00:00
dependabot[bot]
db00b9279f
Bump uvicorn from 0.18.3 to 0.19.0 (#2429)
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.18.3 to 0.19.0.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.18.3...0.19.0)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-11-02 11:19:02 +00:00
dependabot[bot]
9175097128
Bump black from 22.8.0 to 22.10.0 (#2428)
Bumps [black](https://github.com/psf/black) from 22.8.0 to 22.10.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/22.8.0...22.10.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-11-02 11:13:18 +00:00
dependabot[bot]
3b1578f931
Bump mypy from 0.971 to 0.982 (#2427)
Bumps [mypy](https://github.com/python/mypy) from 0.971 to 0.982.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.971...v0.982)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-11-02 09:48:03 +00:00
dependabot[bot]
1b2e3b7cf3
Bump autoflake from 1.4 to 1.7.7 (#2430)
Bumps [autoflake](https://github.com/PyCQA/autoflake) from 1.4 to 1.7.7.
- [Release notes](https://github.com/PyCQA/autoflake/releases)
- [Commits](https://github.com/PyCQA/autoflake/compare/v1.4...v1.7.7)

---
updated-dependencies:
- dependency-name: autoflake
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-02 09:42:50 +00:00
dependabot[bot]
3f0675c73f
Bump coverage from 6.4.4 to 6.5.0 (#2431)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.4.4 to 6.5.0.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/6.4.4...6.5.0)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-01 18:09:45 +01:00
Tom Christie
9e97d7d429
Drop stalebot (#2412)
Sorry @stalebot, but we don't need your work here.

We've a low enough ticket count, and you're just adding noise.
2022-10-18 14:42:28 +01:00
Ramazan Elsunakev
71ee50b277
Add parameters to generics in _client.py (#2266)
Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>
2022-10-12 09:45:06 +01:00
Meder Kamalov
36f16234bc
Add dark mode toggle for documentation (#2403) 2022-10-11 11:54:24 +01:00
Tom Christie
0ebe9259ac
Allow str content for multipart upload files (#2400) 2022-10-06 17:53:51 +01:00
Tom Christie
770d4f2254
Remove unneeded installation on install script (#2398) 2022-10-06 15:43:43 +02:00
Tom Christie
2ac58e007a
Always use latest version of pip. (#2396)
Installation should start by updating `pip` to the latest version.

Resolves issue noted in https://github.com/encode/httpx/pull/2334#issuecomment-1268308195
2022-10-05 18:42:32 +01:00
dependabot[bot]
9f70f54316
Bump cryptography from 37.0.2 to 38.0.1 (#2389)
Bumps [cryptography](https://github.com/pyca/cryptography) from 37.0.2 to 38.0.1.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/37.0.2...38.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-10-04 14:12:43 +01:00
Tom Christie
bb65d9e28e
Pin importlib-metadata (#2393) 2022-10-04 15:04:02 +02:00
dependabot[bot]
801ab9d573
Bump flake8-pie from 0.15.0 to 0.16.0 (#2387)
Bumps [flake8-pie](https://github.com/sbdchd/flake8-pie) from 0.15.0 to 0.16.0.
- [Release notes](https://github.com/sbdchd/flake8-pie/releases)
- [Commits](https://github.com/sbdchd/flake8-pie/commits)

---
updated-dependencies:
- dependency-name: flake8-pie
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-04 13:54:21 +01:00
dependabot[bot]
b1618227cc
Bump mkdocs-material from 8.3.8 to 8.5.5 (#2388)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-01 18:12:37 +02:00
dependabot[bot]
a7711377da
Bump mkdocs from 1.3.1 to 1.4.0 (#2390)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-01 18:06:30 +02:00
Jo
8088fc7ff7
Test under 3.11-dev (#2302)
* Test under 3.11.0-beta.3

* Ignore cgi and importlib warnings

* Ignore src_constant warning

* Install whell before other requirements

* Minor

* Remove uvicorn ignore

* Use 3.11-dev instead

* Add 3.11 to classifiers

* Revert unrelated change

* Bump coverage
2022-09-29 17:00:21 +01:00
Marcelo Trylesinski
cdde07f2aa
Add thread-safety note on the Client docstring (#2380) 2022-09-29 12:09:11 +02:00
Adrian Garcia Badaracco
8152c4facd
add basic type annotation for RequestExtensions and RequestData (#2367) 2022-09-13 09:24:01 -05:00
chaojie
27db35296b
Make chinese README better (#2366) 2022-09-06 00:18:44 +08:00
Tom Christie
d5900cd40e
Fix empty query params (#2354)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2022-09-02 14:24:45 +01:00
dependabot[bot]
3eee17e69e
Bump mkautodoc from 0.1.0 to 0.2.0 (#2361)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-01 19:15:45 +02:00
dependabot[bot]
e34a977a58
Bump trio from 0.20.0 to 0.21.0 (#2360)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-01 18:22:44 +02:00
Marcelo Trylesinski
9f79b5c090
Add fake setup.py (#2351) 2022-09-01 18:13:35 +02:00
dependabot[bot]
d83565932c
Bump black from 22.6.0 to 22.8.0 (#2358)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-01 18:10:01 +02:00
dependabot[bot]
3a8f886afa
Bump uvicorn from 0.18.2 to 0.18.3 (#2357)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-09-01 18:06:47 +02:00
Tom Christie
965b8adec3
Allow tuple or list for multipart values (#2355) 2022-08-30 14:03:31 +01:00
Will Frey
ccd98b1a6d
Relax HeaderTypes type alias definition (#2317)
* Relax `HeaderTypes` type alias definition

This replaces `Dict[..., ...]` with `Mapping[..., ...]` in the union definition for `HeaderTypes`. Closes #2314.

* Update _models.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-08-30 12:46:19 +01:00
Marcelo Trylesinski
a754e71f6f
Replace black Python target version from 3.6 to 3.7 (#2343) 2022-08-25 19:19:28 +02:00
Florimond Manca
f13ab4d288
Replace cgi which will be deprecated in Python 3.11 (#2309)
* Replace cgi which will be deprecated in Python 3.11

* Update httpx/_utils.py
2022-08-25 12:23:04 +02:00
Ofek Lev
45b7cfaad3
Update package metadata (#2334)
Co-authored-by: Florimond Manca <florimond.manca@protonmail.com>
2022-08-22 18:55:06 +02:00
Marcelo Trylesinski
2b2269d5d8
Add script to make sure CHANGELOG is always in sync with __version__ (#2297)
* Add script to make sure CHANGELOG is always in sync with `__version__`

* Fix version

* Change file permission

* Change head by sed
2022-08-20 14:09:30 +02:00
Fred Thomsen
5af6123fff
Update docs to reflect supported python versions (#2338)
Replaces Python 3.6 references with Python 3.7.

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2022-08-16 17:20:35 -05:00
Adrian Garcia Badaracco
1526048c94
allow setting an explicit multipart boundary via headers (#2278) 2022-08-15 10:20:07 -05:00
dependabot[bot]
2434e650ee
Bump types-chardet from 4.0.4 to 5.0.3 (#2324)
Bumps [types-chardet](https://github.com/python/typeshed) from 4.0.4 to 5.0.3.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-chardet
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-02 08:15:21 +02:00
Marcelo Trylesinski
303c6e8a3c
Bump mkdocs from 1.3.0 to 1.3.1 (#2328) 2022-08-02 08:06:54 +02:00
dependabot[bot]
a3b08c8e5e
Bump flake8-bugbear from 22.4.25 to 22.7.1 (#2323)
Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 22.4.25 to 22.7.1.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/22.4.25...22.7.1)

---
updated-dependencies:
- dependency-name: flake8-bugbear
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-01 20:49:31 +02:00
dependabot[bot]
1a16028654
Bump pytest-asyncio from 0.18.3 to 0.19.0 (#2327)
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.18.3 to 0.19.0.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.18.3...v0.19.0)

---
updated-dependencies:
- dependency-name: pytest-asyncio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-01 18:27:02 +02:00
dependabot[bot]
eb92a439e8
Bump mypy from 0.961 to 0.971 (#2325)
Bumps [mypy](https://github.com/python/mypy) from 0.961 to 0.971.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.961...v0.971)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-01 18:14:05 +02:00
Florimond Manca
93de1980fa
Expand docs about retries (#2311)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-07-29 13:21:58 +01:00
Ofek Lev
9884965233
Update link to trustme (#2318) 2022-07-25 17:23:08 +08:00
Florimond Manca
3a82176f1f
Rework docs structure (#2308) 2022-07-21 08:43:18 +02:00
Florimond Manca
3aa4410158
Pin markdown==3.3.7 to prevent mkautodoc loading failure (#2313) 2022-07-19 10:42:49 +02:00
Florimond Manca
943a942836
Fix link to "http2 explained" (#2307) 2022-07-18 14:34:06 +02:00
dependabot[bot]
67e9c14952
Bump chardet from 4.0.0 to 5.0.0 (#2289)
Bumps [chardet](https://github.com/chardet/chardet) from 4.0.0 to 5.0.0.
- [Release notes](https://github.com/chardet/chardet/releases)
- [Commits](https://github.com/chardet/chardet/compare/4.0.0...5.0.0)

---
updated-dependencies:
- dependency-name: chardet
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-07-05 08:55:23 +01:00
dependabot[bot]
d041aab111
Bump actions/setup-python from 3 to 4 (#2293) 2022-07-04 14:32:05 +01:00
dependabot[bot]
6d59c0126f
Bump mkdocs-material from 8.2.14 to 8.3.8 (#2291) 2022-07-04 09:27:41 +01:00
dependabot[bot]
8d186cfb76
Bump uvicorn from 0.17.6 to 0.18.2 (#2292)
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.17.6 to 0.18.2.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.17.6...0.18.2)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-02 13:37:12 +01:00
dependabot[bot]
4a392f2296
Bump black from 22.3.0 to 22.6.0 (#2290) 2022-07-01 17:42:21 +01:00
Daniel Holth
aad60a4f12
add [chunk_size] for a?iter_.* methods (#2281)
```
    def iter_bytes(
        self, chunk_size: typing.Optional[int] = None
    ) -> typing.Iterator[bytes]:
```
2022-06-24 14:36:45 +01:00
dependabot[bot]
344e01e98a
Bump mypy from 0.910 to 0.961 (#2269)
* Bump mypy from 0.910 to 0.961

Bumps [mypy](https://github.com/python/mypy) from 0.910 to 0.961.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.910...v0.961)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Handle py37 iscoroutine type guard oddity

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: florimondmanca <florimond.manca@protonmail.com>
2022-06-19 12:50:04 +02:00
dependabot[bot]
132ccd9834
Bump pytest-asyncio from 0.16.0 to 0.18.3 (#2198)
* Bump pytest-asyncio from 0.16.0 to 0.18.3

Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.16.0 to 0.18.3.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.16.0...v0.18.3)

---
updated-dependencies:
- dependency-name: pytest-asyncio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update setup.cfg

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-06-17 11:19:35 +01:00
dependabot[bot]
28ed42e5c0
Bump twine from 4.0.0 to 4.0.1 (#2256)
Bumps [twine](https://github.com/pypa/twine) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/4.0.0...4.0.1)

---
updated-dependencies:
- dependency-name: twine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-16 14:16:06 +01:00
dependabot[bot]
cc034e0412
Bump flake8-bugbear from 22.1.11 to 22.4.25 (#2257)
Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 22.1.11 to 22.4.25.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/22.1.11...22.4.25)

---
updated-dependencies:
- dependency-name: flake8-bugbear
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-16 13:10:27 +01:00
dependabot[bot]
924e1f7c4b
Bump coverage from 6.0.2 to 6.4.1 (#2268)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.0.2 to 6.4.1.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/6.0.2...6.4.1)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-16 12:46:35 +01:00
Josh
dd8a1d6cda
Remove 3.6 from the package classifiers (#2267) 2022-06-16 12:31:19 +01:00
Tom Christie
9baf3a6cd2
Drop RawURL (#2241) 2022-05-30 14:54:47 +01:00
Will Frey
5b06aea1d6
Update _client.py (#2247)
Update the type annotation for the default_encoding parameter of AsyncClient's initializer to accept a callable of the form (bytes) -> str.
2022-05-26 13:53:31 +01:00
Tom Christie
89cdd903b2
Version 0.23.0 (#2214)
* Version 0.23.0

* Update changelog

* Update httpcore dependancy

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md
2022-05-23 16:31:12 +01:00
Tom Christie
1c33a2854e
Make charset auto-detection optional. (#2165)
* Add Response(..., default_encoding=...)

* Add tests for Response(..., default_encoding=...)

* Add Client(..., default_encoding=...)

* Switch default encoding to 'utf-8' instead of 'autodetect'

* Make charset_normalizer an optional dependancy, not a mandatory one.

* Documentation

* Use callable for default_encoding

* Update tests for new charset autodetection API

* Update docs for new charset autodetection API

* Update requirements

* Drop charset_normalizer from requirements
2022-05-23 16:27:32 +01:00
Kieran Klukas
940d61b239
Removed curio from async.md (#2240) 2022-05-23 11:09:15 +02:00
Michael Oliver
14a1704be5
Switch to explicit typing.Optional throughout (#2096)
* Fix issue with Mypy `--strict` and `AsyncExitStack`

* Enable `no_implicit_optional` in Mypy

* Ignore internal type errors

* Bump `httpcore`

* Remove unneeded type: ignore comments

Co-authored-by: Florimond Manca <florimond.manca@protonmail.com>
Co-authored-by: Michael Oliver <michael@michaeloliver.dev>
2022-05-20 11:12:25 +01:00
Tom Christie
9673a3555c
Drop async_generator requirement (#2228)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2022-05-16 13:23:15 +01:00
Michael Oliver
5eba32a61f
Remove RequestBodyUnavailable from module docstring (#2226)
Co-authored-by: Michael Oliver <michael@michaeloliver.dev>
2022-05-16 10:37:13 +01:00
dependabot[bot]
6f31bc4bea
Bump mkdocs-material from 8.1.4 to 8.2.14 (#2218)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.1.4 to 8.2.14.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.1.4...8.2.14)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 12:05:52 +01:00
dependabot[bot]
c5eb4b8b96
Bump cryptography from 36.0.2 to 37.0.2 (#2217)
Bumps [cryptography](https://github.com/pyca/cryptography) from 36.0.2 to 37.0.2.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/36.0.2...37.0.2)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-05-10 10:27:13 +01:00
dependabot[bot]
1a526cf56b
Bump actions/checkout from 2 to 3 (#2216)
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 10:11:38 +01:00
dependabot[bot]
7a53543428
Bump actions/setup-python from 1 to 3 (#2215)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 1 to 3.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 10:07:37 +01:00
Naveen
f224bd5911
chore: Included githubactions in the dependabot config (#2206)
This should help with keeping the GitHub actions updated on new releases. This will also help with keeping it secure.

Dependabot helps in keeping the supply chain secure https://docs.github.com/en/code-security/dependabot

GitHub actions up to date https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot

https://github.com/ossf/scorecard/blob/main/docs/checks.md#dependency-update-tool
Signed-off-by: naveensrinivasan <172697+naveensrinivasan@users.noreply.github.com>

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-05-10 10:02:42 +01:00
Tom Christie
850b4801d6
Case insensitive algorithm detection for digest authentication. (#2204)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2022-05-09 10:42:57 +01:00
dependabot[bot]
e58b491d11
Bump types-certifi from 2021.10.8.0 to 2021.10.8.2 (#2205)
Bumps [types-certifi](https://github.com/python/typeshed) from 2021.10.8.0 to 2021.10.8.2.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-certifi
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-05-05 13:18:23 +01:00
nyuszika7h
782f507b63
docs: Fix proxy examples (#2183)
Per https://www.python-httpx.org/compatibility/#proxy-keys, there should
always be a `://` after the protocol. The given examples raise an
exception when used as-is.

Co-authored-by: Florimond Manca <florimond.manca@protonmail.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-05-05 13:11:18 +01:00
dependabot[bot]
8bdd6731ff
Bump trio-typing from 0.5.1 to 0.7.0 (#2195)
Bumps [trio-typing](https://github.com/python-trio/trio-typing) from 0.5.1 to 0.7.0.
- [Release notes](https://github.com/python-trio/trio-typing/releases)
- [Commits](https://github.com/python-trio/trio-typing/compare/v0.5.1...v0.7.0)

---
updated-dependencies:
- dependency-name: trio-typing
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-05-05 13:06:44 +01:00
dependabot[bot]
70e7cd766f
Bump pytest from 7.0.1 to 7.1.2 (#2196)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.1 to 7.1.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/7.0.1...7.1.2)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-05-05 13:03:44 +01:00
dependabot[bot]
fba190b5b9
Bump trio from 0.19.0 to 0.20.0 (#2197)
Bumps [trio](https://github.com/python-trio/trio) from 0.19.0 to 0.20.0.
- [Release notes](https://github.com/python-trio/trio/releases)
- [Commits](https://github.com/python-trio/trio/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: trio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-05-05 12:59:29 +01:00
Ninzero
2b26412868
Clarify custom auth (#2202) 2022-05-04 11:11:53 +02:00
Alan Li
e9b0c85dd4
Patch copy_with (#2185)
* Patch `copy_with`

* Add a new test for `copy_with`
2022-05-03 11:33:13 +01:00
dependabot[bot]
b07fe7b074
Bump uvicorn from 0.14.0 to 0.17.6 (#2163)
Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.14.0 to 0.17.6.
- [Release notes](https://github.com/encode/uvicorn/releases)
- [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/encode/uvicorn/compare/0.14.0...0.17.6)

---
updated-dependencies:
- dependency-name: uvicorn
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-18 10:07:17 +02:00
dependabot[bot]
3af5146788
Bump cryptography from 36.0.1 to 36.0.2 (#2162)
Bumps [cryptography](https://github.com/pyca/cryptography) from 36.0.1 to 36.0.2.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/36.0.1...36.0.2)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-04-05 14:49:03 +01:00
dependabot[bot]
c54df4eab3
Bump twine from 3.7.1 to 4.0.0 (#2161)
Bumps [twine](https://github.com/pypa/twine) from 3.7.1 to 4.0.0.
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/3.7.1...4.0.0)

---
updated-dependencies:
- dependency-name: twine
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-05 09:31:05 +01:00
dependabot[bot]
782d190d44
Bump wheel from 0.37.0 to 0.37.1 (#2160)
Bumps [wheel](https://github.com/pypa/wheel) from 0.37.0 to 0.37.1.
- [Release notes](https://github.com/pypa/wheel/releases)
- [Changelog](https://github.com/pypa/wheel/blob/main/docs/news.rst)
- [Commits](https://github.com/pypa/wheel/compare/0.37.0...0.37.1)

---
updated-dependencies:
- dependency-name: wheel
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-05 09:20:40 +01:00
Johannes
781076cb63
Fix raise_for_status exception docs (#2159)
Closes #2126
2022-04-01 16:06:24 +01:00
Michael Oliver
550fff933f
Add request getter/setter to HTTPError (#2158) 2022-04-01 11:10:32 +01:00
Tom Christie
3350d7e683
Close responses when on cancellations occur during reading. (#2156)
* Test case for clean stream closing on cancellations

* Test case for clean stream closing on cancellations

* Linting on tests

* responses should close on any BaseException
2022-03-31 13:41:40 +01:00
Ofek Lev
67c297069f
Drop EOL Python 3.6 (#2097)
Co-authored-by: Florimond Manca <florimond.manca@protonmail.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2022-03-30 17:31:05 +02:00
Jorge
f7a024cee3
Corrected typo in advanced.md (#2155) 2022-03-30 16:58:00 +02:00
Marcelo Trylesinski
3f9da8e0f5
Bump black to 22.3.0 (#2153)
* Bump black to 22.3.0

* Apply black
2022-03-30 09:56:57 +01:00
Tom Christie
4747c316d8
Update stale.yml (#2150) 2022-03-28 13:26:41 +01:00
Raffaele Salmaso
476127697f
Allow rich 12 (#2122) 2022-03-28 12:59:07 +01:00
Florimond Manca
c5b2dd167b
Upgrade mkdocs to 1.3.0 to fix scripts/docs failure (#2147) 2022-03-28 05:46:47 +02:00
kk
c82885ac04
Fix cli docs --proxies #2124 (#2125) 2022-03-14 17:51:39 +01:00
Dan Claudiu Pop
43a1c1c826
Add robox to third party packages doc (#2113)
* Add robox to third party packages doc

* Update third_party_packages.md
2022-03-08 10:53:15 +00:00
dependabot[bot]
754b55691d
Bump flake8-bugbear from 21.9.2 to 22.1.11 (#2100)
Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 21.9.2 to 22.1.11.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/21.9.2...22.1.11)

---
updated-dependencies:
- dependency-name: flake8-bugbear
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-03-04 19:07:52 +00:00
dependabot[bot]
67651f8d05
Bump pytest from 6.2.5 to 7.0.1 (#2103)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.5 to 7.0.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/6.2.5...7.0.1)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-03 10:09:05 +00:00
Ben Fasoli
6820b1d9c5
Fix invalid max_keepalive_connections argument name (#2094)
The argument to `httpx.Limits` is named `max_keepalive_connections` but is referenced as `max_keepalive` in the docs.
2022-02-25 14:10:35 +00:00
Udasi Tharani
2a08edc471
adding keepalive_expiry param to docs and docstring (#2090) 2022-02-22 10:40:14 +00:00
Tom Christie
7883c98556
Create stale.yml (#2085) 2022-02-17 09:32:47 +00:00
Tom Christie
d07c4b4407
Move URL and QueryParams to new '_urls.py' module (#2084) 2022-02-16 21:02:13 +00:00
Tom Christie
2072aa2361
Docs tweaks (#2075) 2022-02-09 16:05:20 +00:00
waterfountain1996
4c307488cd
Suppress binary output on the command line (#2049) (#2076) 2022-02-09 15:40:39 +00:00
Yasser Tahiri
a416106250
Refactor httpx/_utils.py (#1943)
* Removes unnecessary call to keys() when iterating over a dictionary

* Simplify conditionals into a form like a switch statement

* Merge else clause's nested if statement into elif

* Update httpx/_utils.py

* Update httpx/_utils.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-02-09 12:12:09 +00:00
iscai-msft
7d3a5347a9
update eerror docs to use StreamClosed instead of old ResponseClosed (#1913)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-02-09 11:53:49 +00:00
waterfountain1996
4401d55ecf
Preserve Authorization header on HTTPS redirect (#1850) (#2074)
* Preserve Authorization header on HTTPS redirect (#1850)

* Update httpx/_client.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-02-09 11:18:21 +00:00
toppk
4f8068a7ad
update docstring/docs for transport using request/response models (#2070)
* update docstring/docs for transport based on pull #1840

* Update httpx/_transports/base.py

* lint'd

Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-02-08 14:58:08 +00:00
Tom Christie
420911bc69
Ensure that iter_bytes does not ever yield any zero-length chunks (#2068) 2022-02-07 09:19:26 +00:00
Tom Christie
0088253b32
Always rewind files on multipart uploads. (#2065)
* Test for multipart POST same file twice.

* Always rewind files on multipart uploads

* Linting
2022-02-04 14:48:57 +00:00
Will McGugan
2814fd379c
Fix console markup escaping issue (#1866)
* fix for markup escaping in progress

* whitespace

* restore new line

* Update _main.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
2022-02-04 12:33:32 +00:00
Michał Górny
2146c75f4e
Allow rich-11 (#2057)
Relax the version bind on rich to allow v11.  There are no test
regressions with the new version, and CLI seems to work correctly.
2022-02-01 12:28:12 +00:00
Simon K
7b6af4d123
make docs accurate that content= is also excluded in some requests (#1922)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-01-27 11:58:33 +00:00
Tom Christie
15c51b9dd5
Version 0.22.0 (#2048)
* SOCKS proxy support

* Version 0.22.0

* Link to socksio
2022-01-26 14:47:40 +00:00
Tom Christie
31944b90f1
Update publish.yml (#2040) 2022-01-24 12:14:00 +00:00
Vytautas Liuolia
d299e6ff59
Do not recommend the deprecated data= kwarg for passing bytes. (#2044)
(`data=` is meant for sending form data.)
2022-01-23 21:37:27 +01:00
Adrian Garcia Badaracco
321d4aa509
Fix Headers.update to correctly handle repeated headers (#2038) 2022-01-21 08:35:10 -08:00
Tom Christie
8dc9b6bd59
SOCKS proxy support (#2034) 2022-01-19 14:58:19 +00:00
Tom Christie
7f0d43daad
Dont perform implicit close/warning on __del__ (#2026)
* Version 0.21.3

* Don't perform implict close on __del__
2022-01-13 10:37:37 +00:00
Adrian Garcia Badaracco
0f1ff50a1e
Allow custom headers in multipart/form-data requests (#1936)
* feat: allow passing multipart headers

* Add test for including content-type in headers

* lint

* override content_type with headers

* compare tuples based on length

* incorporate suggestion

* remove .title() on headers
2022-01-13 08:49:14 +00:00
Denis Laxalde
3eaf69a772
Drop mention of backend selection for AsyncHTTPTransport() in docs (#2019)
There is no 'backend' parameter to AsyncHTTPTransport and it seems that
the backend is detected automatically for anyio as it is for other async
libraries.
2022-01-10 09:39:13 +00:00
Tom Christie
c6c8cb1fe2
Fix typo in change-log. (#2018)
Version was specified incorrectly.

See: https://github.com/encode/httpx/pull/2016#issuecomment-1006721639
2022-01-07 10:51:17 +00:00
Tom Christie
07f786c2f8
Version 0.21.3 (#2017) 2022-01-06 14:36:58 +00:00
Tom Christie
b7dc0c3df6
Fix for stream uploads that subclass SyncByteStream/AsyncByteStream (#2016) 2022-01-06 14:06:35 +00:00
dependabot[bot]
99ba25c3c5
Bump pytest-asyncio from 0.15.1 to 0.16.0 (#2005)
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.15.1 to 0.16.0.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.15.1...v0.16.0)

---
updated-dependencies:
- dependency-name: pytest-asyncio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-01-06 14:01:10 +00:00
Tom Christie
f6b76571fa
Version 0.21.2 (#2011) 2022-01-05 16:24:42 +00:00
Tom Christie
eb46c2d809
CI should just use Python 3.9, not "3.9.5" (#2010) 2022-01-05 16:04:56 +00:00
Tom Christie
406700317b
HTTP/2 support for tunnelled proxy cases. (#2009)
* Cap upload chunk sizes

* Use '.read' for file streaming, where possible

* Direct iteration should not apply chunk sizes

* HTTP/2 support for proxies
2022-01-05 16:01:47 +00:00
dependabot[bot]
9d543750b9
Bump mkdocs-material from 8.0.2 to 8.1.4 (#2006)
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.0.2 to 8.1.4.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.0.2...8.1.4)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2022-01-05 14:27:25 +00:00
dependabot[bot]
56fce6190a
Bump isort from 5.9.3 to 5.10.1 (#1999)
Bumps [isort](https://github.com/pycqa/isort) from 5.9.3 to 5.10.1.
- [Release notes](https://github.com/pycqa/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pycqa/isort/compare/5.9.3...5.10.1)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-03 16:00:45 +00:00
dependabot[bot]
b019d6ec75
Bump twine from 3.4.2 to 3.7.1 (#1998)
Bumps [twine](https://github.com/pypa/twine) from 3.4.2 to 3.7.1.
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/3.4.2...3.7.1)

---
updated-dependencies:
- dependency-name: twine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-03 15:54:21 +00:00
dependabot[bot]
7b95ddf399
Bump cryptography from 3.4.8 to 36.0.1 (#2000)
Bumps [cryptography](https://github.com/pyca/cryptography) from 3.4.8 to 36.0.1.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/3.4.8...36.0.1)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-03 13:25:59 +00:00
Mohammadreza Jafari
83c81aa9b8
Tiny refactor on _multipart.py & _main.py (#1971)
* remove unnecessary casting in f-string

* merge nested condition
2021-12-21 18:15:34 +01:00
Kian Meng, Ang
82ba15b521
Fix typos (#1968) 2021-12-14 15:04:01 +01:00
dependabot[bot]
3c5884eeaa
Bump mkdocs-material from 7.2.6 to 8.0.2 (#1958)
* Bump mkdocs-material from 7.2.6 to 8.0.2

Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.2.6 to 8.0.2.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Upgrade guide](https://github.com/squidfunk/mkdocs-material/blob/master/docs/upgrade.md)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.2.6...8.0.2)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update requirements.txt

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-12-06 14:38:50 +00:00
dependabot[bot]
29611cf91d
Bump pytest from 6.2.4 to 6.2.5 (#1956) 2021-12-01 20:37:39 +00:00
dependabot[bot]
b16c5f2cef
Bump trustme from 0.8.0 to 0.9.0 (#1955) 2021-12-01 19:40:17 +00:00
dependabot[bot]
a6e03ca66e
Bump black from 21.10b0 to 21.11b1 (#1954) 2021-12-01 18:19:45 +00:00
wo0d
efc841a9e8
Fix typo on _merge_url (#1949)
seperator -> separator
2021-11-23 09:04:31 +00:00
Tom Christie
6f5865f860
Read upload files using read(CHUNK_SIZE) rather than iter(). (#1948)
* Cap upload chunk sizes

* Use '.read' for file streaming, where possible

* Direct iteration should not apply chunk sizes
2021-11-22 13:15:39 +00:00
Kevin Anderson
e232226d77
readmes/README.scn.md (#1946)
* Create README_chinese.md

* Update README_chinese.md

* Update README_chinese.md

* Update README_chinese.md

* Update README_chinese.md
2021-11-22 12:22:45 +00:00
Tom Christie
716749e3fd
Version 0.21.1 (#1941) 2021-11-16 11:51:56 +00:00
Tom Christie
4882e98049
Fix response.url annotation (#1940) 2021-11-16 11:47:46 +00:00
Tom Christie
b4f60694eb
Version 0.21 release notes (#1938) 2021-11-15 14:35:09 +00:00
Tom Christie
61188feeae
Version 0.21 (#1935)
* Integrate with httpcore 0.14

* Fix pool timeout test

* Add request extensions to API

* Add certificate and connection info to client, using 'trace' extension

* Fix test_pool_timeout flakiness
2021-11-15 14:30:54 +00:00
dependabot[bot]
c531263f42
Bump flake8-bugbear from 21.4.3 to 21.9.2 (#1917)
Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 21.4.3 to 21.9.2.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/21.4.3...21.9.2)

---
updated-dependencies:
- dependency-name: flake8-bugbear
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-15 11:00:30 +00:00
dependabot[bot]
35b98a9f60
Bump black from 21.9b0 to 21.10b0 (#1921)
Bumps [black](https://github.com/psf/black) from 21.9b0 to 21.10b0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/commits)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-15 10:24:12 +00:00
dependabot[bot]
9082ed91e1
Bump types-certifi from 0.1.4 to 2021.10.8.0 (#1918)
Bumps [types-certifi](https://github.com/python/typeshed) from 0.1.4 to 2021.10.8.0.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-certifi
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-12 13:50:39 +00:00
Tyler Chamberlain
da8f959af0
Expand docs note for async custom handler responses (#1916)
* Expand note for async custom handler responses

Custom response handlers need to run `response.read()` before they can read the content of the response. However when using an AsyncClient this will produce an error of `RuntimeError: Attempted to call a sync iterator on an async stream.`. Took me some digging to figure out I just needed to use `response.aread()` here instead of `response.read()` so figured I would an MR with an expansion on the note for anyone else.
Thanks!

* Update advanced.md
2021-11-01 11:39:18 +00:00
Adrian Garcia Badaracco
62b1666dc6
doc: Update comments about file types in _types.py (#1898)
* doc: Update comments about file types in _types.py

https://github.com/encode/httpx/discussions/1897

* Update httpx/_types.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-10-18 11:19:42 +01:00
dependabot[bot]
fa03b489f0
Bump flake8-pie from 0.5.0 to 0.15.0 (#1879)
* Bump flake8-pie from 0.5.0 to 0.15.0

Bumps [flake8-pie](https://github.com/sbdchd/flake8-pie) from 0.5.0 to 0.15.0.
- [Release notes](https://github.com/sbdchd/flake8-pie/releases)
- [Commits](https://github.com/sbdchd/flake8-pie/commits)

---
updated-dependencies:
- dependency-name: flake8-pie
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Don't use PIE on 3.6

* Adhere or ignore new rules

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Florimond Manca <florimond.manca@protonmail.com>
2021-10-15 12:03:53 +02:00
dependabot[bot]
c59be190e4
Bump black from 20.8b1 to 21.9b0 (#1877)
Bumps [black](https://github.com/psf/black) from 20.8b1 to 21.9b0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/commits)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Florimond Manca <florimond.manca@protonmail.com>
2021-10-15 11:44:37 +02:00
Tom Christie
623b0ddeea
Don't pickle request/response extensions (#1892) 2021-10-13 13:45:29 +01:00
dependabot[bot]
885314a364
Bump wheel from 0.36.2 to 0.37.0 (#1878)
Bumps [wheel](https://github.com/pypa/wheel) from 0.36.2 to 0.37.0.
- [Release notes](https://github.com/pypa/wheel/releases)
- [Changelog](https://github.com/pypa/wheel/blob/main/docs/news.rst)
- [Commits](https://github.com/pypa/wheel/compare/0.36.2...0.37.0)

---
updated-dependencies:
- dependency-name: wheel
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 12:02:04 +01:00
dependabot[bot]
6bc4a8d7ea
Bump coverage from 5.3 to 6.0.2 (#1891)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 5.3 to 6.0.2.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/coverage-5.3...6.0.2)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 11:43:07 +01:00
dependabot[bot]
d591ddbe52
Bump isort from 5.9.1 to 5.9.3 (#1881)
Bumps [isort](https://github.com/pycqa/isort) from 5.9.1 to 5.9.3.
- [Release notes](https://github.com/pycqa/isort/releases)
- [Changelog](https://github.com/PyCQA/isort/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pycqa/isort/compare/5.9.1...5.9.3)

---
updated-dependencies:
- dependency-name: isort
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 11:37:01 +01:00
Tom Christie
35164b7a64
Version 0.20 (#1890)
* Version 0.20

* Add date to changelog

* Freeze charset-normalizer to a known version for testing consistency
2021-10-13 10:43:58 +01:00
Marcelo Trylesinski
deb1a2b921
Update index.md (#1883)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-10-06 13:26:49 +01:00
Marcelo Trylesinski
2212dda7c7
Bump Python 3.10 in the CI (#1886)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-10-06 10:13:34 +01:00
Tom Christie
1752e4d672
Run CI on 3.9.5 (#1887)
* Update test-suite.yml

* Update test-suite.yml
2021-10-06 09:46:45 +01:00
Tom Christie
e1abaf146f
Tidy up the formatting of HTTP/2 requests (#1860)
* Tidy up the formatting of HTTP/2 requests

* Black linting
2021-09-14 13:36:22 +01:00
Tom Christie
d41840e18d
Fix typo. (#1859) 2021-09-14 12:48:06 +01:00
Tom Christie
7e01677f0a
List 'click' and 'rich' in the project dependencies (#1858) 2021-09-14 10:44:07 +01:00
Tom Christie
f2992442fd
Update README.md (#1857) 2021-09-14 09:54:50 +01:00
Tom Christie
ee9250d60b
Add cli support (#1855)
* Add cli support

* Add setup.py

* Import main to 'httpx.main'

* Add 'cli' to requirements

* Add tests for command-line client

* Drop most CLI tests

* Add test_json

* Add test_redirects

* Coverage exclusion over _main.py in order to test more clearly

* Black formatting

* Add test_follow_redirects

* Add test_post, test_verbose, test_auth

* Add test_errors

* Remove test_errors

* Add test_download

* Change test_errors - perhaps the empty host header was causing the socket error?

* Update test_errors to not break socket

* Update docs

* Update version to 1.0.0.beta0

* Tweak CHANGELOG

* Fix up images in README

* Tweak images in README

* Update README
2021-09-14 09:44:43 +01:00
Tom Christie
a761e17abc
is_informational / is_success / is_redirect / is_client_error / is_server_error (#1854) 2021-09-13 13:52:58 +01:00
Tom Christie
ff9813e84d
Transport API as plain request -> response method. (#1840)
* Responses as context managers

* timeout -> request.extensions

* Transport API -> request/response signature

* Fix top-level httpx.stream()

* Drop response context manager methods

* Simplify ASGI tests

* Black formatting
2021-09-13 13:34:46 +01:00
Tom Christie
47266d763b
Switch follow redirects default (#1808)
* Switch default on allow_redirects to False

* allow_redirects -> follow_redirects

* Update follow_redirects default in top-level API

* Update docs on follow_redirects
2021-09-13 13:21:22 +01:00
Adrian Garcia Badaracco
0b4a83257b
BUG: wsgi.error should be TextIO, not BytesIO in WSGI transport (#1828)
* wsgi.error should be StringIO, not BytesIO

Based on the documentation at https://modwsgi.readthedocs.io/en/master/user-guides/debugging-techniques.html#apache-error-log-files

* Default to sys.stderr and add test

* rename log_file param to wsgi_errors

Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-09-07 15:44:35 +01:00
Robert Craigie
c24bbb85a6
Fix conditional imports for pyright (#1839) 2021-09-03 14:51:42 +01:00
dependabot[bot]
513a6aa067
Bump mkdocs-material from 7.1.8 to 7.2.6 (#1835)
* Bump mkdocs-material from 7.1.8 to 7.2.6

Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.8 to 7.2.6.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.8...7.2.6)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update requirements.txt

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-09-03 12:26:01 +01:00
dependabot[bot]
15defd1b5e
Bump twine from 3.4.1 to 3.4.2 (#1838)
Bumps [twine](https://github.com/pypa/twine) from 3.4.1 to 3.4.2.
- [Release notes](https://github.com/pypa/twine/releases)
- [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pypa/twine/compare/3.4.1...3.4.2)

---
updated-dependencies:
- dependency-name: twine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-03 10:33:38 +01:00
dependabot[bot]
d6ecc516bf
Bump cryptography from 3.4.7 to 3.4.8 (#1837)
Bumps [cryptography](https://github.com/pyca/cryptography) from 3.4.7 to 3.4.8.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/3.4.7...3.4.8)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-09-03 10:30:01 +01:00
dependabot[bot]
05df3d05a4
Bump trio-typing from 0.5.0 to 0.5.1 (#1836)
Bumps [trio-typing](https://github.com/python-trio/trio-typing) from 0.5.0 to 0.5.1.
- [Release notes](https://github.com/python-trio/trio-typing/releases)
- [Commits](https://github.com/python-trio/trio-typing/commits)

---
updated-dependencies:
- dependency-name: trio-typing
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-03 10:26:48 +01:00
Marcelo Trylesinski
62b7988a15
Add dependabot (#1823)
* Add dependabot

* Delete automerge.yml

Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-09-03 10:19:15 +01:00
Stanis Trendelenburg
0d3fcc74a5
Close WSGI iterable when WSGIByteStream is closed (#1830)
* Make test fail when WSGI iterable is not closed

* Close WSGI iterable when WSGIByteStream is closed
2021-09-01 16:21:01 +01:00
TAHRI Ahmed R
ecbece178f
📝 Docs patch following PR #1791 section compatibility.encoding (#1812)
* 📝 Docs patch following PR #1791 section compatibility.encoding

Reintroducing charset detection

* 📝 Amend sentence in 3080a9d66e

Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-08-31 13:44:12 +01:00
Marcelo Trylesinski
0ccc3fa9be
📌 Pin development requirements (#1721)
* 📌 Pin development requirements

*  Remove attrs dependency

* Add note about decision here

Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Joe <jianghang@didiglobal.com>
2021-08-31 13:13:42 +01:00
Tom Christie
10b60d47c7
Fix iter_bytes with empty content (#1827) 2021-08-31 11:52:52 +01:00
Hugo van Kemenade
06498df528
Test under Python 3.10.0 RC 1 (#1820) 2021-08-27 21:01:06 +08:00
Patrick Arminio
317653585c
Fix typo in RemoteProtocolError description (#1817) 2021-08-26 09:26:06 +08:00
Florimond Manca
35de3dfeb6
Fix 0.19.0 release date in CHANGELOG (#1811) 2021-08-20 10:40:05 +01:00
Tom Christie
0d7c4caada
Version 0.19.0 (#1809)
* Update CHANGELOG

* Update CHANGELOG

* Version 0.19.0

* Update CHANGELOG
2021-08-19 12:37:25 +01:00
Tom Christie
2d9c3580e0
Switch event hooks to also run on redirects. (#1806)
* Switch event hooks to also run on redirects

* Bump coverage

* Add pragma: no cover, because sometime ya just gotta be pragmatic

* Update docs with note about response.read()
2021-08-18 15:12:39 +01:00
Tom Christie
4986743b3d
Resolve Python 3.6 tests hanging. (#1807)
* Update README.md

* Update requirements.txt

* Update README.md
2021-08-18 14:08:52 +01:00
Tom Christie
927c88d34f
Stricter enforcement of either 'with httpx.Client() as client' or 'client = httpx.Client()' lifespan styles. (#1800) 2021-08-13 15:17:15 +01:00
Antonio Larrosa
77193b2ab6
Add a network pytest mark for tests that use the network (#1669)
* Add a network pytest mark for tests that use the network

Sometimes it's useful to have the tests that use the network
marked so they can be skipped easily when we know the network
is not available.

This is useful for example on SUSE and openSUSE's build servers.
When building the httpx packages (actually, any package in the
distribution) the network is disabled so we can assure
reproducible builds (among other benefits). With this mark, it's
easier to skip tests that can not succeed.

* Add a better explanation for the network marker

Co-authored-by: Florimond Manca <15911462+florimondmanca@users.noreply.github.com>

Co-authored-by: Joe <nigelchiang@outlook.com>
Co-authored-by: Florimond Manca <15911462+florimondmanca@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-08-13 14:13:43 +01:00
Tom Christie
d5143120d1
Use either brotli or brotlicffi. (#1618)
* Use either brotli (recommended for CPython) or brotlicffi (Recommended for PyPy and others)

* Add comments in places where we switch behaviour depending on brotli/brotlicffi

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2021-08-13 11:52:45 +01:00
Tom Christie
acb5e6ac50
Add charset_normalizer detection. (#1791)
* Add charset_normalizer detection

* Tweak JSON tests for slightly different charset decoding behaviour

* Add charset-normalizer to docs
2021-08-13 11:38:53 +01:00
Tom Christie
77246617ca
Drop mode argument, 'httpx.Proxy(..., mode=...)' (#1795) 2021-08-13 11:34:56 +01:00
Amin Alaee
20e66d2048
enforce-upload-files-binary-type (#1736)
* enforce-upload-files-binary-type

* Update test_multipart.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-08-13 11:27:42 +01:00
Tom Christie
50790af69b
Update HTTP/2 support in extras_require (#1799)
* Update extras_require

* Update setup.py
2021-08-13 11:07:13 +01:00
Tom Christie
541a0afe56
Add Client(allow_redirect=<...>) (#1790) 2021-08-10 13:30:55 +01:00
Michał Górny
ee24e67180
Fix JSON wrong encoding tests on big endian platforms (#1781)
Fix test_json_without_specified_encoding_*_error tests on big endian
platforms.  The tests wrongly assume that data encoded as "utf-32-be"
can not be decoded as "utf-32".  This is true on little endian platforms
but on big endian platforms "utf-32" is equivalent to "utf-32-be".
To avoid the problem, explicitly decode as "utf-32-le", as this should
trigger the expected exception independently of platform's endianness.
2021-08-05 17:05:38 +01:00
Michał Górny
8c64e0c65f
Inline SERVER_SCOPE and remove typing_extensions requirement (#1773) 2021-07-26 15:08:58 +01:00
Almaz
b839478661
Replace for loops with comprehensions (#1759) 2021-07-21 14:30:55 +01:00
Adrian Garcia Badaracco
b169013115
Fix typo in wsgi.py (#1749) 2021-07-14 10:08:21 +01:00
Thomas Grainger
ab64f7c41f
check sys.version_info and ssl.OPENSSL_VERSION_INFO once (#1720) 2021-06-28 13:20:32 +01:00
Thomas Grainger
1737fc6229
use context.minimum_version in py3.7+ where available (#1714) 2021-06-28 13:12:30 +01:00
Florimond Manca
3d192aed45
Allow passing additional pytest args to scripts/test (#1710) 2021-06-24 11:15:59 +01:00
Joe
6b93787514
Test under Python 3.10b3 (#1707) 2021-06-22 13:25:19 +01:00
Sidharth Vinod
bb7f7dfda8
Add httpx-caching (#1694) 2021-06-17 13:30:33 +01:00
Sidharth Vinod
5096663181
Fix third party documentation link (#1691) 2021-06-17 11:37:03 +01:00
Tom Christie
f312e629bf
Version 0.18.2 (#1690)
* Version 0.18.2
2021-06-17 11:29:22 +01:00
Tom Christie
b43af721cd
Treat warnings as errors (#1687)
* Treat warnings as errors

* Defensive programming in Client.__del__ to avoid possible warnings on partially initialized instances

* Linting

* Ignore linting getattr errors in __del__

* getattr requires a default

* Tighten up closing of auth_flow generators

* Switch multipart test to open file in a context manager

* Ignore warnings on uvicorn

* Drop -Werror from addopts

* Warings specified entirely in 'filterwarnings' section

* Use ssl.PROTOCOL_TLS_CLIENT instead of deprecated ssl.PROTOCOL_TLS

* Push 'check_hostname = False' above 'context.verify_mode = ssl.CERT_NONE'

* Introduce set_minimum_tls_version_1_2 compatible across different python versions

* Commenting

* Add missing annotation

* Exclude _compat from coverage

Co-authored-by: Joe <nigelchiang@outlook.com>
Co-authored-by: jianghang <jianghang@didiglobal.com>
2021-06-16 15:34:12 +01:00
laggardkernel
47d712c01c
Fix typo (#1688) 2021-06-16 07:52:39 +02:00
Vibhu Agarwal
f8cb7f5f02
[Docs] Add AnyIO under "Supported async environments" (#1673)
* [Docs] Add AnyIO under "Supported async environments"

* Update docs/async.md

Co-authored-by: Florimond Manca <15911462+florimondmanca@users.noreply.github.com>

Co-authored-by: Florimond Manca <15911462+florimondmanca@users.noreply.github.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-06-16 07:19:39 +02:00
Mei
604d09d7d7
Add netrc environment variable into the documentation (#1675)
* Adding netrc environment variable documentation

* Update docs/environment_variables.md

Co-authored-by: Tom Christie <tom@tomchristie.com>

* Modifications about netrcfile environment variable

* Change uppercase "A" in netrcfile env variable into lowercase

* Added some words and a dot before "my_netrc" in the console example

* changed a typo "rather that" into "rather than" in advanced.md

* changed netrc environment variable in example part

* modified title for netrc environment variable part in doc

* Deleted the dot in title of netrc environment variable

Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-06-16 07:04:47 +02:00
Marcelo Trylesinski
3231211aa3
👷 Add Python 3.10 beta to the CI (#1682)
* 👷 Add Python 3.10 beta to the CI
* ⬆️ Upgrade pytest from 5.* to 6.*
2021-06-15 11:59:30 +01:00
Marcelo Trylesinski
f818a028f9
Update PULL_REQUEST_TEMPLATE.md (#1681)
Small typo on the PR template.
2021-06-14 15:44:27 +01:00
Marcelo Trylesinski
4a3e5e12c9
Add types-certifi to satisfy mypy 0.902 (#1679) 2021-06-14 14:41:22 +01:00
K900
776dbb578b
docs: slightly clarify event hooks (#1645)
* Be more specific about when the hooks are called
* Explicitly mention and demonstrate that hooks are allowed to modify requests

See https://github.com/encode/httpx/discussions/1637
2021-05-21 10:30:30 +01:00
bli74
9b17671f15
Support HTTP/2 prior-knowledge, using httpx.Client(http1=False, http2=True). (#1624)
* Pass flag http1 to httpcore.

* Update httpcore version, reorder parameter list

Co-authored-by: ebertli <bert.lindemann@ericsson.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-05-11 10:40:46 +01:00
Tom Christie
69409bb8b9
Switch to USE_CLIENT_DEFAULT instead of UNSET. (#1634)
* Add failing test case for 'content=io.BytesIO(...)'

* Refactor peek_filelike_length to return an Optional[int]

* Peek filelength on file-like objects when rendering 'content=...'

* Switch to USE_CLIENT_DEFAULT

* Switch to USE_CLIENT_DEFAULT

* Linting
2021-05-11 10:25:01 +01:00
Jaakko Lappalainen
7025dd1952
fix typo in http2 section (#1632) 2021-05-09 00:49:14 +08:00
Vytautas Liuolia
2e4b308d7a
Make the MockTransport example more robust/correct (#1621) 2021-05-02 20:04:12 +02:00
Tom Christie
2129a9789a
Prefer Content-Length over Transfer-Encoding: chunked for content=<file-like> cases. (#1619)
* Add failing test case for 'content=io.BytesIO(...)'

* Refactor peek_filelike_length to return an Optional[int]

* Peek filelength on file-like objects when rendering 'content=...'
2021-04-30 10:40:42 +01:00
Tom Christie
54f4194c38
Version 0.18.1 (#1617)
* Version 0.18.1

* Update CHANGELOG.md
2021-04-29 13:57:12 +01:00
Tom Christie
589c5a0bc3
Map httpcore transport close exceptions to httpx exceptions. (#1606) 2021-04-29 12:37:37 +01:00
Tom Christie
81f385c484
Add missing timeout=... to top-level httpx.stream() function. (#1613) 2021-04-29 11:08:48 +01:00
Tom Christie
4fbf2751ec
Stream tweak (#1607) 2021-04-28 20:39:11 +01:00
Tom Christie
760af43b4f
Update brotli support to use the brotlicffi package (#1605)
* Update brotli support to use the brotlicffi package
2021-04-28 10:09:29 +01:00
Tom Christie
0c2cb240df
Version 0.18.0 (#1576)
* Version 0.18.0
2021-04-27 15:20:22 +01:00
Tom Christie
0a8b44e67d
Perform port normalization for http, https, ws, wss, and ftp schemes (#1603) 2021-04-27 14:06:23 +01:00
Tom Christie
c927f3e965
Code comments for URL model (#1602) 2021-04-27 11:23:52 +01:00
Tom Christie
e67b0dd15b
Expand URL interface (#1601)
* Expand URL interface

* Add URL query param manipulation methods
2021-04-27 09:01:14 +01:00
Tom Christie
2abb2f214a
Immutable QueryParams (#1600)
* Tweak QueryParams implementation

* Immutable QueryParams
2021-04-26 14:57:02 +01:00
Tom Christie
8fe32c52de
Tweak QueryParams implementation (#1598) 2021-04-26 14:06:12 +01:00
Tom Christie
6e55ca1af9
Escalate 0.17 deprecation warnings to becoming fully deprecated. (#1597) 2021-04-26 11:03:11 +01:00
Tom Christie
39d8ee619e
Differentiate between 'url.host' and 'url.raw_host' (#1590)
* Differentiate between 'url.host' and 'url.raw_host'
2021-04-23 11:00:53 +01:00
Tom Christie
d98e9e7ae7
Support HTTPCore 0.13 (#1588)
* Support HTTPCore 0.13

* Update httpcore minimum version

* Call into 'handle_async_request', not 'arequest'

* Drop unintentional commit

* Update tests
2021-04-21 14:43:18 +01:00
Hannes Ljungberg
2d571046e1
Make Request and Response picklable (#1579)
* Make Request and Response picklable

* fixup! Make Request and Response picklable

* Apply suggestions from code review

* Apply suggestions from code review

* Update tests/models/test_requests.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2021-04-21 11:11:00 +01:00
Tom Christie
9b8f5af759
httpx.ResponseClosed -> httpx.StreamClosed (#1584)
* ResponseClosed -> StreamClosed

* Update docs for StreamClosed
2021-04-21 10:51:35 +01:00
Tom Christie
6a99f6f2b3
Deprecate per-request cookies (#1574)
* Deprecate per-request cookies

* Update docs/compatibility.md

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

* Update httpx/_client.py

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

* Update compatibility.md

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>
2021-04-19 11:18:32 +01:00
Tom Christie
966550b342
For non-streaming cases, populate request.content automatically. (#1583)
* For non-streaming cases, populate request.content

* Linting
2021-04-19 11:13:45 +01:00
Tom Christie
8fd5b71016
Drop StreamContextManager in favour of contextlib.contextmanager/asynccontextmanager (#1577)
* Drop StreamContextManager in favour of using contextlib.contextmanager/asyncontextmanager

* Use type: ignore to avoid mypy errors on 3.6
2021-04-19 11:07:07 +01:00
Hannes Ljungberg
ed19995747
Drop Response.call_next leftover attribute (#1578) 2021-04-16 21:05:34 +02:00
Tom Christie
397aad98fd
Escalate the distinction between data=... and content=... to be stricter (#1573) 2021-04-16 10:06:12 +01:00
Tom Christie
073a3284ab
Drop 'Response(on_close=...)' from API (#1572) 2021-04-16 10:03:37 +01:00
ascopes
4870cb5adf
Update verify parameter description (#1575)
Included that the verify parameter can be an SSL context, as it currently is missing information
that is present in the advanced usage section of the documentation, and the type hints are not
showing up in the interface specification.

Amend verify docs for client
2021-04-16 10:03:08 +01:00
Tom Christie
110ce85652
Stream interface (#1550)
* Add SyncByteStream, AsyncByteStream to interface

* request.stream and response.stream as httpx.SyncByteStream/httpx.AsyncByteStream

* Update httpx/_transports/base.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update httpx/_transports/default.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Move response classes in transports to module level

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2021-04-13 13:14:04 +01:00
Hemanth kumar
535df6c998
Added docs for using client-side ssl certificates (#1570)
* Added docs for using client-side ssl certificates

* Update docs/advanced.md

Co-authored-by: Joe <nigelchiang@outlook.com>

* Update docs/advanced.md

Co-authored-by: Joe <nigelchiang@outlook.com>

* Update docs/advanced.md

* Update docs/advanced.md

* Update docs/advanced.md

Co-authored-by: Hemanth <hemanth@actionfi.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Joe <nigelchiang@outlook.com>
2021-04-13 20:08:22 +08:00
Tom Christie
eb7433bf8e
Fix up license whitespacing (#1563) 2021-04-09 13:08:34 +01:00
Colin Bounouar
4dec569749
Add httpx-auth and pytest-httpx to third party documentation (#1560) 2021-04-07 23:02:44 +02:00
Jaakko Lappalainen
c1ccdbcb81
added docs for startup/shutdown of ASGI apps (#1554) 2021-04-05 10:55:57 +02:00
Jonas Lundberg
52dd95fb5c
Fix extenstions typo in AsyncHTTPTransport (#1549) 2021-04-01 15:32:20 +01:00
Tom Christie
da2a334f2d
Tweak issue config.yml (#1542)
* Update config.yml

* Update the issue template for neater formatting
2021-03-26 15:07:18 +00:00
Tom Christie
e65a33d3cf
Neater close logic (#1541) 2021-03-26 15:06:05 +00:00
Tom Christie
c26425aa58
Handle data={"key": [None|int|float|bool]} cases. (#1539)
* Fix Content-Length for unicode file contents with multipart

* Handle bool and None cases for URLEncoded data

* Handle int, float, bool, and None for multipart or urlencoded data

* Update httpx/_utils.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2021-03-26 12:54:04 +00:00
Tom Christie
c75ddc26c7
Fix Content-Length for unicode file contents with multipart (#1537) 2021-03-25 15:35:02 +00:00
Louis Maddox
68cf1ff88a
Fix typo to match library default (#1535)
Matches default given at https://github.com/encode/httpx/blob/master/httpx/_config.py#L359
2021-03-25 11:03:47 +00:00
Tom Christie
437b55c520
Refine project workflow (#1534)
* Refine project workflow

* Fix link to third party packages
2021-03-24 16:48:57 +00:00
Tom Christie
1a6e254f72
Transport API (#1522)
* Added httpx.BaseTransport and httpx.AsyncBaseTransport

* Test coverage and default transports to calling .close on __exit__

* BaseTransport documentation

* Use 'handle_request' for the transport API.

* Docs tweaks

* Docs tweaks

* Minor docstring tweak

* Transport API docs

* Drop 'Optional' on Transport API

* Docs tweaks

* Tweak CHANGELOG

* Drop erronous example.py

* Push httpcore exception wrapping out of client into transport (#1524)

* Push httpcore exception wrapping out of client into transport

* Include close/aclose extensions in docstring

* Comment about the request property on RequestError exceptions

* Extensions reason_phrase and http_version as bytes (#1526)

* Extensions reason_phrase and http_version as bytes

* Update BaseTransport docstring

* Neaten up our try...except structure for ensuring responses (#1525)

* Fix CHANGELOG typo

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Fix CHANGELOG typo

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* stream: Iterator[bytes] -> stream: Iterable[bytes]

* Use proper bytestream interfaces when calling into httpcore

* Grungy typing workaround due to httpcore using Iterator instead of Iterable in bytestream types

* Update docs/advanced.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Consistent typing imports across tranports

* Update docs/advanced.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2021-03-24 12:36:34 +00:00
Tom Christie
6cb1672459
Fix some cases of merging with base_url (#1532)
* Fix some cases of merging with base_url

* Fix for joining relative URLs

* Improve code comment in _merge_url implementation
2021-03-24 10:51:33 +00:00
laggardkernel
ea5ffce14f
Fix redirect description about HEAD (#1520)
* Fix redirect description about HEAD

* Apply suggestions from code review

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2021-03-21 15:59:31 +01:00
Tom Christie
e05a5372eb
Version 0.17.1 (#1505)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2021-03-15 09:18:29 +00:00
Tom Christie
2e6930fd77
Version 0.17.1 (#1505) 2021-03-12 18:29:20 +00:00
Marat Sharafutdinov
24a55d73c3
Fix CertTypes (#1503) 2021-03-10 09:49:37 +00:00
Simon L
c09e61d50c
Incompatible with httpcore 0.12.0 (#1495) 2021-03-03 12:25:21 +01:00
Florimond Manca
06559f8266
Fix docs syntax highlighting (#1489) 2021-02-28 18:31:20 +01:00
Tom Christie
59f65e2b98
Version 0.17.0 (#1403)
* Version 0.17.0

* Update changelog

* Tweak verbs

* Fix backtick

Co-authored-by: Jamie Hewland <jamie.hewland@hpe.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
Co-authored-by: Jamie Hewland <jamie.hewland@hpe.com>
2021-02-28 17:05:01 +01:00
Adam Hooper
0f280af8b1
map_exceptions in request.aclose() (#1465)
* map_exceptions in request.aclose()
2021-02-17 11:32:43 +00:00
Tom Christie
bd4caa873c
Tweak create_ssl_context defaults. (#1447)
* Version 0.17.0

* create_ssl_config uses trust_env=True default

* Drop CHANGELOG notes, to be included in a version PR instead
2021-02-17 11:27:10 +00:00
Tom Christie
084f35648b
Allow handler to optionally be async when MockTransport is used with AsyncClient (#1449) 2021-02-17 11:10:21 +00:00
Quentin Pradet
645ae4ed9c
Explain SSL_CERT_DIR specific format (#1470)
It's easy to believe that any .pem files there will get picked up automatically, but that's not the case.
2021-02-17 10:11:08 +00:00
Quentin Pradet
5af6ab0038
Explain REQUESTS_CA_BUNDLE migration (#1471) 2021-02-17 10:06:28 +00:00
Aber
02a692aba5
Handle default ports in WSGITransport (#1469)
* Maybe port is `None`

https://www.python.org/dev/peps/pep-3333/#environ-variables

> SERVER_NAME, SERVER_PORT
> When HTTP_HOST is not set, these variables can be combined to determine a default. See the URL Reconstruction section below for more detail. SERVER_NAME and SERVER_PORT are required strings and must never be empty.

* Add unit test

* Compute default port

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2021-02-16 13:33:17 +01:00
Adam Johnson
d964343fa1
Add Changelog link to PyPI page (#1462)
Makes it easy to find out about changes, and it gets a little "present" icon, for example https://pypi.org/project/django-linear-migrations/
2021-02-09 15:10:42 +01:00
David Bordeynik
2847869475
fix-1457: URL's full_path -> raw_path from pull #1285 in docs as well (#1458) 2021-02-07 09:33:45 +01:00
Simon Willison
c52e7d212f
Added missing Request __init__ parameters (#1456)
Co-authored-by: Joe <nigelchiang@outlook.com>
2021-02-07 11:48:23 +08:00
Daniel Saxton
9542a17831
Fix doc capitalization (#1460) 2021-02-07 00:23:30 +01:00
Tom Christie
89fb0cbc69
Add HTTPTransport and AsyncHTTPTransport (#1399)
* Add keepalive_expiry to Limits config

* keepalive_expiry should be optional. In line with httpcore.

* HTTPTransport and AsyncHTTPTransport

* Update docs for httpx.HTTPTransport()

* Update type hints

* Fix docs typo

* Additional mount example

* Tweak context manager methods

* Add 'httpx.HTTPTransport(proxy=...)'

* Use explicit keyword arguments throughout httpx.HTTPTransport

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2021-01-08 10:23:56 +00:00
nkitsaini
181639322e
Update README to reflect new estimate for v1.0 release (#1445)
Co-authored-by: Ankit <ankit@jpqr.com>
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2021-01-07 07:57:25 +01:00
Tom Christie
9c7c2ace99
Add httpx.MockTransport() (#1401)
* Add httpx.MockTransport

* Add docs on MockTransport

* Add pointer to RESPX

* Add note on pytest-httpx

* Tweak existing docs example to use 'httpx.MockTransport'

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2021-01-06 11:04:26 +00:00
Konstantin
3bf18637c1
Remove double "then" in docs/http2.md (#1442) 2020-12-30 17:21:35 +01:00
Florimond Manca
25781a7625
Add troubleshooting guide, with initial proxies entries (#1435)
* Add troubleshooting guide, with initial proxies entries

* Drop unrelated issue
2020-12-29 13:40:53 +01:00
Florimond Manca
7f9bb5f32d
Remove stale reference to "Travis" (#1440) 2020-12-29 13:38:04 +01:00
shan7030
f4165e9e09
Add curio docs in Supported async environments (#1437)
Fixes: #1418
2020-12-25 16:57:14 +01:00
Colton Eakins
e3a7b6d731
Updates compatibility guide to address event hooks (#1436)
* Updates compatibility guide to address event hooks

In `requests`, event hook callbacks can mutate response/request objects. In HTTPX, this is not the case.
Added text to address this difference, and added a link to the best alternate HTTPX offers in this circumstance.
 
More context:
https://github.com/encode/httpx/issues/1343#issuecomment-703223097

* Apply suggestions from code review

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-12-23 20:09:43 +01:00
Gerhard van Andel
3c89b91d6b
Update exceptions.md (#1432) 2020-12-20 20:43:33 +01:00
SarunasAzna
86964054d6
Allow tuple as input of query parameters. (#1426)
* Allow tuple as input of query parameters.

In the documentation it is stated that params can be dict, string or two
tuples. This allows to used two tuples. Previously it was possible to
use only tuple inside a list.

* tests for two tuples

* use isinstance to check the type of query params

* change list|tuple to in Sequence

* update documentation

* fix typing
2020-12-12 18:38:37 +01:00
Florimond Manca
584a40513f
Tweak advanced timeouts docs (#1420) 2020-12-08 14:31:00 +01:00
Florimond Manca
7ea6019c70
Document content encoding differences with Requests (#1416)
* Document content encoding differences with Requests

* Apply suggestions from code review

* Tweak copy for Windows-1252

Co-authored-by: Jamie Hewland <jamie.hewland@hpe.com>

Co-authored-by: Jamie Hewland <jamie.hewland@hpe.com>
2020-12-06 01:16:59 +01:00
Andrés Álvarez
28cbe77676
Add repr to Cookies for displaying available cookies (#1411)
* Add repr to Cookies for displaying available cookies

* Add unit test

* Simplify repr

* Remove file
2020-12-03 22:06:42 +01:00
Florimond Manca
9005bd5df6
Properly encoded slashes when using base_url (#1407) 2020-12-02 13:01:11 +00:00
Tom Christie
d0835da230
Add keepalive_expiry to Limits config (#1398)
* Add keepalive_expiry to Limits config

* keepalive_expiry should be optional. In line with httpcore.
2020-11-25 15:32:37 +00:00
Tom Christie
27df5e49c7
Support for chunk_size (#1277)
* Support iter_raw(chunk_size=...) and aiter_raw(chunk_size=...)

* Unit tests for ByteChunker

* Support iter_bytes(chunk_size=...)

* Add TextChunker

* Support iter_text(chunk_size=...)

* Fix merge with master

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-11-25 15:28:06 +00:00
Tom Christie
c4d2e6fa28
Add support for Mount API (#1362)
* Add support for Mount API

* Add test cases

* Add test case for all: mounted transport

* Use 'transport' variable, in preference to 'proxy'

* Add docs for mounted transports
2020-11-24 10:35:51 +00:00
Tom Christie
2961f267fd
WSGI 'PATH_INFO' should be URL unquoted (#1391) 2020-11-16 09:39:51 +00:00
Tom Christie
0af6d9a254
Use relative links for interlinking markdown files in docs (#1390) 2020-11-13 15:03:30 +00:00
Tom Christie
16f41a2baa
Drop NBSP characters (#1389) 2020-11-13 12:38:03 +00:00
podhmo
f8f543057a
fix code example, in async.md (#1388) 2020-11-11 09:10:33 +00:00
Tom Christie
ceccb964e6
Update butterfly logo (#1382) 2020-11-10 10:16:07 +00:00
Kyungmin Lee
84dca25b8e
Fix typo (#1386)
* Fix typo

three greater-than signs -> three dots

* Fix typo

three greater-than signs -> three dots

* Fix typo

three greater-than signs -> three dots
2020-11-08 10:28:38 +01:00
plotski
589c6e0f2f
Include invalid name/value when raising TypeError in DataField (#1368) 2020-11-05 10:46:22 +00:00
Aber
1bc3b0188a
Add new project to Third Party Packages (#1364)
* Add new project to Third Party Packages

* Fix typo

Co-authored-by: Josep Cugat <jcugat@gmail.com>

* Update third-party-packages.md

Co-authored-by: Josep Cugat <jcugat@gmail.com>
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-10-24 23:32:51 +02:00
Florimond Manca
2f54548dad
Drop unecessary host="localhost" in https_server fixture to fix CI build (#1367) 2020-10-24 23:25:31 +02:00
Tom Christie
07229b8dff
Close AsyncClient properly in test_async_next_request (#1361) 2020-10-14 08:38:53 +02:00
Simon Willison
ca5f524943
Add raw_path to scope in ASGITransport (#1357)
* Add raw_path to scope in ASGITransport, closes #1356

* Tweaked test
2020-10-09 16:46:26 +01:00
Tom Christie
92ca4d0cc6
Version 0.16.1 (#1354) 2020-10-08 13:14:15 +01:00
cdeler
72a1f2c759
Replacing pytest-cov by coverage (#1353) 2020-10-08 13:05:30 +01:00
Tom Christie
0123bca335
Force lowercase ASGI headers (#1351)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-10-08 12:04:10 +01:00
cdeler
7fda99fcef
Correctly handle ipv6 addresses as a part of URL (#1349)
* Correctly handle ipv6 addresses as a host

* Fixed typo

* Added an extra rfc reference

* Update tests/models/test_url.py

* Update tests/models/test_url.py
2020-10-08 11:37:13 +03:00
Tom Christie
dbbcf438cd
Version 0.16 (#1347) 2020-10-06 15:29:40 +01:00
Michael K
160e3088cd
Run tests against Python 3.9 stable (#1348)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-10-06 14:57:30 +01:00
Tom Christie
c725387b2d
Preserve header casing (#1338) 2020-10-06 14:57:10 +01:00
Tom Christie
0eed6a3734
Drop .next()/.anext() in favour of response.next_request (#1339)
* Drop response.next()/response.anext() in favour of response.next_request

* Drop NotRedirectResponse
2020-10-06 14:53:07 +01:00
Tom Christie
2a2bbe58a6
Tighten client closed-state behaviour (#1346)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-10-06 13:38:05 +01:00
Michael K
a3eb0f99dc
Run tests against Python 3.9 and add trove classifier (#1342)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-10-06 13:12:52 +01:00
Jair Henrique
4487ac067c
Add VCR.py to third party packages doc. (#1345) 2020-10-05 22:11:52 +02:00
Changsheng
4b8774041d
Allow covariants of __enter__, __aenter__, and @classmethod (#1336)
* Allow covariants of __enter__, __aenter__, and @classmethod

The problem we currently have is the return type of classes such as
Client does not allow covariants when subclassing or context manager.
In other words:

```python
class Base:
    def __enter__(self) -> Base:  # XXX
        return self

class Derived(Base):
    ...

with Derived() as derived:
   # The type of derived is Base but not Derived. It is WRONG
    ...
```

There are three approaches to improve type annotations.
1. Just do not type-annotate and let the type checker infer
   `return self`.
2. Use a generic type with a covariant bound
   `_AsyncClient = TypeVar('_AsyncClient', bound=AsyncClient)`
3. Use a generic type `T = TypeVar('T')` or `Self = TypeVar('Self')`

They have pros and cons.
1. It just works and is not friendly to developers as there is no type
   annotation at the first sight. A developer has to reveal its type via
   a type checker. Aslo, documentation tools that rely on type
   annotations lack the type. I haven't found any python docuementation
   tools that rely on type inference to infer `return self`. There are
   some tools simply check annotations.

2. This approach is correct and has a nice covariant bound that adds
   type safety. It is also nice to documentation tools and _somewhat_
   friendly to developers. Type checkers, pyright that I use, always
   shows the the bounded type '_AsyncClient' rather than the subtype.
   Aslo, it requires more key strokes. Not good, not good.

   It is used by `BaseException.with_traceback`
   See https://github.com/python/typeshed/pull/4298/files

3. This approach always type checks, and I believe it _will_ be the
   official solution in the future. Fun fact, Rust has a Self type
   keyword. It is slightly unfriendly to documentation, but is simple to
   implement and easy to understand for developers. Most importantly,
   type checkers love it.

   See https://github.com/python/mypy/issues/1212

But, we can have 2 and 3 combined:

```python
_Base = typing.TypeVar('_Base', bound=Base)

class Base:
   def __enter__(self: _Base) -> _Base:
      return self

class Derive(Base): ...

with Derived() as derived:
   ...  # type of derived is Derived and it's a subtype of Base
```

* revert back type of of SteamContextManager to Response

* Remove unused type definitions

* Add comment and link to PEP484 for clarification

* Switch to `T = TypeVar("T", covariant=True)`

* fixup! Switch to `T = TypeVar("T", covariant=True)`

* Add back bound=xxx in TypeVar

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-10-02 16:34:57 +01:00
Tom Christie
65b69fabdb
Fix typo in CHANGELOG (#1337) 2020-10-02 13:15:23 +01:00
Tom Christie
c9354282ba
Version 0.15.5 (#1335)
* Version 0.15.5

* Tweak ChangeLog
2020-10-01 13:57:08 +01:00
Tom Christie
3f51392bea
Add response.next_request (#1334)
* Add response.next_request

* Add response.next_request

* Add response.next_request to the docs
2020-10-01 13:52:03 +01:00
Johannes
32d37cfdf1
Small namedtuple refactor (#1329)
Plus order consistency because why not..
2020-09-27 22:15:04 +02:00
Tom Christie
a7a76fbb12
Version 0.15.4 (#1327)
* Version 0.15.4

* Update CHANGELOG

* Update CHANGELOG

* Update CHANGELOG
2020-09-25 12:34:52 +01:00
Musale Martin
815ef94ed9
Support header comparisons with dict or list. (#1326)
* Support header comparisons with dict or list.

* Add check for no headers item

* Fixup testcases affected by headers comparison using dict or list

* Update test_responses.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-09-25 12:28:34 +01:00
Tom Christie
666cbbdfe8
Fix automatic .read() when Response instances are created with content=<str> (#1324) 2020-09-25 11:29:17 +01:00
Tom Christie
320bfe1d0e
Fix warning test case (#1322) 2020-09-24 18:50:30 +02:00
Johannes
ac7704e78c
Remove seed-isort-config (#1321)
As of isort 5 seed-isort-config is no longer needed.

We can get rid of some explicit config too as it's now more clever.
2020-09-24 11:08:07 +01:00
Florimond Manca
24da8d40ff
Release 0.15.3 (#1318) 2020-09-24 11:39:02 +02:00
daa
78afd08e0f
Properly close stream on async response close (#1316) 2020-09-24 09:42:26 +02:00
Tom Christie
8ceb34f486
Version 0.15.2 (#1314)
* Fix response.elapsed

* Version 0.15.2
2020-09-23 11:28:22 +01:00
Tom Christie
befa57c6f9
Fix response.elapsed (#1313) 2020-09-23 11:26:12 +01:00
Tom Christie
d25f2bfeff
Fix stream unsetting auth (#1312)
* Fix ASGITransport path escaping

* Add failing test case for auth with streaming

* Fix .stream setting auth=None
2020-09-23 11:25:54 +01:00
emlazzarin
257b8fab6a
update arg name to max_keepalive_connections (#1309)
The old name `max_keepalive` was removed recently, so this updates the docs.
2020-09-23 10:42:21 +01:00
Tom Christie
27e67b32e9
Version 0.15.1 (#1308)
* Update CHANGELOG.md

* Update __version__.py
2020-09-23 11:12:50 +02:00
Tom Christie
e53f995994
Fix ASGITransport path escaping (#1307) 2020-09-23 09:37:19 +01:00
Tom Christie
c923f1af91
Update docs for 0.15 (#1306) 2020-09-22 11:57:14 +01:00
Tom Christie
a63b038267
Include 0.15.0 release date. (#1305) 2020-09-22 11:51:52 +01:00
Tom Christie
5d11756585
Include curio support in CHANGELOG (#1304) 2020-09-22 11:49:33 +01:00
Tom Christie
f932af9172
Version 0.15.0 (#1301)
* Version 0.15.0

* Update CHANGELOG.md

Co-authored-by: Jamie Hewland <jamie.hewland@hpe.com>

* Escalate deprecations into removals.

* Deprecate overly verbose timeout parameter names

* Fully deprecate max_keepalive in favour of explicit max_keepalive_connections

* Fully deprecate PoolLimits in favour of Limits

* Deprecate instantiating 'Timeout' without fully explicit values

* Include deprecation notes in changelog

* Use httpcore 0.11.x

Co-authored-by: Jamie Hewland <jamie.hewland@hpe.com>
2020-09-22 11:44:28 +01:00
Tom Christie
8e4a8a1c73
Finesse URL properties (#1285)
* url.userinfo should be URL encoded bytes

* Neater copy_with implementation

* Finesse API around URL properties and copy_with

* Docstring for URL, and drop url.authority

* Support url.copy_with(raw_path=...)

* Docstrings on URL methods

* Tweak docstring
2020-09-21 11:35:25 +01:00
Tom Christie
f3c29416f1
Support Response(text=...), Response(html=...), Response(json=...) (#1297)
* Refactor content_streams internally

* Tidy up multipart

* Use ByteStream annotation internally

* Support Response(text=...), Response(html=...), Response(json=...)

* Add tests for Response(text=..., html=..., json=...)
2020-09-21 11:19:19 +01:00
Tom Christie
8ee08afe96
Update _client.py (#1300) 2020-09-18 14:17:03 +01:00
Tom Christie
d8050ed753
Neater Host header logic on redirects (#1299) 2020-09-18 12:00:43 +01:00
Tom Christie
ed27682686
NetRC lookups should use host, not host+port (#1298) 2020-09-18 11:50:13 +01:00
Tom Christie
354c4cac1f
Refactor content streams (#1296)
* Refactor content_streams internally

* Tidy up multipart

* Use ByteStream annotation internally
2020-09-18 10:50:15 +01:00
Tom Christie
fbb21fb1ae
Drop ContentStream (#1295)
* Drop ContentStream
2020-09-18 08:41:09 +01:00
Tom Christie
e1f7791e97
Requests from transport API (#1293)
* Refactoring to support instantiating requests from transport API

* Minor refactoring
2020-09-17 11:59:42 +01:00
Tom Christie
09f94edd93
encode -> encode_request (#1292) 2020-09-17 09:33:36 +01:00
Tom Christie
ff0febbaa9
Update README.md (#1291)
* Update README.md

* Update index.md
2020-09-17 09:11:41 +01:00
Stephen Brown II
a394df59da
Fix function name in event hooks docs (#1290) 2020-09-16 09:45:20 +08:00
Tom Christie
feb404f86b
Seperate content=... and data=... parameters (#1266)
* Seperate content=... and data=... parameters

* Update compatibility.md
2020-09-15 13:36:10 +01:00
Tom Christie
54f7708e2b
Event hooks (#1246)
* Add EventHooks internal datastructure

* Add support for 'request' and 'response' event hooks

* Support Client.event_hooks property

* Handle exceptions raised by response event hooks

* Docs for event hooks

* Only support 'request' and 'response' event hooks

* Add event_hooks to top-level API

* Event hooks

* Formatting

* Formatting

* Fix up event hooks test

* Add test case to confirm that redirects/event hooks don't currently play together correctly

* Refactor test cases

* Make response.request clear in response event hooks docs

* Drop merge marker

* Request event hook runs as soon as we have an auth-constructed request
2020-09-15 12:05:39 +01:00
Tom Christie
d0fe113945
Drop chardet (#1269)
* Internal refactoring to swap auth/redirects ordering

* Drop chardet for charset detection

* Drop chardet in favour of simpler charset autodetection

* Revert unintentionally included changes

* Update test case

* Refactor to prefer different decoding style

* Update text decoding docs/docstrings

* Resolve typo

* Update docs/quickstart.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-09-15 11:20:19 +01:00
Tom Christie
2d6c30d061
Refactor test_auth.py to use MockTransport class. (#1288)
* Use tests.utils.MockTransport

* Use tests.utils.MockTransport
2020-09-14 17:44:05 +01:00
cdeler
62c6c1c8ad
Refactoring of models api (#1284)
* Made Request.prepare private (i.e. renamed it to _prepare)

* Added bytes as a new possible QueryParamTypes
2020-09-14 12:16:45 +01:00
Tom Christie
b58bd8e8e5
Pin flake8-pie (#1286)
* Pin `flake8-pie`

* Update requirements.txt

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-09-12 19:41:47 +01:00
Tom Christie
c2afd2d9bf
Refactor tests to use MockTransport(<handler_function>) (#1281)
* Support Response(content=<bytes iterator>)

* Update test for merged master

* Add MockTransport for test cases

* Use MockTransport for redirect tests

* Reduce change footprint

* Reduce change footprint

* Clean up headers slightly

* Update requirements.txt

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-09-12 11:16:10 +01:00
Tom Christie
8a5050ea41
Refactor to use isinstance(..., typing.Iterator) (#1282) 2020-09-11 14:37:51 +01:00
Tom Christie
5ee6135256
Support Response(content=<bytes iterator>) (#1265)
* Support Response(content=<bytes iterator>)

* Update test for merged master
2020-09-11 10:28:18 +01:00
Bart
4bd08bed22
Update compatibility.md: mention differing query parameter handling (#1262)
* Update compatibility.md

* Update docs/compatibility.md

* Update docs/compatibility.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-09-10 21:22:14 +02:00
Tom Christie
6006721c6d
Minor decoder refactoring. (#1276)
* Switch auth/redirect methods to follow flow of execution better

* Drop response.decoder property

* Decoder -> ContentDecoder

* Decoder -> ContentDecoder
2020-09-10 15:10:31 +01:00
Tom Christie
59074c7bc0
Progress examples (#1272)
* Progress examples

* Update advanced.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-09-10 12:28:08 +01:00
Tom Christie
930f3773e2
Switch auth/redirect methods to follow flow of execution better (#1273) 2020-09-10 12:44:36 +02:00
cdeler
ed16eb3a3d
Add progress to streaming download (#1268)
* Added last_raw_chunk_size to the Response object (#1208)

* Added example with progress bar (#1208)

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Apply suggestions from code review

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* PR review
Changed last_raw_chunk_size to num_bytes_downloaded ;
Edited the example according to documentaion

* Update docs/advanced.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update docs/advanced.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update docs/advanced.md

* Update docs/advanced.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-09-10 12:16:00 +03:00
Tom Christie
4d950e5780
Swap auth/redirects ordering (#1267)
* Internal refactoring to swap auth/redirects ordering

* Test for auth with cross domain redirect
2020-09-10 09:12:05 +01:00
Florimond Manca
016e4ee210
Add support for sync-specific or async-specific auth flows (#1217)
* Add support for async auth flows

* Move body logic to Auth, add sync_auth_flow, add NoAuth

* Update tests

* Stick to next() / __anext__()

* Fix undefined name errors

* Add docs

* Add unit tests for auth classes

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-09-09 14:37:20 +01:00
cdeler
15187e7c21
Fixed test_multiple_set_cookie (#1270)
* Fixed test_multiple_set_cookie

* Update test_cookies.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-09-09 10:01:39 +01:00
Tom Christie
a783fe5758
Drop request.timer attribute. (#1249)
* Drop request.timer attribute
* Response(..., elapsed_func=...)
2020-09-07 09:06:14 +01:00
cdeler
78f24203ce
Edited documentation about proxy-envs usage (#404) (#1257)
* Edited documentation about proxy-envs usage (#404)

* PR review (#404)

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* PR review (#404)

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Fix typo

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-09-06 14:52:37 +03:00
Florimond Manca
98bb548a73
Use shebang-style headers in environment variables docs (#1260) 2020-09-05 10:30:14 +02:00
Tyler Wozniak
42c66863d0
Raise a proper type error on invalid URL type (#1259)
* Added test for expected URL class behavior

* Updated URL class, tests pass

* Updated to include type in error message
2020-09-04 23:14:59 +02:00
Florimond Manca
8fa87650b2
Drop urllib3 in favor of public gist (#1182)
* Drop urllib3 in favor of public gist

* Drop urllib3 coverage omit

* Drop recommendation to use urllib3 transport during Requests migration

* Add urllib3-transport to 3p pkgs

* Drop urllib3 from dependencies list in README / docs home page

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-09-04 22:56:36 +02:00
Taras Sotnikov
642aabdac0
Fix HTTPError doc (#1255) 2020-09-04 18:22:05 +01:00
Tom Christie
19b863af40
Header refinements (#1248)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-09-02 21:32:48 +01:00
cdeler
def9f1c320
Issue warning on unclosed AsyncClient. (#1197)
* Made Client and AsyncClient checking for being closed in __del__ (#871)

* Changed the AsyncClient closing warning type from ResourceWarning (which is too quiet) to UserWarning  (#871)

* Fixed tests and client's __exit__ and __aexit__ after the difficult merge (#871)

* Update test_proxies.py

* Update test_proxies.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-09-02 13:02:59 +01:00
Tom Christie
ec06ba244e
Version 0.14.3 (#1247) 2020-09-02 12:17:23 +01:00
Tom Christie
de502a44c6
Drop Response(..., request=...) style in test cases. (#1243)
* Drop Response(..., request=...) style in test cases except where required

* Lowercase variable name
2020-09-02 10:10:32 +01:00
Stephen Brown II
46b9282c6e
Add "Early Hints" and "Too Early" to Status Codes (#1244)
To align with Python 3.9: https://docs.python.org/3.9/whatsnew/3.9.html#http

> HTTP status codes 103 EARLY_HINTS, 418 IM_A_TEAPOT and 425 TOO_EARLY are added to http.HTTPStatus. (Contributed by Dong-hee Na in bpo-39509 and Ross Rhodes in bpo-39507.)

da52be4769/Lib/http/__init__.py
2020-09-02 10:04:47 +01:00
Tom Christie
e39a6d9ef4
Use sync client in test cases (#1241)
* Use sync client in test cases

* Use plain client __init__ style in preference to context manager

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-09-01 22:44:52 +02:00
Tom Christie
cf5970336a
Minor test refactoring (#1242) 2020-09-01 22:41:30 +02:00
tbascoul
e0b4528b17
Make the response's request parameter optional (#1238)
* Make the response's request parameter optional

* Fix _models coverage

* Move DecodingError in _models

* Update httpx/_models.py

* Update _models.py

* Update test_responses.py

* Update test_responses.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-09-01 15:14:57 +01:00
Tom Christie
33d339a262
Handle multiple auth headers correctly (#1240)
Handle multiple auth headers correctly
2020-09-01 14:08:10 +01:00
cdeler
fa7661b306
Closing AsyncClient in all tests (#871) (#1219)
All over the AsyncClient invocation is made using context manager or with try-finally block
2020-08-31 18:02:28 +03:00
Eduardo Enriquez
aad8209928
Replace httpx.URL for str in tests (#1237)
Co-authored-by: Eduardo Enriquez (eduzen) <eduardo.enriquez@freshbooks.com>
2020-08-30 08:01:37 +02:00
Moritz E. Beber
9200cb0695
chore: direct questions towards Gitter chat (#1225) 2020-08-27 16:57:05 +01:00
Florimond Manca
f5c27ec7f4
Use and pin black 20 (#1229) 2020-08-27 14:57:53 +01:00
Tom Christie
28c72050e0
Better test case consistency. Prefer import httpx and httpx.Client. (#1222)
* Prefer httpx.Client over httpx.AsyncClient in test cases, unless required.

* Prefer httpx.Client in test_headers

* Consistent httpx imports and httpx.Client usage

* Use 'import httpx' consistently in tests. Prefer httpx.Client.
2020-08-26 14:10:23 +01:00
Tom Christie
534400ee42
Context managed transports (#1218)
* Context managed transports

* Update httpx/_client.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update httpx/_client.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update tests/client/test_client.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update tests/client/test_async_client.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Code comment around close/__enter__/__exit__ interaction

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-08-26 12:05:05 +01:00
Tom Christie
4161d7ace9
Version 0.14.2 (#1207)
* Version 0.14.2

* Update CHANGELOG.md

* Update release notes
2020-08-24 11:03:26 +01:00
cdeler
6e6ece66c6
Made cookies construct-able from a list of tuples (#1211)
* Added test which checks that cookie might be built from a list of tuples (#1209)

* Made cookies constructable from a list of tuples (#1209)
2020-08-24 10:44:48 +01:00
Joe
15c1e42c20
Packaging dependancy tweaks (#1206)
* Remove idna and add brotli to extras

* Update dependency docs

* Update BrotliDecoder error message

* Add nocover

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-08-21 12:30:57 +01:00
Tom Christie
19515e8a8b
Fix request auto headers (#1205)
* Failing test case

* Fix auto_headers in Request.prepare()

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-08-21 12:03:15 +01:00
Tom Christie
f4b407a7c4
Adjust 1.0 expectations. (#1202)
* Adjust 1.0 expectations.

Not sure why I thought saying "expected September 2020" was a good idea.
Like *maybe* that'll happen, but no problem with us taking our time if there's areas we want to be really firm about first. *Eg finer details in the Transport API*.

* Update index.md
2020-08-20 16:30:45 +01:00
Joe
924fa8c9dc
Add stream docstring (#1200)
* Add stream() docstring

* Update docs
2020-08-20 10:28:28 +01:00
Joe
bd8165a1b1
Cleanup docstring leftover for #1183 (#1201)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-08-20 17:03:20 +08:00
Joe
84ca2010e1
Add proxies parameter to top-level API functions (#1198)
* Add `proxies` parameter to top-level API functions

* Fix typo
2020-08-20 15:55:35 +08:00
Tom Christie
25507acdc9
Include underlying httpcore exception tracebacks (#1199) 2020-08-19 17:38:52 +01:00
Joe
03cd88c336
Map httpcore exceptions for Response read methods (#1190)
* Map httpcore exceptions for `Response.iter_*` methods

* Tweak

* Change wording

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-08-19 12:10:04 +01:00
Hugo van Kemenade
d10b7cdc51
Use pycon for Python console code blocks (#1187)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-08-18 15:17:44 +02:00
Tom Christie
9fa95cce6a
Drop redundant hasattr check (#1193) 2020-08-18 11:24:07 +01:00
Florimond Manca
cb620e67c7
Add Client.auth setter (#1185) 2020-08-17 14:51:52 +02:00
Florimond Manca
34ba0e14b0
Document Unix Domain Socket usage (#1186) 2020-08-17 14:49:09 +02:00
Florimond Manca
09c3e90e3b
Add test for HEAD redirect behavior (#1184) 2020-08-16 08:12:17 +02:00
Skyler Dong
a226c60861
Fix #1146 Set default allow_redirects to true (#1183)
* Set default allow_redirects to true

* Set default allow_redirects to true

* Fix line-too-long linting error by removing comments
2020-08-16 07:33:46 +02:00
Simon Willison
891d865044
Corrected docstring on async def request() (#1181) 2020-08-16 01:57:44 +08:00
Josep Cugat
842ccfafe6
Add exported members test (#1179)
Taken from https://github.com/encode/httpcore/pull/156
Added as a followup of https://github.com/encode/httpx/pull/1177#issuecomment-674252582
2020-08-15 12:24:26 +02:00
Joe
1729e45031
Make name in httpx.__init__ private (#1177) 2020-08-15 11:19:41 +08:00
Tom Christie
655773e1c1
Handle URL quoting username and password components. (#1159)
* Handle URL quoting username and password components

* Tweak userinfo quoting
2020-08-11 17:18:12 +01:00
Tom Christie
557ad70242
Include invalid url exception in docs (#1166)
* Advanced transport docs

* Include InvalidURL in exception docs

* Update docs/exceptions.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-08-11 17:14:12 +01:00
Tom Christie
477824aeaa
Advanced transport docs (#1165)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-08-11 17:05:44 +01:00
Joe
5b6d33e29c
Ignore transfer-encoding if content-length presents (#1170)
* Ignore transfer-encoding if content-length presents

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-08-11 23:43:57 +08:00
Tom Christie
c277fb816a
Set __module__ = 'httpx' on everything we expose via the public API. (#1155)
* Set __module__='httpx' on everything we expose via the public API

* Linting

* Update httpx/__init__.py

* Update httpx/__init__.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-08-11 15:40:39 +01:00
Florimond Manca
a4463d044f
Allow disabling auth per-request using auth=None (#1115)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-08-11 15:18:27 +02:00
Tom Christie
db4e417a13
Version 0.14.1 (#1164) 2020-08-11 12:04:03 +01:00
Felix Hildén
f540bd0bcf
Expand client docstrings (#1152)
* Add AsyncClient.aclose to API documentation

* Expand client docstrings
* Add docstrings for all verbs and close methods
* Include parameter merge information and see also

* Update _client.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-08-11 10:08:53 +01:00
Joe
45de714592
Map rfc3986 exceptions (#1163)
* Map rfc3896 exceptions
2020-08-11 09:44:56 +01:00
Tom Christie
7edfe64da6
Minor rfc3986 refactoring (#1157)
* Minor rfc3986 refactoring

* Update _models.py
2020-08-10 16:46:37 +01:00
Tom Christie
4cf74bc405
Fix behaviour with multiple Set-Cookie headers (#1156) 2020-08-10 14:53:51 +01:00
Riccardo Magliocchetti
b9db5e149e
Fix typo in 0.14.0 changelog (#1148) 2020-08-08 08:38:08 +01:00
Tom Christie
a25d924bb9
Minor formatting tweak to CHANGELOG (#1142)
* Update CHANGELOG.md

* Update CHANGELOG.md
2020-08-07 16:12:52 +01:00
Tom Christie
8c7729e42c
Version 0.14.0 (#1083)
* Version 0.14.0

* Update CHANGELOG

* Update CHANGELOG.md

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

* Update CHANGELOG

* max_keepalive_connections

* Add deprecation test

* Update CHANGELOG.md

* Undate dependency pin callout

* Update expected 1.0 release date

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>
2020-08-07 15:50:25 +01:00
Tom Christie
8d9dfb54fc
HTTP/2 becomes fully optional (#1140)
* HTTP/2 becomes fully optional

* Fix linting, coverage
2020-08-07 15:16:21 +01:00
Tom Christie
360b63d4f4
Document exceptions (#1138)
* Document exceptions

* Update exceptions.md
2020-08-07 14:17:49 +01:00
Tom Christie
876e722b24
Update to httpcore 0.10 (#1126)
* Keep HTTPError as a base class for .request() and .raise_for_status()

* Updates for httpcore 0.10

* Update httpx/_exceptions.py

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>

* Use httpcore.SimpleByteStream/httpcore.IteratorByteStream

* Use httpcore.PlainByteStream

* Merge master

* Update to httpcore 0.10.x

Co-authored-by: Stephen Brown II <Stephen.Brown2@gmail.com>
2020-08-07 14:14:11 +01:00
cdeler
0a38695063
Fixed warnings in unit tests suite, caused by #1127 PR (#1137) 2020-08-06 13:41:11 +01:00
cdeler
7123b0f7ba
#1066 make BaseClient's timeout accessible using getters and setters (#1135) 2020-08-06 12:09:21 +01:00
Tom Christie
0e73be83a8
Deprecate URL.is_ssl (#1128) 2020-08-05 19:10:59 +01:00
Tom Christie
3205536a09
Note on differences in proxy keys vs. requests (#1132)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-08-05 19:06:03 +01:00
Tom Christie
a3392c6ea7
Base URL improvements (#1130)
* URL.join(url=...), not URL.join(relative_url=...)

* Fix URL.join()

* Support no argument 'httpx.URL()' usage

* Support client.base_url as a property

* Resolve base_url joining behaviour

* Fix coverage

* Update _client.py
2020-08-05 18:56:25 +01:00
cdeler
7279ed4658
Raise warning if proxy key is eg. "all" instead of "all://". (#1127)
* #1105 added deprecation warning, raised when we try to use proxies={"http": ...} instead of {"http://": ...}. Updated docs and added unit, which check the warning presence

* Update tests/client/test_proxies.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update tests/client/test_proxies.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-08-05 18:41:50 +01:00
Tom Christie
1da46f3de0
Clean up keyword argument name, using URL.join(url=...), not URL.join(relative_url=...). (#1129)
* URL.join(url=...), not URL.join(relative_url=...)

* Fix URL.join()
2020-08-05 18:32:34 +01:00
Florimond Manca
78cf16ace9
Drop HSTS Preloading (#1110)
* Drop HSTS Preloading

* Update test_client.py

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-08-05 13:05:45 +01:00
Tom Christie
d7aa6e0189
Keep HTTPError as a base class for .request() and .raise_for_status() (#1125) 2020-08-03 20:06:18 +01:00
Florimond Manca
0fb28e8cab
Refine docs phrasing on proxy routing (#1124) 2020-08-03 10:29:03 +01:00
Florimond Manca
70cdd95006
Revamp proxies documentation (#1123) 2020-08-02 14:02:43 +02:00
Tom Christie
0e7730bccc
Setting app=... or transport=... should bypass environment proxies. (#1122)
* Setting app= or transport= should bypass proxies

* Tweak
2020-08-02 12:42:36 +01:00
Tom Christie
e5f87434a5
Use get_list consistently (#1119)
* Use get_list consistently

* Ensure DeprecationWarning on getlist vs. get_list
2020-08-02 11:33:50 +01:00
Joe
d76b2c2fb7
Handle bare env proxy hostname gracefully (#1120) 2020-08-02 10:48:09 +01:00
Tom Christie
682cad39eb
Cleaner no proxy support (#1103)
* Add internal URLMatcher class

* Use URLMatcher for proxy lookups in transport_for_url

* Docstring

* Pin pytest

* Add support for no-proxies configurations

* Don't call should_not_proxy on each request

* Drop print statements

* Tweak comment

* Tweak comment on domain wildcards

* Update httpx/_utils.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Pull test_should_not_be_proxied cases into test_proxies_environ

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-08-02 10:00:45 +01:00
Florimond Manca
f67e925f72
Rename PoolLimits to Limits (#1113)
* Rename PoolLimits to Limits

* Lint

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-08-01 20:44:58 +01:00
Florimond Manca
26cd4f54e8
Switch to more concise Timeout parameters (#1111)
* Switch to more concise Timeout parameters

* Update docs

* Rename attributes

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-08-01 19:59:01 +01:00
Florimond Manca
a9284214e2
Make Headers.keys(), Headers.values() and Headers.items() return set-like views (#1114) 2020-08-01 20:15:15 +02:00
Florimond Manca
a110072bba
Client.trust_env becomes read-only (#1112) 2020-08-01 12:04:38 +02:00
Florimond Manca
71d5234305
Rename URLMatcher -> URLPattern (#1109) 2020-08-01 11:07:31 +02:00
Tom Christie
c6022dea9f
Drop the auto-prompting 'do you want to run scripts/lint now' (#1107) 2020-08-01 09:39:28 +01:00
Tom Christie
9409900898
Exception hierarchy (#1095)
* Exception heirachy

* Exception heirarchy

* Formatting tweaks

* Update httpx/_exceptions.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update httpx/_exceptions.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update httpx/_exceptions.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update httpx/_exceptions.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-31 12:57:49 +01:00
Tom Christie
2ba9c1ed90
Consistent multidict methods (#1089)
* Consistent multidict methods

* Consistent multidict methods and behaviour

* Update httpx/_models.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update httpx/_models.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-31 11:46:35 +01:00
Tom Christie
dba83d45a5
httpx.Timeout must include a default (#1085)
* httpx.Timeout must include a default

* Tweak docstring

* Gentle deprecation for mandatory default on httpx.Timeout(...)

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-31 11:41:53 +01:00
Tom Christie
e1a7d5a4ae
Use public API only from transports (#1096)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-31 11:30:55 +01:00
Tom Christie
16893e414f
Add support for no-proxy configurations (#1099)
* Add internal URLMatcher class

* Use URLMatcher for proxy lookups in transport_for_url

* Docstring

* Pin pytest

* Add support for no-proxies configurations
2020-07-31 10:21:11 +01:00
Tom Christie
df54890c15
URL matching (#1098)
* Add internal URLMatcher class

* Use URLMatcher for proxy lookups in transport_for_url

* Docstring

* Pin pytest

* Update httpx/_utils.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-31 10:11:49 +01:00
Tom Christie
9728d8960f
Ignore PermissionError in netrc_info() (#1104)
* Ignore PermissionError in netrc_info()
2020-07-30 15:40:13 +01:00
Tom Christie
31e587c037 Pin pytest 2020-07-30 15:31:03 +01:00
Can Sarıgöl
926a55a84f
Included create_ssl_context function to create the same context with SSLConfig and serve as API (#996)
* Included create_ssl_context function to create the same context with SSLConfig and serve as API.

* Changed create_ssl_context with SSLConfig into the client implementation and tests.

* Dropped the __repr__ and __eq__ methods from SSLConfig and removed SSLConfig using from tests

* Fixed test issue regarding cert_authority trust of ssl context

Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-27 19:46:46 +01:00
Tom Christie
35f09d1394
Single consistent name for status codes (#1088)
* Single consistent name for status codes
* Gentle deprecation for httpx.StatusCode
2020-07-27 14:35:01 +01:00
Tom Christie
2e60e145d7
Drop extraneous proxy argument on URLLib3Transport. (#1090)
* Update urllib3.py

Drop `proxy` argument that has been accidentally left over on `URLLib3Transport`.
It's not used anywhere, and it's not relevant since we seperate `URLLib3Transport` and `URLLib3ProxyTransport` classes.

* Update urllib3.py
2020-07-26 20:45:17 +01:00
Tom Christie
6421575f5f
Tighten public API on auth classes (#1084)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-26 19:05:33 +01:00
Tom Christie
50b17fdd65
Keep Proxy._build_auth_header as a private method (#1087) 2020-07-26 18:27:07 +01:00
Tom Christie
2d491c9e7d
Full coverage, with exception of URLLib3Transport (#1086) 2020-07-24 15:24:06 +01:00
Tom Christie
c089480260
URL.port becomes Optional[int] (#1080)
* URL.port becomes Optional[int], not int

* Minor _transport_for_url refactor

* Add docstring
2020-07-24 11:42:13 +01:00
Tom Christie
696c1eff03
Parameterize invalid URL tests (#1079) 2020-07-23 10:40:00 +01:00
Tom Christie
7e6e35160f
Drop URL(allow_relative=bool) (#1073)
* Drop URL(allow_relative=bool)

* Update httpx/_models.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Linting

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-23 10:16:51 +01:00
Tom Christie
083d2e9aa0
Include 'request' on TooManyRedirect and RequestBodyUnavailable (#1077) 2020-07-23 09:55:21 +01:00
Tom Christie
01287bac99
Refactor map_exceptions (#1076) 2020-07-23 09:55:07 +01:00
Tom Christie
247ee0dc49
Drop private Origin model (#1070)
* Drop private Origin model

* Drop Origin from docs

* Update tests/test_utils.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Drop full_path test

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-23 09:54:45 +01:00
Tom Christie
20f4911e80
Fixes for LineDecoding (#1075)
* Fixes #1033 - Ensure that text that spans 3 invocations before newline is handled - don't clobber the buffer between invocations that haven't seen any lines.
This seems like a one character fix + the test.

* Update the tests.

* Undo formatting/indentation.

* Update long comment.

* Fix trailing cr line decoding

Co-authored-by: Sheridan C Rawlins <scr@verizonmedia.com>
2020-07-23 09:45:35 +01:00
Tom Christie
bd339212de
client.netrc should be private (#1071) 2020-07-23 09:44:54 +01:00
Tom Christie
8ed7e52a37
Drop fullpath setter (#1069) 2020-07-23 09:43:11 +01:00
François Voron
0641606619
Skip HSTS preloading on single-label domains (#1074)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-07-21 10:40:10 +01:00
Sheridan C Rawlins
ad5e6eb8b0
Fixes #1033 - Ensure that text that spans 3 invocations before newline is handled - don't clobber the buffer between invocations that haven't seen any lines. (#1034)
Fixes #1033 - Ensure that text that spans 3 invocations before newline is handled - don't clobber the buffer between invocations that haven't seen any lines.
2020-07-21 10:33:45 +01:00
François Voron
27b0dbc22d
Raise HTTPStatusError in raise_from_status (#1072) 2020-07-20 13:10:57 +01:00
Tom Christie
a89d4ad625
More public API tightening (#1065) 2020-07-17 11:19:24 +01:00
Tom Christie
e107e0f842
Tighten public client methods (#997)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-17 10:20:52 +01:00
Florimond Manca
09672a99dd
Use relative tests directory references (#1052)
* Use relative tests directory references

* Use absolute TESTS_DIR

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-07-16 16:28:31 +02:00
Tom Christie
ff93a011a4
Support QueryParams(None) (#1060) 2020-07-16 11:16:29 +01:00
Tom Christie
ccd4711a0a
Drop deprecated proxies API (#1058) 2020-07-16 09:44:41 +01:00
Tom Christie
3f96e74961
Drop deprecated API (#1057)
* Drop ASGIDispatch, WSGIDispatch, which has been replaced by ASGITransport, WSGITransport.
* Drop dispatch=... on client, which has been replaced by transport=...
* Drop soft_limit, hard_limit, which have been replaced by max_keepalive and max_connections.
* Drop Response.stream and Response.raw, which have been replaced by aiter_bytes and aiter_raw.
* Drop internal usage of as_network_error in favour of map_exceptions.
2020-07-15 12:19:56 +01:00
Florimond Manca
df24a0fc96
Add "manual streaming mode" docs (#1046)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-07-07 14:37:12 +02:00
Florimond Manca
0296c2bbb9
Type check tests (#1054) 2020-07-07 11:10:43 +02:00
Florimond Manca
3230cb3133
Add missing Response.next() (#1055) 2020-07-07 10:54:50 +02:00
Josep Cugat
330a82508d
Pin isort version (#1053) 2020-07-05 18:19:26 +02:00
Taneli Hukkinen
6a20cfc0b8
Simplify bytes to hex string conversion (#1049)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-05 12:16:55 +02:00
Taneli Hukkinen
2f0e0480da
Remove needless whitespace strips (#1048) 2020-07-05 11:58:44 +02:00
Taneli Hukkinen
8dea2c23d7
Make isort configuration compatible with isort v5 (#1050)
* Make isort configuration compatible with isort v5

* Use isort's black profile
2020-07-04 22:31:09 +01:00
Florimond Manca
bacc2d1835
Map HTTPCore exceptions (#1044)
* Map HTTPCore exceptions

* Expose new TimeoutException
2020-07-03 15:56:10 +02:00
Florimond Manca
fab427972b
Add exceptions missing from top-level package (#1045)
* Expose missing exceptions: CloseError, ConnectError, ReadError, WriteError

* Lint
2020-07-03 15:18:24 +02:00
Taneli Hukkinen
6cf9039593
Improve list/dict comprehensions (#1036)
* Improve list/dict comprehensions

* Dont make needless list() conversions before bytes.join()

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-07-02 17:45:00 +02:00
Taneli Hukkinen
c53172d716
Remove unused type checker ignores (#1038)
* Remove unused type checker ignores

* Add back a type ignore required in py36
2020-07-02 17:30:15 +02:00
Taneli Hukkinen
cfd07cc290
Remove .encode("ascii") call from null byte (#1037) 2020-06-29 23:15:22 +02:00
euri10
4d287956fd
Add support for multiple files per POST field (#1032)
* Changed RequestFiles type

* Changed RequestFiles type 2

* Added test for multiple files same field

* Lint

* Mypy no idea

* Added doc

* Fixed some docs typos

* Checking the right instance type and deleting the mypy ignore

* Docs clarification

* Back on images form field, with other files modified
2020-06-24 19:17:27 +02:00
Florimond Manca
0f7d644b8d
Add note on data fields in multipart form encoding (#1022)
* Add note on data fields in multipart form encoding

* Fix message
2020-06-15 20:40:17 +02:00
Florimond Manca
b481166481
Refactor ASGITransport.request() (#1021) 2020-06-13 19:59:09 +02:00
Florimond Manca
838f417ce0
Run ASGI tests on trio too (#1020) 2020-06-13 19:42:36 +02:00
Yeray Diaz Diaz
3196be937a
Update contributing docs (#1011)
* Update contributing docs

* Update docs/contributing.md

Co-authored-by: Josep Cugat <jcugat@gmail.com>

* Update docs/contributing.md

Co-authored-by: Josep Cugat <jcugat@gmail.com>

* Update docs/contributing.md

Co-authored-by: Josep Cugat <jcugat@gmail.com>

Co-authored-by: Josep Cugat <jcugat@gmail.com>
2020-06-02 10:31:56 +01:00
Josep Cugat
620b0670db
Increase test coverage - take 2 (#1012)
* Fix HttpError -> HTTPError typo

* Increased test coverage

* Increase coverage threshold

* Reuse sync/async transport code in test_auth.py

* Removed close_client check inside StreamContextManager

It's never set as True when used in async

* Reuse sync/async transport code in test_redirects.py
2020-06-02 10:24:45 +01:00
Josep Cugat
8c84210555
Increased test coverage & cleanup (#1003)
* Remove unused/untested headers copy() method

Last usage was removed in #804

* Remove unused premature_close server endpoint

Last usage was removed in #804

* Increased test coverage

* Revert removal of headers copy() method

Documented and added tests for it.

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-05-30 23:22:03 +02:00
Josep Cugat
89a8100b6c
Replace remaining occurrences of dispatch with transport (#1010)
* Replace remaining occurrences of dispatch with transport

* Remove unused AsyncDispatcher

Was removed in #804

* Remove hard_limit warning in test
2020-05-30 23:18:48 +02:00
Alexander Pushkov
8ed6904646
Remove the UDS section from docs (#1009) 2020-05-30 20:43:15 +02:00
Tom Christie
56d880030e
Version 0.13.3 (#1006) 2020-05-29 11:28:27 +01:00
Tom Christie
1e1a56f99b
Include missing keepalive_expiry configuration (#1005) 2020-05-29 11:10:36 +01:00
Josep Cugat
093cb42500
Improve error when redirecting with custom schemes (#1002)
Fixes #822
2020-05-28 13:11:17 +02:00
Hasan Ramezani
21d7e16559
Fixed mypy errors in test_async_client.py and test_client.py (#985) 2020-05-27 21:29:52 +02:00
Tom Christie
f5e1aa5517
Version 0.13.2 (#1001) 2020-05-27 15:58:41 +01:00
Tom Christie
3721a7869e
Use AnyStr where appropriate (#999)
* Use AnyStr where appropriate

* Update httpx/_types.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update _types.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-05-27 15:17:08 +01:00
Tom Christie
7c8158a852
Add "Content-Length: 0" on POST, PUT, PATCH if required. (#995)
* Add Content-Length: 0 on POST, PUT, PATCH if required.

* Update tests/client/test_client.py

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-05-26 15:03:55 +01:00
Florimond Manca
66a4537959
Add mypy config for tests directory (#991) 2020-05-25 08:39:51 +02:00
Florimond Manca
c14350336e
Tweak handling of None values in timeout type hints (#992) 2020-05-25 07:56:20 +02:00
Florimond Manca
440b5ab95f
Remove unused concurrency test utils (#989) 2020-05-24 11:38:24 +02:00
Florimond Manca
aa630d36c2
Tweak Question issue template (#988)
"Not getting an aswer" from the community chat is not a required item to submit a question issue. :-)
2020-05-24 10:57:49 +02:00
Tom Christie
a4145ce2fb
Add 'http2' for Client (#982) 2020-05-22 11:52:08 +01:00
Tom Christie
ebc9c29365
Version 0.13.1 (#981) 2020-05-22 10:34:01 +01:00
Jamie Hewland
e724abdcd1
Fix pool option warnings for default connection pool (#980)
* warn_deprecated: Set stacklevel=2 to get real source of warning

* Use new pool limit options in default pool

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-05-22 10:31:21 +01:00
Tom Christie
c30acf09fb
Add urllib3proxy transport to init (#979)
* Update HTTP/2 docs

* Include URLLib3ProxyTransport in top-level API

* Linting
2020-05-22 10:30:56 +01:00
Tom Christie
bafa32aa67 Update HTTP/2 docs 2020-05-22 10:16:08 +01:00
Tom Christie
2c2c6a71a9
Version 0.13 (#971)
* Version 0.13

* Update docs to 0.13 as the latest

* Whitespacing tweak

* More explicit notes about transport API renamings

* Update changelog

Co-authored-by: florimondmanca <florimond.manca@gmail.com>
2020-05-22 09:41:02 +01:00
Tom Christie
b57482d7fe Add URLLib3ProxyTransport 2020-05-22 09:36:04 +01:00
Florimond Manca
9f58afd8f9
Tighten multipart implementation types (#975) 2020-05-21 17:41:36 +02:00
Florimond Manca
f1b3d74abb
Tweak files type hints (#976) 2020-05-21 17:34:45 +02:00
Florimond Manca
ab9ace2749
Fix bytes support in multipart uploads (#974) 2020-05-21 16:25:31 +02:00
Florimond Manca
a58be59adb
Add note on streaming uploads (#973) 2020-05-21 16:13:16 +02:00
Tom Christie
3686f2d454
Drop URLLib3Dispatch compat shim (#972) 2020-05-21 14:30:43 +01:00
Tom Christie
225f1e259a
Use URLLib3 configuration options for URLLib3 transport (#970)
* Use URLLib3 configuration options for URLLib3 transport

* Drop redundant code
2020-05-21 14:20:16 +01:00
Tom Christie
99a5c78bbb
Update dependencies in docs (#969)
* Update dependencies in docs

* Update README.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Update docs/index.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-05-21 13:43:34 +01:00
Tom Christie
991915a935
Rename pool limit options (#968)
* Pass proxy_url

* Rename hard_limit/soft_limit

* Use 'warn_deprecated' function

* Update PoolLimits docs

* Linting

* Update httpcore dependancy

* Update port in Transport API to be 'Optional[int]'
2020-05-21 13:26:20 +01:00
Tom Christie
9b6605c3d5
Updates for httpcore 0.9 interface. (#967)
* Pass proxy_url

* Update httpcore dependancy

* Update port in Transport API to be 'Optional[int]'
2020-05-21 13:21:43 +01:00
Yeray Diaz Diaz
d2816c9c48
Transport API (#963)
* Deprecate Client arg 'dispatch' and use 'transport'

* Remove line in test from coverage

* Document custom transports

* _dispatch > _transports

Also rename *Dispatch classes to *Transport and added aliases

* Fix linting issues

* Missed one _transports import

* Promote URLLib3Transport to public API

* Remove duplicate arg doc

* Assert that urllib3 is imported to use URLLib3Transport

* `AsyncClient`, not asynchronous `Client`

* Add warning category to warn calls

* Update docs/advanced.md

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>

* Add warn_deprecated utility function

* Amend docs references to dispatch

* Add concrete implementation example

* Clearer transport implementation description

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-05-21 12:22:17 +01:00
Tom Christie
ba073c8a46
Clean up docs scripts (#953)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-05-15 20:49:08 +01:00
Tom Christie
0a8b0c65bd
Enforce coverage (#955)
* Enforce coverage when running tests

* Enforce test coverage
2020-05-15 16:17:33 +01:00
Tom Christie
51b70b3fca
If scripts/check fails then prompt and autofix. (#952)
* If scripts/check fails then prompt and autofix

* Update scripts/test

Co-authored-by: Yeray Diaz Diaz <yeraydiazdiaz@gmail.com>

* Update test

Co-authored-by: Yeray Diaz Diaz <yeraydiazdiaz@gmail.com>
2020-05-15 13:22:46 +01:00
Yeray Diaz Diaz
790a3ead5d
Document pool_limits and SSL context on verify (#942)
Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-05-13 11:53:35 +01:00
Tom Christie
d6b3794395
Attempt to resolve test flakiness (#946) 2020-05-12 16:02:25 +01:00
Tom Christie
97d488a95c
Version 0.13.0.dev2 (#941)
* Update __version__.py

* Update CHANGELOG.md

* Update setup.py

* Update CHANGELOG.md
2020-05-12 10:20:22 +01:00
Jamie Hewland
d568ecda53
ASGI: Wait for response to complete before sending disconnect message (#919)
* asgi: Wait for response to complete before sending disconnect message

* Dial back type checking + remove concurrency module

* Remove somewhat redundant comment
2020-05-12 10:06:53 +01:00
Yeray Diaz Diaz
560b119d32
Version 0.13.0.dev1 (#935) 2020-05-06 15:49:00 +01:00
Yeray Diaz Diaz
4af3dd7abc
Pass http2 flag to proxy dispatch (#934) 2020-05-06 15:08:38 +01:00
Yeray Diaz Diaz
afbd4c846a
Document dev proxy setup (#933) 2020-05-06 12:11:05 +01:00
Tom Christie
d34c89a819
Update test-suite.yml (#925)
Run Test Suite on commits to master.
2020-05-04 12:52:23 +01:00
Florimond Manca
8c182e271f
Mark Response.json() as returning Any (#923) 2020-05-02 14:29:56 +02:00
Ryan Balfanz
3b9ebe0523
Fix small typo (#920) 2020-05-01 20:37:01 +02:00
Tom Christie
30102612b7
Fix VERSION_FILE in publish script. (#916) 2020-05-01 13:47:29 +01:00
Jamie Hewland
8710079d5d
asgi: Send http.disconnect message on receive() after response complete (#909)
Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-05-01 11:51:35 +02:00
Tom Christie
bed3d8bd38
Version 0.13.0.dev0 (#915)
* Version 0.13.0.dev0

* Bump httpcore

* Include packaging in requirements

* Include note on UDS support

* Update CHANGELOG
2020-04-30 17:09:49 +01:00
Tom Christie
e586e5df37
Publish workflow (#913)
* Publish workflow

* Set PYTHONPATH to allow auto docs to import package

* Seperate install, build, publish steps
2020-04-30 15:22:54 +01:00
Nicholas Guriev
97cf9dadd2
Proper warning level of deprecation notice (#908)
This enables us to control emitted messages via the PYTHONWARNINGS
environment variable or by -W option.
2020-04-25 09:42:03 +02:00
Tom Christie
c6872e1fea
Update README.md 2020-04-24 16:47:24 +01:00
Tom Christie
19bf2112a1
Test Suite on GitHub Actions (#907)
* Test Suite on GitHub Actions

* Add linting checks

* Update badges. Drop unused codecov, pending GitHub action support,
2020-04-24 16:46:45 +01:00
David Vo
9085582211
Avoid hasattr in hot loop in Brotli decoder (#906) 2020-04-19 09:03:56 +02:00
Florimond Manca
4c01117dd2
Move types definitions to a dedicated module (#902) 2020-04-15 13:12:09 +02:00
Florimond Manca
5829ecb648
Implement streaming multipart uploads (#857)
* Implement streaming multipart uploads

* Tweak seekable check

* Don't handle duplicate computation yet

* Add memory test for multipart streaming

* Lint

* 1 pct is enough

* Tweak lazy computations, fallback to non-streaming for broken filelikes

* Reduce diff size

* Drop memory test

* Cleanup
2020-04-10 20:40:04 +02:00
Florimond Manca
af75908b7a
Drop concurrency backends (#901)
* Drop concurrency backends

* Put back as_network_error for use in urllib3 dispatcher
2020-04-10 18:48:01 +02:00
Tom Christie
3046e920ea
Httpcore interface (#804)
* First pass as switching dispatchers over to httpcore interface

* Updates for httpcore interface

* headers in dispatch API as plain list of bytes

* Integrate against httpcore 0.6

* Integrate against httpcore interface

* Drop UDS, since not supported by httpcore

* Fix base class for mock dispatchers in tests

* Merge master and mark as potential '0.13.dev0' release
2020-04-08 13:32:10 +01:00
Tom Christie
631ba97635
Revert "Add content-length header for empty bytestream (#866)" (#898)
This reverts commit 939f3ce7ce.
2020-04-08 13:15:22 +01:00
Florimond Manca
4ef5de4002
Add issue templates (#880)
* Add issue templates

* Fix typos/phrasing

* Swap docs and GH search

Co-Authored-By: Yeray Diaz Diaz <yeraydiazdiaz@gmail.com>

* Drop code example in favor of prose

* Update .github/ISSUE_TEMPLATE/2-bug-report.md

Co-Authored-By: Yeray Diaz Diaz <yeraydiazdiaz@gmail.com>

Co-authored-by: Yeray Diaz Diaz <yeraydiazdiaz@gmail.com>
2020-03-29 15:55:12 +02:00
Ed Singleton
94323f98ac
Fix support for generator-based WSGI apps (#887)
* Handle generator WSGI app

* Lint code

* Add type annotations

* Add more tests

* Refactor test to use application_factory

* Remove content length as it's misleading

* Add test for WSGI generator

* Add test for empty generator

* Remove previous tests

* Move docstring to a comment

* Fix whitespace

* Fix name of function

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update tests/test_wsgi.py

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update tests/test_wsgi.py

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update httpx/_dispatch/wsgi.py

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-03-29 13:13:01 +02:00
Florimond Manca
1c9b852783
Improve type hinting for HTTPError.request (#886) 2020-03-29 13:12:05 +02:00
Florimond Manca
d63108d613
Fix casing nits (#889) 2020-03-29 12:46:38 +02:00
Florimond Manca
3acd72a6ee
Remove stitches in requirements.txt (#888)
* Remove stitches in requirements.txt

* Put back attrs pin
2020-03-29 12:44:56 +02:00
Florimond Manca
8f400c269e
Drop usage of AnyStr (#882) 2020-03-29 11:39:31 +02:00
Ed Singleton
fc980e7792
Fix Client Docstring and add AsyncClient to API docs (#883)
* Fix docstring of Client

* Add AsyncClient to API docs

* Add AsyncClient to ToC

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-03-28 17:22:10 +01:00
Dima Boger
939f3ce7ce
Add content-length header for empty bytestream (#866)
Also, fixed related tests
2020-03-28 16:05:06 +01:00
Florimond Manca
21f533774f
Add 'Third Party Packages' docs page (#876) 2020-03-26 15:43:04 +01:00
Srikanth Chekuri
5cf1acc403
Add docs on request instances to Requests compatibility guide (#823)
* Update docs/compatibility.md

* Add prepare_request equivalent of httpx to compatibility document

* Add difference b/w httpx.Request and requests.Request arguments to compatibility document

* Update docs/compatibility.md
* Remove Request arguments section
* Add new section Request instantiation with necessary info

* Update docs/compatibility.md

Commit the suggested changes

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-03-25 22:33:16 +01:00
Tom Christie
23486b5438
Version 0.12.1 (#868) 2020-03-19 20:55:13 +00:00
Dima Boger
430285f55b
Fix multipart edge cases with data={} and/or files={} (#861)
* Add reproducible test example for empty multipart

* Possible fix for empty combination of files/data

* Return bytestream with empty data/files

* Remove content-length in test

Co-authored-by: florimondmanca <florimond.manca@gmail.com>
2020-03-16 22:34:50 +01:00
Florimond Manca
43ec09c3cb
Fix type checking breakage (#865) 2020-03-15 10:30:58 +01:00
Tom Christie
a82adcc933
Update CHANGELOG.md (#854) 2020-03-09 14:51:04 +00:00
Tom Christie
6e40c1b26a
Version 0.12 (#852)
* Version 0.12

* Update docs to reference 0.12
2020-03-09 10:18:34 +00:00
Tom Christie
970886629d
Version 0.12 (#849)
* Version 0.12

* Tweak whicespacing
2020-03-06 09:10:50 +00:00
Tom Christie
ae45c23cc5
Do not mark wheel as universal (#851) 2020-03-05 12:56:47 +00:00
Evan Lurvey
707e54c484
Enable NO_PROXY environment variable support (#835)
* Enabling NO_PROXY env support

* Enabling NO_PROXY env var support and writing tests

* Update tests/client/test_proxies.py

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update tests/client/test_proxies.py

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-03-02 16:51:34 +01:00
Tom Christie
3333152b54
Don't support broken dict-of-dicts case for data argument (#811) 2020-03-02 10:34:04 +00:00
Florimond Manca
7256366ba9
Revamp docs on Client instances (#836)
* Revamp docs on Client instances

* Apply suggestions from code review

Co-Authored-By: Hugo van Kemenade <hugovk@users.noreply.github.com>

* Reword 'Why use a Client?' section

* Apply suggestions from code review

Co-Authored-By: Hugo van Kemenade <hugovk@users.noreply.github.com>

Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2020-03-01 21:38:01 +01:00
Yeray Diaz Diaz
112fb9c07d
Remove proxies from Request docstring (#839) 2020-03-01 20:34:16 +01:00
Florimond Manca
53804173bb
Drop backend parameter on AsyncClient (#791)
* Drop backend parameter on AsyncClient

* Fix master merge

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-02-27 20:42:18 +00:00
Tom Christie
2714f32238
Close proxy dispatch classes on Client.close() (#826) 2020-02-27 20:41:28 +00:00
zueve
19555f6103
Forward cert param to SSLConf if verify=False (#796) 2020-02-27 11:32:10 +00:00
Piotr Staroszczyk
50d337e807
When setting headers, fallback to utf-8 if no encoding is specified yet. (#820)
* Headers: fallback to utf8 encoding

* Headers.__setitem__: use utf8 only if encoding is not set
2020-02-24 12:29:51 +00:00
Piotr Staroszczyk
efe9f61bc2
Drop RedirectLoop exception (#819)
* drop RedirectLoop exception

* tests is package to allow run it easly

* bring back test for redirect loop
2020-02-24 10:09:52 +00:00
Tom Christie
db33c071e1
Add 'chat' to docs (#818) 2020-02-19 11:38:14 +00:00
Primož Godec
3e12fd0656
Adding NetworkError to __init__ (#814) 2020-02-12 15:19:48 +00:00
George Kettleborough
b3db9ff0b6
Add Auth.requires_response_body attribute (#803)
* Add Auth.requires_response_body attribute

If set then responses are read by the client before being sent back into the auth flow

* Update tests and docs

* PR fixes

* Change example methods
2020-02-10 12:10:11 +00:00
Colin Bounouar
cfe2278096
Document that iter_* methods are synchronous (#807) 2020-02-06 13:40:42 +01:00
Florimond Manca
6614831739
Use a base_url in app dispatcher examples (#799) 2020-02-03 13:05:56 +01:00
Florimond Manca
82dc6f32f8 Switch to private module names (#785)
* Rename modules

* Update names in package

* Fix tests

* Review docs
2020-01-28 14:34:43 +00:00
Tim Gates
c2eb0bd40f Fix simple typo: conncurrent -> concurrent (#793)
Closes #792
2020-01-26 01:24:41 -05:00
Jung Sang-jun
b1e99a5cf9 Fix ProxyMode for code in dispatch/proxy_http (#788)
FORWARD_ONLY seems correct
2020-01-22 10:50:06 +00:00
Yeray Diaz Diaz
b23420392e
Detect credentials in proxy URLs and create Proxy-authorization header (#780)
* Detect auth in proxy URLs and create Proxy-authorization header

* Add credentials and SOCKS details to proxy documentation

* Use URL.copy_with to remove credentials from URL
2020-01-20 13:50:50 +00:00
Tom Christie
b128bfaf21
Version 0.11.1 (#773)
* Version 0.11.1

* Update changelog

* Update CHANGELOG.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-01-17 21:31:56 +00:00
Tom Christie
1f8fb28d93
Handle redirect with malformed Location headers missing host. (#774) 2020-01-17 11:42:51 +00:00
Tom Christie
5a63540e8a
Fix for streaming a redirect response body with allow_redirects=False (#766) 2020-01-17 09:45:37 +00:00
Tom Christie
be82404168
Fix urllib3 proxy instantiation (#763) 2020-01-14 14:09:08 +00:00
Florimond Manca
7d84408cda Add an async example to intro snippets (#750) 2020-01-14 09:01:09 +00:00
Tom Christie
780d1843ca
Support both zlib and deflate encodings (#758)
* Support both zlib and deflate encodings

* Helpful test docstrings
2020-01-14 09:00:52 +00:00
Hugo van Kemenade
956129fbf7 Add docs and repo to project_urls metadata (#753)
* Add docs and repo to project_urls metadata

For programmatic access to metadata on PyPI.

* Add Trove classifier: Python 3 Only
2020-01-13 10:26:31 +00:00
Francesco Pongiluppi
7f0e791f9d Fix syntax highlighting (#757)
Fixes syntax highlighting in quickstart documentation
2020-01-13 10:26:00 +00:00
Florimond Manca
bdfabe1e9a Setup custom domain (#751) 2020-01-11 09:38:09 +00:00
wynnw
b7c0d3446c Fix typo on "synonyms" (#748) 2020-01-10 23:46:31 +01:00
Mason Hall
b112b23152 Fix typo in async.md (#747) 2020-01-10 18:31:43 +01:00
Jakob Jul Elben
918c55de90 Update api.py (#745) 2020-01-09 18:10:06 +00:00
Luís Gustavo
c225e95b1d Fix typo in 'Streaming responses' docs (#744) 2020-01-09 12:23:33 +00:00
Tom Christie
7b576fae79
Update CHANGELOG.md 2020-01-09 10:25:23 +00:00
Tom Christie
367bff621c Use www.python-httpx.org domain 2020-01-09 09:49:25 +00:00
Tom Christie
d515e4b1aa Fix repo URL 2020-01-09 09:45:54 +00:00
Tom Christie
2c5fdc51f4
Version 0.11.0 (#737)
* Version 0.11.0

* Update CHANGELOG.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update CHANGELOG.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update CHANGELOG.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Bump status to beta

* Update release date to today

* Update changelog

* Use www.python-httpx.org domain

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-01-09 09:35:56 +00:00
Tom Christie
97807bf607 Link to changelog 2020-01-09 09:22:54 +00:00
Tom Christie
b932d94d99 Use www.python-httpx.org domain 2020-01-09 09:18:32 +00:00
Tom Christie
2038919b7e
Proposed 0.11 docs (#727)
* Proposed 0.11 docs

* Add async section and link in

* Update docs/advanced.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update docs/async.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update docs/async.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update docs/quickstart.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update docs/async.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update docs/async.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update docs/async.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Docs updates

* Use context-managed client instances in examples

* Update README with links to docs site, rather than to .md documents

* "99% test coverage"

* Update docs/async.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Update docs/async.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Add Client.close method to API docs

* Update docs/async.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-01-08 12:51:52 +00:00
Tom Christie
eac4b9ba74
Improve docstring for PoolLimits (#712)
* Improve docstring for PoolLimits

* Linting for docstring
2020-01-08 12:33:35 +00:00
Tom Christie
ee37a762ef
Reintroduce sync API. (#735)
* BaseClient and AsyncClient

* Introduce 'httpx.Client'

* Top level API -> sync

* Top level API -> sync

* Add WSGI support, drop deprecated imports

* Wire up timeouts to urllib3

* Wire up pool_limits

* Add urllib3 proxy support

* Pull #734 into sync Client

* Update AsyncClient docstring

* Simpler WSGI implementation

* Set body=None when no content

* Wrap urllib3 connection/read exceptions
2020-01-08 12:31:50 +00:00
Yurii Ohorodnik
387f04732b Fixed redirect loop (#734)
* fixed redirect loop

* linted with flake8

* linted with flake8
2020-01-07 16:13:06 +00:00
Tom Christie
c842c8ff20
proxies_to_dispatchers -> get_proxy_map (#733) 2020-01-07 13:37:57 +00:00
Tom Christie
12dd157fea
Public Auth API (#732)
* Public Auth API

* Minor docs tweak

* Request.aread and Request.content

* Support requires_request_body

* Update tests/models/test_requests.py

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-01-07 13:20:23 +00:00
Tom Christie
c5037f06e4
Drop per-request 'cert'/'verify'/'trust_env', and 'stream=bool' arguments (#730) 2020-01-07 12:24:26 +00:00
Andrés Álvarez
2b92a78c41 Drop Origin from public API (#688)
- Drop the url.origin property.
  - Drop Origin from the top-level export.
  - Use origin = Origin(url) in our internal usage, rather than
    url.origin.

Closes #656

Co-authored-by: Tom Christie <tom@tomchristie.com>
2020-01-07 10:39:47 +00:00
Tom Christie
f17ab37b2f
Prep for introducing SyncClient (#713)
* Reorganize method ordering in client

* Move 'request'

* Use 'httpx.Proxy' for proxy configuration
2020-01-07 10:27:01 +00:00
Florimond Manca
e1afbfa7b4 Wrap network errors in HTTPX-specific exceptions (#707) 2020-01-07 10:01:11 +00:00
Tom Christie
e19bd9bc4b
Dispatcher -> AsyncDispatcher (#725)
* Dispatcher -> AsyncDispatcher

* Fix invalid renamings

* Fix invalid renamings
2020-01-06 12:08:14 +00:00
Tom Christie
6ac49dacdd
Drop run and run_in_threadpool (#710)
* Drop run and run_in_threadpool

* Fix server restart errors

* Re-introduce 'sleep' as a concurrency test utility

* Simpler test concurrency utils

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2020-01-06 11:14:43 +00:00
Florimond Manca
bd57b650a8 Release max_connections for keepalive connections when closing the connection pool (#721) 2020-01-06 11:13:06 +00:00
Kousik Mitra
0ed0314569 Raise an exception in DigestAuth if non-replayable request is passed (#685)
* DigestAuth will raise exception if non-replayble request is passed #670

* Added new exception RequestBodyUnavailble exception to raise non replayable request error #670

* Changed RedirectBodyUnavailable exception to RequestBodyUnavailble to raise non-replayable exception #670

* fixed class declaration

* Added RequestBodyUnavailable exception. Imported it in module level

* Added RequestBodyUnavailable in the module list

* Code reformat

* Added Test to check if exception is raising if non-replayble request body is passed to DigestAuth #670
2020-01-04 10:00:34 +01:00
Florimond Manca
bc6163c55a
Record history of requests made during authentication (#718)
* Record history of requests made during authentication

* Add asserts on digest auth history

Co-Authored-By: Gaurav Dhameeja <gdhameeja@gmail.com>
2020-01-03 22:59:16 +01:00
Florimond Manca
910aa9094c
Repurpose RedirectBodyUnavailable as RequestBodyUnavailable (#690) 2020-01-03 22:25:55 +01:00
Florimond Manca
ff44d2d1b8 Fix typo in 'Redirection and History' docs (#719) 2020-01-03 21:14:13 +00:00
Tom Christie
79a9748ae6
Load SSL Context on init (#709) 2020-01-02 16:52:23 +00:00
Tom Christie
f5eaec7ab3
More coverage improvements (#711)
* More coverage improvements

* Drop redundant 'sleep' usage in test utils
2020-01-02 15:33:26 +00:00
Tom Christie
11e7604d1a
Sync streaming interface on responses (#695)
* Sync streaming interface on responses

* Fix test case

* Test coverage for sync response APIs

* Address review comments
2020-01-02 12:56:11 +00:00
Tom Christie
b0bf2a7513
SSLConfig refactor (#706)
* SSLConfig includes 'http2' argument on init.

* Pass SSL config to HTTPConnection as a single argument

* Don't run SSL context loading in threadpool
2020-01-02 10:54:04 +00:00
Tom Christie
a9f4d018e1
Version 0.10.1 (#701) 2020-01-01 15:36:11 +00:00
Tom Christie
b8394f1e00
Bump coverage (#705)
* Bump coverage

* Tests for iterative text decoding with 'aiter_text'

* nocover on xfail exception cases

* nocover API that is pending deprecation

* Tweak test to removed uncovered line

* Ingest request body in RedirectBodyUnavailable test case
2020-01-01 15:33:24 +00:00
Joe
f5d4bb8074 Make timeout parameter None-able & cleanup __all__. (#704)
* Timeout type annotation includes `None`.
* Cleanup `__all__` exports.
2020-01-01 13:45:38 +00:00
Tom Christie
c050480387
Drop unused Request.cookies (#703) 2020-01-01 13:05:49 +00:00
Tom Christie
fe86c0c4b4
Drop unused SSLConfig.with_overrides (#702) 2019-12-31 15:24:50 +00:00
Tom Christie
2c4a1dd519
Drop OpenConnection (#700) 2019-12-31 13:18:23 +00:00
Tom Christie
56afde1f9f
Lock around stream.write and stream.close operations (#699) 2019-12-31 13:18:00 +00:00
David Larlet
22663bc66e Update link to timeout fine tuning in quickstart (#696) 2019-12-31 12:02:14 +00:00
Gabriel Strauss
de8b95533d Adds check to enforce single consumption of AsyncIteratorStream. (#697) 2019-12-31 12:01:43 +00:00
Tom Christie
35b7516674
Version 0.10.0 (#691)
* Version 0.10.0

* Update CHANGELOG.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Include changelog for 'response.request is no longer optional'.

* Add response.elapsed note to changelog

* Also ref original PR for response.elapsed behavior

Co-authored-by: Florimond Manca <florimond.manca@gmail.com>
2019-12-29 17:00:34 +00:00
Tom Christie
6a1ee0eb97
response.elapsed now reflects entire request/response time. (#692)
* Changed behaviour of elapsed on response

* Fixed api docs for Response elapsed

* Minor tweaks to 'request.elapsed'

* Response instantiated with content should have elapsed==0

* Fix elapsed time on immediately closed responses.
2019-12-29 16:56:18 +00:00
Tom Christie
1f9d0154df
Switch from an Event primitive to a Lock primitive (#693) 2019-12-29 16:38:54 +00:00
Florimond Manca
e284b84bf9 Rename Client to AsyncClient (with compat synonym) (#680)
* Rename Client to AsyncClient (with compat synonym)

* Document motivation for AsyncClient renaming

Co-authored-by: Tom Christie <tom@tomchristie.com>
2019-12-29 15:34:23 +00:00
Florimond Manca
3462999366 Rename 'next' to 'anext' on Response (#676)
* Rename 'next' to 'anext' on Response

* Drop iscoroutinefunction() check

Co-authored-by: Tom Christie <tom@tomchristie.com>
2019-12-29 15:34:07 +00:00
Florimond Manca
d5da7430a2 Rename 'close' to 'aclose' on Client (#675)
* Switch to aclose on Client

* Fix reference to aclose in API docs
2019-12-29 15:15:09 +00:00
Florimond Manca
f9d18a8758 Rename 'read/close' to 'aread/aclose' on Response (#674)
* Switch to aread/aclose on responses

* Linting

Co-authored-by: Tom Christie <tom@tomchristie.com>
2019-12-29 15:14:53 +00:00
Tom Christie
25b40db757
Drop Request.read() (#679) 2019-12-29 15:02:03 +00:00
Florimond Manca
e9ebd1df98 Drop per-request cert, verify, and trust_env (#617)
* Drop per-request cert/verify/trust_env

* Remove cert/verify from the dispatcher API

* Apply lint

* Reintroduce cert/verify/trust_env on client methods, with errors
2019-12-29 15:01:20 +00:00
Tom Christie
f3b799912e
Gracefully end_stream early on no-body requests. (#682) 2019-12-23 10:49:36 +00:00
Florimond Manca
0ee0005154
Type-check test_cookies.py (#677) 2019-12-23 10:51:06 +01:00
Florimond Manca
49e4d155ee
Type-check test_auth.py (#665)
* Type-check test_auth.py

* Drop request cast
2019-12-21 18:48:55 +01:00
Florimond Manca
e30ec85016
Fix out-of-date methods on Response API docs (#673) 2019-12-21 17:21:14 +01:00
Florimond Manca
d0427bead0
Clean up 'backend' fixture (#664)
* Clean up 'backend' fixture

* Add docstring to 'async_environment' fixture
2019-12-21 16:08:40 +01:00
Florimond Manca
56c8edaf66
Make 'request' non-optional on responses (#666)
* Make 'request' non-optional on Response

* Lint

* Remove remaining mention to null request
2019-12-21 15:38:25 +01:00
Florimond Manca
9e88f2e2fb Remove httpxprof (#663) 2019-12-21 14:17:14 +00:00
Andrés Álvarez
e34df7a7a1 Rename concurrency directory to backends (#662)
Closes #659
2019-12-20 16:53:42 +00:00
Tom Christie
cee1fccaca
Use Streams API for both requests and responses. (#648)
* Internal ContentStreams API
2019-12-20 16:05:04 +00:00
Tom Christie
36af9d9597
Rationalize backend Semaphore interface slightly (#660) 2019-12-20 15:14:55 +00:00
Tom Christie
bb6ab205fe
Upgrade h11 to support both 0.8 and 0.9 (#658) 2019-12-20 15:14:30 +00:00
Tom Christie
9904684d35
Version 0.9.5 (#657) 2019-12-20 11:18:22 +00:00
Tom Christie
74e5115b86
params argument on URL should merge, not replace. (#653) 2019-12-20 10:46:35 +00:00
Tom Christie
5ee512d803
Fix Host header and HSTS when the default port is explicitly included in URL (#649) 2019-12-20 10:25:42 +00:00
Tom Christie
6c69e0936b
No I/O auth (#644)
* No I/O on Auth
2019-12-18 15:25:31 +00:00
Tom Christie
8d4f182500
Added RequestContent (#636)
* Request Content

* Added RequestContent interface

* Docstrings on 'encode(data, files, json)'

* Update httpx/content.py

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* can_rewind -> can_replay
2019-12-18 10:51:34 +00:00
Florimond Manca
2c2d49760d
Force compiling 'regex' from source (#642) 2019-12-16 22:26:47 +01:00
Tom Christie
0783baea3e
Disable HTTP/2 strict header validation (#637) 2019-12-16 12:19:25 +00:00
Florimond Manca
c337938af5 asyncio: Don't wait for the stream to close (#640)
* Don't wait for stream_writer to close

* Add comment on why wait_closed() is not called
2019-12-16 12:11:58 +00:00
Arkie
4bb69e3e3e Allow explicit Content-Type to take precedence (#633) 2019-12-13 10:49:15 +00:00
Tom Christie
b847ab07a3
Version 0.9.4 (#631) 2019-12-13 10:44:02 +00:00
Tom Christie
52c2c9762f
Bump up flow control defaults (#629)
* Bump up flow control defaults

* Linting

* Simplify mock HTTP/2 stream in tests
2019-12-12 11:57:53 +00:00
Tom Christie
499de51f2b
Keep-alive timeouts. (#627)
* Add .time() to backend

* Add connection timeouts

* Add test case for keep alive timeouts

* Update httpx/dispatch/connection_pool.py

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Cleanups from review

* Use .expires_at, rather than .timeout_at
2019-12-12 11:52:49 +00:00
Tom Christie
77da6db8e8 Fix typo on "URLs" (#630) 2019-12-12 12:13:25 +01:00
Tom Christie
1d25bd58a8
Ensure H2 state is only accessed by the connection, not per-stream. (#628)
* Ensure H2 state is only accessed by the connection, not per-stream

* Formatting tweak
2019-12-12 10:40:12 +00:00
Tom Christie
869714fbf5
Drop unused methods from BaseEvent (#626) 2019-12-11 12:31:05 +00:00
Tom Christie
00a875db93
Refactor get_semaphore (#625) 2019-12-11 12:30:56 +00:00
Tom Christie
66e0f4b6f8
Tweak backport_start_tls implementation, and add 'nocover'. (#622) 2019-12-09 18:30:00 +00:00
Tom Christie
02d16bfb34 Trivial-case test for response.elapsed 2019-12-09 17:12:20 +00:00
Tom Christie
3124a38123
Minor config cleanup (#621) 2019-12-09 10:59:55 +00:00
Florimond Manca
e95111ac07
Add OpenConnection base class (#616)
* Add OpenConnection base class

* Move is_http2 property around
2019-12-09 11:34:02 +01:00
Tom Christie
eba29da632
Drop 'fork' (#619) 2019-12-08 19:52:46 +00:00
Tom Christie
83fc0921c1
Drop TimeoutFlag (#618) 2019-12-08 19:43:33 +00:00
Florimond Manca
ab41a5d5c3
Refactor tests in the light of backend auto-detection (#615)
* Refactor tests in the light of backend auto-detection

* Test passing explicit backend separately

* Drop 'backend=backend'

* Fix usage of asyncio.run() on 3.6
2019-12-07 15:17:35 +01:00
Tom Christie
f57bb2f142
HTTP/2 refactoring (#612)
* HTTP/2 refactoring

* Clean up flow control

* Remove extra blank line
2019-12-07 14:14:09 +00:00
Tom Christie
bc54dd0399
Backend operations like .read(), .write() now have a manadatory timeout argument. (#611) 2019-12-07 11:09:58 +00:00
Tom Christie
f55db15a01
Add 'fork' to auto backend (#614)
* Add 'fork' to auto backend

* Version 0.9.3
2019-12-07 09:07:33 +00:00
Tom Christie
2394aabcb9 Add 0.9.2 note 2019-12-07 08:51:14 +00:00
Tom Christie
3a0df657b3 Version 0.9.2 2019-12-07 08:49:34 +00:00
Tom Christie
0aa1815153 0.9.1 note 2019-12-06 17:48:04 +00:00
Tom Christie
76f3ff9dd9 Release 0.9.1 due to build artifacts issue 2019-12-06 17:45:56 +00:00
Tom Christie
5cb48d981f
Update index.md 2019-12-06 15:34:45 +00:00
Tom Christie
85cc8f8008
Update README.md 2019-12-06 15:34:21 +00:00
Tom Christie
8d1bc9e60f
Version 0.9 (#606)
* Version 0.9

* Final CHANGELOG entries for 0.9
2019-12-06 15:32:22 +00:00
Tom Christie
ec40d04382
Add aiter methods on response (#610) 2019-12-06 15:20:09 +00:00
Tom Christie
d15dc0b1f8
Tighten up top-level API to only expose public API (#608)
* Tighten up top-level API to only expose public API

* Leave HTTPProxyMode for backwards compat, raising warnings.

* Add missing import
2019-12-06 15:20:01 +00:00
Florimond Manca
e1f5b8ba57 Move tunnel_start_tls() to HTTPConnection (#609) 2019-12-06 13:00:38 +00:00
Florimond Manca
df9dc6d516 Cleanup comments on handling MultiError (#607) 2019-12-06 12:58:21 +00:00
Tom Christie
ddc4885543
Update advanced.md 2019-12-06 11:35:29 +00:00
Tom Christie
1e23855709
Warn if cert / verify / trust_env are passed to client.request() (#597)
* Add cert and verify warnings on Client.request

* Resolve typo

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Resolve typo

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* trust_env should be set on client init

* Update docs for per-Client SSL settings

* Update requests compat docs
2019-12-06 11:29:22 +00:00
Florimond Manca
c38fd68ed7 Drop BackgroundManager in favor of fork(func1, func2) (#603)
* Drop BackgroundManager in favor of fork(func1, func2)

* Please mypy
2019-12-06 10:49:24 +00:00
Tom Christie
bb6e52f356
Rename BaseSocketStream cases from connection.stream to connection.socket (#601) 2019-12-05 19:54:19 +00:00
Tom Christie
8d8ea8bbba
Add Client.stream() method. (#600)
* Add Client.stream() method.

* Add top-level stream API

* Documentation
2019-12-05 17:25:43 +00:00
Tom Christie
38a9d77342
Update changelog to include 'master' (#596)
* Update changelog to include 'master'

* Include PR references
2019-12-05 12:28:45 +00:00
Tom Christie
e56e120175
Drop write_no_block from backends. (#594)
* Drop write_no_block

* Drop redundant code from Trio backend
2019-12-05 11:46:11 +00:00
Tom Christie
fc95c7e71e
stream -> stream_bytes, raw -> stream_raw (#599) 2019-12-05 11:39:28 +00:00
Tom Christie
dad379736d
Fix Timeout -> TimeoutException in test case (#598) 2019-12-05 11:03:27 +00:00
Tom Christie
f8794cb3ce
Improve backend docs, particularly wrt. autodetection (#595)
* Improve backend docs, particularly wrt. autodetection

* Resolve typo.
2019-12-05 10:27:16 +00:00
Tom Christie
1c9167e3b7
Update CHANGELOG.md 2019-12-05 10:03:24 +00:00
Tom Christie
a738327678
Tweak docstring 2019-12-05 09:40:24 +00:00
Tom Christie
2f54b200de
Allow default+override timeout style (#593)
* Allow styles like: httpx.Timeout(5.0, pool_timeout=None)

* Update timeout docs

* Minor tweaks to sub headings in timeout docs

* Fixing up Timeout docs

* RequestTimeout -> TimeoutException

* Tweak timeout docs
2019-12-05 09:38:48 +00:00
Tom Christie
81edb1b45b
Update index.md 2019-12-04 17:21:42 +00:00
Tom Christie
cb735b9b0d
Update README.md 2019-12-04 17:21:17 +00:00
Tom Christie
c08ae7796f
Alpha note, and recommendations on pinning versions (#590) 2019-12-04 11:57:02 +00:00
Tom Christie
eb7c6b0342
Differentiate between timeout=None and timeout=UNSET. (#592)
* TimeoutConfig -> Timeout

* Timeout=None should mean what it says.

* Drop optional timeout on internal client methods
2019-12-04 11:54:39 +00:00
Tom Christie
c033ed1b65
TimeoutConfig -> Timeout (#591) 2019-12-04 11:39:45 +00:00
han-solo
d223de8ff3 Fixed version requirement for rfc3986. Issue reference #504 (#589) 2019-12-04 10:05:33 +00:00
Tom Christie
293bc70947
Update README.md 2019-12-04 09:54:05 +00:00
Tom Christie
5076952202
Rename http2 switch (#586)
* Simplify HTTP version config, and switch HTTP/2 off by default

* HTTP/2 docs

* HTTP/2 interlinking in docs

* Add concurrency auto-detection

* Add sniffio

* Rename HTTP2 switch on client

* http_2 -> http2
2019-12-02 19:52:29 +00:00
Tom Christie
3cbe7315e8
Concurrency autodetection (#585)
* Simplify HTTP version config, and switch HTTP/2 off by default

* HTTP/2 docs

* HTTP/2 interlinking in docs

* Add concurrency auto-detection

* Add sniffio
2019-12-02 19:26:16 +00:00
Tom Christie
30229f1652
Better HTTP/2 defaults. (#584)
* Simplify HTTP version config, and switch HTTP/2 off by default

* HTTP/2 docs

* HTTP/2 interlinking in docs
2019-12-02 17:07:04 +00:00
Tom Christie
12d00b238e
Minor test fixes (#583)
* Minor test fixes

* Fix multipart test to less ambiguous file extension -> mime type check

* Include a no-file-extension case in multipart tests
2019-12-02 12:11:15 +00:00
Mattwmaster58
33cb39733f Clarify multipart documentation (#580)
*Clarify multipart behvaiour
2019-12-02 11:56:25 +00:00
Florimond Manca
3f68601222 Don't use background task in HTTP/1.1 dispatcher (#569) 2019-12-01 10:16:28 +00:00
Tom Christie
871b0b5cb9
Update advanced.md 2019-11-30 21:46:07 +00:00
Mattwmaster58
ed949508a6 Files without a filename should not set a Content-Type in multipart data. (#520)
* File upoads with no filename should not set a Content-Type in their multipart data.
* Update type annotations to allow file uploads to be a string
2019-11-30 19:46:44 +00:00
Tom Christie
7d45db068b
Link to ASGI docs (#577) 2019-11-30 18:38:27 +00:00
Tom Christie
248aa580a1
Add Response.stream_lines (#575) 2019-11-30 18:02:46 +00:00
Tom Christie
fdaa01275a
Add Response.is_error (#574) 2019-11-30 17:43:48 +00:00
Casey
9df76ccfe9 Preserve list type query paramaters when merging QueryParams objects (#546) (#547) 2019-11-30 15:35:09 +00:00
Tom Christie
095b69184a
Drop MessageLoggerASGIMiddleware. (#573) 2019-11-30 14:28:39 +00:00
Tom Christie
43331cfb3d Ensure Authorization header has priority over .netrc 2019-11-30 12:06:16 +00:00
Florimond Manca
8d55d78574 Drop nox in favor of vanilla scripts (#566)
* Drop nox in favor of vanilla scripts

* Use named stages

* Fix attrs dependency resolution madness

* Add missing mkautodoc dev dependency

* Add missing install step on windows build

* Explicitly define stage order so that timed out Windows build runs last

* Add missing dev dependency on Black

* Clean up contributing guide

* Separate docs into docs-build and docs-serve
2019-11-30 11:50:13 +00:00
Florimond Manca
3218c35341
Refactor start_tls tests (#567)
* Refactor start_tls tests

* Clean up read_response()
2019-11-29 22:16:32 +01:00
Tom Christie
364378a814
Pool timeouts should be on the TimeoutConfig, not PoolLimits (#563)
* Pool timeouts should be on the TimeoutConfig, not PoolLimits

* Linting

* Fix type annotation

* Linting
2019-11-29 12:01:51 +00:00
Tom Christie
b1393ec2f1
Drop iterate_in_threadpool and iterate (#564)
* Drop iterate_in_threadpool

* Drop iterate from concurrency backends
2019-11-29 11:45:40 +00:00
Tom Christie
5fccc04da4
Drop Queue from concurrency backends, since it's no longer required (#562)
* Drop Queue from concurrency backends, since it's no longer required

* Drop unused import
2019-11-29 11:21:46 +00:00
Tom Christie
e045b86d7f
Clean up '_dispatcher_for_request' into 'dispatcher_for_url' (#561) 2019-11-29 11:21:40 +00:00
Tom Christie
44ad295572
Simplify ASGI dispatch (#560)
* Simplify ASGI dispatch

* Blackify

* Linting
2019-11-29 09:07:53 +00:00
Tom Christie
296c9b459e
Drop erronous references to AsyncClient (#559) 2019-11-28 12:33:53 +00:00
Tom Christie
abe0799d70
Refactor netrc handling (#558)
* Refactor netrc handling

* Linting

* Import sorting

* Import sorting
2019-11-28 12:31:15 +00:00
Tom Christie
99ee84e20d
Update client.py 2019-11-27 14:35:37 +00:00
Tom Christie
831e79f50a Version 0.8.0 2019-11-27 13:35:02 +00:00
Tom Christie
00e150f6a5
Client handles redirect + auth (#552)
* Drop sync client

* Drop unused imports

* Async only

* Update tests/test_decoders.py

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Linting

* Update docs for async-only

* Import sorting

* Add async notes to docs

* Update README for 0.8 async switch

* Move auth away from middleware where possible

* Drop middleware sub-package

* Client.dispatcher -> Client.dispatch

* Docs tweak

* Linting

* Fix type checking issue

* Import ordering

* Fix up docstrings

* Minor docs fixes

* Linting

* Remove unused import
2019-11-27 12:10:10 +00:00
Tom Christie
206c5372a6
Drop sync (#544)
Drop sync client
2019-11-27 10:43:42 +00:00
Taoufik
1c326d53c6 Stringify the given file name (#545) 2019-11-26 09:05:19 +00:00
Florimond Manca
a05ba2e914
Fix race condition on stream.read (#535)
* Fix race condition on stream.read

* Refactor run_concurrently
2019-11-22 09:34:09 +01:00
toppk
f06ca87f97 Handle h11.Connection.next_event() RemoteProtocolError (#524)
raise (new) ConnectionClosed exception if this occurs when
socket is closed, otherwise reuse ProtocolError exception.

Add test case for ConnectionClosed behavior.

Resolves #96
2019-11-22 09:33:40 +01:00
Florimond Manca
926d6cd6e4
Document when to use AsyncClient (#534)
* Document when to use AsyncClient

* Strip advice on reverting to Requests
2019-11-22 09:19:39 +01:00
Jonas Lundberg
f0e6acb6e2 Add Unix Domain Sockets section to advanced docs page (#542) 2019-11-20 08:48:58 +01:00
Jonas Lundberg
7a96a2c896 Add support for unix domain sockets (#511)
* Add and implement open_uds_stream in concurrency backends

* Add uds arg to BaseClient and select tcp or uds in HttpConnection

* Make open stream methods in backends more explicit

* Close sentence

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>

* Refactor uds concurrency test

* Remove redundant uds test assertions
2019-11-19 23:02:08 +01:00
Florimond Manca
a5f9983037
Release 0.7.8 (#536) 2019-11-19 22:06:04 +01:00
Jonas Lundberg
2dbcaf859f Document mocking compatibility (#537) 2019-11-18 12:16:06 +00:00
Seth Michael Larson
331be99cbf Add support for proxy tunnels for Python 3.6 + asyncio. (#521)
* Backport start_tls() support for Python 3.6

* Remove version check in start_tls test
2019-11-17 12:50:54 +01:00
Tom Christie
6045ee242f
Version 0.7.7 (#532) 2019-11-15 21:56:02 +00:00
Tom Christie
5aca0c0172
Fix redirect cookie behavior (#529)
* Fix redirect cookie behavior
* Drop flake8-comprehensions
* Add redirect cookie tests
2019-11-15 21:31:15 +00:00
Jonas Lundberg
1a32cf036a Rename BaseTCPStream/TCPStream to BaseSocketStream/SocketStream (#517) 2019-11-08 17:09:38 +01:00
Florimond Manca
586acddd1a
Improve robustness of live HTTP/2 test (#512) 2019-11-08 00:35:34 +01:00
Florimond Manca
95b2b24302
Add docs on SSL certificates (#510)
* Add docs on SSL certificates

* Update docs on verify and cert params

* Tweak wording

* Tweak wording about localhost

* Remove advanced warning

* Rephrase introduction of local HTTPS section
2019-11-07 10:46:36 +01:00
Florimond Manca
08069e9368
Add DEBUG logs of HTTP requests (#502) 2019-11-06 22:56:25 +01:00
Florimond Manca
2fcf23bbfe
Refactor debug and trace log tests (#506) 2019-11-06 12:04:20 +01:00
Florimond Manca
07586f97e8
Convert debug logs to trace logs (#500)
* Convert debug logs to trace logs

* Update environment variables docs

* Update logging test
2019-11-02 22:40:15 +01:00
Florimond Manca
717b34139b
Remove pin on uvloop in test-requirements (#498) 2019-11-02 22:31:33 +01:00
Florimond Manca
a62947826f
Release 0.7.6 (#499) 2019-11-02 12:09:26 +01:00
Tom Christie
1ce3cc3269 First pass at autodoc support (#464)
* First pass at autodoc support

* Add mkautodoc requirement for docs builds

* Linting

* pip install httpx when building docs, to make it available to mkautodoc

* Fix code example in docstring slightly

* Use latest mkautodoc to resolve rendering of code snippets in docstrings

* Fill in 'Helper functions' API docs

* First pass at documenting Client

* Add autodoc for Client

* Update to mkautodoc 0.1

* Fix typos
2019-10-30 16:21:39 +01:00
Florimond Manca
e3140a0803
Reorganize timeout config tests (#491) 2019-10-22 22:04:42 +02:00
Jamie Hewland
88d73de752 asyncio: Wait for the stream to close when closing (#494) 2019-10-22 22:01:40 +02:00
Jt Miclat
f68b3df81c Change Python 3.8-dev to 3.8 in Travis (#477) 2019-10-20 12:17:22 -05:00
Yeray Diaz Diaz
9ec2cfc5dc
Multipart files tweaks (#482)
* Allow filenames as None in multipart encoding

* Allow str file contents in multipart encode

* Some formatting changes on `advanced.md`

* Document multipart file encoding in the advanced docs

* Update docs/advanced.md

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>
2019-10-20 13:25:00 +01:00
Jamie Hewland
644e8fc5b6 Make start_tls a method on streams & return a new stream (#484)
* Move start_tls to stream & return a new stream

* asyncio: Keep a reference to the inner stream when upgrading to TLS
2019-10-20 12:59:16 +02:00
Florimond Manca
ad38db82f9
Document client-level configuration (#488)
* Document client-level configuration

* Fix typo
2019-10-19 15:05:26 +02:00
Yeray Diaz Diaz
09db6ec935 Drop proxies parameter from the high level API (#485)
* Drop `proxies` argument from high level API

* Update state of Digest auth in docs

* Add note on not supporting proxies at request level

* Grammar tweak

Co-Authored-By: Florimond Manca <florimond.manca@gmail.com>
2019-10-19 14:51:27 +02:00
Florimond Manca
074cd25b04
Document client block-usage and close() (#487)
* Document client context manager vs close() usage

* Convert client snippets to use context-managed syntax
2019-10-19 13:52:44 +02:00
mariaS210
7200717e82 Add logic for determining if a URL should be proxied (#472) 2019-10-17 10:58:11 -05:00
Mateusz Woś
2984499f28 Add timeout fine-tunning advanced docs section (#476)
* Add timeout fine-tunning advanced docs section

* Rephrase part of timeout documentation
2019-10-16 18:27:45 +02:00
Can Sarıgöl
84731c8be5 Cache netrc authentication per-client (#400) 2019-10-16 08:31:47 -05:00
Florimond Manca
5e4f54d643
Add test script (#451) 2019-10-12 18:14:44 +02:00
camellia256
49ed77a706 Rely on getproxies for all proxy environment variables (#470) 2019-10-12 10:34:50 -05:00
Seth Michael Larson
c1f51277d3
Release 0.7.5 (#468) 2019-10-10 09:18:22 -05:00
Seth Michael Larson
a80c43294b
Add hints for debugging CI issues (#399) 2019-10-10 07:05:22 -05:00
Florimond Manca
65b8593c7c Allow serving docs via nox (#450) 2019-10-10 07:02:17 -05:00
Florimond Manca
38a136833f Add start_tls to Trio backend (#467) 2019-10-10 07:01:23 -05:00
Can Sarıgöl
e5d0ad2a33 Add Windows to the build matrix (#457)
* Added win32 to the build matrix

* removed os: linux due to travis default

* chopped empty line

* applied only python 3.7 tests on Windows

* added desc for windows allow failures

* removed duplicate desc
2019-10-09 23:22:18 +02:00
Florimond Manca
7361d60943
Make nox always reuse virtualenvs by default (#460)
* Always reuse venvs by default

* Update contributing guide

* Install with --upgrade
2019-10-09 20:22:32 +02:00
thebigmunch
391786696a Fix some grammar in Advanced docs (#461) 2019-10-09 20:01:37 +02:00
ImPerat0R_
97a104abc3 Add language syntax highlighting in Quickstart (#466) 2019-10-09 18:34:54 +02:00
Florimond Manca
57ae7ea22b Allow lists in query params (#386) 2019-10-08 15:12:04 -05:00
Jt Miclat
31730e7095 Add documentation for requests.Session compatibility (#449) 2019-10-05 19:20:18 -05:00
Florimond Manca
fc3df514e8 Don't check trio import in definition of backend fixture params (#447) 2019-10-05 19:12:06 -05:00
Josep Cugat
a1179e55e1 Remove wheels package from test-requirements.txt (#448) 2019-10-05 08:26:34 -05:00
Florimond Manca
24346e2039 Fix flaky Response.elapsed tests (#446) 2019-10-04 15:36:06 -05:00
Andrew M. White
b479ceb24f Ensure py.typed makes it into source distributions. (#441) 2019-10-04 14:46:57 -05:00
Can Sarıgöl
dd3fbcc8d7 Don't include username/password components in Host header (#417)
* removed auth and port from host of header

* used URL attribute rather _uri_reference

* reverted removing port into host

* reverted username and password from header

* applied new copy_with with username and password
2019-10-04 10:33:18 +01:00
Can Sarıgöl
e6da325e8b added authority copy feature in URL.copy_with (#436) 2019-10-04 09:17:25 +01:00
nwalsh1995
f504399781 Fix link to parallel page from async page (#440) 2019-10-03 20:08:27 -05:00
Davit Tovmasyan
2f2a4e4ef0 Document the files parameter on .post(), .patch(), and .put() (#409) (#414) 2019-10-03 19:47:19 -05:00
Jt Miclat
85fa89c49c Document compatibility difference for get, delete, head, and options (#418) 2019-10-03 19:46:24 -05:00
Josep Cugat
9e1cc26f8a Build and upload universal wheels to PyPI (#439) 2019-10-03 16:35:38 -05:00
Josep Cugat
86a0eb0268 Revert "Use Python 3.8 asyncio.Stream where possible (#369)" (#423)
This reverts commit 71cbde8ba4.
2019-10-03 09:18:10 +01:00
Kyle Galbraith
b65bce5924 Fix typos, spelling issues, and grammar in docs (#426) 2019-10-02 11:46:54 -05:00
Seth Michael Larson
9bbd0409ab
Python 3.8-dev builds on Travis (#425) 2019-10-02 10:34:00 -05:00
Josep Cugat
a0282569d5 Make HTTPError importable from the top-level (#421) 2019-10-01 13:37:05 -05:00
Stephen Brown II
db3e3a0231 Add flake8-pie plugin (#419) 2019-10-01 12:02:29 -05:00
Quentin Pradet
7f76a642a8 Add AsyncIO and Trio trove classifiers (#416) 2019-10-01 09:30:20 +02:00
Dustin Ingram
5df822ca11 Fix broken docs (#415)
* Fix broken link to parallel request page

* Fix incomplete code block on quickstart page
2019-09-30 20:19:53 +01:00
Seth Michael Larson
05f5dc26de
Get test suite back to ~100% line coverage (#406) 2019-09-29 13:58:22 -05:00
Ahmed Maher
5ced56b5b5 Expose ASGIDispatch & WSGIDispatch in the 'dispatch' namespace. (#407) 2019-09-28 14:55:19 -05:00
Jt Miclat
ea137a96d9 Fix wrongly deleted params introduced #408 (#410) 2019-09-28 19:47:26 +02:00
Jamie Hewland
71cbde8ba4 Use Python 3.8 asyncio.Stream where possible (#369) 2019-09-28 12:23:14 -05:00
Jt Miclat
6752f7d6f6 Remove data, json and files parameters in delete() function (#408) 2019-09-28 12:18:40 -05:00
178 changed files with 30204 additions and 10486 deletions

236
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,236 @@
# Contributing
Thank you for being interested in contributing to HTTPX.
There are many ways you can contribute to the project:
- Try HTTPX and [report bugs/issues you find](https://github.com/encode/httpx/issues/new)
- [Implement new features](https://github.com/encode/httpx/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
- [Review Pull Requests of others](https://github.com/encode/httpx/pulls)
- Write documentation
- Participate in discussions
## Reporting Bugs or Other Issues
Found something that HTTPX should support?
Stumbled upon some unexpected behaviour?
Contributions should generally start out with [a discussion](https://github.com/encode/httpx/discussions).
Possible bugs may be raised as a "Potential Issue" discussion, feature requests may
be raised as an "Ideas" discussion. We can then determine if the discussion needs
to be escalated into an "Issue" or not, or if we'd consider a pull request.
Try to be more descriptive as you can and in case of a bug report,
provide as much information as possible like:
- OS platform
- Python version
- Installed dependencies and versions (`python -m pip freeze`)
- Code snippet
- Error traceback
You should always try to reduce any examples to the *simplest possible case*
that demonstrates the issue.
Some possibly useful tips for narrowing down potential issues...
- Does the issue exist on HTTP/1.1, or HTTP/2, or both?
- Does the issue exist with `Client`, `AsyncClient`, or both?
- When using `AsyncClient` does the issue exist when using `asyncio` or `trio`, or both?
## Development
To start developing HTTPX create a **fork** of the
[HTTPX repository](https://github.com/encode/httpx) on GitHub.
Then clone your fork with the following command replacing `YOUR-USERNAME` with
your GitHub username:
```shell
$ git clone https://github.com/YOUR-USERNAME/httpx
```
You can now install the project and its dependencies using:
```shell
$ cd httpx
$ scripts/install
```
## Testing and Linting
We use custom shell scripts to automate testing, linting,
and documentation building workflow.
To run the tests, use:
```shell
$ scripts/test
```
!!! warning
The test suite spawns testing servers on ports **8000** and **8001**.
Make sure these are not in use, so the tests can run properly.
You can run a single test script like this:
```shell
$ scripts/test -- tests/test_multipart.py
```
To run the code auto-formatting:
```shell
$ scripts/lint
```
Lastly, to run code checks separately (they are also run as part of `scripts/test`), run:
```shell
$ scripts/check
```
## Documenting
Documentation pages are located under the `docs/` folder.
To run the documentation site locally (useful for previewing changes), use:
```shell
$ scripts/docs
```
## Resolving Build / CI Failures
Once you've submitted your pull request, the test suite will automatically run, and the results will show up in GitHub.
If the test suite fails, you'll want to click through to the "Details" link, and try to identify why the test suite failed.
<p align="center" style="margin: 0 0 10px">
<img src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/gh-actions-fail.png" alt='Failing PR commit status'>
</p>
Here are some common ways the test suite can fail:
### Check Job Failed
<p align="center" style="margin: 0 0 10px">
<img src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/gh-actions-fail-check.png" alt='Failing GitHub action lint job'>
</p>
This job failing means there is either a code formatting issue or type-annotation issue.
You can look at the job output to figure out why it's failed or within a shell run:
```shell
$ scripts/check
```
It may be worth it to run `$ scripts/lint` to attempt auto-formatting the code
and if that job succeeds commit the changes.
### Docs Job Failed
This job failing means the documentation failed to build. This can happen for
a variety of reasons like invalid markdown or missing configuration within `mkdocs.yml`.
### Python 3.X Job Failed
<p align="center" style="margin: 0 0 10px">
<img src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/gh-actions-fail-test.png" alt='Failing GitHub action test job'>
</p>
This job failing means the unit tests failed or not all code paths are covered by unit tests.
If tests are failing you will see this message under the coverage report:
`=== 1 failed, 435 passed, 1 skipped, 1 xfailed in 11.09s ===`
If tests succeed but coverage doesn't reach our current threshold, you will see this
message under the coverage report:
`FAIL Required test coverage of 100% not reached. Total coverage: 99.00%`
## Releasing
*This section is targeted at HTTPX maintainers.*
Before releasing a new version, create a pull request that includes:
- **An update to the changelog**:
- We follow the format from [keepachangelog](https://keepachangelog.com/en/1.0.0/).
- [Compare](https://github.com/encode/httpx/compare/) `master` with the tag of the latest release, and list all entries that are of interest to our users:
- Things that **must** go in the changelog: added, changed, deprecated or removed features, and bug fixes.
- Things that **should not** go in the changelog: changes to documentation, tests or tooling.
- Try sorting entries in descending order of impact / importance.
- Keep it concise and to-the-point. 🎯
- **A version bump**: see `__version__.py`.
For an example, see [#1006](https://github.com/encode/httpx/pull/1006).
Once the release PR is merged, create a
[new release](https://github.com/encode/httpx/releases/new) including:
- Tag version like `0.13.3`.
- Release title `Version 0.13.3`
- Description copied from the changelog.
Once created this release will be automatically uploaded to PyPI.
If something goes wrong with the PyPI job the release can be published using the
`scripts/publish` script.
## Development proxy setup
To test and debug requests via a proxy it's best to run a proxy server locally.
Any server should do but HTTPCore's test suite uses
[`mitmproxy`](https://mitmproxy.org/) which is written in Python, it's fully
featured and has excellent UI and tools for introspection of requests.
You can install `mitmproxy` using `pip install mitmproxy` or [several
other ways](https://docs.mitmproxy.org/stable/overview-installation/).
`mitmproxy` does require setting up local TLS certificates for HTTPS requests,
as its main purpose is to allow developers to inspect requests that pass through
it. We can set them up follows:
1. [`pip install trustme-cli`](https://github.com/sethmlarson/trustme-cli/).
2. `trustme-cli -i example.org www.example.org`, assuming you want to test
connecting to that domain, this will create three files: `server.pem`,
`server.key` and `client.pem`.
3. `mitmproxy` requires a PEM file that includes the private key and the
certificate so we need to concatenate them:
`cat server.key server.pem > server.withkey.pem`.
4. Start the proxy server `mitmproxy --certs server.withkey.pem`, or use the
[other mitmproxy commands](https://docs.mitmproxy.org/stable/) with different
UI options.
At this point the server is ready to start serving requests, you'll need to
configure HTTPX as described in the
[proxy section](https://www.python-httpx.org/advanced/#http-proxying) and
the [SSL certificates section](https://www.python-httpx.org/advanced/#ssl-certificates),
this is where our previously generated `client.pem` comes in:
```
import httpx
ssl_context = httpx.SSLContext()
ssl_context.load_verify_locations("/path/to/client.pem")
with httpx.Client(proxy="http://127.0.0.1:8080/", ssl_context=ssl_context) as client:
response = client.get("https://example.org")
print(response.status_code) # should print 200
```
Note, however, that HTTPS requests will only succeed to the host specified
in the SSL/TLS certificate we generated, HTTPS requests to other hosts will
raise an error like:
```
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate
verify failed: Hostname mismatch, certificate is not valid for
'duckduckgo.com'. (_ssl.c:1108)
```
If you want to make requests to more hosts you'll need to regenerate the
certificates and include all the hosts you intend to connect to in the
seconds step, i.e.
`trustme-cli -i example.org www.example.org duckduckgo.com www.duckduckgo.com`

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: encode

16
.github/ISSUE_TEMPLATE/1-issue.md vendored Normal file
View File

@ -0,0 +1,16 @@
---
name: Issue
about: Please only raise an issue if you've been advised to do so after discussion. Thanks! 🙏
---
The starting point for issues should usually be a discussion...
https://github.com/encode/httpx/discussions
Possible bugs may be raised as a "Potential Issue" discussion, feature requests may be raised as an "Ideas" discussion. We can then determine if the discussion needs to be escalated into an "Issue" or not.
This will help us ensure that the "Issues" list properly reflects ongoing or needed work on the project.
---
- [ ] Initially raised as discussion #...

11
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,11 @@
# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
blank_issues_enabled: false
contact_links:
- name: Discussions
url: https://github.com/encode/httpx/discussions
about: >
The "Discussions" forum is where you want to start. 💖
- name: Chat
url: https://gitter.im/encode/community
about: >
Our community chat forum.

12
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,12 @@
<!-- Thanks for contributing to HTTPX! 💚
Given this is a project maintained by volunteers, please read this template to not waste your time, or ours! 😁 -->
# Summary
<!-- Write a small summary about what is happening here. -->
# Checklist
- [ ] I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
- [ ] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
- [ ] I've updated the documentation accordingly.

14
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "monthly"
groups:
python-packages:
patterns:
- "*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly

29
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,29 @@
name: Publish
on:
push:
tags:
- '*'
jobs:
publish:
name: "Publish release"
runs-on: "ubuntu-latest"
environment:
name: deploy
steps:
- uses: "actions/checkout@v4"
- uses: "actions/setup-python@v6"
with:
python-version: 3.9
- name: "Install dependencies"
run: "scripts/install"
- name: "Build package & docs"
run: "scripts/build"
- name: "Publish to PyPI & deploy docs"
run: "scripts/publish"
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}

34
.github/workflows/test-suite.yml vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Test Suite
on:
push:
branches: ["master"]
pull_request:
branches: ["master", "version-*"]
jobs:
tests:
name: "Python ${{ matrix.python-version }}"
runs-on: "ubuntu-latest"
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: "actions/checkout@v4"
- uses: "actions/setup-python@v6"
with:
python-version: "${{ matrix.python-version }}"
allow-prereleases: true
- name: "Install dependencies"
run: "scripts/install"
- name: "Run linting checks"
run: "scripts/check"
- name: "Build package & docs"
run: "scripts/build"
- name: "Run tests"
run: "scripts/test"
- name: "Enforce coverage"
run: "scripts/coverage"

3
.gitignore vendored
View File

@ -7,5 +7,6 @@ htmlcov/
site/
*.egg-info/
venv*/
.nox
.python-version
build/
dist/

View File

@ -1,35 +0,0 @@
dist: xenial
language: python
cache: pip
branches:
only:
- master
matrix:
include:
- python: 3.7
env: NOX_SESSION=check
- python: 3.7
env: NOX_SESSION=docs
- python: 3.6
env: NOX_SESSION=test-3.6
- python: 3.7
env: NOX_SESSION=test-3.7
- python: 3.8-dev
env: NOX_SESSION=test-3.8
dist: bionic # Required to get OpenSSL 1.1.1+
install:
- pip install --upgrade nox
script:
- nox -s ${NOX_SESSION}
after_script:
- if [ -f .coverage ]; then
python -m pip install codecov;
codecov --required;
fi

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,12 @@
Copyright © 2019, [Encode OSS Ltd](https://www.encode.io/).
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* 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.
* 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.
* 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.
* 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.
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.
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.

View File

@ -1,3 +0,0 @@
include README.md
include CHANGELOG.md
include LICENSE.md

102
README.md
View File

@ -1,51 +1,70 @@
<p align="center">
<a href="https://www.encode.io/httpx/"><img width="350" height="208" src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/logo.jpg" alt='HTTPX'></a>
<a href="https://www.python-httpx.org/"><img width="350" height="208" src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/butterfly.png" alt='HTTPX'></a>
</p>
<p align="center"><strong>HTTPX</strong> <em>- A next-generation HTTP client for Python.</em></p>
<p align="center">
<a href="https://travis-ci.org/encode/httpx">
<img src="https://travis-ci.org/encode/httpx.svg?branch=master" alt="Build Status">
</a>
<a href="https://codecov.io/gh/encode/httpx">
<img src="https://codecov.io/gh/encode/httpx/branch/master/graph/badge.svg" alt="Coverage">
<a href="https://github.com/encode/httpx/actions">
<img src="https://github.com/encode/httpx/workflows/Test%20Suite/badge.svg" alt="Test Suite">
</a>
<a href="https://pypi.org/project/httpx/">
<img src="https://badge.fury.io/py/httpx.svg" alt="Package version">
</a>
</p>
**Note**: *This project should be considered as an "alpha" release. It is substantially API complete, but there are still some areas that need more work.*
HTTPX is a fully featured HTTP client library for Python 3. It includes **an integrated command line client**, has support for both **HTTP/1.1 and HTTP/2**, and provides both **sync and async APIs**.
---
Let's get started...
Install HTTPX using pip:
```python
```shell
$ pip install httpx
```
Now, let's get started:
```pycon
>>> import httpx
>>> r = httpx.get('https://www.example.org/')
>>> r
<Response [200 OK]>
>>> r.status_code
200
>>> r.http_version
'HTTP/1.1'
>>> r.headers['content-type']
'text/html; charset=UTF-8'
>>> r.text
'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>...'
```
Or, using the command-line client.
```shell
$ pip install 'httpx[cli]' # The command line client is an optional dependency.
```
Which now allows us to use HTTPX directly from the command-line...
<p align="center">
<img width="700" src="docs/img/httpx-help.png" alt='httpx --help'>
</p>
Sending a request...
<p align="center">
<img width="700" src="docs/img/httpx-request.png" alt='httpx http://httpbin.org/json'>
</p>
## Features
HTTPX builds on the well-established usability of `requests`, and gives you:
* A requests-compatible API.
* HTTP/2 and HTTP/1.1 support.
* Support for [issuing HTTP requests in parallel](https://www.encode.io/httpx/parallel/). *(Coming soon)*
* Standard synchronous interface, but [with `async`/`await` support if you need it](https://www.encode.io/httpx/async/).
* Ability to [make requests directly to WSGI or ASGI applications](https://www.encode.io/httpx/advanced/#calling-into-python-web-apps).
* A broadly [requests-compatible API](https://www.python-httpx.org/compatibility/).
* An integrated command-line client.
* HTTP/1.1 [and HTTP/2 support](https://www.python-httpx.org/http2/).
* Standard synchronous interface, but with [async support if you need it](https://www.python-httpx.org/async/).
* Ability to make requests directly to [WSGI applications](https://www.python-httpx.org/advanced/transports/#wsgi-transport) or [ASGI applications](https://www.python-httpx.org/advanced/transports/#asgi-transport).
* Strict timeouts everywhere.
* Fully type annotated.
* 100% test coverage.
@ -62,7 +81,7 @@ Plus all the standard features of `requests`...
* Automatic Content Decoding
* Unicode Response Bodies
* Multipart File Uploads
* HTTP(S) Proxy Support *(TODO)*
* HTTP(S) Proxy Support
* Connection Timeouts
* Streaming Downloads
* .netrc Support
@ -76,40 +95,53 @@ Install with pip:
$ pip install httpx
```
httpx requires Python 3.6+
Or, to include the optional HTTP/2 support, use:
```shell
$ pip install httpx[http2]
```
HTTPX requires Python 3.9+.
## Documentation
Project documentation is available at [www.encode.io/httpx/](https://www.encode.io/httpx/).
Project documentation is available at [https://www.python-httpx.org/](https://www.python-httpx.org/).
For a run-through of all the basics, head over to the [QuickStart](https://www.encode.io/httpx/quickstart/).
For a run-through of all the basics, head over to the [QuickStart](https://www.python-httpx.org/quickstart/).
For more advanced topics, see the [Advanced Usage](https://www.encode.io/httpx/advanced/) section, or
the specific topics on making [Parallel Requests](https://www.encode.io/httpx/parallel/) or using the
[Async Client](https://www.encode.io/httpx/async/).
For more advanced topics, see the [Advanced Usage](https://www.python-httpx.org/advanced/) section, the [async support](https://www.python-httpx.org/async/) section, or the [HTTP/2](https://www.python-httpx.org/http2/) section.
The [Developer Interface](https://www.encode.io/httpx/api/) provides a comprehensive API reference.
The [Developer Interface](https://www.python-httpx.org/api/) provides a comprehensive API reference.
To find out about tools that integrate with HTTPX, see [Third Party Packages](https://www.python-httpx.org/third_party_packages/).
## Contribute
If you want to contribute with HTTPX check out the [Contributing Guide](https://www.encode.io/httpx/contributing/) to learn how to start.
If you want to contribute with HTTPX check out the [Contributing Guide](https://www.python-httpx.org/contributing/) to learn how to start.
## Dependencies
The httpx project relies on these excellent libraries:
The HTTPX project relies on these excellent libraries:
* `h2` - HTTP/2 support.
* `h11` - HTTP/1.1 support.
* `httpcore` - The underlying transport implementation for `httpx`.
* `h11` - HTTP/1.1 support.
* `certifi` - SSL certificates.
* `chardet` - Fallback auto-detection for response encoding.
* `hstspreload` - determines whether IDNA-encoded host should be only accessed via HTTPS.
* `idna` - Internationalized domain name support.
* `rfc3986` - URL parsing & normalization.
* `brotlipy` - Decoding for "brotli" compressed responses. *(Optional)*
* `sniffio` - Async library autodetection.
As well as these optional installs:
* `h2` - HTTP/2 support. *(Optional, with `httpx[http2]`)*
* `socksio` - SOCKS proxy support. *(Optional, with `httpx[socks]`)*
* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)*
* `click` - Command line client support. *(Optional, with `httpx[cli]`)*
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)*
* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)*
A huge amount of credit is due to `requests` for the API layout that
much of this work follows, as well as to `urllib3` for plenty of design
inspiration around the lower level networking details.
inspiration around the lower-level networking details.
<p align="center">&mdash; ⭐️ &mdash;</p>
<p align="center"><i>HTTPX is <a href="https://github.com/encode/httpx/blob/master/LICENSE.md">BSD licensed</a> code. Designed & built in Brighton, England.</i></p>
---
<p align="center"><i>HTTPX is <a href="https://github.com/encode/httpx/blob/master/LICENSE.md">BSD licensed</a> code.<br/>Designed & crafted with care.</i><br/>&mdash; 🦋 &mdash;</p>

View File

@ -1,8 +0,0 @@
coverage:
status:
patch:
default:
target: '100'
project:
default:
target: '100'

1
docs/CNAME Normal file
View File

@ -0,0 +1 @@
www.python-httpx.org

View File

@ -1,162 +0,0 @@
# Advanced Usage
## Client Instances
Using a Client instance to make requests will give you HTTP connection pooling,
will provide cookie persistence, and allows you to apply configuration across
all outgoing requests.
```python
>>> client = httpx.Client()
>>> r = client.get('https://example.org/')
>>> r
<Response [200 OK]>
```
## Calling into Python Web Apps
You can configure an `httpx` client to call directly into a Python web
application, using either the WSGI or ASGI protocol.
This is particularly useful for two main use-cases:
* Using `httpx` as a client, inside test cases.
* Mocking out external services, during tests or in dev/staging environments.
Here's an example of integrating against a Flask application:
```python
from flask import Flask
import httpx
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
client = httpx.Client(app=app)
r = client.get('http://example/')
assert r.status_code == 200
assert r.text == "Hello World!"
```
For some more complex cases you might need to customize the WSGI or ASGI
dispatch. This allows you to:
* Inspect 500 error responses, rather than raise exceptions, by setting `raise_app_exceptions=False`.
* Mount the WSGI or ASGI application at a subpath, by setting `script_name` (WSGI) or `root_path` (ASGI).
* Use a given the client address for requests, by setting `remote_addr` (WSGI) or `client` (ASGI).
For example:
```python
# Instantiate a client that makes WSGI requests with a client IP of "1.2.3.4".
dispatch = httpx.WSGIDispatch(app=app, remote_addr="1.2.3.4")
client = httpx.Client(dispatch=dispatch)
```
## Build Request
You can use `Client.build_request()` to build a request and
make modifications before sending the request.
```python
>>> client = httpx.Client()
>>> req = client.build_request("OPTIONS", "https://example.com")
>>> req.url.full_path = "*" # Build an 'OPTIONS *' request for CORS
>>> client.send(r)
<Response [200 OK]>
```
## Specify the version of HTTP protocol
One can set the version of HTTP protocol for the client in case you want to make the requests using specific version.
For example:
```python
h11_client = httpx.Client(http_versions=["HTTP/1.1"])
h11_response = h11_client.get("https://myserver.com")
h2_client = httpx.Client(http_versions=["HTTP/2"])
h2_response = h2_client.get("https://myserver.com")
```
## .netrc Support
HTTPX supports .netrc file. In `trust_env=True` cases, if auth parameter is
not defined, HTTPX tries to add auth into request's header from .netrc file.
As default `trust_env` is true. To set false:
```python
>>> httpx.get('https://example.org/', trust_env=False)
```
If `NETRC` environment is empty, HTTPX tries to use default files.
(`~/.netrc`, `~/_netrc`)
To change `NETRC` environment:
```python
>>> import os
>>> os.environ["NETRC"] = "my_default_folder/.my_netrc"
```
.netrc file content example:
```
machine netrcexample.org
login example-username
password example-password
...
```
## HTTP Proxying
HTTPX supports setting up proxies the same way that Requests does via the `proxies` parameter.
For example to forward all HTTP traffic to `http://127.0.0.1:3080` and all HTTPS traffic
to `http://127.0.0.1:3081` your `proxies` config would look like this:
```python
>>> client = httpx.Client(proxies={
"http": "http://127.0.0.1:3080",
"https": "http://127.0.0.1:3081"
})
```
Proxies can be configured for a specific scheme and host, all schemes of a host,
all hosts for a scheme, or for all requests. When determining which proxy configuration
to use for a given request this same order is used.
```python
>>> client = httpx.Client(proxies={
"http://example.com": "...", # Host+Scheme
"all://example.com": "...", # Host
"http": "...", # Scheme
"all": "...", # All
})
>>> client = httpx.Client(proxies="...") # Shortcut for 'all'
```
!!! warning
To make sure that proxies cannot read your traffic,
and even if the proxy_url uses HTTPS, it is recommended to
use HTTPS and tunnel requests if possible.
By default `HTTPProxy` will operate as a forwarding proxy for `http://...` requests
and will establish a `CONNECT` TCP tunnel for `https://` requests. This doesn't change
regardless of the `proxy_url` being `http` or `https`.
Proxies can be configured to have different behavior such as forwarding or tunneling all requests:
```python
proxy = httpx.HTTPProxy(
proxy_url="https://127.0.0.1",
proxy_mode=httpx.HTTPProxyMode.TUNNEL_ONLY
)
client = httpx.Client(proxies=proxy)
# This request will be tunnelled instead of forwarded.
client.get("http://example.com")
```

View File

@ -0,0 +1,232 @@
Authentication can either be included on a per-request basis...
```pycon
>>> auth = httpx.BasicAuth(username="username", password="secret")
>>> client = httpx.Client()
>>> response = client.get("https://www.example.com/", auth=auth)
```
Or configured on the client instance, ensuring that all outgoing requests will include authentication credentials...
```pycon
>>> auth = httpx.BasicAuth(username="username", password="secret")
>>> client = httpx.Client(auth=auth)
>>> response = client.get("https://www.example.com/")
```
## Basic authentication
HTTP basic authentication is an unencrypted authentication scheme that uses a simple encoding of the username and password in the request `Authorization` header. Since it is unencrypted it should typically only be used over `https`, although this is not strictly enforced.
```pycon
>>> auth = httpx.BasicAuth(username="finley", password="secret")
>>> client = httpx.Client(auth=auth)
>>> response = client.get("https://httpbin.org/basic-auth/finley/secret")
>>> response
<Response [200 OK]>
```
## Digest authentication
HTTP digest authentication is a challenge-response authentication scheme. Unlike basic authentication it provides encryption, and can be used over unencrypted `http` connections. It requires an additional round-trip in order to negotiate the authentication.
```pycon
>>> auth = httpx.DigestAuth(username="olivia", password="secret")
>>> client = httpx.Client(auth=auth)
>>> response = client.get("https://httpbin.org/digest-auth/auth/olivia/secret")
>>> response
<Response [200 OK]>
>>> response.history
[<Response [401 UNAUTHORIZED]>]
```
## NetRC authentication
HTTPX can be configured to use [a `.netrc` config file](https://everything.curl.dev/usingcurl/netrc) for authentication.
The `.netrc` config file allows authentication credentials to be associated with specified hosts. When a request is made to a host that is found in the netrc file, the username and password will be included using HTTP basic authentication.
Example `.netrc` file:
```
machine example.org
login example-username
password example-password
machine python-httpx.org
login other-username
password other-password
```
Some examples of configuring `.netrc` authentication with `httpx`.
Use the default `.netrc` file in the users home directory:
```pycon
>>> auth = httpx.NetRCAuth()
>>> client = httpx.Client(auth=auth)
```
Use an explicit path to a `.netrc` file:
```pycon
>>> auth = httpx.NetRCAuth(file="/path/to/.netrc")
>>> client = httpx.Client(auth=auth)
```
Use the `NETRC` environment variable to configure a path to the `.netrc` file,
or fallback to the default.
```pycon
>>> auth = httpx.NetRCAuth(file=os.environ.get("NETRC"))
>>> client = httpx.Client(auth=auth)
```
The `NetRCAuth()` class uses [the `netrc.netrc()` function from the Python standard library](https://docs.python.org/3/library/netrc.html). See the documentation there for more details on exceptions that may be raised if the `.netrc` file is not found, or cannot be parsed.
## Custom authentication schemes
When issuing requests or instantiating a client, the `auth` argument can be used to pass an authentication scheme to use. The `auth` argument may be one of the following...
* A two-tuple of `username`/`password`, to be used with basic authentication.
* An instance of `httpx.BasicAuth()`, `httpx.DigestAuth()`, or `httpx.NetRCAuth()`.
* A callable, accepting a request and returning an authenticated request instance.
* An instance of subclasses of `httpx.Auth`.
The most involved of these is the last, which allows you to create authentication flows involving one or more requests. A subclass of `httpx.Auth` should implement `def auth_flow(request)`, and yield any requests that need to be made...
```python
class MyCustomAuth(httpx.Auth):
def __init__(self, token):
self.token = token
def auth_flow(self, request):
# Send the request, with a custom `X-Authentication` header.
request.headers['X-Authentication'] = self.token
yield request
```
If the auth flow requires more than one request, you can issue multiple yields, and obtain the response in each case...
```python
class MyCustomAuth(httpx.Auth):
def __init__(self, token):
self.token = token
def auth_flow(self, request):
response = yield request
if response.status_code == 401:
# If the server issues a 401 response then resend the request,
# with a custom `X-Authentication` header.
request.headers['X-Authentication'] = self.token
yield request
```
Custom authentication classes are designed to not perform any I/O, so that they may be used with both sync and async client instances. If you are implementing an authentication scheme that requires the request body, then you need to indicate this on the class using a `requires_request_body` property.
You will then be able to access `request.content` inside the `.auth_flow()` method.
```python
class MyCustomAuth(httpx.Auth):
requires_request_body = True
def __init__(self, token):
self.token = token
def auth_flow(self, request):
response = yield request
if response.status_code == 401:
# If the server issues a 401 response then resend the request,
# with a custom `X-Authentication` header.
request.headers['X-Authentication'] = self.sign_request(...)
yield request
def sign_request(self, request):
# Create a request signature, based on `request.method`, `request.url`,
# `request.headers`, and `request.content`.
...
```
Similarly, if you are implementing a scheme that requires access to the response body, then use the `requires_response_body` property. You will then be able to access response body properties and methods such as `response.content`, `response.text`, `response.json()`, etc.
```python
class MyCustomAuth(httpx.Auth):
requires_response_body = True
def __init__(self, access_token, refresh_token, refresh_url):
self.access_token = access_token
self.refresh_token = refresh_token
self.refresh_url = refresh_url
def auth_flow(self, request):
request.headers["X-Authentication"] = self.access_token
response = yield request
if response.status_code == 401:
# If the server issues a 401 response, then issue a request to
# refresh tokens, and resend the request.
refresh_response = yield self.build_refresh_request()
self.update_tokens(refresh_response)
request.headers["X-Authentication"] = self.access_token
yield request
def build_refresh_request(self):
# Return an `httpx.Request` for refreshing tokens.
...
def update_tokens(self, response):
# Update the `.access_token` and `.refresh_token` tokens
# based on a refresh response.
data = response.json()
...
```
If you _do_ need to perform I/O other than HTTP requests, such as accessing a disk-based cache, or you need to use concurrency primitives, such as locks, then you should override `.sync_auth_flow()` and `.async_auth_flow()` (instead of `.auth_flow()`). The former will be used by `httpx.Client`, while the latter will be used by `httpx.AsyncClient`.
```python
import asyncio
import threading
import httpx
class MyCustomAuth(httpx.Auth):
def __init__(self):
self._sync_lock = threading.RLock()
self._async_lock = asyncio.Lock()
def sync_get_token(self):
with self._sync_lock:
...
def sync_auth_flow(self, request):
token = self.sync_get_token()
request.headers["Authorization"] = f"Token {token}"
yield request
async def async_get_token(self):
async with self._async_lock:
...
async def async_auth_flow(self, request):
token = await self.async_get_token()
request.headers["Authorization"] = f"Token {token}"
yield request
```
If you only want to support one of the two methods, then you should still override it, but raise an explicit `RuntimeError`.
```python
import httpx
import sync_only_library
class MyCustomAuth(httpx.Auth):
def sync_auth_flow(self, request):
token = sync_only_library.get_token(...)
request.headers["Authorization"] = f"Token {token}"
yield request
async def async_auth_flow(self, request):
raise RuntimeError("Cannot use a sync authentication class with httpx.AsyncClient")
```

328
docs/advanced/clients.md Normal file
View File

@ -0,0 +1,328 @@
!!! hint
If you are coming from Requests, `httpx.Client()` is what you can use instead of `requests.Session()`.
## Why use a Client?
!!! note "TL;DR"
If you do anything more than experimentation, one-off scripts, or prototypes, then you should use a `Client` instance.
**More efficient usage of network resources**
When you make requests using the top-level API as documented in the [Quickstart](../quickstart.md) guide, HTTPX has to establish a new connection _for every single request_ (connections are not reused). As the number of requests to a host increases, this quickly becomes inefficient.
On the other hand, a `Client` instance uses [HTTP connection pooling](https://en.wikipedia.org/wiki/HTTP_persistent_connection). This means that when you make several requests to the same host, the `Client` will reuse the underlying TCP connection, instead of recreating one for every single request.
This can bring **significant performance improvements** compared to using the top-level API, including:
- Reduced latency across requests (no handshaking).
- Reduced CPU usage and round-trips.
- Reduced network congestion.
**Extra features**
`Client` instances also support features that aren't available at the top-level API, such as:
- Cookie persistence across requests.
- Applying configuration across all outgoing requests.
- Sending requests through HTTP proxies.
- Using [HTTP/2](../http2.md).
The other sections on this page go into further detail about what you can do with a `Client` instance.
## Usage
The recommended way to use a `Client` is as a context manager. This will ensure that connections are properly cleaned up when leaving the `with` block:
```python
with httpx.Client() as client:
...
```
Alternatively, you can explicitly close the connection pool without block-usage using `.close()`:
```python
client = httpx.Client()
try:
...
finally:
client.close()
```
## Making requests
Once you have a `Client`, you can send requests using `.get()`, `.post()`, etc. For example:
```pycon
>>> with httpx.Client() as client:
... r = client.get('https://example.com')
...
>>> r
<Response [200 OK]>
```
These methods accept the same arguments as `httpx.get()`, `httpx.post()`, etc. This means that all features documented in the [Quickstart](../quickstart.md) guide are also available at the client level.
For example, to send a request with custom headers:
```pycon
>>> with httpx.Client() as client:
... headers = {'X-Custom': 'value'}
... r = client.get('https://example.com', headers=headers)
...
>>> r.request.headers['X-Custom']
'value'
```
## Sharing configuration across requests
Clients allow you to apply configuration to all outgoing requests by passing parameters to the `Client` constructor.
For example, to apply a set of custom headers _on every request_:
```pycon
>>> url = 'http://httpbin.org/headers'
>>> headers = {'user-agent': 'my-app/0.0.1'}
>>> with httpx.Client(headers=headers) as client:
... r = client.get(url)
...
>>> r.json()['headers']['User-Agent']
'my-app/0.0.1'
```
## Merging of configuration
When a configuration option is provided at both the client-level and request-level, one of two things can happen:
- For headers, query parameters and cookies, the values are combined together. For example:
```pycon
>>> headers = {'X-Auth': 'from-client'}
>>> params = {'client_id': 'client1'}
>>> with httpx.Client(headers=headers, params=params) as client:
... headers = {'X-Custom': 'from-request'}
... params = {'request_id': 'request1'}
... r = client.get('https://example.com', headers=headers, params=params)
...
>>> r.request.url
URL('https://example.com?client_id=client1&request_id=request1')
>>> r.request.headers['X-Auth']
'from-client'
>>> r.request.headers['X-Custom']
'from-request'
```
- For all other parameters, the request-level value takes priority. For example:
```pycon
>>> with httpx.Client(auth=('tom', 'mot123')) as client:
... r = client.get('https://example.com', auth=('alice', 'ecila123'))
...
>>> _, _, auth = r.request.headers['Authorization'].partition(' ')
>>> import base64
>>> base64.b64decode(auth)
b'alice:ecila123'
```
If you need finer-grained control on the merging of client-level and request-level parameters, see [Request instances](#request-instances).
## Other Client-only configuration options
Additionally, `Client` accepts some configuration options that aren't available at the request level.
For example, `base_url` allows you to prepend an URL to all outgoing requests:
```pycon
>>> with httpx.Client(base_url='http://httpbin.org') as client:
... r = client.get('/headers')
...
>>> r.request.url
URL('http://httpbin.org/headers')
```
For a list of all available client parameters, see the [`Client`](../api.md#client) API reference.
---
## Request instances
For maximum control on what gets sent over the wire, HTTPX supports building explicit [`Request`](../api.md#request) instances:
```python
request = httpx.Request("GET", "https://example.com")
```
To dispatch a `Request` instance across to the network, create a [`Client` instance](#client-instances) and use `.send()`:
```python
with httpx.Client() as client:
response = client.send(request)
...
```
If you need to mix client-level and request-level options in a way that is not supported by the default [Merging of parameters](#merging-of-parameters), you can use `.build_request()` and then make arbitrary modifications to the `Request` instance. For example:
```python
headers = {"X-Api-Key": "...", "X-Client-ID": "ABC123"}
with httpx.Client(headers=headers) as client:
request = client.build_request("GET", "https://api.example.com")
print(request.headers["X-Client-ID"]) # "ABC123"
# Don't send the API key for this particular request.
del request.headers["X-Api-Key"]
response = client.send(request)
...
```
## Monitoring download progress
If you need to monitor download progress of large responses, you can use response streaming and inspect the `response.num_bytes_downloaded` property.
This interface is required for properly determining download progress, because the total number of bytes returned by `response.content` or `response.iter_content()` will not always correspond with the raw content length of the response if HTTP response compression is being used.
For example, showing a progress bar using the [`tqdm`](https://github.com/tqdm/tqdm) library while a response is being downloaded could be done like this…
```python
import tempfile
import httpx
from tqdm import tqdm
with tempfile.NamedTemporaryFile() as download_file:
url = "https://speed.hetzner.de/100MB.bin"
with httpx.stream("GET", url) as response:
total = int(response.headers["Content-Length"])
with tqdm(total=total, unit_scale=True, unit_divisor=1024, unit="B") as progress:
num_bytes_downloaded = response.num_bytes_downloaded
for chunk in response.iter_bytes():
download_file.write(chunk)
progress.update(response.num_bytes_downloaded - num_bytes_downloaded)
num_bytes_downloaded = response.num_bytes_downloaded
```
![tqdm progress bar](../img/tqdm-progress.gif)
Or an alternate example, this time using the [`rich`](https://github.com/willmcgugan/rich) library…
```python
import tempfile
import httpx
import rich.progress
with tempfile.NamedTemporaryFile() as download_file:
url = "https://speed.hetzner.de/100MB.bin"
with httpx.stream("GET", url) as response:
total = int(response.headers["Content-Length"])
with rich.progress.Progress(
"[progress.percentage]{task.percentage:>3.0f}%",
rich.progress.BarColumn(bar_width=None),
rich.progress.DownloadColumn(),
rich.progress.TransferSpeedColumn(),
) as progress:
download_task = progress.add_task("Download", total=total)
for chunk in response.iter_bytes():
download_file.write(chunk)
progress.update(download_task, completed=response.num_bytes_downloaded)
```
![rich progress bar](../img/rich-progress.gif)
## Monitoring upload progress
If you need to monitor upload progress of large responses, you can use request content generator streaming.
For example, showing a progress bar using the [`tqdm`](https://github.com/tqdm/tqdm) library.
```python
import io
import random
import httpx
from tqdm import tqdm
def gen():
"""
this is a complete example with generated random bytes.
you can replace `io.BytesIO` with real file object.
"""
total = 32 * 1024 * 1024 # 32m
with tqdm(ascii=True, unit_scale=True, unit='B', unit_divisor=1024, total=total) as bar:
with io.BytesIO(random.randbytes(total)) as f:
while data := f.read(1024):
yield data
bar.update(len(data))
httpx.post("https://httpbin.org/post", content=gen())
```
![tqdm progress bar](../img/tqdm-progress.gif)
## Multipart file encoding
As mentioned in the [quickstart](../quickstart.md#sending-multipart-file-uploads)
multipart file encoding is available by passing a dictionary with the
name of the payloads as keys and either tuple of elements or a file-like object or a string as values.
```pycon
>>> with open('report.xls', 'rb') as report_file:
... files = {'upload-file': ('report.xls', report_file, 'application/vnd.ms-excel')}
... r = httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
...
"files": {
"upload-file": "<... binary content ...>"
},
...
}
```
More specifically, if a tuple is used as a value, it must have between 2 and 3 elements:
- The first element is an optional file name which can be set to `None`.
- The second element may be a file-like object or a string which will be automatically
encoded in UTF-8.
- An optional third element can be used to specify the
[MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_Types)
of the file being uploaded. If not specified HTTPX will attempt to guess the MIME type based
on the file name, with unknown file extensions defaulting to "application/octet-stream".
If the file name is explicitly set to `None` then HTTPX will not include a content-type
MIME header field.
```pycon
>>> files = {'upload-file': (None, 'text content', 'text/plain')}
>>> r = httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
...
"files": {},
"form": {
"upload-file": "text-content"
},
...
}
```
!!! tip
It is safe to upload large files this way. File uploads are streaming by default, meaning that only one chunk will be loaded into memory at a time.
Non-file data fields can be included in the multipart form using by passing them to `data=...`.
You can also send multiple files in one go with a multiple file field form.
To do that, pass a list of `(field, <file>)` items instead of a dictionary, allowing you to pass multiple items with the same `field`.
For instance this request sends 2 files, `foo.png` and `bar.png` in one request on the `images` form field:
```pycon
>>> with open('foo.png', 'rb') as foo_file, open('bar.png', 'rb') as bar_file:
... files = [
... ('images', ('foo.png', foo_file, 'image/png')),
... ('images', ('bar.png', bar_file, 'image/png')),
... ]
... r = httpx.post("https://httpbin.org/post", files=files)
```

View File

@ -0,0 +1,65 @@
HTTPX allows you to register "event hooks" with the client, that are called
every time a particular type of event takes place.
There are currently two event hooks:
* `request` - Called after a request is fully prepared, but before it is sent to the network. Passed the `request` instance.
* `response` - Called after the response has been fetched from the network, but before it is returned to the caller. Passed the `response` instance.
These allow you to install client-wide functionality such as logging, monitoring or tracing.
```python
def log_request(request):
print(f"Request event hook: {request.method} {request.url} - Waiting for response")
def log_response(response):
request = response.request
print(f"Response event hook: {request.method} {request.url} - Status {response.status_code}")
client = httpx.Client(event_hooks={'request': [log_request], 'response': [log_response]})
```
You can also use these hooks to install response processing code, such as this
example, which creates a client instance that always raises `httpx.HTTPStatusError`
on 4xx and 5xx responses.
```python
def raise_on_4xx_5xx(response):
response.raise_for_status()
client = httpx.Client(event_hooks={'response': [raise_on_4xx_5xx]})
```
!!! note
Response event hooks are called before determining if the response body
should be read or not.
If you need access to the response body inside an event hook, you'll
need to call `response.read()`, or for AsyncClients, `response.aread()`.
The hooks are also allowed to modify `request` and `response` objects.
```python
def add_timestamp(request):
request.headers['x-request-timestamp'] = datetime.now(tz=datetime.utc).isoformat()
client = httpx.Client(event_hooks={'request': [add_timestamp]})
```
Event hooks must always be set as a **list of callables**, and you may register
multiple event hooks for each type of event.
As well as being able to set event hooks on instantiating the client, there
is also an `.event_hooks` property, that allows you to inspect and modify
the installed hooks.
```python
client = httpx.Client()
client.event_hooks['request'] = [log_request]
client.event_hooks['response'] = [log_response, raise_on_4xx_5xx]
```
!!! note
If you are using HTTPX's async support, then you need to be aware that
hooks registered with `httpx.AsyncClient` MUST be async functions,
rather than plain functions.

242
docs/advanced/extensions.md Normal file
View File

@ -0,0 +1,242 @@
# Extensions
Request and response extensions provide a untyped space where additional information may be added.
Extensions should be used for features that may not be available on all transports, and that do not fit neatly into [the simplified request/response model](https://www.encode.io/httpcore/extensions/) that the underlying `httpcore` package uses as its API.
Several extensions are supported on the request:
```python
# Request timeouts actually implemented as an extension on
# the request, ensuring that they are passed throughout the
# entire call stack.
client = httpx.Client()
response = client.get(
"https://www.example.com",
extensions={"timeout": {"connect": 5.0}}
)
response.request.extensions["timeout"]
{"connect": 5.0}
```
And on the response:
```python
client = httpx.Client()
response = client.get("https://www.example.com")
print(response.extensions["http_version"]) # b"HTTP/1.1"
# Other server responses could have been
# b"HTTP/0.9", b"HTTP/1.0", or b"HTTP/1.1"
```
## Request Extensions
### `"trace"`
The trace extension allows a callback handler to be installed to monitor the internal
flow of events within the underlying `httpcore` transport.
The simplest way to explain this is with an example:
```python
import httpx
def log(event_name, info):
print(event_name, info)
client = httpx.Client()
response = client.get("https://www.example.com/", extensions={"trace": log})
# connection.connect_tcp.started {'host': 'www.example.com', 'port': 443, 'local_address': None, 'timeout': None}
# connection.connect_tcp.complete {'return_value': <httpcore.backends.sync.SyncStream object at 0x1093f94d0>}
# connection.start_tls.started {'ssl_context': <ssl.SSLContext object at 0x1093ee750>, 'server_hostname': b'www.example.com', 'timeout': None}
# connection.start_tls.complete {'return_value': <httpcore.backends.sync.SyncStream object at 0x1093f9450>}
# http11.send_request_headers.started {'request': <Request [b'GET']>}
# http11.send_request_headers.complete {'return_value': None}
# http11.send_request_body.started {'request': <Request [b'GET']>}
# http11.send_request_body.complete {'return_value': None}
# http11.receive_response_headers.started {'request': <Request [b'GET']>}
# http11.receive_response_headers.complete {'return_value': (b'HTTP/1.1', 200, b'OK', [(b'Age', b'553715'), (b'Cache-Control', b'max-age=604800'), (b'Content-Type', b'text/html; charset=UTF-8'), (b'Date', b'Thu, 21 Oct 2021 17:08:42 GMT'), (b'Etag', b'"3147526947+ident"'), (b'Expires', b'Thu, 28 Oct 2021 17:08:42 GMT'), (b'Last-Modified', b'Thu, 17 Oct 2019 07:18:26 GMT'), (b'Server', b'ECS (nyb/1DCD)'), (b'Vary', b'Accept-Encoding'), (b'X-Cache', b'HIT'), (b'Content-Length', b'1256')])}
# http11.receive_response_body.started {'request': <Request [b'GET']>}
# http11.receive_response_body.complete {'return_value': None}
# http11.response_closed.started {}
# http11.response_closed.complete {'return_value': None}
```
The `event_name` and `info` arguments here will be one of the following:
* `{event_type}.{event_name}.started`, `<dictionary of keyword arguments>`
* `{event_type}.{event_name}.complete`, `{"return_value": <...>}`
* `{event_type}.{event_name}.failed`, `{"exception": <...>}`
Note that when using async code the handler function passed to `"trace"` must be an `async def ...` function.
The following event types are currently exposed...
**Establishing the connection**
* `"connection.connect_tcp"`
* `"connection.connect_unix_socket"`
* `"connection.start_tls"`
**HTTP/1.1 events**
* `"http11.send_request_headers"`
* `"http11.send_request_body"`
* `"http11.receive_response"`
* `"http11.receive_response_body"`
* `"http11.response_closed"`
**HTTP/2 events**
* `"http2.send_connection_init"`
* `"http2.send_request_headers"`
* `"http2.send_request_body"`
* `"http2.receive_response_headers"`
* `"http2.receive_response_body"`
* `"http2.response_closed"`
The exact set of trace events may be subject to change across different versions of `httpcore`. If you need to rely on a particular set of events it is recommended that you pin installation of the package to a fixed version.
### `"sni_hostname"`
The server's hostname, which is used to confirm the hostname supplied by the SSL certificate.
If you want to connect to an explicit IP address rather than using the standard DNS hostname lookup, then you'll need to use this request extension.
For example:
``` python
# Connect to '185.199.108.153' but use 'www.encode.io' in the Host header,
# and use 'www.encode.io' when SSL verifying the server hostname.
client = httpx.Client()
headers = {"Host": "www.encode.io"}
extensions = {"sni_hostname": "www.encode.io"}
response = client.get(
"https://185.199.108.153/path",
headers=headers,
extensions=extensions
)
```
### `"timeout"`
A dictionary of `str: Optional[float]` timeout values.
May include values for `'connect'`, `'read'`, `'write'`, or `'pool'`.
For example:
```python
# Timeout if a connection takes more than 5 seconds to established, or if
# we are blocked waiting on the connection pool for more than 10 seconds.
client = httpx.Client()
response = client.get(
"https://www.example.com",
extensions={"timeout": {"connect": 5.0, "pool": 10.0}}
)
```
This extension is how the `httpx` timeouts are implemented, ensuring that the timeout values are associated with the request instance and passed throughout the stack. You shouldn't typically be working with this extension directly, but use the higher level `timeout` API instead.
### `"target"`
The target that is used as [the HTTP target instead of the URL path](https://datatracker.ietf.org/doc/html/rfc2616#section-5.1.2).
This enables support constructing requests that would otherwise be unsupported.
* URL paths with non-standard escaping applied.
* Forward proxy requests using an absolute URI.
* Tunneling proxy requests using `CONNECT` with hostname as the target.
* Server-wide `OPTIONS *` requests.
Some examples:
Using the 'target' extension to send requests without the standard path escaping rules...
```python
# Typically a request to "https://www.example.com/test^path" would
# connect to "www.example.com" and send an HTTP/1.1 request like...
#
# GET /test%5Epath HTTP/1.1
#
# Using the target extension we can include the literal '^'...
#
# GET /test^path HTTP/1.1
#
# Note that requests must still be valid HTTP requests.
# For example including whitespace in the target will raise a `LocalProtocolError`.
extensions = {"target": b"/test^path"}
response = httpx.get("https://www.example.com", extensions=extensions)
```
The `target` extension also allows server-wide `OPTIONS *` requests to be constructed...
```python
# This will send the following request...
#
# CONNECT * HTTP/1.1
extensions = {"target": b"*"}
response = httpx.request("CONNECT", "https://www.example.com", extensions=extensions)
```
## Response Extensions
### `"http_version"`
The HTTP version, as bytes. Eg. `b"HTTP/1.1"`.
When using HTTP/1.1 the response line includes an explicit version, and the value of this key could feasibly be one of `b"HTTP/0.9"`, `b"HTTP/1.0"`, or `b"HTTP/1.1"`.
When using HTTP/2 there is no further response versioning included in the protocol, and the value of this key will always be `b"HTTP/2"`.
### `"reason_phrase"`
The reason-phrase of the HTTP response, as bytes. For example `b"OK"`. Some servers may include a custom reason phrase, although this is not recommended.
HTTP/2 onwards does not include a reason phrase on the wire.
When no key is included, a default based on the status code may be used.
### `"stream_id"`
When HTTP/2 is being used the `"stream_id"` response extension can be accessed to determine the ID of the data stream that the response was sent on.
### `"network_stream"`
The `"network_stream"` extension allows developers to handle HTTP `CONNECT` and `Upgrade` requests, by providing an API that steps outside the standard request/response model, and can directly read or write to the network.
The interface provided by the network stream:
* `read(max_bytes, timeout = None) -> bytes`
* `write(buffer, timeout = None)`
* `close()`
* `start_tls(ssl_context, server_hostname = None, timeout = None) -> NetworkStream`
* `get_extra_info(info) -> Any`
This API can be used as the foundation for working with HTTP proxies, WebSocket upgrades, and other advanced use-cases.
See the [network backends documentation](https://www.encode.io/httpcore/network-backends/) for more information on working directly with network streams.
**Extra network information**
The network stream abstraction also allows access to various low-level information that may be exposed by the underlying socket:
```python
response = httpx.get("https://www.example.com")
network_stream = response.extensions["network_stream"]
client_addr = network_stream.get_extra_info("client_addr")
server_addr = network_stream.get_extra_info("server_addr")
print("Client address", client_addr)
print("Server address", server_addr)
```
The socket SSL information is also available through this interface, although you need to ensure that the underlying connection is still open, in order to access it...
```python
with httpx.stream("GET", "https://www.example.com") as response:
network_stream = response.extensions["network_stream"]
ssl_object = network_stream.get_extra_info("ssl_object")
print("TLS version", ssl_object.version())
```

83
docs/advanced/proxies.md Normal file
View File

@ -0,0 +1,83 @@
HTTPX supports setting up [HTTP proxies](https://en.wikipedia.org/wiki/Proxy_server#Web_proxy_servers) via the `proxy` parameter to be passed on client initialization or top-level API functions like `httpx.get(..., proxy=...)`.
<div align="center">
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/Open_proxy_h2g2bob.svg/480px-Open_proxy_h2g2bob.svg.png"/>
<figcaption><em>Diagram of how a proxy works (source: Wikipedia). The left hand side "Internet" blob may be your HTTPX client requesting <code>example.com</code> through a proxy.</em></figcaption>
</div>
## HTTP Proxies
To route all traffic (HTTP and HTTPS) to a proxy located at `http://localhost:8030`, pass the proxy URL to the client...
```python
with httpx.Client(proxy="http://localhost:8030") as client:
...
```
For more advanced use cases, pass a mounts `dict`. For example, to route HTTP and HTTPS requests to 2 different proxies, respectively located at `http://localhost:8030`, and `http://localhost:8031`, pass a `dict` of proxy URLs:
```python
proxy_mounts = {
"http://": httpx.HTTPTransport(proxy="http://localhost:8030"),
"https://": httpx.HTTPTransport(proxy="http://localhost:8031"),
}
with httpx.Client(mounts=proxy_mounts) as client:
...
```
For detailed information about proxy routing, see the [Routing](#routing) section.
!!! tip "Gotcha"
In most cases, the proxy URL for the `https://` key _should_ use the `http://` scheme (that's not a typo!).
This is because HTTP proxying requires initiating a connection with the proxy server. While it's possible that your proxy supports doing it via HTTPS, most proxies only support doing it via HTTP.
For more information, see [FORWARD vs TUNNEL](#forward-vs-tunnel).
## Authentication
Proxy credentials can be passed as the `userinfo` section of the proxy URL. For example:
```python
with httpx.Client(proxy="http://username:password@localhost:8030") as client:
...
```
## Proxy mechanisms
!!! note
This section describes **advanced** proxy concepts and functionality.
### FORWARD vs TUNNEL
In general, the flow for making an HTTP request through a proxy is as follows:
1. The client connects to the proxy (initial connection request).
2. The proxy transfers data to the server on your behalf.
How exactly step 2/ is performed depends on which of two proxying mechanisms is used:
* **Forwarding**: the proxy makes the request for you, and sends back the response it obtained from the server.
* **Tunnelling**: the proxy establishes a TCP connection to the server on your behalf, and the client reuses this connection to send the request and receive the response. This is known as an [HTTP Tunnel](https://en.wikipedia.org/wiki/HTTP_tunnel). This mechanism is how you can access websites that use HTTPS from an HTTP proxy (the client "upgrades" the connection to HTTPS by performing the TLS handshake with the server over the TCP connection provided by the proxy).
### Troubleshooting proxies
If you encounter issues when setting up proxies, please refer to our [Troubleshooting guide](../troubleshooting.md#proxies).
## SOCKS
In addition to HTTP proxies, `httpcore` also supports proxies using the SOCKS protocol.
This is an optional feature that requires an additional third-party library be installed before use.
You can install SOCKS support using `pip`:
```shell
$ pip install httpx[socks]
```
You can now configure a client to make requests via a proxy using the SOCKS protocol:
```python
httpx.Client(proxy='socks5://user:pass@host:port')
```

View File

@ -0,0 +1,13 @@
You can control the connection pool size using the `limits` keyword
argument on the client. It takes instances of `httpx.Limits` which define:
- `max_keepalive_connections`, number of allowable keep-alive connections, or `None` to always
allow. (Defaults 20)
- `max_connections`, maximum number of allowable connections, or `None` for no limits.
(Default 100)
- `keepalive_expiry`, time limit on idle keep-alive connections in seconds, or `None` for no limits. (Default 5)
```python
limits = httpx.Limits(max_keepalive_connections=5, max_connections=10)
client = httpx.Client(limits=limits)
```

89
docs/advanced/ssl.md Normal file
View File

@ -0,0 +1,89 @@
When making a request over HTTPS, HTTPX needs to verify the identity of the requested host. To do this, it uses a bundle of SSL certificates (a.k.a. CA bundle) delivered by a trusted certificate authority (CA).
### Enabling and disabling verification
By default httpx will verify HTTPS connections, and raise an error for invalid SSL cases...
```pycon
>>> httpx.get("https://expired.badssl.com/")
httpx.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:997)
```
You can disable SSL verification completely and allow insecure requests...
```pycon
>>> httpx.get("https://expired.badssl.com/", verify=False)
<Response [200 OK]>
```
### Configuring client instances
If you're using a `Client()` instance you should pass any `verify=<...>` configuration when instantiating the client.
By default the [certifi CA bundle](https://certifiio.readthedocs.io/en/latest/) is used for SSL verification.
For more complex configurations you can pass an [SSL Context](https://docs.python.org/3/library/ssl.html) instance...
```python
import certifi
import httpx
import ssl
# This SSL context is equivalent to the default `verify=True`.
ctx = ssl.create_default_context(cafile=certifi.where())
client = httpx.Client(verify=ctx)
```
Using [the `truststore` package](https://truststore.readthedocs.io/) to support system certificate stores...
```python
import ssl
import truststore
import httpx
# Use system certificate stores.
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
client = httpx.Client(verify=ctx)
```
Loding an alternative certificate verification store using [the standard SSL context API](https://docs.python.org/3/library/ssl.html)...
```python
import httpx
import ssl
# Use an explicitly configured certificate store.
ctx = ssl.create_default_context(cafile="path/to/certs.pem") # Either cafile or capath.
client = httpx.Client(verify=ctx)
```
### Client side certificates
Client side certificates allow a remote server to verify the client. They tend to be used within private organizations to authenticate requests to remote servers.
You can specify client-side certificates, using the [`.load_cert_chain()`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_cert_chain) API...
```python
ctx = ssl.create_default_context()
ctx.load_cert_chain(certfile="path/to/client.pem") # Optionally also keyfile or password.
client = httpx.Client(verify=ctx)
```
### Working with `SSL_CERT_FILE` and `SSL_CERT_DIR`
`httpx` does respect the `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables by default. For details, refer to [the section on the environment variables page](../environment_variables.md#ssl_cert_file).
### Making HTTPS requests to a local server
When making requests to local servers, such as a development server running on `localhost`, you will typically be using unencrypted HTTP connections.
If you do need to make HTTPS connections to a local server, for example to test an HTTPS-only service, you will need to create and use your own certificates. Here's one way to do it...
1. Use [trustme](https://github.com/python-trio/trustme) to generate a pair of server key/cert files, and a client cert file.
2. Pass the server key/cert files when starting your local server. (This depends on the particular web server you're using. For example, [Uvicorn](https://www.uvicorn.org) provides the `--ssl-keyfile` and `--ssl-certfile` options.)
3. Configure `httpx` to use the certificates stored in `client.pem`.
```python
ctx = ssl.create_default_context(cafile="client.pem")
client = httpx.Client(verify=ctx)
```

View File

@ -0,0 +1,75 @@
When accessing `response.text`, we need to decode the response bytes into a unicode text representation.
By default `httpx` will use `"charset"` information included in the response `Content-Type` header to determine how the response bytes should be decoded into text.
In cases where no charset information is included on the response, the default behaviour is to assume "utf-8" encoding, which is by far the most widely used text encoding on the internet.
## Using the default encoding
To understand this better let's start by looking at the default behaviour for text decoding...
```python
import httpx
# Instantiate a client with the default configuration.
client = httpx.Client()
# Using the client...
response = client.get(...)
print(response.encoding) # This will either print the charset given in
# the Content-Type charset, or else "utf-8".
print(response.text) # The text will either be decoded with the Content-Type
# charset, or using "utf-8".
```
This is normally absolutely fine. Most servers will respond with a properly formatted Content-Type header, including a charset encoding. And in most cases where no charset encoding is included, UTF-8 is very likely to be used, since it is so widely adopted.
## Using an explicit encoding
In some cases we might be making requests to a site where no character set information is being set explicitly by the server, but we know what the encoding is. In this case it's best to set the default encoding explicitly on the client.
```python
import httpx
# Instantiate a client with a Japanese character set as the default encoding.
client = httpx.Client(default_encoding="shift-jis")
# Using the client...
response = client.get(...)
print(response.encoding) # This will either print the charset given in
# the Content-Type charset, or else "shift-jis".
print(response.text) # The text will either be decoded with the Content-Type
# charset, or using "shift-jis".
```
## Using auto-detection
In cases where the server is not reliably including character set information, and where we don't know what encoding is being used, we can enable auto-detection to make a best-guess attempt when decoding from bytes to text.
To use auto-detection you need to set the `default_encoding` argument to a callable instead of a string. This callable should be a function which takes the input bytes as an argument and returns the character set to use for decoding those bytes to text.
There are two widely used Python packages which both handle this functionality:
* [`chardet`](https://chardet.readthedocs.io/) - This is a well established package, and is a port of [the auto-detection code in Mozilla](https://www-archive.mozilla.org/projects/intl/chardet.html).
* [`charset-normalizer`](https://charset-normalizer.readthedocs.io/) - A newer package, motivated by `chardet`, with a different approach.
Let's take a look at installing autodetection using one of these packages...
```shell
$ pip install httpx
$ pip install chardet
```
Once `chardet` is installed, we can configure a client to use character-set autodetection.
```python
import httpx
import chardet
def autodetect(content):
return chardet.detect(content).get("encoding")
# Using a client with character-set autodetection enabled.
client = httpx.Client(default_encoding=autodetect)
response = client.get(...)
print(response.encoding) # This will either print the charset given in
# the Content-Type charset, or else the auto-detected
# character set.
print(response.text)
```

71
docs/advanced/timeouts.md Normal file
View File

@ -0,0 +1,71 @@
HTTPX is careful to enforce timeouts everywhere by default.
The default behavior is to raise a `TimeoutException` after 5 seconds of
network inactivity.
## Setting and disabling timeouts
You can set timeouts for an individual request:
```python
# Using the top-level API:
httpx.get('http://example.com/api/v1/example', timeout=10.0)
# Using a client instance:
with httpx.Client() as client:
client.get("http://example.com/api/v1/example", timeout=10.0)
```
Or disable timeouts for an individual request:
```python
# Using the top-level API:
httpx.get('http://example.com/api/v1/example', timeout=None)
# Using a client instance:
with httpx.Client() as client:
client.get("http://example.com/api/v1/example", timeout=None)
```
## Setting a default timeout on a client
You can set a timeout on a client instance, which results in the given
`timeout` being used as the default for requests made with this client:
```python
client = httpx.Client() # Use a default 5s timeout everywhere.
client = httpx.Client(timeout=10.0) # Use a default 10s timeout everywhere.
client = httpx.Client(timeout=None) # Disable all timeouts by default.
```
## Fine tuning the configuration
HTTPX also allows you to specify the timeout behavior in more fine grained detail.
There are four different types of timeouts that may occur. These are **connect**,
**read**, **write**, and **pool** timeouts.
* The **connect** timeout specifies the maximum amount of time to wait until
a socket connection to the requested host is established. If HTTPX is unable to connect
within this time frame, a `ConnectTimeout` exception is raised.
* The **read** timeout specifies the maximum duration to wait for a chunk of
data to be received (for example, a chunk of the response body). If HTTPX is
unable to receive data within this time frame, a `ReadTimeout` exception is raised.
* The **write** timeout specifies the maximum duration to wait for a chunk of
data to be sent (for example, a chunk of the request body). If HTTPX is unable
to send data within this time frame, a `WriteTimeout` exception is raised.
* The **pool** timeout specifies the maximum duration to wait for acquiring
a connection from the connection pool. If HTTPX is unable to acquire a connection
within this time frame, a `PoolTimeout` exception is raised. A related
configuration here is the maximum number of allowable connections in the
connection pool, which is configured by the `limits` argument.
You can configure the timeout behavior for any of these values...
```python
# A client with a 60s timeout for connecting, and a 10s timeout elsewhere.
timeout = httpx.Timeout(10.0, connect=60.0)
client = httpx.Client(timeout=timeout)
response = client.get('http://example.com/')
```

454
docs/advanced/transports.md Normal file
View File

@ -0,0 +1,454 @@
HTTPX's `Client` also accepts a `transport` argument. This argument allows you
to provide a custom Transport object that will be used to perform the actual
sending of the requests.
## HTTP Transport
For some advanced configuration you might need to instantiate a transport
class directly, and pass it to the client instance. One example is the
`local_address` configuration which is only available via this low-level API.
```pycon
>>> import httpx
>>> transport = httpx.HTTPTransport(local_address="0.0.0.0")
>>> client = httpx.Client(transport=transport)
```
Connection retries are also available via this interface. Requests will be retried the given number of times in case an `httpx.ConnectError` or an `httpx.ConnectTimeout` occurs, allowing smoother operation under flaky networks. If you need other forms of retry behaviors, such as handling read/write errors or reacting to `503 Service Unavailable`, consider general-purpose tools such as [tenacity](https://github.com/jd/tenacity).
```pycon
>>> import httpx
>>> transport = httpx.HTTPTransport(retries=1)
>>> client = httpx.Client(transport=transport)
```
Similarly, instantiating a transport directly provides a `uds` option for
connecting via a Unix Domain Socket that is only available via this low-level API:
```pycon
>>> import httpx
>>> # Connect to the Docker API via a Unix Socket.
>>> transport = httpx.HTTPTransport(uds="/var/run/docker.sock")
>>> client = httpx.Client(transport=transport)
>>> response = client.get("http://docker/info")
>>> response.json()
{"ID": "...", "Containers": 4, "Images": 74, ...}
```
## WSGI Transport
You can configure an `httpx` client to call directly into a Python web application using the WSGI protocol.
This is particularly useful for two main use-cases:
* Using `httpx` as a client inside test cases.
* Mocking out external services during tests or in dev or staging environments.
### Example
Here's an example of integrating against a Flask application:
```python
from flask import Flask
import httpx
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"
transport = httpx.WSGITransport(app=app)
with httpx.Client(transport=transport, base_url="http://testserver") as client:
r = client.get("/")
assert r.status_code == 200
assert r.text == "Hello World!"
```
### Configuration
For some more complex cases you might need to customize the WSGI transport. This allows you to:
* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
* Mount the WSGI application at a subpath by setting `script_name` (WSGI).
* Use a given client address for requests by setting `remote_addr` (WSGI).
For example:
```python
# Instantiate a client that makes WSGI requests with a client IP of "1.2.3.4".
transport = httpx.WSGITransport(app=app, remote_addr="1.2.3.4")
with httpx.Client(transport=transport, base_url="http://testserver") as client:
...
```
## ASGI Transport
You can configure an `httpx` client to call directly into an async Python web application using the ASGI protocol.
This is particularly useful for two main use-cases:
* Using `httpx` as a client inside test cases.
* Mocking out external services during tests or in dev or staging environments.
### Example
Let's take this Starlette application as an example:
```python
from starlette.applications import Starlette
from starlette.responses import HTMLResponse
from starlette.routing import Route
async def hello(request):
return HTMLResponse("Hello World!")
app = Starlette(routes=[Route("/", hello)])
```
We can make requests directly against the application, like so:
```python
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
r = await client.get("/")
assert r.status_code == 200
assert r.text == "Hello World!"
```
### Configuration
For some more complex cases you might need to customise the ASGI transport. This allows you to:
* Inspect 500 error responses rather than raise exceptions by setting `raise_app_exceptions=False`.
* Mount the ASGI application at a subpath by setting `root_path`.
* Use a given client address for requests by setting `client`.
For example:
```python
# Instantiate a client that makes ASGI requests with a client IP of "1.2.3.4",
# on port 123.
transport = httpx.ASGITransport(app=app, client=("1.2.3.4", 123))
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
...
```
See [the ASGI documentation](https://asgi.readthedocs.io/en/latest/specs/www.html#connection-scope) for more details on the `client` and `root_path` keys.
### ASGI startup and shutdown
It is not in the scope of HTTPX to trigger ASGI lifespan events of your app.
However it is suggested to use `LifespanManager` from [asgi-lifespan](https://github.com/florimondmanca/asgi-lifespan#usage) in pair with `AsyncClient`.
## Custom transports
A transport instance must implement the low-level Transport API which deals
with sending a single request, and returning a response. You should either
subclass `httpx.BaseTransport` to implement a transport to use with `Client`,
or subclass `httpx.AsyncBaseTransport` to implement a transport to
use with `AsyncClient`.
At the layer of the transport API we're using the familiar `Request` and
`Response` models.
See the `handle_request` and `handle_async_request` docstrings for more details
on the specifics of the Transport API.
A complete example of a custom transport implementation would be:
```python
import json
import httpx
class HelloWorldTransport(httpx.BaseTransport):
"""
A mock transport that always returns a JSON "Hello, world!" response.
"""
def handle_request(self, request):
return httpx.Response(200, json={"text": "Hello, world!"})
```
Or this example, which uses a custom transport and `httpx.Mounts` to always redirect `http://` requests.
```python
class HTTPSRedirect(httpx.BaseTransport):
"""
A transport that always redirects to HTTPS.
"""
def handle_request(self, request):
url = request.url.copy_with(scheme="https")
return httpx.Response(303, headers={"Location": str(url)})
# A client where any `http` requests are always redirected to `https`
transport = httpx.Mounts({
'http://': HTTPSRedirect()
'https://': httpx.HTTPTransport()
})
client = httpx.Client(transport=transport)
```
A useful pattern here is custom transport classes that wrap the default HTTP implementation. For example...
```python
class DebuggingTransport(httpx.BaseTransport):
def __init__(self, **kwargs):
self._wrapper = httpx.HTTPTransport(**kwargs)
def handle_request(self, request):
print(f">>> {request}")
response = self._wrapper.handle_request(request)
print(f"<<< {response}")
return response
def close(self):
self._wrapper.close()
transport = DebuggingTransport()
client = httpx.Client(transport=transport)
```
Here's another case, where we're using a round-robin across a number of different proxies...
```python
class ProxyRoundRobin(httpx.BaseTransport):
def __init__(self, proxies, **kwargs):
self._transports = [
httpx.HTTPTransport(proxy=proxy, **kwargs)
for proxy in proxies
]
self._idx = 0
def handle_request(self, request):
transport = self._transports[self._idx]
self._idx = (self._idx + 1) % len(self._transports)
return transport.handle_request(request)
def close(self):
for transport in self._transports:
transport.close()
proxies = [
httpx.Proxy("http://127.0.0.1:8081"),
httpx.Proxy("http://127.0.0.1:8082"),
httpx.Proxy("http://127.0.0.1:8083"),
]
transport = ProxyRoundRobin(proxies=proxies)
client = httpx.Client(transport=transport)
```
## Mock transports
During testing it can often be useful to be able to mock out a transport,
and return pre-determined responses, rather than making actual network requests.
The `httpx.MockTransport` class accepts a handler function, which can be used
to map requests onto pre-determined responses:
```python
def handler(request):
return httpx.Response(200, json={"text": "Hello, world!"})
# Switch to a mock transport, if the TESTING environment variable is set.
if os.environ.get('TESTING', '').upper() == "TRUE":
transport = httpx.MockTransport(handler)
else:
transport = httpx.HTTPTransport()
client = httpx.Client(transport=transport)
```
For more advanced use-cases you might want to take a look at either [the third-party
mocking library, RESPX](https://lundberg.github.io/respx/), or the [pytest-httpx library](https://github.com/Colin-b/pytest_httpx).
## Mounting transports
You can also mount transports against given schemes or domains, to control
which transport an outgoing request should be routed via, with [the same style
used for specifying proxy routing](#routing).
```python
import httpx
class HTTPSRedirectTransport(httpx.BaseTransport):
"""
A transport that always redirects to HTTPS.
"""
def handle_request(self, method, url, headers, stream, extensions):
scheme, host, port, path = url
if port is None:
location = b"https://%s%s" % (host, path)
else:
location = b"https://%s:%d%s" % (host, port, path)
stream = httpx.ByteStream(b"")
headers = [(b"location", location)]
extensions = {}
return 303, headers, stream, extensions
# A client where any `http` requests are always redirected to `https`
mounts = {'http://': HTTPSRedirectTransport()}
client = httpx.Client(mounts=mounts)
```
A couple of other sketches of how you might take advantage of mounted transports...
Disabling HTTP/2 on a single given domain...
```python
mounts = {
"all://": httpx.HTTPTransport(http2=True),
"all://*example.org": httpx.HTTPTransport()
}
client = httpx.Client(mounts=mounts)
```
Mocking requests to a given domain:
```python
# All requests to "example.org" should be mocked out.
# Other requests occur as usual.
def handler(request):
return httpx.Response(200, json={"text": "Hello, World!"})
mounts = {"all://example.org": httpx.MockTransport(handler)}
client = httpx.Client(mounts=mounts)
```
Adding support for custom schemes:
```python
# Support URLs like "file:///Users/sylvia_green/websites/new_client/index.html"
mounts = {"file://": FileSystemTransport()}
client = httpx.Client(mounts=mounts)
```
### Routing
HTTPX provides a powerful mechanism for routing requests, allowing you to write complex rules that specify which transport should be used for each request.
The `mounts` dictionary maps URL patterns to HTTP transports. HTTPX matches requested URLs against URL patterns to decide which transport should be used, if any. Matching is done from most specific URL patterns (e.g. `https://<domain>:<port>`) to least specific ones (e.g. `https://`).
HTTPX supports routing requests based on **scheme**, **domain**, **port**, or a combination of these.
### Wildcard routing
Route everything through a transport...
```python
mounts = {
"all://": httpx.HTTPTransport(proxy="http://localhost:8030"),
}
```
### Scheme routing
Route HTTP requests through one transport, and HTTPS requests through another...
```python
mounts = {
"http://": httpx.HTTPTransport(proxy="http://localhost:8030"),
"https://": httpx.HTTPTransport(proxy="http://localhost:8031"),
}
```
### Domain routing
Proxy all requests on domain "example.com", let other requests pass through...
```python
mounts = {
"all://example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
}
```
Proxy HTTP requests on domain "example.com", let HTTPS and other requests pass through...
```python
mounts = {
"http://example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
}
```
Proxy all requests to "example.com" and its subdomains, let other requests pass through...
```python
mounts = {
"all://*example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
}
```
Proxy all requests to strict subdomains of "example.com", let "example.com" and other requests pass through...
```python
mounts = {
"all://*.example.com": httpx.HTTPTransport(proxy="http://localhost:8030"),
}
```
### Port routing
Proxy HTTPS requests on port 1234 to "example.com"...
```python
mounts = {
"https://example.com:1234": httpx.HTTPTransport(proxy="http://localhost:8030"),
}
```
Proxy all requests on port 1234...
```python
mounts = {
"all://*:1234": httpx.HTTPTransport(proxy="http://localhost:8030"),
}
```
### No-proxy support
It is also possible to define requests that _shouldn't_ be routed through the transport.
To do so, pass `None` as the proxy URL. For example...
```python
mounts = {
# Route requests through a proxy by default...
"all://": httpx.HTTPTransport(proxy="http://localhost:8031"),
# Except those for "example.com".
"all://example.com": None,
}
```
### Complex configuration example
You can combine the routing features outlined above to build complex proxy routing configurations. For example...
```python
mounts = {
# Route all traffic through a proxy by default...
"all://": httpx.HTTPTransport(proxy="http://localhost:8030"),
# But don't use proxies for HTTPS requests to "domain.io"...
"https://domain.io": None,
# And use another proxy for requests to "example.com" and its subdomains...
"all://*example.com": httpx.HTTPTransport(proxy="http://localhost:8031"),
# And yet another proxy if HTTP is used,
# and the "internal" subdomain on port 5550 is requested...
"http://internal.example.com:5550": httpx.HTTPTransport(proxy="http://localhost:8032"),
}
```
### Environment variables
There are also environment variables that can be used to control the dictionary of the client mounts.
They can be used to configure HTTP proxying for clients.
See documentation on [`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`](../environment_variables.md#http_proxy-https_proxy-all_proxy)
and [`NO_PROXY`](../environment_variables.md#no_proxy) for more information.

View File

@ -8,40 +8,45 @@
enable HTTP/2 and connection pooling for more efficient and
long-lived connections.
* `get(url, [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `options(url, [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `head(url, [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `post(url, [data], [json], [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `put(url, [data], [json], [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `patch(url, [data], [json], [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `delete(url, [data], [json], [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `request(method, url, [data], [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `build_request(method, url, [data], [files], [json], [params], [headers], [cookies])`
::: httpx.request
:docstring:
::: httpx.get
:docstring:
::: httpx.options
:docstring:
::: httpx.head
:docstring:
::: httpx.post
:docstring:
::: httpx.put
:docstring:
::: httpx.patch
:docstring:
::: httpx.delete
:docstring:
::: httpx.stream
:docstring:
## `Client`
*An HTTP client, with connection pooling, HTTP/2, redirects, cookie persistence, etc.*
::: httpx.Client
:docstring:
:members: headers cookies params auth request get head options post put patch delete stream build_request send close
```python
>>> client = httpx.Client()
>>> response = client.get('https://example.org')
```
## `AsyncClient`
::: httpx.AsyncClient
:docstring:
:members: headers cookies params auth request get head options post put patch delete stream build_request send aclose
* `def __init__([auth], [params], [headers], [cookies], [verify], [cert], [timeout], [pool_limits], [max_redirects], [app], [dispatch])`
* `.params` - **QueryParams**
* `.headers` - **Headers**
* `.cookies` - **Cookies**
* `def .get(url, [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `def .options(url, [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `def .head(url, [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `def .post(url, [data], [json], [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `def .put(url, [data], [json], [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `def .patch(url, [data], [json], [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `def .delete(url, [data], [json], [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `def .request(method, url, [data], [params], [headers], [cookies], [auth], [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `def .build_request(method, url, [data], [files], [json], [params], [headers], [cookies])`
* `def .send(request, [stream], [allow_redirects], [verify], [cert], [timeout], [proxies])`
* `def .close()`
## `Response`
@ -58,35 +63,44 @@
* `.encoding` - **str**
* `.is_redirect` - **bool**
* `.request` - **Request**
* `.next_request` - **Optional[Request]**
* `.cookies` - **Cookies**
* `.history` - **List[Response]**
* `.elapsed` - **[timedelta](https://docs.python.org/3/library/datetime.html)**
* The amount of time elapsed between sending the first byte and parsing the headers (not including time spent reading
the response). Use
* The amount of time elapsed between sending the request and calling `close()` on the corresponding response received for that request.
[total_seconds()](https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds) to correctly get
the total elapsed seconds.
* `def .raise_for_status()` - **None**
* `def .raise_for_status()` - **Response**
* `def .json()` - **Any**
* `def .read()` - **bytes**
* `def .stream()` - **bytes iterator**
* `def .raw()` - **bytes iterator**
* `def .iter_raw([chunk_size])` - **bytes iterator**
* `def .iter_bytes([chunk_size])` - **bytes iterator**
* `def .iter_text([chunk_size])` - **text iterator**
* `def .iter_lines()` - **text iterator**
* `def .close()` - **None**
* `def .next()` - **Response**
* `def .aread()` - **bytes**
* `def .aiter_raw([chunk_size])` - **async bytes iterator**
* `def .aiter_bytes([chunk_size])` - **async bytes iterator**
* `def .aiter_text([chunk_size])` - **async text iterator**
* `def .aiter_lines()` - **async text iterator**
* `def .aclose()` - **None**
* `def .anext()` - **Response**
## `Request`
*An HTTP request. Can be constructed explicitly for more control over exactly
what gets sent over the wire.*
```python
```pycon
>>> request = httpx.Request("GET", "https://example.org", headers={'host': 'example.org'})
>>> response = client.send(request)
```
* `def __init__(method, url, [params], [data], [json], [headers], [cookies])`
* `def __init__(method, url, [params], [headers], [cookies], [content], [data], [files], [json], [stream])`
* `.method` - **str**
* `.url` - **URL**
* `.content` - **byte** or **byte async iterator**
* `.content` - **byte**, **byte iterator**, or **byte async iterator**
* `.headers` - **Headers**
* `.cookies` - **Cookies**
@ -94,60 +108,44 @@ what gets sent over the wire.*
*A normalized, IDNA supporting URL.*
```python
```pycon
>>> url = URL("https://example.org/")
>>> url.host
'example.org'
```
* `def __init__(url, allow_relative=False, params=None)`
* `def __init__(url, **kwargs)`
* `.scheme` - **str**
* `.authority` - **str**
* `.host` - **str**
* `.port` - **int**
* `.path` - **str**
* `.query` - **str**
* `.full_path` - **str**
* `.raw_path` - **str**
* `.fragment` - **str**
* `.is_ssl` - **bool**
* `.origin` - **Origin**
* `.is_absolute_url` - **bool**
* `.is_relative_url` - **bool**
* `def .copy_with([scheme], [authority], [path], [query], [fragment])` - **URL**
* `def .resolve_with(url)` - **URL**
## `Origin`
*A normalized, IDNA supporting set of scheme/host/port info.*
```python
>>> Origin('https://example.org') == Origin('HTTPS://EXAMPLE.ORG:443')
True
```
* `def __init__(url)`
* `.scheme` - **str**
* `.is_ssl` - **bool**
* `.host` - **str**
* `.port` - **int**
## `Headers`
*A case-insensitive multi-dict.*
```python
```pycon
>>> headers = Headers({'Content-Type': 'application/json'})
>>> headers['content-type']
'application/json'
```
* `def __init__(self, headers)`
* `def __init__(self, headers, encoding=None)`
* `def copy()` - **Headers**
## `Cookies`
*A dict-like cookie store.*
```python
```pycon
>>> cookies = Cookies()
>>> cookies.set("name", "value", domain="example.org")
```
@ -161,3 +159,18 @@ True
* `def delete(name, [domain], [path])`
* `def clear([domain], [path])`
* *Standard mutable mapping interface*
## `Proxy`
*A configuration of the proxy server.*
```pycon
>>> proxy = Proxy("http://proxy.example.com:8030")
>>> client = Client(proxy=proxy)
```
* `def __init__(url, [ssl_context], [auth], [headers])`
* `.url` - **URL**
* `.auth` - **tuple[str, str]**
* `.headers` - **Headers**
* `.ssl_context` - **SSLContext**

View File

@ -1,4 +1,4 @@
# Async Client
# Async Support
HTTPX offers a standard synchronous API by default, but also gives you
the option of an async client if you need it.
@ -14,67 +14,131 @@ async client for sending outgoing HTTP requests.
To make asynchronous requests, you'll need an `AsyncClient`.
```python
```pycon
>>> async with httpx.AsyncClient() as client:
>>> r = await client.get('https://www.example.com/')
... r = await client.get('https://www.example.com/')
...
>>> r
<Response [200 OK]>
```
!!! tip
Use [IPython](https://ipython.readthedocs.io/en/stable/) to try this code interactively, as it supports executing `async`/`await` expressions in the console.
Use [IPython](https://ipython.readthedocs.io/en/stable/) or Python 3.9+ with `python -m asyncio` to try this code interactively, as they support executing `async`/`await` expressions in the console.
!!! note
The `async with` syntax ensures that all active connections are closed on exit.
## API Differences
It is safe to access response content (e.g. `r.text`) both inside and outside the `async with` block, unless you are using response streaming. In that case, you should `.read()`, `.stream()`, or `.close()` the response *inside* the `async with` block.
If you're using an async client then there are a few bits of API that
use async methods.
## API Differences
### Making requests
If you're using streaming responses then there are a few bits of API that
use async methods:
The request methods are all async, so you should use `response = await client.get(...)` style for all of the following:
* `AsyncClient.get(url, ...)`
* `AsyncClient.options(url, ...)`
* `AsyncClient.head(url, ...)`
* `AsyncClient.post(url, ...)`
* `AsyncClient.put(url, ...)`
* `AsyncClient.patch(url, ...)`
* `AsyncClient.delete(url, ...)`
* `AsyncClient.request(method, url, ...)`
* `AsyncClient.send(request, ...)`
### Opening and closing clients
Use `async with httpx.AsyncClient()` if you want a context-managed client...
```python
>>> async with httpx.AsyncClient() as client:
>>> r = await client.get('https://www.example.com/', stream=True)
>>> async for chunk in r.stream():
>>> ...
async with httpx.AsyncClient() as client:
...
```
The async response methods are:
!!! warning
In order to get the most benefit from connection pooling, make sure you're not instantiating multiple client instances - for example by using `async with` inside a "hot loop". This can be achieved either by having a single scoped client that's passed throughout wherever it's needed, or by having a single global client instance.
* `.read()`
* `.stream()`
* `.raw()`
* `.close()`
If you're making [parallel requests](/parallel/), then you'll also need to use an async API:
Alternatively, use `await client.aclose()` if you want to close a client explicitly:
```python
>>> async with httpx.AsyncClient() as client:
>>> async with client.parallel() as parallel:
>>> pending_one = parallel.get('https://example.com/1')
>>> pending_two = parallel.get('https://example.com/2')
>>> response_one = await pending_one.get_response()
>>> response_two = await pending_two.get_response()
client = httpx.AsyncClient()
...
await client.aclose()
```
The async parallel methods are:
### Streaming responses
* `.parallel()` *Used as an "async with" context manager.*
* `.get_response()`
* `.next_response()`
The `AsyncClient.stream(method, url, ...)` method is an async context block.
## Supported async libraries
```pycon
>>> client = httpx.AsyncClient()
>>> async with client.stream('GET', 'https://www.example.com/') as response:
... async for chunk in response.aiter_bytes():
... ...
```
You can use `AsyncClient` with any of the following async libraries.
The async response streaming methods are:
!!! tip
You will typically be using `AsyncClient` in async programs that run on `asyncio`. If that's the case, or if you're not sure what this is all about, you can safely ignore this section.
* `Response.aread()` - For conditionally reading a response inside a stream block.
* `Response.aiter_bytes()` - For streaming the response content as bytes.
* `Response.aiter_text()` - For streaming the response content as text.
* `Response.aiter_lines()` - For streaming the response content as lines of text.
* `Response.aiter_raw()` - For streaming the raw response bytes, without applying content decoding.
* `Response.aclose()` - For closing the response. You don't usually need this, since `.stream` block closes the response automatically on exit.
### [asyncio](https://docs.python.org/3/library/asyncio.html) (Default)
For situations when context block usage is not practical, it is possible to enter "manual mode" by sending a [`Request` instance](advanced/clients.md#request-instances) using `client.send(..., stream=True)`.
By default, `AsyncClient` uses `asyncio` to perform asynchronous operations and I/O calls.
Example in the context of forwarding the response to a streaming web endpoint with [Starlette](https://www.starlette.io):
```python
import httpx
from starlette.background import BackgroundTask
from starlette.responses import StreamingResponse
client = httpx.AsyncClient()
async def home(request):
req = client.build_request("GET", "https://www.example.com/")
r = await client.send(req, stream=True)
return StreamingResponse(r.aiter_text(), background=BackgroundTask(r.aclose))
```
!!! warning
When using this "manual streaming mode", it is your duty as a developer to make sure that `Response.aclose()` is called eventually. Failing to do so would leave connections open, most likely resulting in resource leaks down the line.
### Streaming requests
When sending a streaming request body with an `AsyncClient` instance, you should use an async bytes generator instead of a bytes generator:
```python
async def upload_bytes():
... # yield byte content
await client.post(url, content=upload_bytes())
```
### Explicit transport instances
When instantiating a transport instance directly, you need to use `httpx.AsyncHTTPTransport`.
For instance:
```pycon
>>> import httpx
>>> transport = httpx.AsyncHTTPTransport(retries=1)
>>> async with httpx.AsyncClient(transport=transport) as client:
>>> ...
```
## Supported async environments
HTTPX supports either `asyncio` or `trio` as an async environment.
It will auto-detect which of those two to use as the backend
for socket operations and concurrency primitives.
### [AsyncIO](https://docs.python.org/3/library/asyncio.html)
AsyncIO is Python's [built-in library](https://docs.python.org/3/library/asyncio.html)
for writing concurrent code with the async/await syntax.
```python
import asyncio
@ -82,26 +146,49 @@ import httpx
async def main():
async with httpx.AsyncClient() as client:
...
response = await client.get('https://www.example.com/')
print(response)
asyncio.run(main())
```
### [trio](https://github.com/python-trio/trio)
### [Trio](https://github.com/python-trio/trio)
To make asynchronous requests in `trio` programs, pass a `TrioBackend` to the `AsyncClient`:
Trio is [an alternative async library](https://trio.readthedocs.io/en/stable/),
designed around the [the principles of structured concurrency](https://en.wikipedia.org/wiki/Structured_concurrency).
```python
import trio
import httpx
from httpx.concurrency.trio import TrioBackend
import trio
async def main():
async with httpx.AsyncClient(backend=TrioBackend()) as client:
...
async with httpx.AsyncClient() as client:
response = await client.get('https://www.example.com/')
print(response)
trio.run(main)
```
!!! important
`trio` must be installed to import and use the `TrioBackend`.
The `trio` package must be installed to use the Trio backend.
### [AnyIO](https://github.com/agronholm/anyio)
AnyIO is an [asynchronous networking and concurrency library](https://anyio.readthedocs.io/) that works on top of either `asyncio` or `trio`. It blends in with native libraries of your chosen backend (defaults to `asyncio`).
```python
import httpx
import anyio
async def main():
async with httpx.AsyncClient() as client:
response = await client.get('https://www.example.com/')
print(response)
anyio.run(main, backend='trio')
```
## Calling into Python Web Apps
For details on calling directly into ASGI applications, see [the `ASGITransport` docs](../advanced/transports#asgitransport).

56
docs/code_of_conduct.md Normal file
View File

@ -0,0 +1,56 @@
# Code of Conduct
We expect contributors to our projects and online spaces to follow [the Python Software Foundations Code of Conduct](https://www.python.org/psf/conduct/).
The Python community is made up of members from around the globe with a diverse set of skills, personalities, and experiences. It is through these differences that our community experiences great successes and continued growth. When you're working with members of the community, this Code of Conduct will help steer your interactions and keep Python a positive, successful, and growing community.
## Our Community
Members of the Python community are **open, considerate, and respectful**. Behaviours that reinforce these values contribute to a positive environment, and include:
* **Being open.** Members of the community are open to collaboration, whether it's on PEPs, patches, problems, or otherwise.
* **Focusing on what is best for the community.** We're respectful of the processes set forth in the community, and we work within them.
* **Acknowledging time and effort.** We're respectful of the volunteer efforts that permeate the Python community. We're thoughtful when addressing the efforts of others, keeping in mind that often times the labor was completed simply for the good of the community.
* **Being respectful of differing viewpoints and experiences.** We're receptive to constructive comments and criticism, as the experiences and skill sets of other members contribute to the whole of our efforts.
* **Showing empathy towards other community members.** We're attentive in our communications, whether in person or online, and we're tactful when approaching differing views.
* **Being considerate.** Members of the community are considerate of their peers -- other Python users.
* **Being respectful.** We're respectful of others, their positions, their skills, their commitments, and their efforts.
* **Gracefully accepting constructive criticism.** When we disagree, we are courteous in raising our issues.
* **Using welcoming and inclusive language.** We're accepting of all who wish to take part in our activities, fostering an environment where anyone can participate and everyone can make a difference.
## Our Standards
Every member of our community has the right to have their identity respected. The Python community is dedicated to providing a positive experience for everyone, regardless of age, gender identity and expression, sexual orientation, disability, physical appearance, body size, ethnicity, nationality, race, or religion (or lack thereof), education, or socio-economic status.
## Inappropriate Behavior
Examples of unacceptable behavior by participants include:
* Harassment of any participants in any form
* Deliberate intimidation, stalking, or following
* Logging or taking screenshots of online activity for harassment purposes
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Violent threats or language directed against another person
* Incitement of violence or harassment towards any individual, including encouraging a person to commit suicide or to engage in self-harm
* Creating additional online accounts in order to harass another person or circumvent a ban
* Sexual language and imagery in online communities or in any conference venue, including talks
* Insults, put downs, or jokes that are based upon stereotypes, that are exclusionary, or that hold others up for ridicule
* Excessive swearing
* Unwelcome sexual attention or advances
* Unwelcome physical contact, including simulated physical contact (eg, textual descriptions like "hug" or "backrub") without consent or after a request to stop
* Pattern of inappropriate social contact, such as requesting/assuming inappropriate levels of intimacy with others
* Sustained disruption of online community discussions, in-person presentations, or other in-person events
* Continued one-on-one communication after requests to cease
* Other conduct that is inappropriate for a professional audience including people of many different backgrounds
Community members asked to stop any inappropriate behavior are expected to comply immediately.
## Enforcement
We take Code of Conduct violations seriously, and will act to ensure our spaces are welcoming, inclusive, and professional environments to communicate in.
If you need to raise a Code of Conduct report, you may do so privately by email to tom@tomchristie.com.
Reports will be treated confidentially.
Alternately you may [make a report to the Python Software Foundation](https://www.python.org/psf/conduct/reporting/).

View File

@ -1,20 +1,232 @@
# Requests Compatibility Guide
HTTPX aims to be compatible with the `requests` API wherever possible.
HTTPX aims to be broadly compatible with the `requests` API, although there are a
few design differences in places.
This documentation outlines places where the API differs...
## QuickStart
## Redirects
Pretty much all the API mentioned in the `requests` QuickStart should be identical
to the API in our own documentation. The following exceptions apply:
Unlike `requests`, HTTPX does **not follow redirects by default**.
* `Response.url` - Returns a `URL` instance, rather than a string. Use `str(response.url)` if you need a string instance.
* `httpx.codes` - In our documentation we prefer the uppercased versions, such as `codes.NOT_FOUND`,
but also provide lower-cased versions for API compatibility with `requests`.
* `stream=True`. - Streaming responses provide the `.stream()` and `.raw()` byte iterator interfaces, rather than the `.iter_content()` method and the `.raw` socket interface.
We differ in behaviour here [because auto-redirects can easily mask unnecessary network
calls being made](https://github.com/encode/httpx/discussions/1785).
## Advanced Usage
You can still enable behaviour to automatically follow redirects, but you need to
do so explicitly...
!!! warning
TODO
```python
response = client.get(url, follow_redirects=True)
```
Or else instantiate a client, with redirect following enabled by default...
```python
client = httpx.Client(follow_redirects=True)
```
## Client instances
The HTTPX equivalent of `requests.Session` is `httpx.Client`.
```python
session = requests.Session(**kwargs)
```
is generally equivalent to
```python
client = httpx.Client(**kwargs)
```
## Request URLs
Accessing `response.url` will return a `URL` instance, rather than a string.
Use `str(response.url)` if you need a string instance.
## Determining the next redirect request
The `requests` library exposes an attribute `response.next`, which can be used to obtain the next redirect request.
```python
session = requests.Session()
request = requests.Request("GET", ...).prepare()
while request is not None:
response = session.send(request, allow_redirects=False)
request = response.next
```
In HTTPX, this attribute is instead named `response.next_request`. For example:
```python
client = httpx.Client()
request = client.build_request("GET", ...)
while request is not None:
response = client.send(request)
request = response.next_request
```
## Request Content
For uploading raw text or binary content we prefer to use a `content` parameter,
in order to better separate this usage from the case of uploading form data.
For example, using `content=...` to upload raw content:
```python
# Uploading text, bytes, or a bytes iterator.
httpx.post(..., content=b"Hello, world")
```
And using `data=...` to send form data:
```python
# Uploading form data.
httpx.post(..., data={"message": "Hello, world"})
```
Using the `data=<text/byte content>` will raise a deprecation warning,
and is expected to be fully removed with the HTTPX 1.0 release.
## Upload files
HTTPX strictly enforces that upload files must be opened in binary mode, in order
to avoid character encoding issues that can result from attempting to upload files
opened in text mode.
## Content encoding
HTTPX uses `utf-8` for encoding `str` request bodies. For example, when using `content=<str>` the request body will be encoded to `utf-8` before being sent over the wire. This differs from Requests which uses `latin1`. If you need an explicit encoding, pass encoded bytes explicitly, e.g. `content=<str>.encode("latin1")`.
For response bodies, assuming the server didn't send an explicit encoding then HTTPX will do its best to figure out an appropriate encoding. HTTPX makes a guess at the encoding to use for decoding the response using `charset_normalizer`. Fallback to that or any content with less than 32 octets will be decoded using `utf-8` with the `error="replace"` decoder strategy.
## Cookies
If using a client instance, then cookies should always be set on the client rather than on a per-request basis.
This usage is supported:
```python
client = httpx.Client(cookies=...)
client.post(...)
```
This usage is **not** supported:
```python
client = httpx.Client()
client.post(..., cookies=...)
```
We prefer enforcing a stricter API here because it provides clearer expectations around cookie persistence, particularly when redirects occur.
## Status Codes
In our documentation we prefer the uppercased versions, such as `codes.NOT_FOUND`, but also provide lower-cased versions for API compatibility with `requests`.
Requests includes various synonyms for status codes that HTTPX does not support.
## Streaming responses
HTTPX provides a `.stream()` interface rather than using `stream=True`. This ensures that streaming responses are always properly closed outside of the stream block, and makes it visually clearer at which points streaming I/O APIs may be used with a response.
For example:
```python
with httpx.stream("GET", "https://www.example.com") as response:
...
```
Within a `stream()` block request data is made available with:
* `.iter_bytes()` - Instead of `response.iter_content()`
* `.iter_text()` - Instead of `response.iter_content(decode_unicode=True)`
* `.iter_lines()` - Corresponding to `response.iter_lines()`
* `.iter_raw()` - Use this instead of `response.raw`
* `.read()` - Read the entire response body, making `response.text` and `response.content` available.
## Timeouts
HTTPX defaults to including reasonable [timeouts](quickstart.md#timeouts) for all network operations, while Requests has no timeouts by default.
To get the same behavior as Requests, set the `timeout` parameter to `None`:
```python
httpx.get('https://www.example.com', timeout=None)
```
## Proxy keys
HTTPX uses the mounts argument for HTTP proxying and transport routing.
It can do much more than proxies and allows you to configure more than just the proxy route.
For more detailed documentation, see [Mounting Transports](advanced/transports.md#mounting-transports).
When using `httpx.Client(mounts={...})` to map to a selection of different transports, we use full URL schemes, such as `mounts={"http://": ..., "https://": ...}`.
This is different to the `requests` usage of `proxies={"http": ..., "https": ...}`.
This change is for better consistency with more complex mappings, that might also include domain names, such as `mounts={"all://": ..., httpx.HTTPTransport(proxy="all://www.example.com": None})` which maps all requests onto a proxy, except for requests to "www.example.com" which have an explicit exclusion.
Also note that `requests.Session.request(...)` allows a `proxies=...` parameter, whereas `httpx.Client.request(...)` does not allow `mounts=...`.
## SSL configuration
When using a `Client` instance, the ssl configurations should always be passed on client instantiation, rather than passed to the request method.
If you need more than one different SSL configuration, you should use different client instances for each SSL configuration.
## Request body on HTTP methods
The HTTP `GET`, `DELETE`, `HEAD`, and `OPTIONS` methods are specified as not supporting a request body. To stay in line with this, the `.get`, `.delete`, `.head` and `.options` functions do not support `content`, `files`, `data`, or `json` arguments.
If you really do need to send request data using these http methods you should use the generic `.request` function instead.
```python
httpx.request(
method="DELETE",
url="https://www.example.com/",
content=b'A request body on a DELETE request.'
)
```
## Checking for success and failure responses
We don't support `response.is_ok` since the naming is ambiguous there, and might incorrectly imply an equivalence to `response.status_code == codes.OK`. Instead we provide the `response.is_success` property, which can be used to check for a 2xx response.
## Request instantiation
There is no notion of [prepared requests](https://requests.readthedocs.io/en/stable/user/advanced/#prepared-requests) in HTTPX. If you need to customize request instantiation, see [Request instances](advanced/clients.md#request-instances).
Besides, `httpx.Request()` does not support the `auth`, `timeout`, `follow_redirects`, `mounts`, `verify` and `cert` parameters. However these are available in `httpx.request`, `httpx.get`, `httpx.post` etc., as well as on [`Client` instances](advanced/clients.md#client-instances).
## Mocking
If you need to mock HTTPX the same way that test utilities like `responses` and `requests-mock` does for `requests`, see [RESPX](https://github.com/lundberg/respx).
## Caching
If you use `cachecontrol` or `requests-cache` to add HTTP Caching support to the `requests` library, you can use [Hishel](https://hishel.com) for HTTPX.
## Networking layer
`requests` defers most of its HTTP networking code to the excellent [`urllib3` library](https://urllib3.readthedocs.io/en/latest/).
On the other hand, HTTPX uses [HTTPCore](https://github.com/encode/httpcore) as its core HTTP networking layer, which is a different project than `urllib3`.
## Query Parameters
`requests` omits `params` whose values are `None` (e.g. `requests.get(..., params={"foo": None})`). This is not supported by HTTPX.
For both query params (`params=`) and form data (`data=`), `requests` supports sending a list of tuples (e.g. `requests.get(..., params=[('key1', 'value1'), ('key1', 'value2')])`). This is not supported by HTTPX. Instead, use a dictionary with lists as values. E.g.: `httpx.get(..., params={'key1': ['value1', 'value2']})` or with form data: `httpx.post(..., data={'key1': ['value1', 'value2']})`.
## Event Hooks
`requests` allows event hooks to mutate `Request` and `Response` objects. See [examples](https://requests.readthedocs.io/en/master/user/advanced/#event-hooks) given in the documentation for `requests`.
In HTTPX, event hooks may access properties of requests and responses, but event hook callbacks cannot mutate the original request/response.
If you are looking for more control, consider checking out [Custom Transports](advanced/transports.md#custom-transports).
## Exceptions and Errors
`requests` exception hierarchy is slightly different to the `httpx` exception hierarchy. `requests` exposes a top level `RequestException`, where as `httpx` exposes a top level `HTTPError`. see the exceptions exposes in requests [here](https://requests.readthedocs.io/en/latest/_modules/requests/exceptions/). See the `httpx` error hierarchy [here](https://www.python-httpx.org/exceptions/).

View File

@ -1,7 +1,7 @@
# Contributing
Thank you for being interested in contributing with HTTPX.
There are many ways you can contribute with the project:
Thank you for being interested in contributing to HTTPX.
There are many ways you can contribute to the project:
- Try HTTPX and [report bugs/issues you find](https://github.com/encode/httpx/issues/new)
- [Implement new features](https://github.com/encode/httpx/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
@ -12,10 +12,13 @@ There are many ways you can contribute with the project:
## Reporting Bugs or Other Issues
Found something that HTTPX should support?
Stumbled upon some unexpected behavior?
Stumbled upon some unexpected behaviour?
Contributions should generally start out with [a discussion](https://github.com/encode/httpx/discussions).
Possible bugs may be raised as a "Potential Issue" discussion, feature requests may
be raised as an "Ideas" discussion. We can then determine if the discussion needs
to be escalated into an "Issue" or not, or if we'd consider a pull request.
Feel free to open an issue at the
[issue tracker](https://github.com/encode/httpx/issues).
Try to be more descriptive as you can and in case of a bug report,
provide as much information as possible like:
@ -25,6 +28,15 @@ provide as much information as possible like:
- Code snippet
- Error traceback
You should always try to reduce any examples to the *simplest possible case*
that demonstrates the issue.
Some possibly useful tips for narrowing down potential issues...
- Does the issue exist on HTTP/1.1, or HTTP/2, or both?
- Does the issue exist with `Client`, `AsyncClient`, or both?
- When using `AsyncClient` does the issue exist when using `asyncio` or `trio`, or both?
## Development
To start developing HTTPX create a **fork** of the
@ -37,84 +49,107 @@ your GitHub username:
$ git clone https://github.com/YOUR-USERNAME/httpx
```
With the repository cloned you can access its folder, set up the
virtual environment, install the project requirements,
and then install HTTPX on edit mode:
You can now install the project and its dependencies using:
```shell
$ cd httpx
$ python3 -m venv venv
$ source venv/bin/activate
$ pip install -r test-requirements.txt
$ pip install -e .
$ scripts/install
```
!!! note
Feel free to replace this step with your development environment setup
(pyenv, pipenv, virtualenvwrapper, docker, etc).
## Testing and Linting
We use [nox](https://nox.thea.codes/en/stable/) to automate testing, linting,
and documentation building workflow. Make sure you have it installed
at your system before starting.
We use custom shell scripts to automate testing, linting,
and documentation building workflow.
Install nox with:
To run the tests, use:
```shell
$ python3 -m pip install --user nox
```
Alternatively, use [pipx](https://github.com/pipxproject/pipx) if you prefer
to keep it into an isolated environment:
```shell
$ pipx install nox
```
Now, with nox installed run the complete pipeline with:
```shell
$ nox
$ scripts/test
```
!!! warning
The test suite spawns a testing server at the port **8000**.
Make sure this isn't being used, so the tests can run properly.
The test suite spawns testing servers on ports **8000** and **8001**.
Make sure these are not in use, so the tests can run properly.
To run the code auto-formatting separately:
Any additional arguments will be passed to `pytest`. See the [pytest documentation](https://docs.pytest.org/en/latest/how-to/usage.html) for more information.
For example, to run a single test script:
```shell
$ nox -s lint
$ scripts/test tests/test_multipart.py
```
Also, if you need to run the tests only:
To run the code auto-formatting:
```shell
$ nox -s test
$ scripts/lint
```
You can also run a single test script like this:
Lastly, to run code checks separately (they are also run as part of `scripts/test`), run:
```shell
$ nox -s test -- tests/test_multipart.py
$ scripts/check
```
## Documenting
To work with the documentation, make sure you have `mkdocs` and
`mkdocs-material` installed on your environment:
Documentation pages are located under the `docs/` folder.
To run the documentation site locally (useful for previewing changes), use:
```shell
$ pip install mkdocs mkdocs-material
$ scripts/docs
```
To spawn the docs server run:
## Resolving Build / CI Failures
Once you've submitted your pull request, the test suite will automatically run, and the results will show up in GitHub.
If the test suite fails, you'll want to click through to the "Details" link, and try to identify why the test suite failed.
<p align="center" style="margin: 0 0 10px">
<img src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/gh-actions-fail.png" alt='Failing PR commit status'>
</p>
Here are some common ways the test suite can fail:
### Check Job Failed
<p align="center" style="margin: 0 0 10px">
<img src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/gh-actions-fail-check.png" alt='Failing GitHub action lint job'>
</p>
This job failing means there is either a code formatting issue or type-annotation issue.
You can look at the job output to figure out why it's failed or within a shell run:
```shell
$ mkdocs serve
$ scripts/check
```
It may be worth it to run `$ scripts/lint` to attempt auto-formatting the code
and if that job succeeds commit the changes.
### Docs Job Failed
This job failing means the documentation failed to build. This can happen for
a variety of reasons like invalid markdown or missing configuration within `mkdocs.yml`.
### Python 3.X Job Failed
<p align="center" style="margin: 0 0 10px">
<img src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/gh-actions-fail-test.png" alt='Failing GitHub action test job'>
</p>
This job failing means the unit tests failed or not all code paths are covered by unit tests.
If tests are failing you will see this message under the coverage report:
`=== 1 failed, 435 passed, 1 skipped, 1 xfailed in 11.09s ===`
If tests succeed but coverage doesn't reach our current threshold, you will see this
message under the coverage report:
`FAIL Required test coverage of 100% not reached. Total coverage: 99.00%`
## Releasing
*This section is targeted at HTTPX maintainers.*
@ -130,6 +165,68 @@ Before releasing a new version, create a pull request that includes:
- Keep it concise and to-the-point. 🎯
- **A version bump**: see `__version__.py`.
For an example, see [#362](https://github.com/encode/httpx/pull/362).
For an example, see [#1006](https://github.com/encode/httpx/pull/1006).
Once the release PR is merged, run `$ scripts/publish` to publish the new release to PyPI.
Once the release PR is merged, create a
[new release](https://github.com/encode/httpx/releases/new) including:
- Tag version like `0.13.3`.
- Release title `Version 0.13.3`
- Description copied from the changelog.
Once created this release will be automatically uploaded to PyPI.
If something goes wrong with the PyPI job the release can be published using the
`scripts/publish` script.
## Development proxy setup
To test and debug requests via a proxy it's best to run a proxy server locally.
Any server should do but HTTPCore's test suite uses
[`mitmproxy`](https://mitmproxy.org/) which is written in Python, it's fully
featured and has excellent UI and tools for introspection of requests.
You can install `mitmproxy` using `pip install mitmproxy` or [several
other ways](https://docs.mitmproxy.org/stable/overview-installation/).
`mitmproxy` does require setting up local TLS certificates for HTTPS requests,
as its main purpose is to allow developers to inspect requests that pass through
it. We can set them up follows:
1. [`pip install trustme-cli`](https://github.com/sethmlarson/trustme-cli/).
2. `trustme-cli -i example.org www.example.org`, assuming you want to test
connecting to that domain, this will create three files: `server.pem`,
`server.key` and `client.pem`.
3. `mitmproxy` requires a PEM file that includes the private key and the
certificate so we need to concatenate them:
`cat server.key server.pem > server.withkey.pem`.
4. Start the proxy server `mitmproxy --certs server.withkey.pem`, or use the
[other mitmproxy commands](https://docs.mitmproxy.org/stable/) with different
UI options.
At this point the server is ready to start serving requests, you'll need to
configure HTTPX as described in the
[proxy section](https://www.python-httpx.org/advanced/proxies/#http-proxies) and
the [SSL certificates section](https://www.python-httpx.org/advanced/ssl/),
this is where our previously generated `client.pem` comes in:
```python
ctx = ssl.create_default_context(cafile="/path/to/client.pem")
client = httpx.Client(proxy="http://127.0.0.1:8080/", verify=ctx)
```
Note, however, that HTTPS requests will only succeed to the host specified
in the SSL/TLS certificate we generated, HTTPS requests to other hosts will
raise an error like:
```
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate
verify failed: Hostname mismatch, certificate is not valid for
'duckduckgo.com'. (_ssl.c:1108)
```
If you want to make requests to more hosts you'll need to regenerate the
certificates and include all the hosts you intend to connect to in the
seconds step, i.e.
`trustme-cli -i example.org www.example.org duckduckgo.com www.duckduckgo.com`

10
docs/css/custom.css Normal file
View File

@ -0,0 +1,10 @@
div.autodoc-docstring {
padding-left: 20px;
margin-bottom: 30px;
border-left: 5px solid rgba(230, 230, 230);
}
div.autodoc-members {
padding-left: 20px;
margin-bottom: 15px;
}

View File

@ -1,97 +1,62 @@
Environment Variables
=====================
# Environment Variables
The HTTPX library can be configured via environment variables.
Environment variables are used by default. To ignore environment variables, `trust_env` has to be set `False`.
There are two ways to set `trust_env` to disable environment variables:
Environment variables are used by default. To ignore environment variables, `trust_env` has to be set `False`. There are two ways to set `trust_env` to disable environment variables:
* On the client via `httpx.Client(trust_env=False)`
* Per request via `client.get("<url>", trust_env=False)`
* On the client via `httpx.Client(trust_env=False)`.
* Using the top-level API, such as `httpx.get("<url>", trust_env=False)`.
Here is a list of environment variables that HTTPX recognizes
and what function they serve:
Here is a list of environment variables that HTTPX recognizes and what function they serve:
`HTTPX_DEBUG`
-----------
## Proxies
Valid values: `1`, `true`
The environment variables documented below are used as a convention by various HTTP tooling, including:
If this environment variable is set to a valid value then low-level
details about the execution of HTTP requests will be logged to `stderr`.
* [cURL](https://github.com/curl/curl/blob/master/docs/MANUAL.md#environment-variables)
* [requests](https://github.com/psf/requests/blob/master/docs/user/advanced.rst#proxies)
This can help you debug issues and see what's exactly being sent
over the wire and to which location.
For more information on using proxies in HTTPX, see [HTTP Proxying](advanced/proxies.md#http-proxying).
Example:
### `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`
```python
# test_script.py
Valid values: A URL to a proxy
`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` set the proxy to be used for `http`, `https`, or all requests respectively.
```bash
export HTTP_PROXY=http://my-external-proxy.com:1234
# This request will be sent through the proxy
python -c "import httpx; httpx.get('http://example.com')"
# This request will be sent directly, as we set `trust_env=False`
python -c "import httpx; httpx.get('http://example.com', trust_env=False)"
import httpx
client = httpx.Client()
client.get("https://google.com")
```
```console
user@host:~$ HTTPX_DEBUG=1 python test_script.py
20:54:17.585 - httpx.dispatch.connection_pool - acquire_connection origin=Origin(scheme='https' host='www.google.com' port=443)
20:54:17.585 - httpx.dispatch.connection_pool - new_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443))
20:54:17.590 - httpx.dispatch.connection - start_connect host='www.google.com' port=443 timeout=TimeoutConfig(timeout=5.0)
20:54:17.651 - httpx.dispatch.connection - connected http_version='HTTP/2'
20:54:17.651 - httpx.dispatch.http2 - send_headers stream_id=1 headers=[(b':method', b'GET'), (b':authority', b'www.google.com'), ...]
20:54:17.652 - httpx.dispatch.http2 - end_stream stream_id=1
20:54:17.681 - httpx.dispatch.http2 - receive_event stream_id=0 event=<RemoteSettingsChanged changed_settings:{...}>
20:54:17.681 - httpx.dispatch.http2 - receive_event stream_id=0 event=<WindowUpdated stream_id:0, delta:983041>
20:54:17.682 - httpx.dispatch.http2 - receive_event stream_id=0 event=<SettingsAcknowledged changed_settings:{}>
20:54:17.739 - httpx.dispatch.http2 - receive_event stream_id=1 event=<ResponseReceived stream_id:1, headers:[(b':status', b'200'), ...]>
20:54:17.741 - httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:5224 data:>
20:54:17.742 - httpx.dispatch.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:59, data:>
20:54:17.742 - httpx.dispatch.http2 - receive_event stream_id=1 event=<StreamEnded stream_id:1>
20:54:17.742 - httpx.dispatch.http2 - receive_event stream_id=0 event=<PingReceived ping_data:0000000000000000>
20:54:17.743 - httpx.dispatch.connection_pool - release_connection connection=HTTPConnection(origin=Origin(scheme='https' host='www.google.com' port=443))
### `NO_PROXY`
Valid values: a comma-separated list of hostnames/urls
`NO_PROXY` disables the proxy for specific urls
```bash
export HTTP_PROXY=http://my-external-proxy.com:1234
export NO_PROXY=http://127.0.0.1,python-httpx.org
# As in the previous example, this request will be sent through the proxy
python -c "import httpx; httpx.get('http://example.com')"
# These requests will be sent directly, bypassing the proxy
python -c "import httpx; httpx.get('http://127.0.0.1:5000/my-api')"
python -c "import httpx; httpx.get('https://www.python-httpx.org')"
```
`SSLKEYLOGFILE`
-----------
## `SSL_CERT_FILE`
Valid values: a filename
If this environment variable is set, TLS keys will be appended to the specified file, creating it if it doesn't exist, whenever key material is generated or received. The keylog file is designed for debugging purposes only.
Support for `SSLKEYLOGFILE` requires Python 3.8 and OpenSSL 1.1.1 or newer.
Example:
```python
# test_script.py
import httpx
client = httpx.Client()
client.get("https://google.com")
```
```console
SSLKEYLOGFILE=test.log python test_script.py
cat test.log
# TLS secrets log file, generated by OpenSSL / Python
SERVER_HANDSHAKE_TRAFFIC_SECRET XXXX
EXPORTER_SECRET XXXX
SERVER_TRAFFIC_SECRET_0 XXXX
CLIENT_HANDSHAKE_TRAFFIC_SECRET XXXX
CLIENT_TRAFFIC_SECRET_0 XXXX
SERVER_HANDSHAKE_TRAFFIC_SECRET XXXX
EXPORTER_SECRET XXXX
SERVER_TRAFFIC_SECRET_0 XXXX
CLIENT_HANDSHAKE_TRAFFIC_SECRET XXXX
CLIENT_TRAFFIC_SECRET_0 XXXX
```
`SSL_CERT_FILE`
-----------
Valid values: a filename
if this environment variable is set then HTTPX will load
If this environment variable is set then HTTPX will load
CA certificate from the specified file instead of the default
location.
@ -101,31 +66,14 @@ Example:
SSL_CERT_FILE=/path/to/ca-certs/ca-bundle.crt python -c "import httpx; httpx.get('https://example.com')"
```
`SSL_CERT_DIR`
-----------
## `SSL_CERT_DIR`
Valid values: a directory
Valid values: a directory following an [OpenSSL specific layout](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_load_verify_locations.html).
if this environment variable is set then HTTPX will load
CA certificates from the specified location instead of the default
location.
If this environment variable is set and the directory follows an [OpenSSL specific layout](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_load_verify_locations.html) (ie. you ran `c_rehash`) then HTTPX will load CA certificates from this directory instead of the default location.
Example:
```console
SSL_CERT_DIR=/path/to/ca-certs/ python -c "import httpx; httpx.get('https://example.com')"
```
`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`
----------------------------------------
Valid values: A URL to a proxy
Sets the proxy to be used for `http`, `https`, or all requests respectively.
```bash
export HTTP_PROXY=http://127.0.0.1:3080
# This request will be sent through the proxy
python -c "import httpx; httpx.get('http://example.com')"
```

124
docs/exceptions.md Normal file
View File

@ -0,0 +1,124 @@
# Exceptions
This page lists exceptions that may be raised when using HTTPX.
For an overview of how to work with HTTPX exceptions, see [Exceptions (Quickstart)](quickstart.md#exceptions).
## The exception hierarchy
* HTTPError
* RequestError
* TransportError
* TimeoutException
* ConnectTimeout
* ReadTimeout
* WriteTimeout
* PoolTimeout
* NetworkError
* ConnectError
* ReadError
* WriteError
* CloseError
* ProtocolError
* LocalProtocolError
* RemoteProtocolError
* ProxyError
* UnsupportedProtocol
* DecodingError
* TooManyRedirects
* HTTPStatusError
* InvalidURL
* CookieConflict
* StreamError
* StreamConsumed
* ResponseNotRead
* RequestNotRead
* StreamClosed
---
## Exception classes
::: httpx.HTTPError
:docstring:
::: httpx.RequestError
:docstring:
::: httpx.TransportError
:docstring:
::: httpx.TimeoutException
:docstring:
::: httpx.ConnectTimeout
:docstring:
::: httpx.ReadTimeout
:docstring:
::: httpx.WriteTimeout
:docstring:
::: httpx.PoolTimeout
:docstring:
::: httpx.NetworkError
:docstring:
::: httpx.ConnectError
:docstring:
::: httpx.ReadError
:docstring:
::: httpx.WriteError
:docstring:
::: httpx.CloseError
:docstring:
::: httpx.ProtocolError
:docstring:
::: httpx.LocalProtocolError
:docstring:
::: httpx.RemoteProtocolError
:docstring:
::: httpx.ProxyError
:docstring:
::: httpx.UnsupportedProtocol
:docstring:
::: httpx.DecodingError
:docstring:
::: httpx.TooManyRedirects
:docstring:
::: httpx.HTTPStatusError
:docstring:
::: httpx.InvalidURL
:docstring:
::: httpx.CookieConflict
:docstring:
::: httpx.StreamError
:docstring:
::: httpx.StreamConsumed
:docstring:
::: httpx.StreamClosed
:docstring:
::: httpx.ResponseNotRead
:docstring:
::: httpx.RequestNotRead
:docstring:

68
docs/http2.md Normal file
View File

@ -0,0 +1,68 @@
# HTTP/2
HTTP/2 is a major new iteration of the HTTP protocol, that provides a far more
efficient transport, with potential performance benefits. HTTP/2 does not change
the core semantics of the request or response, but alters the way that data is
sent to and from the server.
Rather than the text format that HTTP/1.1 uses, HTTP/2 is a binary format.
The binary format provides full request and response multiplexing, and efficient
compression of HTTP headers. The stream multiplexing means that where HTTP/1.1
requires one TCP stream for each concurrent request, HTTP/2 allows a single TCP
stream to handle multiple concurrent requests.
HTTP/2 also provides support for functionality such as response prioritization,
and server push.
For a comprehensive guide to HTTP/2 you may want to check out "[http2 explained](https://http2-explained.haxx.se/)".
## Enabling HTTP/2
When using the `httpx` client, HTTP/2 support is not enabled by default, because
HTTP/1.1 is a mature, battle-hardened transport layer, and our HTTP/1.1
implementation may be considered the more robust option at this point in time.
It is possible that a future version of `httpx` may enable HTTP/2 support by default.
If you're issuing highly concurrent requests you might want to consider
trying out our HTTP/2 support. You can do so by first making sure to install
the optional HTTP/2 dependencies...
```shell
$ pip install httpx[http2]
```
And then instantiating a client with HTTP/2 support enabled:
```python
client = httpx.AsyncClient(http2=True)
...
```
You can also instantiate a client as a context manager, to ensure that all
HTTP connections are nicely scoped, and will be closed once the context block
is exited.
```python
async with httpx.AsyncClient(http2=True) as client:
...
```
HTTP/2 support is available on both `Client` and `AsyncClient`, although it's
typically more useful in async contexts if you're issuing lots of concurrent
requests.
## Inspecting the HTTP version
Enabling HTTP/2 support on the client does not *necessarily* mean that your
requests and responses will be transported over HTTP/2, since both the client
*and* the server need to support HTTP/2. If you connect to a server that only
supports HTTP/1.1 the client will use a standard HTTP/1.1 connection instead.
You can determine which version of the HTTP protocol was used by examining
the `.http_version` property on the response.
```python
client = httpx.AsyncClient(http2=True)
response = await client.get(...)
print(response.http_version) # "HTTP/1.0", "HTTP/1.1", or "HTTP/2".
```

BIN
docs/img/butterfly.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
docs/img/httpx-help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

BIN
docs/img/httpx-request.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

BIN
docs/img/rich-progress.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
docs/img/speakeasy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
docs/img/tqdm-progress.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -1,5 +1,5 @@
<p align="center" style="margin: 0 0 10px">
<img width="350" height="208" src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/logo.jpg" alt='HTTPX'>
<img width="350" height="208" src="https://raw.githubusercontent.com/encode/httpx/master/docs/img/butterfly.png" alt='HTTPX'>
</p>
<h1 align="center" style="font-size: 3rem; margin: -15px 0">
@ -10,11 +10,8 @@ HTTPX
<div align="center">
<p>
<a href="https://travis-ci.org/encode/httpx">
<img src="https://travis-ci.org/encode/httpx.svg?branch=master" alt="Build Status">
</a>
<a href="https://codecov.io/gh/encode/httpx">
<img src="https://codecov.io/gh/encode/httpx/branch/master/graph/badge.svg" alt="Coverage">
<a href="https://github.com/encode/httpx/actions">
<img src="https://github.com/encode/httpx/workflows/Test%20Suite/badge.svg" alt="Test Suite">
</a>
<a href="https://pypi.org/project/httpx/">
<img src="https://badge.fury.io/py/httpx.svg" alt="Package version">
@ -24,38 +21,54 @@ HTTPX
<em>A next-generation HTTP client for Python.</em>
</div>
!!! warning
This project should be considered as an "alpha" release. It is substantially
API complete, but there are still some areas that need more work.
HTTPX is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2.
---
Let's get started...
Install HTTPX using pip:
```python
```shell
$ pip install httpx
```
Now, let's get started:
```pycon
>>> import httpx
>>> r = httpx.get('https://www.example.org/')
>>> r
<Response [200 OK]>
>>> r.status_code
200
>>> r.http_version
'HTTP/1.1'
>>> r.headers['content-type']
'text/html; charset=UTF-8'
>>> r.text
'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>...'
```
Or, using the command-line client.
```shell
# The command line client is an optional dependency.
$ pip install 'httpx[cli]'
```
Which now allows us to use HTTPX directly from the command-line...
![httpx --help](img/httpx-help.png)
Sending a request...
![httpx http://httpbin.org/json](img/httpx-request.png)
## Features
HTTPX builds on the well-established usability of `requests`, and gives you:
* A requests-compatible API.
* HTTP/2 and HTTP/1.1 support.
* Support for [issuing HTTP requests in parallel](parallel.md). *(Coming soon)*
* Standard synchronous interface, but [with `async`/`await` support if you need it](async.md).
* Ability to [make requests directly to WSGI or ASGI applications](advanced.md#calling-into-python-web-apps).
* A broadly [requests-compatible API](compatibility.md).
* Standard synchronous interface, but with [async support if you need it](async.md).
* HTTP/1.1 [and HTTP/2 support](http2.md).
* Ability to make requests directly to [WSGI applications](advanced/transports.md#wsgi-transport) or [ASGI applications](advanced/transports.md#asgi-transport).
* Strict timeouts everywhere.
* Fully type annotated.
* 100% test coverage.
@ -66,13 +79,13 @@ Plus all the standard features of `requests`...
* Keep-Alive & Connection Pooling
* Sessions with Cookie Persistence
* Browser-style SSL Verification
* Basic/Digest Authentication *(Digest is still TODO)*
* Basic/Digest Authentication
* Elegant Key/Value Cookies
* Automatic Decompression
* Automatic Content Decoding
* Unicode Response Bodies
* Multipart File Uploads
* HTTP(S) Proxy Support *(TODO)*
* HTTP(S) Proxy Support
* Connection Timeouts
* Streaming Downloads
* .netrc Support
@ -82,28 +95,35 @@ Plus all the standard features of `requests`...
For a run-through of all the basics, head over to the [QuickStart](quickstart.md).
For more advanced topics, see the [Advanced Usage](advanced.md) section, or
the specific topics on making [Parallel Requests](parallel.md) or using the
[Async Client](async.md).
For more advanced topics, see the **Advanced** section,
the [async support](async.md) section, or the [HTTP/2](http2.md) section.
The [Developer Interface](api.md) provides a comprehensive API reference.
To find out about tools that integrate with HTTPX, see [Third Party Packages](third_party_packages.md).
## Dependencies
The HTTPX project relies on these excellent libraries:
* `h2` - HTTP/2 support.
* `h11` - HTTP/1.1 support.
* `httpcore` - The underlying transport implementation for `httpx`.
* `h11` - HTTP/1.1 support.
* `certifi` - SSL certificates.
* `chardet` - Fallback auto-detection for response encoding.
* `hstspreload` - determines whether IDNA-encoded host should be only accessed via HTTPS.
* `idna` - Internationalized domain name support.
* `rfc3986` - URL parsing & normalization.
* `brotlipy` - Decoding for "brotli" compressed responses. *(Optional)*
* `sniffio` - Async library autodetection.
As well as these optional installs:
* `h2` - HTTP/2 support. *(Optional, with `httpx[http2]`)*
* `socksio` - SOCKS proxy support. *(Optional, with `httpx[socks]`)*
* `rich` - Rich terminal support. *(Optional, with `httpx[cli]`)*
* `click` - Command line client support. *(Optional, with `httpx[cli]`)*
* `brotli` or `brotlicffi` - Decoding for "brotli" compressed responses. *(Optional, with `httpx[brotli]`)*
* `zstandard` - Decoding for "zstd" compressed responses. *(Optional, with `httpx[zstd]`)*
A huge amount of credit is due to `requests` for the API layout that
much of this work follows, as well as to `urllib3` for plenty of design
inspiration around the lower level networking details.
inspiration around the lower-level networking details.
## Installation
@ -113,4 +133,18 @@ Install with pip:
$ pip install httpx
```
HTTPX requires Python 3.6+
Or, to include the optional HTTP/2 support, use:
```shell
$ pip install httpx[http2]
```
To include the optional brotli and zstandard decoders support, use:
```shell
$ pip install httpx[brotli,zstd]
```
HTTPX requires Python 3.9+
[sync-support]: https://github.com/encode/httpx/issues/572

81
docs/logging.md Normal file
View File

@ -0,0 +1,81 @@
# Logging
If you need to inspect the internal behaviour of `httpx`, you can use Python's standard logging to output information about the underlying network behaviour.
For example, the following configuration...
```python
import logging
import httpx
logging.basicConfig(
format="%(levelname)s [%(asctime)s] %(name)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
level=logging.DEBUG
)
httpx.get("https://www.example.com")
```
Will send debug level output to the console, or wherever `stdout` is directed too...
```
DEBUG [2024-09-28 17:27:40] httpcore.connection - connect_tcp.started host='www.example.com' port=443 local_address=None timeout=5.0 socket_options=None
DEBUG [2024-09-28 17:27:41] httpcore.connection - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x101f1e8e0>
DEBUG [2024-09-28 17:27:41] httpcore.connection - start_tls.started ssl_context=SSLContext(verify=True) server_hostname='www.example.com' timeout=5.0
DEBUG [2024-09-28 17:27:41] httpcore.connection - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x1020f49a0>
DEBUG [2024-09-28 17:27:41] httpcore.http11 - send_request_headers.started request=<Request [b'GET']>
DEBUG [2024-09-28 17:27:41] httpcore.http11 - send_request_headers.complete
DEBUG [2024-09-28 17:27:41] httpcore.http11 - send_request_body.started request=<Request [b'GET']>
DEBUG [2024-09-28 17:27:41] httpcore.http11 - send_request_body.complete
DEBUG [2024-09-28 17:27:41] httpcore.http11 - receive_response_headers.started request=<Request [b'GET']>
DEBUG [2024-09-28 17:27:41] httpcore.http11 - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'OK', [(b'Content-Encoding', b'gzip'), (b'Accept-Ranges', b'bytes'), (b'Age', b'407727'), (b'Cache-Control', b'max-age=604800'), (b'Content-Type', b'text/html; charset=UTF-8'), (b'Date', b'Sat, 28 Sep 2024 13:27:42 GMT'), (b'Etag', b'"3147526947+gzip"'), (b'Expires', b'Sat, 05 Oct 2024 13:27:42 GMT'), (b'Last-Modified', b'Thu, 17 Oct 2019 07:18:26 GMT'), (b'Server', b'ECAcc (dcd/7D43)'), (b'Vary', b'Accept-Encoding'), (b'X-Cache', b'HIT'), (b'Content-Length', b'648')])
INFO [2024-09-28 17:27:41] httpx - HTTP Request: GET https://www.example.com "HTTP/1.1 200 OK"
DEBUG [2024-09-28 17:27:41] httpcore.http11 - receive_response_body.started request=<Request [b'GET']>
DEBUG [2024-09-28 17:27:41] httpcore.http11 - receive_response_body.complete
DEBUG [2024-09-28 17:27:41] httpcore.http11 - response_closed.started
DEBUG [2024-09-28 17:27:41] httpcore.http11 - response_closed.complete
DEBUG [2024-09-28 17:27:41] httpcore.connection - close.started
DEBUG [2024-09-28 17:27:41] httpcore.connection - close.complete
```
Logging output includes information from both the high-level `httpx` logger, and the network-level `httpcore` logger, which can be configured separately.
For handling more complex logging configurations you might want to use the dictionary configuration style...
```python
import logging.config
import httpx
LOGGING_CONFIG = {
"version": 1,
"handlers": {
"default": {
"class": "logging.StreamHandler",
"formatter": "http",
"stream": "ext://sys.stderr"
}
},
"formatters": {
"http": {
"format": "%(levelname)s [%(asctime)s] %(name)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
}
},
'loggers': {
'httpx': {
'handlers': ['default'],
'level': 'DEBUG',
},
'httpcore': {
'handlers': ['default'],
'level': 'DEBUG',
},
}
}
logging.config.dictConfig(LOGGING_CONFIG)
httpx.get('https://www.example.com')
```
The exact formatting of the debug logging may be subject to change across different versions of `httpx` and `httpcore`. If you need to rely on a particular format it is recommended that you pin installation of these packages to fixed versions.

View File

@ -0,0 +1,54 @@
{% import "partials/nav-item.html" as item with context %}
<!-- Determine class according to configuration -->
{% set class = "md-nav md-nav--primary" %}
{% if "navigation.tabs" in features %}
{% set class = class ~ " md-nav--lifted" %}
{% endif %}
{% if "toc.integrate" in features %}
{% set class = class ~ " md-nav--integrated" %}
{% endif %}
<!-- Main navigation -->
<nav
class="{{ class }}"
aria-label="{{ lang.t('nav.title') }}"
data-md-level="0"
>
<!-- Site title -->
<label class="md-nav__title" for="__drawer">
<a
href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}"
title="{{ config.site_name | e }}"
class="md-nav__button md-logo"
aria-label="{{ config.site_name }}"
data-md-component="logo"
>
{% include "partials/logo.html" %}
</a>
{{ config.site_name }}
</label>
<!-- Repository information -->
{% if config.repo_url %}
<div class="md-nav__source">
{% include "partials/source.html" %}
</div>
{% endif %}
<!-- Navigation list -->
<ul class="md-nav__list" data-md-scrollfix>
{% for nav_item in nav %}
{% set path = "__nav_" ~ loop.index %}
{{ item.render(nav_item, path, 1) }}
{% endfor %}
</ul>
<ul class="md-nav__list" data-md-scrollfix style="padding-top: 15px; padding-left: 10px">
<div>
<a href="https://speakeasy.com"><img src="/img/speakeasy.png" width=150px style=></img></a>
</div>
</ul>
</nav>

View File

@ -1,71 +0,0 @@
# Parallel Requests
!!! warning
This page documents some proposed functionality that is not yet released.
See [pull request #52](https://github.com/encode/httpx/pull/52) for the
first-pass of an implementation.
HTTPX allows you to make HTTP requests in parallel in a highly efficient way,
using async under the hood, while still presenting a standard threaded interface.
This has the huge benefit of allowing you to efficiently make parallel HTTP
requests without having to switch out to using async all the way through.
## Making Parallel Requests
Let's make two outgoing HTTP requests in parallel:
```python
>>> with httpx.parallel() as parallel:
>>> pending_one = parallel.get('https://example.com/1')
>>> pending_two = parallel.get('https://example.com/2')
>>> response_one = pending_one.get_response()
>>> response_two = pending_two.get_response()
```
If we're making lots of outgoing requests, we might not want to deal with the
responses sequentially, but rather deal with each response that comes back
as soon as it's available:
```python
>>> with httpx.parallel() as parallel:
>>> for counter in range(1, 10):
>>> parallel.get(f'https://example.com/{counter}')
>>> while parallel.has_pending_responses:
>>> r = parallel.next_response()
```
## Exceptions and Cancellations
The style of using `parallel` blocks ensures that you'll always have well
defined exception and cancellation behaviours. Request exceptions are only ever
raised when calling either `get_response` or `next_response`, and any pending
requests are cancelled on exiting the block.
## Parallel requests with a Client
You can also call `parallel()` from a client instance, which allows you to
control the authentication or dispatch behaviour for all requests within the
block.
```python
>>> client = httpx.Client()
>>> with client.parallel() as parallel:
>>> ...
```
## Async parallel requests
If you're working within an async framework, then you'll want to use a fully
async API for making requests.
```python
>>> client = httpx.AsyncClient()
>>> async with client.parallel() as parallel:
>>> pending_one = await parallel.get('https://example.com/1')
>>> pending_two = await parallel.get('https://example.com/2')
>>> response_one = await pending_one.get_response()
>>> response_two = await pending_two.get_response()
```
See [the Async Client documentation](async.md) for more details.

View File

@ -1,19 +1,14 @@
# QuickStart
!!! note
This page closely follows the layout of the `requests` QuickStart documentation.
The `httpx` library is designed to be API compatible with `requests` wherever
possible.
First, start by importing HTTPX:
First start by importing HTTPX:
```
```pycon
>>> import httpx
```
Now, lets try to get a webpage.
```python
```pycon
>>> r = httpx.get('https://httpbin.org/get')
>>> r
<Response [200 OK]>
@ -21,13 +16,13 @@ Now, lets try to get a webpage.
Similarly, to make an HTTP POST request:
```python
```pycon
>>> r = httpx.post('https://httpbin.org/post', data={'key': 'value'})
```
The PUT, DELETE, HEAD, and OPTIONS requests all follow the same style:
```python
```pycon
>>> r = httpx.put('https://httpbin.org/put', data={'key': 'value'})
>>> r = httpx.delete('https://httpbin.org/delete')
>>> r = httpx.head('https://httpbin.org/get')
@ -38,7 +33,7 @@ The PUT, DELETE, HEAD, and OPTIONS requests all follow the same style:
To include URL query parameters in the request, use the `params` keyword:
```python
```pycon
>>> params = {'key1': 'value1', 'key2': 'value2'}
>>> r = httpx.get('https://httpbin.org/get', params=params)
```
@ -46,14 +41,14 @@ To include URL query parameters in the request, use the `params` keyword:
To see how the values get encoding into the URL string, we can inspect the
resulting URL that was used to make the request:
```python
```pycon
>>> r.url
URL('https://httpbin.org/get?key2=value2&key1=value1')
```
You can also pass a list of items as a value:
```python
```pycon
>>> params = {'key1': 'value1', 'key2': ['value2', 'value3']}
>>> r = httpx.get('https://httpbin.org/get', params=params)
>>> r.url
@ -62,25 +57,35 @@ URL('https://httpbin.org/get?key1=value1&key2=value2&key2=value3')
## Response Content
HTTPX will automatically handle decoding the response content into unicode text.
HTTPX will automatically handle decoding the response content into Unicode text.
```python
```pycon
>>> r = httpx.get('https://www.example.org/')
>>> r.text
'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>...'
```
You can inspect what encoding has been used to decode the response.
You can inspect what encoding will be used to decode the response.
```python
```pycon
>>> r.encoding
'UTF-8'
```
If you need to override the standard behavior and explicitly set the encoding to
In some cases the response may not contain an explicit encoding, in which case HTTPX
will attempt to automatically determine an encoding to use.
```pycon
>>> r.encoding
None
>>> r.text
'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>...'
```
If you need to override the standard behaviour and explicitly set the encoding to
use, then you can do that too.
```python
```pycon
>>> r.encoding = 'ISO-8859-1'
```
@ -88,18 +93,19 @@ use, then you can do that too.
The response content can also be accessed as bytes, for non-text responses:
```python
```pycon
>>> r.content
b'<!doctype html>\n<html>\n<head>\n<title>Example Domain</title>...'
```
Any `gzip` and `deflate` HTTP response encodings will automatically
be decoded for you. If `brotlipy` is installed, then the `brotli` response
encoding will also be supported.
encoding will be supported. If `zstandard` is installed, then `zstd`
response encodings will also be supported.
For example, to create an image from binary data returned by a request, you can use the following code:
```python
```pycon
>>> from PIL import Image
>>> from io import BytesIO
>>> i = Image.open(BytesIO(r.content))
@ -109,7 +115,7 @@ For example, to create an image from binary data returned by a request, you can
Often Web API responses will be encoded as JSON.
```python
```pycon
>>> r = httpx.get('https://api.github.com/events')
>>> r.json()
[{u'repository': {u'open_issues': 0, u'url': 'https://github.com/...' ... }}]
@ -119,8 +125,8 @@ Often Web API responses will be encoded as JSON.
To include additional headers in the outgoing request, use the `headers` keyword argument:
```python
>>> url = 'http://httpbin.org/headers'
```pycon
>>> url = 'https://httpbin.org/headers'
>>> headers = {'user-agent': 'my-app/0.0.1'}
>>> r = httpx.get(url, headers=headers)
```
@ -128,10 +134,10 @@ To include additional headers in the outgoing request, use the `headers` keyword
## Sending Form Encoded Data
Some types of HTTP requests, such as `POST` and `PUT` requests, can include data
in the request body. One common way of including that is as form encoded data,
in the request body. One common way of including that is as form-encoded data,
which is used for HTML forms.
```python
```pycon
>>> data = {'key1': 'value1', 'key2': 'value2'}
>>> r = httpx.post("https://httpbin.org/post", data=data)
>>> print(r.text)
@ -145,9 +151,9 @@ which is used for HTML forms.
}
```
Form encoded data can also include multiple values form a given key.
Form encoded data can also include multiple values from a given key.
```python
```pycon
>>> data = {'key1': ['value1', 'value2']}
>>> r = httpx.post("https://httpbin.org/post", data=data)
>>> print(r.text)
@ -167,9 +173,10 @@ Form encoded data can also include multiple values form a given key.
You can also upload files, using HTTP multipart encoding:
```python
>>> files = {'upload-file': open('report.xls', 'rb')}
>>> r = httpx.post("https://httpbin.org/post", files=files)
```pycon
>>> with open('report.xls', 'rb') as report_file:
... files = {'upload-file': report_file}
... r = httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
...
@ -183,9 +190,10 @@ You can also upload files, using HTTP multipart encoding:
You can also explicitly set the filename and content type, by using a tuple
of items for the file value:
```python
>>> files = {'upload-file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel')}
>>> r = httpx.post("https://httpbin.org/post", files=files)
```pycon
>>> with open('report.xls', 'rb') as report_file:
... files = {'upload-file': ('report.xls', report_file, 'application/vnd.ms-excel')}
... r = httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text)
{
...
@ -196,12 +204,32 @@ of items for the file value:
}
```
If you need to include non-file data fields in the multipart form, use the `data=...` parameter:
```pycon
>>> data = {'message': 'Hello, world!'}
>>> with open('report.xls', 'rb') as report_file:
... files = {'file': report_file}
... r = httpx.post("https://httpbin.org/post", data=data, files=files)
>>> print(r.text)
{
...
"files": {
"file": "<... binary content ...>"
},
"form": {
"message": "Hello, world!",
},
...
}
```
## Sending JSON Encoded Data
Form encoded data is okay if all you need is simple key-value data structure.
Form encoded data is okay if all you need is a simple key-value data structure.
For more complicated data structures you'll often want to use JSON encoding instead.
```python
```pycon
>>> data = {'integer': 123, 'boolean': True, 'list': ['a', 'b', 'c']}
>>> r = httpx.post("https://httpbin.org/post", json=data)
>>> print(r.text)
@ -222,17 +250,22 @@ For more complicated data structures you'll often want to use JSON encoding inst
## Sending Binary Request Data
For other encodings you should use either a `bytes` type, or a generator
that yields `bytes`.
For other encodings, you should use the `content=...` parameter, passing
either a `bytes` type or a generator that yields `bytes`.
You'll probably also want to set a custom `Content-Type` header when uploading
```pycon
>>> content = b'Hello, world'
>>> r = httpx.post("https://httpbin.org/post", content=content)
```
You may also want to set a custom `Content-Type` header when uploading
binary data.
## Response Status Codes
## Response Status Codes
We can inspect the HTTP status code of the response:
```python
```pycon
>>> r = httpx.get('https://httpbin.org/get')
>>> r.status_code
200
@ -240,35 +273,43 @@ We can inspect the HTTP status code of the response:
HTTPX also includes an easy shortcut for accessing status codes by their text phrase.
```python
```pycon
>>> r.status_code == httpx.codes.OK
True
```
We can raise an exception for any Client or Server error responses (4xx or 5xx status codes):
We can raise an exception for any responses which are not a 2xx success code:
```python
```pycon
>>> not_found = httpx.get('https://httpbin.org/status/404')
>>> not_found.status_code
404
>>> not_found.raise_for_status()
Traceback (most recent call last):
File "/Users/tomchristie/GitHub/encode/httpcore/httpx/models.py", line 776, in raise_for_status
raise HttpError(message)
httpx.exceptions.HttpError: 404 Not Found
File "/Users/tomchristie/GitHub/encode/httpcore/httpx/models.py", line 837, in raise_for_status
raise HTTPStatusError(message, response=self)
httpx._exceptions.HTTPStatusError: 404 Client Error: Not Found for url: https://httpbin.org/status/404
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
```
Any successful response codes will simply return `None` rather than raising an exception.
Any successful response codes will return the `Response` instance rather than raising an exception.
``` python
```pycon
>>> r.raise_for_status()
```
The method returns the response instance, allowing you to use it inline. For example:
```pycon
>>> r = httpx.get('...').raise_for_status()
>>> data = httpx.get('...').raise_for_status().json()
```
## Response Headers
The response headers are available as a dictionary-like interface.
```python
```pycon
>>> r.headers
Headers({
'content-encoding': 'gzip',
@ -283,7 +324,7 @@ Headers({
The `Headers` data type is case-insensitive, so you can use any capitalization.
```python
```pycon
>>> r.headers['Content-Type']
'application/json'
@ -291,26 +332,73 @@ The `Headers` data type is case-insensitive, so you can use any capitalization.
'application/json'
```
Multiple values for a single response header are represented as a single comma separated
value, as per [RFC 7230](https://tools.ietf.org/html/rfc7230#section-3.2):
Multiple values for a single response header are represented as a single comma-separated value, as per [RFC 7230](https://tools.ietf.org/html/rfc7230#section-3.2):
> A recipient MAY combine multiple header fields with the same field name into one “field-name: field-value” pair, without changing the semantics of the message, by appending each subsequent field value to the combined field value in order, separated by a comma.
> A recipient MAY combine multiple header fields with the same field name into one “field-name: field-value” pair, without changing the semantics of the message, by appending each subsequent field-value to the combined field value in order, separated by a comma.
## Streaming Responses
For large downloads you may want to use streaming responses that do not load the entire response body into memory at once.
You can stream the binary content of the response...
```pycon
>>> with httpx.stream("GET", "https://www.example.com") as r:
... for data in r.iter_bytes():
... print(data)
```
Or the text of the response...
```pycon
>>> with httpx.stream("GET", "https://www.example.com") as r:
... for text in r.iter_text():
... print(text)
```
Or stream the text, on a line-by-line basis...
```pycon
>>> with httpx.stream("GET", "https://www.example.com") as r:
... for line in r.iter_lines():
... print(line)
```
HTTPX will use universal line endings, normalising all cases to `\n`.
In some cases you might want to access the raw bytes on the response without applying any HTTP content decoding. In this case any content encoding that the web server has applied such as `gzip`, `deflate`, `brotli`, or `zstd` will
not be automatically decoded.
```pycon
>>> with httpx.stream("GET", "https://www.example.com") as r:
... for chunk in r.iter_raw():
... print(chunk)
```
If you're using streaming responses in any of these ways then the `response.content` and `response.text` attributes will not be available, and will raise errors if accessed. However you can also use the response streaming functionality to conditionally load the response body:
```pycon
>>> with httpx.stream("GET", "https://www.example.com") as r:
... if int(r.headers['Content-Length']) < TOO_LONG:
... r.read()
... print(r.text)
```
## Cookies
Any cookies that are set on the response can be easily accessed:
```python
>>> r = httpx.get('http://httpbin.org/cookies/set?chocolate=chip', allow_redirects=False)
```pycon
>>> r = httpx.get('https://httpbin.org/cookies/set?chocolate=chip')
>>> r.cookies['chocolate']
'chip'
```
To include cookies in an outgoing request, use the `cookies` parameter:
```python
```pycon
>>> cookies = {"peanut": "butter"}
>>> r = httpx.get('http://httpbin.org/cookies', cookies=cookies)
>>> r = httpx.get('https://httpbin.org/cookies', cookies=cookies)
>>> r.json()
{'cookies': {'peanut': 'butter'}}
```
@ -318,7 +406,7 @@ To include cookies in an outgoing request, use the `cookies` parameter:
Cookies are returned in a `Cookies` instance, which is a dict-like data structure
with additional API for accessing cookies by their domain or path.
```python
```pycon
>>> cookies = httpx.Cookies()
>>> cookies.set('cookie_on_domain', 'hello, there!', domain='httpbin.org')
>>> cookies.set('cookie_off_domain', 'nope.', domain='example.org')
@ -329,16 +417,25 @@ with additional API for accessing cookies by their domain or path.
## Redirection and History
By default HTTPX will follow redirects for anything except `HEAD` requests.
The `history` property of the response can be used to inspect any followed redirects.
It contains a list of all any redirect responses that were followed, in the order
in which they were made.
By default, HTTPX will **not** follow redirects for all HTTP methods, although
this can be explicitly enabled.
For example, GitHub redirects all HTTP requests to HTTPS.
```python
```pycon
>>> r = httpx.get('http://github.com/')
>>> r.status_code
301
>>> r.history
[]
>>> r.next_request
<Request('GET', 'https://github.com/')>
```
You can modify the default redirection handling with the `follow_redirects` parameter:
```pycon
>>> r = httpx.get('http://github.com/', follow_redirects=True)
>>> r.url
URL('https://github.com/')
>>> r.status_code
@ -347,25 +444,9 @@ URL('https://github.com/')
[<Response [301 Moved Permanently]>]
```
You can modify the default redirection handling with the allow_redirects parameter:
```python
>>> r = httpx.get('http://github.com/', allow_redirects=False)
>>> r.status_code
301
>>> r.history
[]
```
If youre making a `HEAD` request, you can use this to enable redirection:
```python
>>> r = httpx.head('http://github.com/', allow_redirects=True)
>>> r.url
'https://github.com/'
>>> r.history
[<Response [301 Moved Permanently]>]
```
The `history` property of the response can be used to inspect any followed redirects.
It contains a list of any redirect responses that were followed, in the order
in which they were made.
## Timeouts
@ -376,10 +457,18 @@ raise an error rather than hanging indefinitely.
The default timeout for network inactivity is five seconds. You can modify the
value to be more or less strict:
```python
```pycon
>>> httpx.get('https://github.com/', timeout=0.001)
```
You can also disable the timeout behavior completely...
```pycon
>>> httpx.get('https://github.com/', timeout=None)
```
For advanced timeout management, see [Timeout fine-tuning](advanced/timeouts.md#fine-tuning-the-configuration).
## Authentication
HTTPX supports Basic and Digest HTTP authentication.
@ -388,7 +477,7 @@ To provide Basic authentication credentials, pass a 2-tuple of
plaintext `str` or `bytes` objects as the `auth` argument to the request
functions:
```python
```pycon
>>> httpx.get("https://example.com", auth=("my_user", "password123"))
```
@ -397,7 +486,62 @@ a `DigestAuth` object with the plaintext username and password as arguments.
This object can be then passed as the `auth` argument to the request methods
as above:
```python
```pycon
>>> auth = httpx.DigestAuth("my_user", "password123")
>>> httpx.get("https://example.com", auth=auth)
<Response [200 OK]>
```
## Exceptions
HTTPX will raise exceptions if an error occurs.
The most important exception classes in HTTPX are `RequestError` and `HTTPStatusError`.
The `RequestError` class is a superclass that encompasses any exception that occurs
while issuing an HTTP request. These exceptions include a `.request` attribute.
```python
try:
response = httpx.get("https://www.example.com/")
except httpx.RequestError as exc:
print(f"An error occurred while requesting {exc.request.url!r}.")
```
The `HTTPStatusError` class is raised by `response.raise_for_status()` on responses which are not a 2xx success code.
These exceptions include both a `.request` and a `.response` attribute.
```python
response = httpx.get("https://www.example.com/")
try:
response.raise_for_status()
except httpx.HTTPStatusError as exc:
print(f"Error response {exc.response.status_code} while requesting {exc.request.url!r}.")
```
There is also a base class `HTTPError` that includes both of these categories, and can be used
to catch either failed requests, or 4xx and 5xx responses.
You can either use this base class to catch both categories...
```python
try:
response = httpx.get("https://www.example.com/")
response.raise_for_status()
except httpx.HTTPError as exc:
print(f"Error while requesting {exc.request.url!r}.")
```
Or handle each case explicitly...
```python
try:
response = httpx.get("https://www.example.com/")
response.raise_for_status()
except httpx.RequestError as exc:
print(f"An error occurred while requesting {exc.request.url!r}.")
except httpx.HTTPStatusError as exc:
print(f"Error response {exc.response.status_code} while requesting {exc.request.url!r}.")
```
For a full list of available exceptions, see [Exceptions (API Reference)](exceptions.md).

View File

@ -0,0 +1,107 @@
# Third Party Packages
As HTTPX usage grows, there is an expanding community of developers building tools and libraries that integrate with HTTPX, or depend on HTTPX. Here are some of them.
<!-- NOTE: Entries are alphabetised. -->
## Plugins
### Hishel
[GitHub](https://github.com/karpetrosyan/hishel) - [Documentation](https://hishel.com/)
An elegant HTTP Cache implementation for HTTPX and HTTP Core.
### HTTPX-Auth
[GitHub](https://github.com/Colin-b/httpx_auth) - [Documentation](https://colin-b.github.io/httpx_auth/)
Provides authentication classes to be used with HTTPX's [authentication parameter](advanced/authentication.md#customizing-authentication).
### httpx-caching
[Github](https://github.com/johtso/httpx-caching)
This package adds caching functionality to HTTPX
### httpx-secure
[GitHub](https://github.com/Zaczero/httpx-secure)
Drop-in SSRF protection for httpx with DNS caching and custom validation support.
### httpx-socks
[GitHub](https://github.com/romis2012/httpx-socks)
Proxy (HTTP, SOCKS) transports for httpx.
### httpx-sse
[GitHub](https://github.com/florimondmanca/httpx-sse)
Allows consuming Server-Sent Events (SSE) with HTTPX.
### httpx-retries
[GitHub](https://github.com/will-ockmore/httpx-retries) - [Documentation](https://will-ockmore.github.io/httpx-retries/)
A retry layer for HTTPX.
### httpx-ws
[GitHub](https://github.com/frankie567/httpx-ws) - [Documentation](https://frankie567.github.io/httpx-ws/)
WebSocket support for HTTPX.
### pytest-HTTPX
[GitHub](https://github.com/Colin-b/pytest_httpx) - [Documentation](https://colin-b.github.io/pytest_httpx/)
Provides a [pytest](https://docs.pytest.org/en/latest/) fixture to mock HTTPX within test cases.
### RESPX
[GitHub](https://github.com/lundberg/respx) - [Documentation](https://lundberg.github.io/respx/)
A utility for mocking out HTTPX.
### rpc.py
[Github](https://github.com/abersheeran/rpc.py) - [Documentation](https://github.com/abersheeran/rpc.py#rpcpy)
A fast and powerful RPC framework based on ASGI/WSGI. Use HTTPX as the client of the RPC service.
## Libraries with HTTPX support
### Authlib
[GitHub](https://github.com/lepture/authlib) - [Documentation](https://docs.authlib.org/en/latest/)
A python library for building OAuth and OpenID Connect clients and servers. Includes an [OAuth HTTPX client](https://docs.authlib.org/en/latest/client/httpx.html).
### Gidgethub
[GitHub](https://github.com/brettcannon/gidgethub) - [Documentation](https://gidgethub.readthedocs.io/en/latest/index.html)
An asynchronous GitHub API library. Includes [HTTPX support](https://gidgethub.readthedocs.io/en/latest/httpx.html).
### httpdbg
[GitHub](https://github.com/cle-b/httpdbg) - [Documentation](https://httpdbg.readthedocs.io/)
A tool for python developers to easily debug the HTTP(S) client requests in a python program.
### VCR.py
[GitHub](https://github.com/kevin1024/vcrpy) - [Documentation](https://vcrpy.readthedocs.io/)
Record and repeat requests.
## Gists
### urllib3-transport
[GitHub](https://gist.github.com/florimondmanca/d56764d78d748eb9f73165da388e546e)
This public gist provides an example implementation for a [custom transport](advanced/transports.md#custom-transports) implementation on top of the battle-tested [`urllib3`](https://urllib3.readthedocs.io) library.

63
docs/troubleshooting.md Normal file
View File

@ -0,0 +1,63 @@
# Troubleshooting
This page lists some common problems or issues you could encounter while developing with HTTPX, as well as possible solutions.
## Proxies
---
### "`The handshake operation timed out`" on HTTPS requests when using a proxy
**Description**: When using a proxy and making an HTTPS request, you see an exception looking like this:
```console
httpx.ProxyError: _ssl.c:1091: The handshake operation timed out
```
**Similar issues**: [encode/httpx#1412](https://github.com/encode/httpx/issues/1412), [encode/httpx#1433](https://github.com/encode/httpx/issues/1433)
**Resolution**: it is likely that you've set up your proxies like this...
```python
mounts = {
"http://": httpx.HTTPTransport(proxy="http://myproxy.org"),
"https://": httpx.HTTPTransport(proxy="https://myproxy.org"),
}
```
Using this setup, you're telling HTTPX to connect to the proxy using HTTP for HTTP requests, and using HTTPS for HTTPS requests.
But if you get the error above, it is likely that your proxy doesn't support connecting via HTTPS. Don't worry: that's a [common gotcha](advanced/proxies.md#http-proxies).
Change the scheme of your HTTPS proxy to `http://...` instead of `https://...`:
```python
mounts = {
"http://": httpx.HTTPTransport(proxy="http://myproxy.org"),
"https://": httpx.HTTPTransport(proxy="http://myproxy.org"),
}
```
This can be simplified to:
```python
proxy = "http://myproxy.org"
with httpx.Client(proxy=proxy) as client:
...
```
For more information, see [Proxies: FORWARD vs TUNNEL](advanced/proxies.md#forward-vs-tunnel).
---
### Error when making requests to an HTTPS proxy
**Description**: your proxy _does_ support connecting via HTTPS, but you are seeing errors along the lines of...
```console
httpx.ProxyError: [SSL: PRE_MAC_LENGTH_TOO_LONG] invalid alert (_ssl.c:1091)
```
**Similar issues**: [encode/httpx#1424](https://github.com/encode/httpx/issues/1424).
**Resolution**: HTTPX does not properly support HTTPS proxies at this time. If that's something you're interested in having, please see [encode/httpx#1434](https://github.com/encode/httpx/issues/1434) and consider lending a hand there.

View File

@ -1,143 +1,106 @@
from .__version__ import __description__, __title__, __version__
from .api import delete, get, head, options, patch, post, put, request
from .client import AsyncClient, Client
from .concurrency.asyncio import AsyncioBackend
from .concurrency.base import (
BaseBackgroundManager,
BasePoolSemaphore,
BaseTCPStream,
ConcurrencyBackend,
)
from .config import (
USER_AGENT,
CertTypes,
HTTPVersionConfig,
HTTPVersionTypes,
PoolLimits,
SSLConfig,
TimeoutConfig,
TimeoutTypes,
VerifyTypes,
)
from .dispatch.base import AsyncDispatcher, Dispatcher
from .dispatch.connection import HTTPConnection
from .dispatch.connection_pool import ConnectionPool
from .dispatch.proxy_http import HTTPProxy, HTTPProxyMode
from .exceptions import (
ConnectTimeout,
CookieConflict,
DecodingError,
InvalidURL,
NotRedirectResponse,
PoolTimeout,
ProtocolError,
ProxyError,
ReadTimeout,
RedirectBodyUnavailable,
RedirectLoop,
ResponseClosed,
ResponseNotRead,
StreamConsumed,
Timeout,
TooManyRedirects,
WriteTimeout,
)
from .middleware.digest_auth import DigestAuth
from .models import (
URL,
AsyncRequest,
AsyncRequestData,
AsyncResponse,
AsyncResponseContent,
AuthTypes,
Cookies,
CookieTypes,
Headers,
HeaderTypes,
Origin,
QueryParams,
QueryParamTypes,
Request,
RequestData,
RequestFiles,
Response,
ResponseContent,
URLTypes,
)
from .status_codes import StatusCode, codes
from ._api import *
from ._auth import *
from ._client import *
from ._config import *
from ._content import *
from ._exceptions import *
from ._models import *
from ._status_codes import *
from ._transports import *
from ._types import *
from ._urls import *
try:
from ._main import main
except ImportError: # pragma: no cover
def main() -> None: # type: ignore
import sys
print(
"The httpx command line client could not run because the required "
"dependencies were not installed.\nMake sure you've installed "
"everything with: pip install 'httpx[cli]'"
)
sys.exit(1)
__all__ = [
"__description__",
"__title__",
"__version__",
"delete",
"get",
"head",
"options",
"patch",
"post",
"patch",
"put",
"request",
"ASGITransport",
"AsyncBaseTransport",
"AsyncByteStream",
"AsyncClient",
"AsyncHTTPTransport",
"Auth",
"BaseTransport",
"BasicAuth",
"ByteStream",
"Client",
"AsyncioBackend",
"USER_AGENT",
"CertTypes",
"PoolLimits",
"SSLConfig",
"TimeoutConfig",
"VerifyTypes",
"HTTPConnection",
"BasePoolSemaphore",
"BaseBackgroundManager",
"ConnectionPool",
"HTTPProxy",
"HTTPProxyMode",
"CloseError",
"codes",
"ConnectError",
"ConnectTimeout",
"CookieConflict",
"DecodingError",
"InvalidURL",
"NotRedirectResponse",
"PoolTimeout",
"ProtocolError",
"ReadTimeout",
"RedirectBodyUnavailable",
"RedirectLoop",
"ResponseClosed",
"ResponseNotRead",
"StreamConsumed",
"ProxyError",
"Timeout",
"TooManyRedirects",
"WriteTimeout",
"AsyncDispatcher",
"BaseTCPStream",
"ConcurrencyBackend",
"Dispatcher",
"URL",
"URLTypes",
"StatusCode",
"codes",
"TimeoutTypes",
"HTTPVersionTypes",
"HTTPVersionConfig",
"AsyncRequest",
"AsyncRequestData",
"AsyncResponse",
"AsyncResponseContent",
"AuthTypes",
"Cookies",
"CookieTypes",
"Headers",
"HeaderTypes",
"Origin",
"QueryParams",
"QueryParamTypes",
"Request",
"RequestData",
"Response",
"ResponseContent",
"RequestFiles",
"create_ssl_context",
"DecodingError",
"delete",
"DigestAuth",
"FunctionAuth",
"get",
"head",
"Headers",
"HTTPError",
"HTTPStatusError",
"HTTPTransport",
"InvalidURL",
"Limits",
"LocalProtocolError",
"main",
"MockTransport",
"NetRCAuth",
"NetworkError",
"options",
"patch",
"PoolTimeout",
"post",
"ProtocolError",
"Proxy",
"ProxyError",
"put",
"QueryParams",
"ReadError",
"ReadTimeout",
"RemoteProtocolError",
"request",
"Request",
"RequestError",
"RequestNotRead",
"Response",
"ResponseNotRead",
"stream",
"StreamClosed",
"StreamConsumed",
"StreamError",
"SyncByteStream",
"Timeout",
"TimeoutException",
"TooManyRedirects",
"TransportError",
"UnsupportedProtocol",
"URL",
"USE_CLIENT_DEFAULT",
"WriteError",
"WriteTimeout",
"WSGITransport",
]
__locals = locals()
for __name in __all__:
if not __name.startswith("__"):
setattr(__locals[__name], "__module__", "httpx") # noqa

View File

@ -1,3 +1,3 @@
__title__ = "httpx"
__description__ = "A next generation HTTP client, for Python 3."
__version__ = "0.7.4"
__version__ = "0.28.1"

438
httpx/_api.py Normal file
View File

@ -0,0 +1,438 @@
from __future__ import annotations
import typing
from contextlib import contextmanager
from ._client import Client
from ._config import DEFAULT_TIMEOUT_CONFIG
from ._models import Response
from ._types import (
AuthTypes,
CookieTypes,
HeaderTypes,
ProxyTypes,
QueryParamTypes,
RequestContent,
RequestData,
RequestFiles,
TimeoutTypes,
)
from ._urls import URL
if typing.TYPE_CHECKING:
import ssl # pragma: no cover
__all__ = [
"delete",
"get",
"head",
"options",
"patch",
"post",
"put",
"request",
"stream",
]
def request(
method: str,
url: URL | str,
*,
params: QueryParamTypes | None = None,
content: RequestContent | None = None,
data: RequestData | None = None,
files: RequestFiles | None = None,
json: typing.Any | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True,
trust_env: bool = True,
) -> Response:
"""
Sends an HTTP request.
**Parameters:**
* **method** - HTTP method for the new `Request` object: `GET`, `OPTIONS`,
`HEAD`, `POST`, `PUT`, `PATCH`, or `DELETE`.
* **url** - URL for the new `Request` object.
* **params** - *(optional)* Query parameters to include in the URL, as a
string, dictionary, or sequence of two-tuples.
* **content** - *(optional)* Binary content to include in the body of the
request, as bytes or a byte iterator.
* **data** - *(optional)* Form data to include in the body of the request,
as a dictionary.
* **files** - *(optional)* A dictionary of upload files to include in the
body of the request.
* **json** - *(optional)* A JSON serializable object to include in the body
of the request.
* **headers** - *(optional)* Dictionary of HTTP headers to include in the
request.
* **cookies** - *(optional)* Dictionary of Cookie items to include in the
request.
* **auth** - *(optional)* An authentication class to use when sending the
request.
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
* **timeout** - *(optional)* The timeout configuration to use when sending
the request.
* **follow_redirects** - *(optional)* Enables or disables HTTP redirects.
* **verify** - *(optional)* Either `True` to use an SSL context with the
default CA bundle, `False` to disable verification, or an instance of
`ssl.SSLContext` to use a custom context.
* **trust_env** - *(optional)* Enables or disables usage of environment
variables for configuration.
**Returns:** `Response`
Usage:
```
>>> import httpx
>>> response = httpx.request('GET', 'https://httpbin.org/get')
>>> response
<Response [200 OK]>
```
"""
with Client(
cookies=cookies,
proxy=proxy,
verify=verify,
timeout=timeout,
trust_env=trust_env,
) as client:
return client.request(
method=method,
url=url,
content=content,
data=data,
files=files,
json=json,
params=params,
headers=headers,
auth=auth,
follow_redirects=follow_redirects,
)
@contextmanager
def stream(
method: str,
url: URL | str,
*,
params: QueryParamTypes | None = None,
content: RequestContent | None = None,
data: RequestData | None = None,
files: RequestFiles | None = None,
json: typing.Any | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True,
trust_env: bool = True,
) -> typing.Iterator[Response]:
"""
Alternative to `httpx.request()` that streams the response body
instead of loading it into memory at once.
**Parameters**: See `httpx.request`.
See also: [Streaming Responses][0]
[0]: /quickstart#streaming-responses
"""
with Client(
cookies=cookies,
proxy=proxy,
verify=verify,
timeout=timeout,
trust_env=trust_env,
) as client:
with client.stream(
method=method,
url=url,
content=content,
data=data,
files=files,
json=json,
params=params,
headers=headers,
auth=auth,
follow_redirects=follow_redirects,
) as response:
yield response
def get(
url: URL | str,
*,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
"""
Sends a `GET` request.
**Parameters**: See `httpx.request`.
Note that the `data`, `files`, `json` and `content` parameters are not available
on this function, as `GET` requests should not include a request body.
"""
return request(
"GET",
url,
params=params,
headers=headers,
cookies=cookies,
auth=auth,
proxy=proxy,
follow_redirects=follow_redirects,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def options(
url: URL | str,
*,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
"""
Sends an `OPTIONS` request.
**Parameters**: See `httpx.request`.
Note that the `data`, `files`, `json` and `content` parameters are not available
on this function, as `OPTIONS` requests should not include a request body.
"""
return request(
"OPTIONS",
url,
params=params,
headers=headers,
cookies=cookies,
auth=auth,
proxy=proxy,
follow_redirects=follow_redirects,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def head(
url: URL | str,
*,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
"""
Sends a `HEAD` request.
**Parameters**: See `httpx.request`.
Note that the `data`, `files`, `json` and `content` parameters are not available
on this function, as `HEAD` requests should not include a request body.
"""
return request(
"HEAD",
url,
params=params,
headers=headers,
cookies=cookies,
auth=auth,
proxy=proxy,
follow_redirects=follow_redirects,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def post(
url: URL | str,
*,
content: RequestContent | None = None,
data: RequestData | None = None,
files: RequestFiles | None = None,
json: typing.Any | None = None,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
"""
Sends a `POST` request.
**Parameters**: See `httpx.request`.
"""
return request(
"POST",
url,
content=content,
data=data,
files=files,
json=json,
params=params,
headers=headers,
cookies=cookies,
auth=auth,
proxy=proxy,
follow_redirects=follow_redirects,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def put(
url: URL | str,
*,
content: RequestContent | None = None,
data: RequestData | None = None,
files: RequestFiles | None = None,
json: typing.Any | None = None,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
"""
Sends a `PUT` request.
**Parameters**: See `httpx.request`.
"""
return request(
"PUT",
url,
content=content,
data=data,
files=files,
json=json,
params=params,
headers=headers,
cookies=cookies,
auth=auth,
proxy=proxy,
follow_redirects=follow_redirects,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def patch(
url: URL | str,
*,
content: RequestContent | None = None,
data: RequestData | None = None,
files: RequestFiles | None = None,
json: typing.Any | None = None,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True,
) -> Response:
"""
Sends a `PATCH` request.
**Parameters**: See `httpx.request`.
"""
return request(
"PATCH",
url,
content=content,
data=data,
files=files,
json=json,
params=params,
headers=headers,
cookies=cookies,
auth=auth,
proxy=proxy,
follow_redirects=follow_redirects,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def delete(
url: URL | str,
*,
params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None,
auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None,
follow_redirects: bool = False,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
verify: ssl.SSLContext | str | bool = True,
trust_env: bool = True,
) -> Response:
"""
Sends a `DELETE` request.
**Parameters**: See `httpx.request`.
Note that the `data`, `files`, `json` and `content` parameters are not available
on this function, as `DELETE` requests should not include a request body.
"""
return request(
"DELETE",
url,
params=params,
headers=headers,
cookies=cookies,
auth=auth,
proxy=proxy,
follow_redirects=follow_redirects,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)

348
httpx/_auth.py Normal file
View File

@ -0,0 +1,348 @@
from __future__ import annotations
import hashlib
import os
import re
import time
import typing
from base64 import b64encode
from urllib.request import parse_http_list
from ._exceptions import ProtocolError
from ._models import Cookies, Request, Response
from ._utils import to_bytes, to_str, unquote
if typing.TYPE_CHECKING: # pragma: no cover
from hashlib import _Hash
__all__ = ["Auth", "BasicAuth", "DigestAuth", "FunctionAuth", "NetRCAuth"]
class Auth:
"""
Base class for all authentication schemes.
To implement a custom authentication scheme, subclass `Auth` and override
the `.auth_flow()` method.
If the authentication scheme does I/O such as disk access or network calls, or uses
synchronization primitives such as locks, you should override `.sync_auth_flow()`
and/or `.async_auth_flow()` instead of `.auth_flow()` to provide specialized
implementations that will be used by `Client` and `AsyncClient` respectively.
"""
requires_request_body = False
requires_response_body = False
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
"""
Execute the authentication flow.
To dispatch a request, `yield` it:
```
yield request
```
The client will `.send()` the response back into the flow generator. You can
access it like so:
```
response = yield request
```
A `return` (or reaching the end of the generator) will result in the
client returning the last response obtained from the server.
You can dispatch as many requests as is necessary.
"""
yield request
def sync_auth_flow(
self, request: Request
) -> typing.Generator[Request, Response, None]:
"""
Execute the authentication flow synchronously.
By default, this defers to `.auth_flow()`. You should override this method
when the authentication scheme does I/O and/or uses concurrency primitives.
"""
if self.requires_request_body:
request.read()
flow = self.auth_flow(request)
request = next(flow)
while True:
response = yield request
if self.requires_response_body:
response.read()
try:
request = flow.send(response)
except StopIteration:
break
async def async_auth_flow(
self, request: Request
) -> typing.AsyncGenerator[Request, Response]:
"""
Execute the authentication flow asynchronously.
By default, this defers to `.auth_flow()`. You should override this method
when the authentication scheme does I/O and/or uses concurrency primitives.
"""
if self.requires_request_body:
await request.aread()
flow = self.auth_flow(request)
request = next(flow)
while True:
response = yield request
if self.requires_response_body:
await response.aread()
try:
request = flow.send(response)
except StopIteration:
break
class FunctionAuth(Auth):
"""
Allows the 'auth' argument to be passed as a simple callable function,
that takes the request, and returns a new, modified request.
"""
def __init__(self, func: typing.Callable[[Request], Request]) -> None:
self._func = func
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
yield self._func(request)
class BasicAuth(Auth):
"""
Allows the 'auth' argument to be passed as a (username, password) pair,
and uses HTTP Basic authentication.
"""
def __init__(self, username: str | bytes, password: str | bytes) -> None:
self._auth_header = self._build_auth_header(username, password)
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
request.headers["Authorization"] = self._auth_header
yield request
def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str:
userpass = b":".join((to_bytes(username), to_bytes(password)))
token = b64encode(userpass).decode()
return f"Basic {token}"
class NetRCAuth(Auth):
"""
Use a 'netrc' file to lookup basic auth credentials based on the url host.
"""
def __init__(self, file: str | None = None) -> None:
# Lazily import 'netrc'.
# There's no need for us to load this module unless 'NetRCAuth' is being used.
import netrc
self._netrc_info = netrc.netrc(file)
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
auth_info = self._netrc_info.authenticators(request.url.host)
if auth_info is None or not auth_info[2]:
# The netrc file did not have authentication credentials for this host.
yield request
else:
# Build a basic auth header with credentials from the netrc file.
request.headers["Authorization"] = self._build_auth_header(
username=auth_info[0], password=auth_info[2]
)
yield request
def _build_auth_header(self, username: str | bytes, password: str | bytes) -> str:
userpass = b":".join((to_bytes(username), to_bytes(password)))
token = b64encode(userpass).decode()
return f"Basic {token}"
class DigestAuth(Auth):
_ALGORITHM_TO_HASH_FUNCTION: dict[str, typing.Callable[[bytes], _Hash]] = {
"MD5": hashlib.md5,
"MD5-SESS": hashlib.md5,
"SHA": hashlib.sha1,
"SHA-SESS": hashlib.sha1,
"SHA-256": hashlib.sha256,
"SHA-256-SESS": hashlib.sha256,
"SHA-512": hashlib.sha512,
"SHA-512-SESS": hashlib.sha512,
}
def __init__(self, username: str | bytes, password: str | bytes) -> None:
self._username = to_bytes(username)
self._password = to_bytes(password)
self._last_challenge: _DigestAuthChallenge | None = None
self._nonce_count = 1
def auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
if self._last_challenge:
request.headers["Authorization"] = self._build_auth_header(
request, self._last_challenge
)
response = yield request
if response.status_code != 401 or "www-authenticate" not in response.headers:
# If the response is not a 401 then we don't
# need to build an authenticated request.
return
for auth_header in response.headers.get_list("www-authenticate"):
if auth_header.lower().startswith("digest "):
break
else:
# If the response does not include a 'WWW-Authenticate: Digest ...'
# header, then we don't need to build an authenticated request.
return
self._last_challenge = self._parse_challenge(request, response, auth_header)
self._nonce_count = 1
request.headers["Authorization"] = self._build_auth_header(
request, self._last_challenge
)
if response.cookies:
Cookies(response.cookies).set_cookie_header(request=request)
yield request
def _parse_challenge(
self, request: Request, response: Response, auth_header: str
) -> _DigestAuthChallenge:
"""
Returns a challenge from a Digest WWW-Authenticate header.
These take the form of:
`Digest realm="realm@host.com",qop="auth,auth-int",nonce="abc",opaque="xyz"`
"""
scheme, _, fields = auth_header.partition(" ")
# This method should only ever have been called with a Digest auth header.
assert scheme.lower() == "digest"
header_dict: dict[str, str] = {}
for field in parse_http_list(fields):
key, value = field.strip().split("=", 1)
header_dict[key] = unquote(value)
try:
realm = header_dict["realm"].encode()
nonce = header_dict["nonce"].encode()
algorithm = header_dict.get("algorithm", "MD5")
opaque = header_dict["opaque"].encode() if "opaque" in header_dict else None
qop = header_dict["qop"].encode() if "qop" in header_dict else None
return _DigestAuthChallenge(
realm=realm, nonce=nonce, algorithm=algorithm, opaque=opaque, qop=qop
)
except KeyError as exc:
message = "Malformed Digest WWW-Authenticate header"
raise ProtocolError(message, request=request) from exc
def _build_auth_header(
self, request: Request, challenge: _DigestAuthChallenge
) -> str:
hash_func = self._ALGORITHM_TO_HASH_FUNCTION[challenge.algorithm.upper()]
def digest(data: bytes) -> bytes:
return hash_func(data).hexdigest().encode()
A1 = b":".join((self._username, challenge.realm, self._password))
path = request.url.raw_path
A2 = b":".join((request.method.encode(), path))
# TODO: implement auth-int
HA2 = digest(A2)
nc_value = b"%08x" % self._nonce_count
cnonce = self._get_client_nonce(self._nonce_count, challenge.nonce)
self._nonce_count += 1
HA1 = digest(A1)
if challenge.algorithm.lower().endswith("-sess"):
HA1 = digest(b":".join((HA1, challenge.nonce, cnonce)))
qop = self._resolve_qop(challenge.qop, request=request)
if qop is None:
# Following RFC 2069
digest_data = [HA1, challenge.nonce, HA2]
else:
# Following RFC 2617/7616
digest_data = [HA1, challenge.nonce, nc_value, cnonce, qop, HA2]
format_args = {
"username": self._username,
"realm": challenge.realm,
"nonce": challenge.nonce,
"uri": path,
"response": digest(b":".join(digest_data)),
"algorithm": challenge.algorithm.encode(),
}
if challenge.opaque:
format_args["opaque"] = challenge.opaque
if qop:
format_args["qop"] = b"auth"
format_args["nc"] = nc_value
format_args["cnonce"] = cnonce
return "Digest " + self._get_header_value(format_args)
def _get_client_nonce(self, nonce_count: int, nonce: bytes) -> bytes:
s = str(nonce_count).encode()
s += nonce
s += time.ctime().encode()
s += os.urandom(8)
return hashlib.sha1(s).hexdigest()[:16].encode()
def _get_header_value(self, header_fields: dict[str, bytes]) -> str:
NON_QUOTED_FIELDS = ("algorithm", "qop", "nc")
QUOTED_TEMPLATE = '{}="{}"'
NON_QUOTED_TEMPLATE = "{}={}"
header_value = ""
for i, (field, value) in enumerate(header_fields.items()):
if i > 0:
header_value += ", "
template = (
QUOTED_TEMPLATE
if field not in NON_QUOTED_FIELDS
else NON_QUOTED_TEMPLATE
)
header_value += template.format(field, to_str(value))
return header_value
def _resolve_qop(self, qop: bytes | None, request: Request) -> bytes | None:
if qop is None:
return None
qops = re.split(b", ?", qop)
if b"auth" in qops:
return b"auth"
if qops == [b"auth-int"]:
raise NotImplementedError("Digest auth-int support is not yet implemented")
message = f'Unexpected qop value "{qop!r}" in digest auth'
raise ProtocolError(message, request=request)
class _DigestAuthChallenge(typing.NamedTuple):
realm: bytes
nonce: bytes
algorithm: str
opaque: bytes | None
qop: bytes | None

2019
httpx/_client.py Normal file

File diff suppressed because it is too large Load Diff

248
httpx/_config.py Normal file
View File

@ -0,0 +1,248 @@
from __future__ import annotations
import os
import typing
from ._models import Headers
from ._types import CertTypes, HeaderTypes, TimeoutTypes
from ._urls import URL
if typing.TYPE_CHECKING:
import ssl # pragma: no cover
__all__ = ["Limits", "Proxy", "Timeout", "create_ssl_context"]
class UnsetType:
pass # pragma: no cover
UNSET = UnsetType()
def create_ssl_context(
verify: ssl.SSLContext | str | bool = True,
cert: CertTypes | None = None,
trust_env: bool = True,
) -> ssl.SSLContext:
import ssl
import warnings
import certifi
if verify is True:
if trust_env and os.environ.get("SSL_CERT_FILE"): # pragma: nocover
ctx = ssl.create_default_context(cafile=os.environ["SSL_CERT_FILE"])
elif trust_env and os.environ.get("SSL_CERT_DIR"): # pragma: nocover
ctx = ssl.create_default_context(capath=os.environ["SSL_CERT_DIR"])
else:
# Default case...
ctx = ssl.create_default_context(cafile=certifi.where())
elif verify is False:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
elif isinstance(verify, str): # pragma: nocover
message = (
"`verify=<str>` is deprecated. "
"Use `verify=ssl.create_default_context(cafile=...)` "
"or `verify=ssl.create_default_context(capath=...)` instead."
)
warnings.warn(message, DeprecationWarning)
if os.path.isdir(verify):
return ssl.create_default_context(capath=verify)
return ssl.create_default_context(cafile=verify)
else:
ctx = verify
if cert: # pragma: nocover
message = (
"`cert=...` is deprecated. Use `verify=<ssl_context>` instead,"
"with `.load_cert_chain()` to configure the certificate chain."
)
warnings.warn(message, DeprecationWarning)
if isinstance(cert, str):
ctx.load_cert_chain(cert)
else:
ctx.load_cert_chain(*cert)
return ctx
class Timeout:
"""
Timeout configuration.
**Usage**:
Timeout(None) # No timeouts.
Timeout(5.0) # 5s timeout on all operations.
Timeout(None, connect=5.0) # 5s timeout on connect, no other timeouts.
Timeout(5.0, connect=10.0) # 10s timeout on connect. 5s timeout elsewhere.
Timeout(5.0, pool=None) # No timeout on acquiring connection from pool.
# 5s timeout elsewhere.
"""
def __init__(
self,
timeout: TimeoutTypes | UnsetType = UNSET,
*,
connect: None | float | UnsetType = UNSET,
read: None | float | UnsetType = UNSET,
write: None | float | UnsetType = UNSET,
pool: None | float | UnsetType = UNSET,
) -> None:
if isinstance(timeout, Timeout):
# Passed as a single explicit Timeout.
assert connect is UNSET
assert read is UNSET
assert write is UNSET
assert pool is UNSET
self.connect = timeout.connect # type: typing.Optional[float]
self.read = timeout.read # type: typing.Optional[float]
self.write = timeout.write # type: typing.Optional[float]
self.pool = timeout.pool # type: typing.Optional[float]
elif isinstance(timeout, tuple):
# Passed as a tuple.
self.connect = timeout[0]
self.read = timeout[1]
self.write = None if len(timeout) < 3 else timeout[2]
self.pool = None if len(timeout) < 4 else timeout[3]
elif not (
isinstance(connect, UnsetType)
or isinstance(read, UnsetType)
or isinstance(write, UnsetType)
or isinstance(pool, UnsetType)
):
self.connect = connect
self.read = read
self.write = write
self.pool = pool
else:
if isinstance(timeout, UnsetType):
raise ValueError(
"httpx.Timeout must either include a default, or set all "
"four parameters explicitly."
)
self.connect = timeout if isinstance(connect, UnsetType) else connect
self.read = timeout if isinstance(read, UnsetType) else read
self.write = timeout if isinstance(write, UnsetType) else write
self.pool = timeout if isinstance(pool, UnsetType) else pool
def as_dict(self) -> dict[str, float | None]:
return {
"connect": self.connect,
"read": self.read,
"write": self.write,
"pool": self.pool,
}
def __eq__(self, other: typing.Any) -> bool:
return (
isinstance(other, self.__class__)
and self.connect == other.connect
and self.read == other.read
and self.write == other.write
and self.pool == other.pool
)
def __repr__(self) -> str:
class_name = self.__class__.__name__
if len({self.connect, self.read, self.write, self.pool}) == 1:
return f"{class_name}(timeout={self.connect})"
return (
f"{class_name}(connect={self.connect}, "
f"read={self.read}, write={self.write}, pool={self.pool})"
)
class Limits:
"""
Configuration for limits to various client behaviors.
**Parameters:**
* **max_connections** - The maximum number of concurrent connections that may be
established.
* **max_keepalive_connections** - Allow the connection pool to maintain
keep-alive connections below this point. Should be less than or equal
to `max_connections`.
* **keepalive_expiry** - Time limit on idle keep-alive connections in seconds.
"""
def __init__(
self,
*,
max_connections: int | None = None,
max_keepalive_connections: int | None = None,
keepalive_expiry: float | None = 5.0,
) -> None:
self.max_connections = max_connections
self.max_keepalive_connections = max_keepalive_connections
self.keepalive_expiry = keepalive_expiry
def __eq__(self, other: typing.Any) -> bool:
return (
isinstance(other, self.__class__)
and self.max_connections == other.max_connections
and self.max_keepalive_connections == other.max_keepalive_connections
and self.keepalive_expiry == other.keepalive_expiry
)
def __repr__(self) -> str:
class_name = self.__class__.__name__
return (
f"{class_name}(max_connections={self.max_connections}, "
f"max_keepalive_connections={self.max_keepalive_connections}, "
f"keepalive_expiry={self.keepalive_expiry})"
)
class Proxy:
def __init__(
self,
url: URL | str,
*,
ssl_context: ssl.SSLContext | None = None,
auth: tuple[str, str] | None = None,
headers: HeaderTypes | None = None,
) -> None:
url = URL(url)
headers = Headers(headers)
if url.scheme not in ("http", "https", "socks5", "socks5h"):
raise ValueError(f"Unknown scheme for proxy URL {url!r}")
if url.username or url.password:
# Remove any auth credentials from the URL.
auth = (url.username, url.password)
url = url.copy_with(username=None, password=None)
self.url = url
self.auth = auth
self.headers = headers
self.ssl_context = ssl_context
@property
def raw_auth(self) -> tuple[bytes, bytes] | None:
# The proxy authentication as raw bytes.
return (
None
if self.auth is None
else (self.auth[0].encode("utf-8"), self.auth[1].encode("utf-8"))
)
def __repr__(self) -> str:
# The authentication is represented with the password component masked.
auth = (self.auth[0], "********") if self.auth else None
# Build a nice concise representation.
url_str = f"{str(self.url)!r}"
auth_str = f", auth={auth!r}" if auth else ""
headers_str = f", headers={dict(self.headers)!r}" if self.headers else ""
return f"Proxy({url_str}{auth_str}{headers_str})"
DEFAULT_TIMEOUT_CONFIG = Timeout(timeout=5.0)
DEFAULT_LIMITS = Limits(max_connections=100, max_keepalive_connections=20)
DEFAULT_MAX_REDIRECTS = 20

240
httpx/_content.py Normal file
View File

@ -0,0 +1,240 @@
from __future__ import annotations
import inspect
import warnings
from json import dumps as json_dumps
from typing import (
Any,
AsyncIterable,
AsyncIterator,
Iterable,
Iterator,
Mapping,
)
from urllib.parse import urlencode
from ._exceptions import StreamClosed, StreamConsumed
from ._multipart import MultipartStream
from ._types import (
AsyncByteStream,
RequestContent,
RequestData,
RequestFiles,
ResponseContent,
SyncByteStream,
)
from ._utils import peek_filelike_length, primitive_value_to_str
__all__ = ["ByteStream"]
class ByteStream(AsyncByteStream, SyncByteStream):
def __init__(self, stream: bytes) -> None:
self._stream = stream
def __iter__(self) -> Iterator[bytes]:
yield self._stream
async def __aiter__(self) -> AsyncIterator[bytes]:
yield self._stream
class IteratorByteStream(SyncByteStream):
CHUNK_SIZE = 65_536
def __init__(self, stream: Iterable[bytes]) -> None:
self._stream = stream
self._is_stream_consumed = False
self._is_generator = inspect.isgenerator(stream)
def __iter__(self) -> Iterator[bytes]:
if self._is_stream_consumed and self._is_generator:
raise StreamConsumed()
self._is_stream_consumed = True
if hasattr(self._stream, "read"):
# File-like interfaces should use 'read' directly.
chunk = self._stream.read(self.CHUNK_SIZE)
while chunk:
yield chunk
chunk = self._stream.read(self.CHUNK_SIZE)
else:
# Otherwise iterate.
for part in self._stream:
yield part
class AsyncIteratorByteStream(AsyncByteStream):
CHUNK_SIZE = 65_536
def __init__(self, stream: AsyncIterable[bytes]) -> None:
self._stream = stream
self._is_stream_consumed = False
self._is_generator = inspect.isasyncgen(stream)
async def __aiter__(self) -> AsyncIterator[bytes]:
if self._is_stream_consumed and self._is_generator:
raise StreamConsumed()
self._is_stream_consumed = True
if hasattr(self._stream, "aread"):
# File-like interfaces should use 'aread' directly.
chunk = await self._stream.aread(self.CHUNK_SIZE)
while chunk:
yield chunk
chunk = await self._stream.aread(self.CHUNK_SIZE)
else:
# Otherwise iterate.
async for part in self._stream:
yield part
class UnattachedStream(AsyncByteStream, SyncByteStream):
"""
If a request or response is serialized using pickle, then it is no longer
attached to a stream for I/O purposes. Any stream operations should result
in `httpx.StreamClosed`.
"""
def __iter__(self) -> Iterator[bytes]:
raise StreamClosed()
async def __aiter__(self) -> AsyncIterator[bytes]:
raise StreamClosed()
yield b"" # pragma: no cover
def encode_content(
content: str | bytes | Iterable[bytes] | AsyncIterable[bytes],
) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
if isinstance(content, (bytes, str)):
body = content.encode("utf-8") if isinstance(content, str) else content
content_length = len(body)
headers = {"Content-Length": str(content_length)} if body else {}
return headers, ByteStream(body)
elif isinstance(content, Iterable) and not isinstance(content, dict):
# `not isinstance(content, dict)` is a bit oddly specific, but it
# catches a case that's easy for users to make in error, and would
# otherwise pass through here, like any other bytes-iterable,
# because `dict` happens to be iterable. See issue #2491.
content_length_or_none = peek_filelike_length(content)
if content_length_or_none is None:
headers = {"Transfer-Encoding": "chunked"}
else:
headers = {"Content-Length": str(content_length_or_none)}
return headers, IteratorByteStream(content) # type: ignore
elif isinstance(content, AsyncIterable):
headers = {"Transfer-Encoding": "chunked"}
return headers, AsyncIteratorByteStream(content)
raise TypeError(f"Unexpected type for 'content', {type(content)!r}")
def encode_urlencoded_data(
data: RequestData,
) -> tuple[dict[str, str], ByteStream]:
plain_data = []
for key, value in data.items():
if isinstance(value, (list, tuple)):
plain_data.extend([(key, primitive_value_to_str(item)) for item in value])
else:
plain_data.append((key, primitive_value_to_str(value)))
body = urlencode(plain_data, doseq=True).encode("utf-8")
content_length = str(len(body))
content_type = "application/x-www-form-urlencoded"
headers = {"Content-Length": content_length, "Content-Type": content_type}
return headers, ByteStream(body)
def encode_multipart_data(
data: RequestData, files: RequestFiles, boundary: bytes | None
) -> tuple[dict[str, str], MultipartStream]:
multipart = MultipartStream(data=data, files=files, boundary=boundary)
headers = multipart.get_headers()
return headers, multipart
def encode_text(text: str) -> tuple[dict[str, str], ByteStream]:
body = text.encode("utf-8")
content_length = str(len(body))
content_type = "text/plain; charset=utf-8"
headers = {"Content-Length": content_length, "Content-Type": content_type}
return headers, ByteStream(body)
def encode_html(html: str) -> tuple[dict[str, str], ByteStream]:
body = html.encode("utf-8")
content_length = str(len(body))
content_type = "text/html; charset=utf-8"
headers = {"Content-Length": content_length, "Content-Type": content_type}
return headers, ByteStream(body)
def encode_json(json: Any) -> tuple[dict[str, str], ByteStream]:
body = json_dumps(
json, ensure_ascii=False, separators=(",", ":"), allow_nan=False
).encode("utf-8")
content_length = str(len(body))
content_type = "application/json"
headers = {"Content-Length": content_length, "Content-Type": content_type}
return headers, ByteStream(body)
def encode_request(
content: RequestContent | None = None,
data: RequestData | None = None,
files: RequestFiles | None = None,
json: Any | None = None,
boundary: bytes | None = None,
) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
"""
Handles encoding the given `content`, `data`, `files`, and `json`,
returning a two-tuple of (<headers>, <stream>).
"""
if data is not None and not isinstance(data, Mapping):
# We prefer to separate `content=<bytes|str|byte iterator|bytes aiterator>`
# for raw request content, and `data=<form data>` for url encoded or
# multipart form content.
#
# However for compat with requests, we *do* still support
# `data=<bytes...>` usages. We deal with that case here, treating it
# as if `content=<...>` had been supplied instead.
message = "Use 'content=<...>' to upload raw bytes/text content."
warnings.warn(message, DeprecationWarning, stacklevel=2)
return encode_content(data)
if content is not None:
return encode_content(content)
elif files:
return encode_multipart_data(data or {}, files, boundary)
elif data:
return encode_urlencoded_data(data)
elif json is not None:
return encode_json(json)
return {}, ByteStream(b"")
def encode_response(
content: ResponseContent | None = None,
text: str | None = None,
html: str | None = None,
json: Any | None = None,
) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]:
"""
Handles encoding the given `content`, returning a two-tuple of
(<headers>, <stream>).
"""
if content is not None:
return encode_content(content)
elif text is not None:
return encode_text(text)
elif html is not None:
return encode_html(html)
elif json is not None:
return encode_json(json)
return {}, ByteStream(b"")

393
httpx/_decoders.py Normal file
View File

@ -0,0 +1,393 @@
"""
Handlers for Content-Encoding.
See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
"""
from __future__ import annotations
import codecs
import io
import typing
import zlib
from ._exceptions import DecodingError
# Brotli support is optional
try:
# The C bindings in `brotli` are recommended for CPython.
import brotli
except ImportError: # pragma: no cover
try:
# The CFFI bindings in `brotlicffi` are recommended for PyPy
# and other environments.
import brotlicffi as brotli
except ImportError:
brotli = None
# Zstandard support is optional
try:
import zstandard
except ImportError: # pragma: no cover
zstandard = None # type: ignore
class ContentDecoder:
def decode(self, data: bytes) -> bytes:
raise NotImplementedError() # pragma: no cover
def flush(self) -> bytes:
raise NotImplementedError() # pragma: no cover
class IdentityDecoder(ContentDecoder):
"""
Handle unencoded data.
"""
def decode(self, data: bytes) -> bytes:
return data
def flush(self) -> bytes:
return b""
class DeflateDecoder(ContentDecoder):
"""
Handle 'deflate' decoding.
See: https://stackoverflow.com/questions/1838699
"""
def __init__(self) -> None:
self.first_attempt = True
self.decompressor = zlib.decompressobj()
def decode(self, data: bytes) -> bytes:
was_first_attempt = self.first_attempt
self.first_attempt = False
try:
return self.decompressor.decompress(data)
except zlib.error as exc:
if was_first_attempt:
self.decompressor = zlib.decompressobj(-zlib.MAX_WBITS)
return self.decode(data)
raise DecodingError(str(exc)) from exc
def flush(self) -> bytes:
try:
return self.decompressor.flush()
except zlib.error as exc: # pragma: no cover
raise DecodingError(str(exc)) from exc
class GZipDecoder(ContentDecoder):
"""
Handle 'gzip' decoding.
See: https://stackoverflow.com/questions/1838699
"""
def __init__(self) -> None:
self.decompressor = zlib.decompressobj(zlib.MAX_WBITS | 16)
def decode(self, data: bytes) -> bytes:
try:
return self.decompressor.decompress(data)
except zlib.error as exc:
raise DecodingError(str(exc)) from exc
def flush(self) -> bytes:
try:
return self.decompressor.flush()
except zlib.error as exc: # pragma: no cover
raise DecodingError(str(exc)) from exc
class BrotliDecoder(ContentDecoder):
"""
Handle 'brotli' decoding.
Requires `pip install brotlipy`. See: https://brotlipy.readthedocs.io/
or `pip install brotli`. See https://github.com/google/brotli
Supports both 'brotlipy' and 'Brotli' packages since they share an import
name. The top branches are for 'brotlipy' and bottom branches for 'Brotli'
"""
def __init__(self) -> None:
if brotli is None: # pragma: no cover
raise ImportError(
"Using 'BrotliDecoder', but neither of the 'brotlicffi' or 'brotli' "
"packages have been installed. "
"Make sure to install httpx using `pip install httpx[brotli]`."
) from None
self.decompressor = brotli.Decompressor()
self.seen_data = False
self._decompress: typing.Callable[[bytes], bytes]
if hasattr(self.decompressor, "decompress"):
# The 'brotlicffi' package.
self._decompress = self.decompressor.decompress # pragma: no cover
else:
# The 'brotli' package.
self._decompress = self.decompressor.process # pragma: no cover
def decode(self, data: bytes) -> bytes:
if not data:
return b""
self.seen_data = True
try:
return self._decompress(data)
except brotli.error as exc:
raise DecodingError(str(exc)) from exc
def flush(self) -> bytes:
if not self.seen_data:
return b""
try:
if hasattr(self.decompressor, "finish"):
# Only available in the 'brotlicffi' package.
# As the decompressor decompresses eagerly, this
# will never actually emit any data. However, it will potentially throw
# errors if a truncated or damaged data stream has been used.
self.decompressor.finish() # pragma: no cover
return b""
except brotli.error as exc: # pragma: no cover
raise DecodingError(str(exc)) from exc
class ZStandardDecoder(ContentDecoder):
"""
Handle 'zstd' RFC 8878 decoding.
Requires `pip install zstandard`.
Can be installed as a dependency of httpx using `pip install httpx[zstd]`.
"""
# inspired by the ZstdDecoder implementation in urllib3
def __init__(self) -> None:
if zstandard is None: # pragma: no cover
raise ImportError(
"Using 'ZStandardDecoder', ..."
"Make sure to install httpx using `pip install httpx[zstd]`."
) from None
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
self.seen_data = False
def decode(self, data: bytes) -> bytes:
assert zstandard is not None
self.seen_data = True
output = io.BytesIO()
try:
output.write(self.decompressor.decompress(data))
while self.decompressor.eof and self.decompressor.unused_data:
unused_data = self.decompressor.unused_data
self.decompressor = zstandard.ZstdDecompressor().decompressobj()
output.write(self.decompressor.decompress(unused_data))
except zstandard.ZstdError as exc:
raise DecodingError(str(exc)) from exc
return output.getvalue()
def flush(self) -> bytes:
if not self.seen_data:
return b""
ret = self.decompressor.flush() # note: this is a no-op
if not self.decompressor.eof:
raise DecodingError("Zstandard data is incomplete") # pragma: no cover
return bytes(ret)
class MultiDecoder(ContentDecoder):
"""
Handle the case where multiple encodings have been applied.
"""
def __init__(self, children: typing.Sequence[ContentDecoder]) -> None:
"""
'children' should be a sequence of decoders in the order in which
each was applied.
"""
# Note that we reverse the order for decoding.
self.children = list(reversed(children))
def decode(self, data: bytes) -> bytes:
for child in self.children:
data = child.decode(data)
return data
def flush(self) -> bytes:
data = b""
for child in self.children:
data = child.decode(data) + child.flush()
return data
class ByteChunker:
"""
Handles returning byte content in fixed-size chunks.
"""
def __init__(self, chunk_size: int | None = None) -> None:
self._buffer = io.BytesIO()
self._chunk_size = chunk_size
def decode(self, content: bytes) -> list[bytes]:
if self._chunk_size is None:
return [content] if content else []
self._buffer.write(content)
if self._buffer.tell() >= self._chunk_size:
value = self._buffer.getvalue()
chunks = [
value[i : i + self._chunk_size]
for i in range(0, len(value), self._chunk_size)
]
if len(chunks[-1]) == self._chunk_size:
self._buffer.seek(0)
self._buffer.truncate()
return chunks
else:
self._buffer.seek(0)
self._buffer.write(chunks[-1])
self._buffer.truncate()
return chunks[:-1]
else:
return []
def flush(self) -> list[bytes]:
value = self._buffer.getvalue()
self._buffer.seek(0)
self._buffer.truncate()
return [value] if value else []
class TextChunker:
"""
Handles returning text content in fixed-size chunks.
"""
def __init__(self, chunk_size: int | None = None) -> None:
self._buffer = io.StringIO()
self._chunk_size = chunk_size
def decode(self, content: str) -> list[str]:
if self._chunk_size is None:
return [content] if content else []
self._buffer.write(content)
if self._buffer.tell() >= self._chunk_size:
value = self._buffer.getvalue()
chunks = [
value[i : i + self._chunk_size]
for i in range(0, len(value), self._chunk_size)
]
if len(chunks[-1]) == self._chunk_size:
self._buffer.seek(0)
self._buffer.truncate()
return chunks
else:
self._buffer.seek(0)
self._buffer.write(chunks[-1])
self._buffer.truncate()
return chunks[:-1]
else:
return []
def flush(self) -> list[str]:
value = self._buffer.getvalue()
self._buffer.seek(0)
self._buffer.truncate()
return [value] if value else []
class TextDecoder:
"""
Handles incrementally decoding bytes into text
"""
def __init__(self, encoding: str = "utf-8") -> None:
self.decoder = codecs.getincrementaldecoder(encoding)(errors="replace")
def decode(self, data: bytes) -> str:
return self.decoder.decode(data)
def flush(self) -> str:
return self.decoder.decode(b"", True)
class LineDecoder:
"""
Handles incrementally reading lines from text.
Has the same behaviour as the stdllib splitlines,
but handling the input iteratively.
"""
def __init__(self) -> None:
self.buffer: list[str] = []
self.trailing_cr: bool = False
def decode(self, text: str) -> list[str]:
# See https://docs.python.org/3/library/stdtypes.html#str.splitlines
NEWLINE_CHARS = "\n\r\x0b\x0c\x1c\x1d\x1e\x85\u2028\u2029"
# We always push a trailing `\r` into the next decode iteration.
if self.trailing_cr:
text = "\r" + text
self.trailing_cr = False
if text.endswith("\r"):
self.trailing_cr = True
text = text[:-1]
if not text:
# NOTE: the edge case input of empty text doesn't occur in practice,
# because other httpx internals filter out this value
return [] # pragma: no cover
trailing_newline = text[-1] in NEWLINE_CHARS
lines = text.splitlines()
if len(lines) == 1 and not trailing_newline:
# No new lines, buffer the input and continue.
self.buffer.append(lines[0])
return []
if self.buffer:
# Include any existing buffer in the first portion of the
# splitlines result.
lines = ["".join(self.buffer) + lines[0]] + lines[1:]
self.buffer = []
if not trailing_newline:
# If the last segment of splitlines is not newline terminated,
# then drop it from our output and start a new buffer.
self.buffer = [lines.pop()]
return lines
def flush(self) -> list[str]:
if not self.buffer and not self.trailing_cr:
return []
lines = ["".join(self.buffer)]
self.buffer = []
self.trailing_cr = False
return lines
SUPPORTED_DECODERS = {
"identity": IdentityDecoder,
"gzip": GZipDecoder,
"deflate": DeflateDecoder,
"br": BrotliDecoder,
"zstd": ZStandardDecoder,
}
if brotli is None:
SUPPORTED_DECODERS.pop("br") # pragma: no cover
if zstandard is None:
SUPPORTED_DECODERS.pop("zstd") # pragma: no cover

377
httpx/_exceptions.py Normal file
View File

@ -0,0 +1,377 @@
"""
Our exception hierarchy:
* HTTPError
x RequestError
+ TransportError
- TimeoutException
· ConnectTimeout
· ReadTimeout
· WriteTimeout
· PoolTimeout
- NetworkError
· ConnectError
· ReadError
· WriteError
· CloseError
- ProtocolError
· LocalProtocolError
· RemoteProtocolError
- ProxyError
- UnsupportedProtocol
+ DecodingError
+ TooManyRedirects
x HTTPStatusError
* InvalidURL
* CookieConflict
* StreamError
x StreamConsumed
x StreamClosed
x ResponseNotRead
x RequestNotRead
"""
from __future__ import annotations
import contextlib
import typing
if typing.TYPE_CHECKING:
from ._models import Request, Response # pragma: no cover
__all__ = [
"CloseError",
"ConnectError",
"ConnectTimeout",
"CookieConflict",
"DecodingError",
"HTTPError",
"HTTPStatusError",
"InvalidURL",
"LocalProtocolError",
"NetworkError",
"PoolTimeout",
"ProtocolError",
"ProxyError",
"ReadError",
"ReadTimeout",
"RemoteProtocolError",
"RequestError",
"RequestNotRead",
"ResponseNotRead",
"StreamClosed",
"StreamConsumed",
"StreamError",
"TimeoutException",
"TooManyRedirects",
"TransportError",
"UnsupportedProtocol",
"WriteError",
"WriteTimeout",
]
class HTTPError(Exception):
"""
Base class for `RequestError` and `HTTPStatusError`.
Useful for `try...except` blocks when issuing a request,
and then calling `.raise_for_status()`.
For example:
```
try:
response = httpx.get("https://www.example.com")
response.raise_for_status()
except httpx.HTTPError as exc:
print(f"HTTP Exception for {exc.request.url} - {exc}")
```
"""
def __init__(self, message: str) -> None:
super().__init__(message)
self._request: Request | None = None
@property
def request(self) -> Request:
if self._request is None:
raise RuntimeError("The .request property has not been set.")
return self._request
@request.setter
def request(self, request: Request) -> None:
self._request = request
class RequestError(HTTPError):
"""
Base class for all exceptions that may occur when issuing a `.request()`.
"""
def __init__(self, message: str, *, request: Request | None = None) -> None:
super().__init__(message)
# At the point an exception is raised we won't typically have a request
# instance to associate it with.
#
# The 'request_context' context manager is used within the Client and
# Response methods in order to ensure that any raised exceptions
# have a `.request` property set on them.
self._request = request
class TransportError(RequestError):
"""
Base class for all exceptions that occur at the level of the Transport API.
"""
# Timeout exceptions...
class TimeoutException(TransportError):
"""
The base class for timeout errors.
An operation has timed out.
"""
class ConnectTimeout(TimeoutException):
"""
Timed out while connecting to the host.
"""
class ReadTimeout(TimeoutException):
"""
Timed out while receiving data from the host.
"""
class WriteTimeout(TimeoutException):
"""
Timed out while sending data to the host.
"""
class PoolTimeout(TimeoutException):
"""
Timed out waiting to acquire a connection from the pool.
"""
# Core networking exceptions...
class NetworkError(TransportError):
"""
The base class for network-related errors.
An error occurred while interacting with the network.
"""
class ReadError(NetworkError):
"""
Failed to receive data from the network.
"""
class WriteError(NetworkError):
"""
Failed to send data through the network.
"""
class ConnectError(NetworkError):
"""
Failed to establish a connection.
"""
class CloseError(NetworkError):
"""
Failed to close a connection.
"""
# Other transport exceptions...
class ProxyError(TransportError):
"""
An error occurred while establishing a proxy connection.
"""
class UnsupportedProtocol(TransportError):
"""
Attempted to make a request to an unsupported protocol.
For example issuing a request to `ftp://www.example.com`.
"""
class ProtocolError(TransportError):
"""
The protocol was violated.
"""
class LocalProtocolError(ProtocolError):
"""
A protocol was violated by the client.
For example if the user instantiated a `Request` instance explicitly,
failed to include the mandatory `Host:` header, and then issued it directly
using `client.send()`.
"""
class RemoteProtocolError(ProtocolError):
"""
The protocol was violated by the server.
For example, returning malformed HTTP.
"""
# Other request exceptions...
class DecodingError(RequestError):
"""
Decoding of the response failed, due to a malformed encoding.
"""
class TooManyRedirects(RequestError):
"""
Too many redirects.
"""
# Client errors
class HTTPStatusError(HTTPError):
"""
The response had an error HTTP status of 4xx or 5xx.
May be raised when calling `response.raise_for_status()`
"""
def __init__(self, message: str, *, request: Request, response: Response) -> None:
super().__init__(message)
self.request = request
self.response = response
class InvalidURL(Exception):
"""
URL is improperly formed or cannot be parsed.
"""
def __init__(self, message: str) -> None:
super().__init__(message)
class CookieConflict(Exception):
"""
Attempted to lookup a cookie by name, but multiple cookies existed.
Can occur when calling `response.cookies.get(...)`.
"""
def __init__(self, message: str) -> None:
super().__init__(message)
# Stream exceptions...
# These may occur as the result of a programming error, by accessing
# the request/response stream in an invalid manner.
class StreamError(RuntimeError):
"""
The base class for stream exceptions.
The developer made an error in accessing the request stream in
an invalid way.
"""
def __init__(self, message: str) -> None:
super().__init__(message)
class StreamConsumed(StreamError):
"""
Attempted to read or stream content, but the content has already
been streamed.
"""
def __init__(self) -> None:
message = (
"Attempted to read or stream some content, but the content has "
"already been streamed. For requests, this could be due to passing "
"a generator as request content, and then receiving a redirect "
"response or a secondary request as part of an authentication flow."
"For responses, this could be due to attempting to stream the response "
"content more than once."
)
super().__init__(message)
class StreamClosed(StreamError):
"""
Attempted to read or stream response content, but the request has been
closed.
"""
def __init__(self) -> None:
message = "Attempted to read or stream content, but the stream has been closed."
super().__init__(message)
class ResponseNotRead(StreamError):
"""
Attempted to access streaming response content, without having called `read()`.
"""
def __init__(self) -> None:
message = (
"Attempted to access streaming response content,"
" without having called `read()`."
)
super().__init__(message)
class RequestNotRead(StreamError):
"""
Attempted to access streaming request content, without having called `read()`.
"""
def __init__(self) -> None:
message = (
"Attempted to access streaming request content,"
" without having called `read()`."
)
super().__init__(message)
@contextlib.contextmanager
def request_context(
request: Request | None = None,
) -> typing.Iterator[None]:
"""
A context manager that can be used to attach the given request context
to any `RequestError` exceptions that are raised within the block.
"""
try:
yield
except RequestError as exc:
if request is not None:
exc.request = request
raise exc

506
httpx/_main.py Normal file
View File

@ -0,0 +1,506 @@
from __future__ import annotations
import functools
import json
import sys
import typing
import click
import pygments.lexers
import pygments.util
import rich.console
import rich.markup
import rich.progress
import rich.syntax
import rich.table
from ._client import Client
from ._exceptions import RequestError
from ._models import Response
from ._status_codes import codes
if typing.TYPE_CHECKING:
import httpcore # pragma: no cover
def print_help() -> None:
console = rich.console.Console()
console.print("[bold]HTTPX :butterfly:", justify="center")
console.print()
console.print("A next generation HTTP client.", justify="center")
console.print()
console.print(
"Usage: [bold]httpx[/bold] [cyan]<URL> [OPTIONS][/cyan] ", justify="left"
)
console.print()
table = rich.table.Table.grid(padding=1, pad_edge=True)
table.add_column("Parameter", no_wrap=True, justify="left", style="bold")
table.add_column("Description")
table.add_row(
"-m, --method [cyan]METHOD",
"Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD.\n"
"[Default: GET, or POST if a request body is included]",
)
table.add_row(
"-p, --params [cyan]<NAME VALUE> ...",
"Query parameters to include in the request URL.",
)
table.add_row(
"-c, --content [cyan]TEXT", "Byte content to include in the request body."
)
table.add_row(
"-d, --data [cyan]<NAME VALUE> ...", "Form data to include in the request body."
)
table.add_row(
"-f, --files [cyan]<NAME FILENAME> ...",
"Form files to include in the request body.",
)
table.add_row("-j, --json [cyan]TEXT", "JSON data to include in the request body.")
table.add_row(
"-h, --headers [cyan]<NAME VALUE> ...",
"Include additional HTTP headers in the request.",
)
table.add_row(
"--cookies [cyan]<NAME VALUE> ...", "Cookies to include in the request."
)
table.add_row(
"--auth [cyan]<USER PASS>",
"Username and password to include in the request. Specify '-' for the password"
" to use a password prompt. Note that using --verbose/-v will expose"
" the Authorization header, including the password encoding"
" in a trivially reversible format.",
)
table.add_row(
"--proxy [cyan]URL",
"Send the request via a proxy. Should be the URL giving the proxy address.",
)
table.add_row(
"--timeout [cyan]FLOAT",
"Timeout value to use for network operations, such as establishing the"
" connection, reading some data, etc... [Default: 5.0]",
)
table.add_row("--follow-redirects", "Automatically follow redirects.")
table.add_row("--no-verify", "Disable SSL verification.")
table.add_row(
"--http2", "Send the request using HTTP/2, if the remote server supports it."
)
table.add_row(
"--download [cyan]FILE",
"Save the response content as a file, rather than displaying it.",
)
table.add_row("-v, --verbose", "Verbose output. Show request as well as response.")
table.add_row("--help", "Show this message and exit.")
console.print(table)
def get_lexer_for_response(response: Response) -> str:
content_type = response.headers.get("Content-Type")
if content_type is not None:
mime_type, _, _ = content_type.partition(";")
try:
return typing.cast(
str, pygments.lexers.get_lexer_for_mimetype(mime_type.strip()).name
)
except pygments.util.ClassNotFound: # pragma: no cover
pass
return "" # pragma: no cover
def format_request_headers(request: httpcore.Request, http2: bool = False) -> str:
version = "HTTP/2" if http2 else "HTTP/1.1"
headers = [
(name.lower() if http2 else name, value) for name, value in request.headers
]
method = request.method.decode("ascii")
target = request.url.target.decode("ascii")
lines = [f"{method} {target} {version}"] + [
f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers
]
return "\n".join(lines)
def format_response_headers(
http_version: bytes,
status: int,
reason_phrase: bytes | None,
headers: list[tuple[bytes, bytes]],
) -> str:
version = http_version.decode("ascii")
reason = (
codes.get_reason_phrase(status)
if reason_phrase is None
else reason_phrase.decode("ascii")
)
lines = [f"{version} {status} {reason}"] + [
f"{name.decode('ascii')}: {value.decode('ascii')}" for name, value in headers
]
return "\n".join(lines)
def print_request_headers(request: httpcore.Request, http2: bool = False) -> None:
console = rich.console.Console()
http_text = format_request_headers(request, http2=http2)
syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True)
console.print(syntax)
syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True)
console.print(syntax)
def print_response_headers(
http_version: bytes,
status: int,
reason_phrase: bytes | None,
headers: list[tuple[bytes, bytes]],
) -> None:
console = rich.console.Console()
http_text = format_response_headers(http_version, status, reason_phrase, headers)
syntax = rich.syntax.Syntax(http_text, "http", theme="ansi_dark", word_wrap=True)
console.print(syntax)
syntax = rich.syntax.Syntax("", "http", theme="ansi_dark", word_wrap=True)
console.print(syntax)
def print_response(response: Response) -> None:
console = rich.console.Console()
lexer_name = get_lexer_for_response(response)
if lexer_name:
if lexer_name.lower() == "json":
try:
data = response.json()
text = json.dumps(data, indent=4)
except ValueError: # pragma: no cover
text = response.text
else:
text = response.text
syntax = rich.syntax.Syntax(text, lexer_name, theme="ansi_dark", word_wrap=True)
console.print(syntax)
else:
console.print(f"<{len(response.content)} bytes of binary data>")
_PCTRTT = typing.Tuple[typing.Tuple[str, str], ...]
_PCTRTTT = typing.Tuple[_PCTRTT, ...]
_PeerCertRetDictType = typing.Dict[str, typing.Union[str, _PCTRTTT, _PCTRTT]]
def format_certificate(cert: _PeerCertRetDictType) -> str: # pragma: no cover
lines = []
for key, value in cert.items():
if isinstance(value, (list, tuple)):
lines.append(f"* {key}:")
for item in value:
if key in ("subject", "issuer"):
for sub_item in item:
lines.append(f"* {sub_item[0]}: {sub_item[1]!r}")
elif isinstance(item, tuple) and len(item) == 2:
lines.append(f"* {item[0]}: {item[1]!r}")
else:
lines.append(f"* {item!r}")
else:
lines.append(f"* {key}: {value!r}")
return "\n".join(lines)
def trace(
name: str, info: typing.Mapping[str, typing.Any], verbose: bool = False
) -> None:
console = rich.console.Console()
if name == "connection.connect_tcp.started" and verbose:
host = info["host"]
console.print(f"* Connecting to {host!r}")
elif name == "connection.connect_tcp.complete" and verbose:
stream = info["return_value"]
server_addr = stream.get_extra_info("server_addr")
console.print(f"* Connected to {server_addr[0]!r} on port {server_addr[1]}")
elif name == "connection.start_tls.complete" and verbose: # pragma: no cover
stream = info["return_value"]
ssl_object = stream.get_extra_info("ssl_object")
version = ssl_object.version()
cipher = ssl_object.cipher()
server_cert = ssl_object.getpeercert()
alpn = ssl_object.selected_alpn_protocol()
console.print(f"* SSL established using {version!r} / {cipher[0]!r}")
console.print(f"* Selected ALPN protocol: {alpn!r}")
if server_cert:
console.print("* Server certificate:")
console.print(format_certificate(server_cert))
elif name == "http11.send_request_headers.started" and verbose:
request = info["request"]
print_request_headers(request, http2=False)
elif name == "http2.send_request_headers.started" and verbose: # pragma: no cover
request = info["request"]
print_request_headers(request, http2=True)
elif name == "http11.receive_response_headers.complete":
http_version, status, reason_phrase, headers = info["return_value"]
print_response_headers(http_version, status, reason_phrase, headers)
elif name == "http2.receive_response_headers.complete": # pragma: no cover
status, headers = info["return_value"]
http_version = b"HTTP/2"
reason_phrase = None
print_response_headers(http_version, status, reason_phrase, headers)
def download_response(response: Response, download: typing.BinaryIO) -> None:
console = rich.console.Console()
console.print()
content_length = response.headers.get("Content-Length")
with rich.progress.Progress(
"[progress.description]{task.description}",
"[progress.percentage]{task.percentage:>3.0f}%",
rich.progress.BarColumn(bar_width=None),
rich.progress.DownloadColumn(),
rich.progress.TransferSpeedColumn(),
) as progress:
description = f"Downloading [bold]{rich.markup.escape(download.name)}"
download_task = progress.add_task(
description,
total=int(content_length or 0),
start=content_length is not None,
)
for chunk in response.iter_bytes():
download.write(chunk)
progress.update(download_task, completed=response.num_bytes_downloaded)
def validate_json(
ctx: click.Context,
param: click.Option | click.Parameter,
value: typing.Any,
) -> typing.Any:
if value is None:
return None
try:
return json.loads(value)
except json.JSONDecodeError: # pragma: no cover
raise click.BadParameter("Not valid JSON")
def validate_auth(
ctx: click.Context,
param: click.Option | click.Parameter,
value: typing.Any,
) -> typing.Any:
if value == (None, None):
return None
username, password = value
if password == "-": # pragma: no cover
password = click.prompt("Password", hide_input=True)
return (username, password)
def handle_help(
ctx: click.Context,
param: click.Option | click.Parameter,
value: typing.Any,
) -> None:
if not value or ctx.resilient_parsing:
return
print_help()
ctx.exit()
@click.command(add_help_option=False)
@click.argument("url", type=str)
@click.option(
"--method",
"-m",
"method",
type=str,
help=(
"Request method, such as GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. "
"[Default: GET, or POST if a request body is included]"
),
)
@click.option(
"--params",
"-p",
"params",
type=(str, str),
multiple=True,
help="Query parameters to include in the request URL.",
)
@click.option(
"--content",
"-c",
"content",
type=str,
help="Byte content to include in the request body.",
)
@click.option(
"--data",
"-d",
"data",
type=(str, str),
multiple=True,
help="Form data to include in the request body.",
)
@click.option(
"--files",
"-f",
"files",
type=(str, click.File(mode="rb")),
multiple=True,
help="Form files to include in the request body.",
)
@click.option(
"--json",
"-j",
"json",
type=str,
callback=validate_json,
help="JSON data to include in the request body.",
)
@click.option(
"--headers",
"-h",
"headers",
type=(str, str),
multiple=True,
help="Include additional HTTP headers in the request.",
)
@click.option(
"--cookies",
"cookies",
type=(str, str),
multiple=True,
help="Cookies to include in the request.",
)
@click.option(
"--auth",
"auth",
type=(str, str),
default=(None, None),
callback=validate_auth,
help=(
"Username and password to include in the request. "
"Specify '-' for the password to use a password prompt. "
"Note that using --verbose/-v will expose the Authorization header, "
"including the password encoding in a trivially reversible format."
),
)
@click.option(
"--proxy",
"proxy",
type=str,
default=None,
help="Send the request via a proxy. Should be the URL giving the proxy address.",
)
@click.option(
"--timeout",
"timeout",
type=float,
default=5.0,
help=(
"Timeout value to use for network operations, such as establishing the "
"connection, reading some data, etc... [Default: 5.0]"
),
)
@click.option(
"--follow-redirects",
"follow_redirects",
is_flag=True,
default=False,
help="Automatically follow redirects.",
)
@click.option(
"--no-verify",
"verify",
is_flag=True,
default=True,
help="Disable SSL verification.",
)
@click.option(
"--http2",
"http2",
type=bool,
is_flag=True,
default=False,
help="Send the request using HTTP/2, if the remote server supports it.",
)
@click.option(
"--download",
type=click.File("wb"),
help="Save the response content as a file, rather than displaying it.",
)
@click.option(
"--verbose",
"-v",
type=bool,
is_flag=True,
default=False,
help="Verbose. Show request as well as response.",
)
@click.option(
"--help",
is_flag=True,
is_eager=True,
expose_value=False,
callback=handle_help,
help="Show this message and exit.",
)
def main(
url: str,
method: str,
params: list[tuple[str, str]],
content: str,
data: list[tuple[str, str]],
files: list[tuple[str, click.File]],
json: str,
headers: list[tuple[str, str]],
cookies: list[tuple[str, str]],
auth: tuple[str, str] | None,
proxy: str,
timeout: float,
follow_redirects: bool,
verify: bool,
http2: bool,
download: typing.BinaryIO | None,
verbose: bool,
) -> None:
"""
An HTTP command line client.
Sends a request and displays the response.
"""
if not method:
method = "POST" if content or data or files or json else "GET"
try:
with Client(proxy=proxy, timeout=timeout, http2=http2, verify=verify) as client:
with client.stream(
method,
url,
params=list(params),
content=content,
data=dict(data),
files=files, # type: ignore
json=json,
headers=headers,
cookies=dict(cookies),
auth=auth,
follow_redirects=follow_redirects,
extensions={"trace": functools.partial(trace, verbose=verbose)},
) as response:
if download is not None:
download_response(response, download)
else:
response.read()
if response.content:
print_response(response)
except RequestError as exc:
console = rich.console.Console()
console.print(f"[red]{type(exc).__name__}[/red]: {exc}")
sys.exit(1)
sys.exit(0 if response.is_success else 1)

1277
httpx/_models.py Normal file

File diff suppressed because it is too large Load Diff

300
httpx/_multipart.py Normal file
View File

@ -0,0 +1,300 @@
from __future__ import annotations
import io
import mimetypes
import os
import re
import typing
from pathlib import Path
from ._types import (
AsyncByteStream,
FileContent,
FileTypes,
RequestData,
RequestFiles,
SyncByteStream,
)
from ._utils import (
peek_filelike_length,
primitive_value_to_str,
to_bytes,
)
_HTML5_FORM_ENCODING_REPLACEMENTS = {'"': "%22", "\\": "\\\\"}
_HTML5_FORM_ENCODING_REPLACEMENTS.update(
{chr(c): "%{:02X}".format(c) for c in range(0x1F + 1) if c != 0x1B}
)
_HTML5_FORM_ENCODING_RE = re.compile(
r"|".join([re.escape(c) for c in _HTML5_FORM_ENCODING_REPLACEMENTS.keys()])
)
def _format_form_param(name: str, value: str) -> bytes:
"""
Encode a name/value pair within a multipart form.
"""
def replacer(match: typing.Match[str]) -> str:
return _HTML5_FORM_ENCODING_REPLACEMENTS[match.group(0)]
value = _HTML5_FORM_ENCODING_RE.sub(replacer, value)
return f'{name}="{value}"'.encode()
def _guess_content_type(filename: str | None) -> str | None:
"""
Guesses the mimetype based on a filename. Defaults to `application/octet-stream`.
Returns `None` if `filename` is `None` or empty.
"""
if filename:
return mimetypes.guess_type(filename)[0] or "application/octet-stream"
return None
def get_multipart_boundary_from_content_type(
content_type: bytes | None,
) -> bytes | None:
if not content_type or not content_type.startswith(b"multipart/form-data"):
return None
# parse boundary according to
# https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1
if b";" in content_type:
for section in content_type.split(b";"):
if section.strip().lower().startswith(b"boundary="):
return section.strip()[len(b"boundary=") :].strip(b'"')
return None
class DataField:
"""
A single form field item, within a multipart form field.
"""
def __init__(self, name: str, value: str | bytes | int | float | None) -> None:
if not isinstance(name, str):
raise TypeError(
f"Invalid type for name. Expected str, got {type(name)}: {name!r}"
)
if value is not None and not isinstance(value, (str, bytes, int, float)):
raise TypeError(
"Invalid type for value. Expected primitive type,"
f" got {type(value)}: {value!r}"
)
self.name = name
self.value: str | bytes = (
value if isinstance(value, bytes) else primitive_value_to_str(value)
)
def render_headers(self) -> bytes:
if not hasattr(self, "_headers"):
name = _format_form_param("name", self.name)
self._headers = b"".join(
[b"Content-Disposition: form-data; ", name, b"\r\n\r\n"]
)
return self._headers
def render_data(self) -> bytes:
if not hasattr(self, "_data"):
self._data = to_bytes(self.value)
return self._data
def get_length(self) -> int:
headers = self.render_headers()
data = self.render_data()
return len(headers) + len(data)
def render(self) -> typing.Iterator[bytes]:
yield self.render_headers()
yield self.render_data()
class FileField:
"""
A single file field item, within a multipart form field.
"""
CHUNK_SIZE = 64 * 1024
def __init__(self, name: str, value: FileTypes) -> None:
self.name = name
fileobj: FileContent
headers: dict[str, str] = {}
content_type: str | None = None
# This large tuple based API largely mirror's requests' API
# It would be good to think of better APIs for this that we could
# include in httpx 2.0 since variable length tuples(especially of 4 elements)
# are quite unwieldly
if isinstance(value, tuple):
if len(value) == 2:
# neither the 3rd parameter (content_type) nor the 4th (headers)
# was included
filename, fileobj = value
elif len(value) == 3:
filename, fileobj, content_type = value
else:
# all 4 parameters included
filename, fileobj, content_type, headers = value # type: ignore
else:
filename = Path(str(getattr(value, "name", "upload"))).name
fileobj = value
if content_type is None:
content_type = _guess_content_type(filename)
has_content_type_header = any("content-type" in key.lower() for key in headers)
if content_type is not None and not has_content_type_header:
# note that unlike requests, we ignore the content_type provided in the 3rd
# tuple element if it is also included in the headers requests does
# the opposite (it overwrites the headerwith the 3rd tuple element)
headers["Content-Type"] = content_type
if isinstance(fileobj, io.StringIO):
raise TypeError(
"Multipart file uploads require 'io.BytesIO', not 'io.StringIO'."
)
if isinstance(fileobj, io.TextIOBase):
raise TypeError(
"Multipart file uploads must be opened in binary mode, not text mode."
)
self.filename = filename
self.file = fileobj
self.headers = headers
def get_length(self) -> int | None:
headers = self.render_headers()
if isinstance(self.file, (str, bytes)):
return len(headers) + len(to_bytes(self.file))
file_length = peek_filelike_length(self.file)
# If we can't determine the filesize without reading it into memory,
# then return `None` here, to indicate an unknown file length.
if file_length is None:
return None
return len(headers) + file_length
def render_headers(self) -> bytes:
if not hasattr(self, "_headers"):
parts = [
b"Content-Disposition: form-data; ",
_format_form_param("name", self.name),
]
if self.filename:
filename = _format_form_param("filename", self.filename)
parts.extend([b"; ", filename])
for header_name, header_value in self.headers.items():
key, val = f"\r\n{header_name}: ".encode(), header_value.encode()
parts.extend([key, val])
parts.append(b"\r\n\r\n")
self._headers = b"".join(parts)
return self._headers
def render_data(self) -> typing.Iterator[bytes]:
if isinstance(self.file, (str, bytes)):
yield to_bytes(self.file)
return
if hasattr(self.file, "seek"):
try:
self.file.seek(0)
except io.UnsupportedOperation:
pass
chunk = self.file.read(self.CHUNK_SIZE)
while chunk:
yield to_bytes(chunk)
chunk = self.file.read(self.CHUNK_SIZE)
def render(self) -> typing.Iterator[bytes]:
yield self.render_headers()
yield from self.render_data()
class MultipartStream(SyncByteStream, AsyncByteStream):
"""
Request content as streaming multipart encoded form data.
"""
def __init__(
self,
data: RequestData,
files: RequestFiles,
boundary: bytes | None = None,
) -> None:
if boundary is None:
boundary = os.urandom(16).hex().encode("ascii")
self.boundary = boundary
self.content_type = "multipart/form-data; boundary=%s" % boundary.decode(
"ascii"
)
self.fields = list(self._iter_fields(data, files))
def _iter_fields(
self, data: RequestData, files: RequestFiles
) -> typing.Iterator[FileField | DataField]:
for name, value in data.items():
if isinstance(value, (tuple, list)):
for item in value:
yield DataField(name=name, value=item)
else:
yield DataField(name=name, value=value)
file_items = files.items() if isinstance(files, typing.Mapping) else files
for name, value in file_items:
yield FileField(name=name, value=value)
def iter_chunks(self) -> typing.Iterator[bytes]:
for field in self.fields:
yield b"--%s\r\n" % self.boundary
yield from field.render()
yield b"\r\n"
yield b"--%s--\r\n" % self.boundary
def get_content_length(self) -> int | None:
"""
Return the length of the multipart encoded content, or `None` if
any of the files have a length that cannot be determined upfront.
"""
boundary_length = len(self.boundary)
length = 0
for field in self.fields:
field_length = field.get_length()
if field_length is None:
return None
length += 2 + boundary_length + 2 # b"--{boundary}\r\n"
length += field_length
length += 2 # b"\r\n"
length += 2 + boundary_length + 4 # b"--{boundary}--\r\n"
return length
# Content stream interface.
def get_headers(self) -> dict[str, str]:
content_length = self.get_content_length()
content_type = self.content_type
if content_length is None:
return {"Transfer-Encoding": "chunked", "Content-Type": content_type}
return {"Content-Length": str(content_length), "Content-Type": content_type}
def __iter__(self) -> typing.Iterator[bytes]:
for chunk in self.iter_chunks():
yield chunk
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
for chunk in self.iter_chunks():
yield chunk

View File

@ -1,9 +1,15 @@
from __future__ import annotations
from enum import IntEnum
__all__ = ["codes"]
class StatusCode(IntEnum):
class codes(IntEnum):
"""HTTP status codes and reason phrases
Status codes from the following RFCs are all observed:
* RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616
* RFC 6585: Additional HTTP Status Codes
* RFC 3229: Delta encoding in HTTP
@ -15,13 +21,15 @@ class StatusCode(IntEnum):
* RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2)
* RFC 2324: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0)
* RFC 7725: An HTTP Status Code to Report Legal Obstacles
* RFC 8297: An HTTP Status Code for Indicating Hints
* RFC 8470: Using Early Data in HTTP
"""
def __new__(cls, value: int, phrase: str = "") -> "StatusCode":
obj = int.__new__(cls, value) # type: ignore
def __new__(cls, value: int, phrase: str = "") -> codes:
obj = int.__new__(cls, value)
obj._value_ = value
obj.phrase = phrase
obj.phrase = phrase # type: ignore[attr-defined]
return obj
def __str__(self) -> str:
@ -30,37 +38,57 @@ class StatusCode(IntEnum):
@classmethod
def get_reason_phrase(cls, value: int) -> str:
try:
return StatusCode(value).phrase # type: ignore
return codes(value).phrase # type: ignore
except ValueError:
return ""
@classmethod
def is_informational(cls, value: int) -> bool:
"""
Returns `True` for 1xx status codes, `False` otherwise.
"""
return 100 <= value <= 199
@classmethod
def is_success(cls, value: int) -> bool:
"""
Returns `True` for 2xx status codes, `False` otherwise.
"""
return 200 <= value <= 299
@classmethod
def is_redirect(cls, value: int) -> bool:
return value in (
# 301 (Cacheable redirect. Method may change to GET.)
StatusCode.MOVED_PERMANENTLY,
# 302 (Uncacheable redirect. Method may change to GET.)
StatusCode.FOUND,
# 303 (Client should make a GET or HEAD request.)
StatusCode.SEE_OTHER,
# 307 (Equiv. 302, but retain method)
StatusCode.TEMPORARY_REDIRECT,
# 308 (Equiv. 301, but retain method)
StatusCode.PERMANENT_REDIRECT,
)
"""
Returns `True` for 3xx status codes, `False` otherwise.
"""
return 300 <= value <= 399
@classmethod
def is_client_error(cls, value: int) -> bool:
"""
Returns `True` for 4xx status codes, `False` otherwise.
"""
return 400 <= value <= 499
@classmethod
def is_server_error(cls, value: int) -> bool:
"""
Returns `True` for 5xx status codes, `False` otherwise.
"""
return 500 <= value <= 599
@classmethod
def is_error(cls, value: int) -> bool:
"""
Returns `True` for 4xx or 5xx status codes, `False` otherwise.
"""
return 400 <= value <= 599
# informational
CONTINUE = 100, "Continue"
SWITCHING_PROTOCOLS = 101, "Switching Protocols"
PROCESSING = 102, "Processing"
EARLY_HINTS = 103, "Early Hints"
# success
OK = 200, "OK"
@ -108,6 +136,7 @@ class StatusCode(IntEnum):
UNPROCESSABLE_ENTITY = 422, "Unprocessable Entity"
LOCKED = 423, "Locked"
FAILED_DEPENDENCY = 424, "Failed Dependency"
TOO_EARLY = 425, "Too Early"
UPGRADE_REQUIRED = 426, "Upgrade Required"
PRECONDITION_REQUIRED = 428, "Precondition Required"
TOO_MANY_REQUESTS = 429, "Too Many Requests"
@ -128,8 +157,6 @@ class StatusCode(IntEnum):
NETWORK_AUTHENTICATION_REQUIRED = 511, "Network Authentication Required"
codes = StatusCode
#  Include lower-case styles for `requests` compatibility.
# Include lower-case styles for `requests` compatibility.
for code in codes:
setattr(codes, code._name_.lower(), int(code))

View File

@ -0,0 +1,15 @@
from .asgi import *
from .base import *
from .default import *
from .mock import *
from .wsgi import *
__all__ = [
"ASGITransport",
"AsyncBaseTransport",
"BaseTransport",
"AsyncHTTPTransport",
"HTTPTransport",
"MockTransport",
"WSGITransport",
]

187
httpx/_transports/asgi.py Normal file
View File

@ -0,0 +1,187 @@
from __future__ import annotations
import typing
from .._models import Request, Response
from .._types import AsyncByteStream
from .base import AsyncBaseTransport
if typing.TYPE_CHECKING: # pragma: no cover
import asyncio
import trio
Event = typing.Union[asyncio.Event, trio.Event]
_Message = typing.MutableMapping[str, typing.Any]
_Receive = typing.Callable[[], typing.Awaitable[_Message]]
_Send = typing.Callable[
[typing.MutableMapping[str, typing.Any]], typing.Awaitable[None]
]
_ASGIApp = typing.Callable[
[typing.MutableMapping[str, typing.Any], _Receive, _Send], typing.Awaitable[None]
]
__all__ = ["ASGITransport"]
def is_running_trio() -> bool:
try:
# sniffio is a dependency of trio.
# See https://github.com/python-trio/trio/issues/2802
import sniffio
if sniffio.current_async_library() == "trio":
return True
except ImportError: # pragma: nocover
pass
return False
def create_event() -> Event:
if is_running_trio():
import trio
return trio.Event()
import asyncio
return asyncio.Event()
class ASGIResponseStream(AsyncByteStream):
def __init__(self, body: list[bytes]) -> None:
self._body = body
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
yield b"".join(self._body)
class ASGITransport(AsyncBaseTransport):
"""
A custom AsyncTransport that handles sending requests directly to an ASGI app.
```python
transport = httpx.ASGITransport(
app=app,
root_path="/submount",
client=("1.2.3.4", 123)
)
client = httpx.AsyncClient(transport=transport)
```
Arguments:
* `app` - The ASGI application.
* `raise_app_exceptions` - Boolean indicating if exceptions in the application
should be raised. Default to `True`. Can be set to `False` for use cases
such as testing the content of a client 500 response.
* `root_path` - The root path on which the ASGI application should be mounted.
* `client` - A two-tuple indicating the client IP and port of incoming requests.
```
"""
def __init__(
self,
app: _ASGIApp,
raise_app_exceptions: bool = True,
root_path: str = "",
client: tuple[str, int] = ("127.0.0.1", 123),
) -> None:
self.app = app
self.raise_app_exceptions = raise_app_exceptions
self.root_path = root_path
self.client = client
async def handle_async_request(
self,
request: Request,
) -> Response:
assert isinstance(request.stream, AsyncByteStream)
# ASGI scope.
scope = {
"type": "http",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"method": request.method,
"headers": [(k.lower(), v) for (k, v) in request.headers.raw],
"scheme": request.url.scheme,
"path": request.url.path,
"raw_path": request.url.raw_path.split(b"?")[0],
"query_string": request.url.query,
"server": (request.url.host, request.url.port),
"client": self.client,
"root_path": self.root_path,
}
# Request.
request_body_chunks = request.stream.__aiter__()
request_complete = False
# Response.
status_code = None
response_headers = None
body_parts = []
response_started = False
response_complete = create_event()
# ASGI callables.
async def receive() -> dict[str, typing.Any]:
nonlocal request_complete
if request_complete:
await response_complete.wait()
return {"type": "http.disconnect"}
try:
body = await request_body_chunks.__anext__()
except StopAsyncIteration:
request_complete = True
return {"type": "http.request", "body": b"", "more_body": False}
return {"type": "http.request", "body": body, "more_body": True}
async def send(message: typing.MutableMapping[str, typing.Any]) -> None:
nonlocal status_code, response_headers, response_started
if message["type"] == "http.response.start":
assert not response_started
status_code = message["status"]
response_headers = message.get("headers", [])
response_started = True
elif message["type"] == "http.response.body":
assert not response_complete.is_set()
body = message.get("body", b"")
more_body = message.get("more_body", False)
if body and request.method != "HEAD":
body_parts.append(body)
if not more_body:
response_complete.set()
try:
await self.app(scope, receive, send)
except Exception: # noqa: PIE-786
if self.raise_app_exceptions:
raise
response_complete.set()
if status_code is None:
status_code = 500
if response_headers is None:
response_headers = {}
assert response_complete.is_set()
assert status_code is not None
assert response_headers is not None
stream = ASGIResponseStream(body_parts)
return Response(status_code, headers=response_headers, stream=stream)

86
httpx/_transports/base.py Normal file
View File

@ -0,0 +1,86 @@
from __future__ import annotations
import typing
from types import TracebackType
from .._models import Request, Response
T = typing.TypeVar("T", bound="BaseTransport")
A = typing.TypeVar("A", bound="AsyncBaseTransport")
__all__ = ["AsyncBaseTransport", "BaseTransport"]
class BaseTransport:
def __enter__(self: T) -> T:
return self
def __exit__(
self,
exc_type: type[BaseException] | None = None,
exc_value: BaseException | None = None,
traceback: TracebackType | None = None,
) -> None:
self.close()
def handle_request(self, request: Request) -> Response:
"""
Send a single HTTP request and return a response.
Developers shouldn't typically ever need to call into this API directly,
since the Client class provides all the higher level user-facing API
niceties.
In order to properly release any network resources, the response
stream should *either* be consumed immediately, with a call to
`response.stream.read()`, or else the `handle_request` call should
be followed with a try/finally block to ensuring the stream is
always closed.
Example usage:
with httpx.HTTPTransport() as transport:
req = httpx.Request(
method=b"GET",
url=(b"https", b"www.example.com", 443, b"/"),
headers=[(b"Host", b"www.example.com")],
)
resp = transport.handle_request(req)
body = resp.stream.read()
print(resp.status_code, resp.headers, body)
Takes a `Request` instance as the only argument.
Returns a `Response` instance.
"""
raise NotImplementedError(
"The 'handle_request' method must be implemented."
) # pragma: no cover
def close(self) -> None:
pass
class AsyncBaseTransport:
async def __aenter__(self: A) -> A:
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None = None,
exc_value: BaseException | None = None,
traceback: TracebackType | None = None,
) -> None:
await self.aclose()
async def handle_async_request(
self,
request: Request,
) -> Response:
raise NotImplementedError(
"The 'handle_async_request' method must be implemented."
) # pragma: no cover
async def aclose(self) -> None:
pass

View File

@ -0,0 +1,406 @@
"""
Custom transports, with nicely configured defaults.
The following additional keyword arguments are currently supported by httpcore...
* uds: str
* local_address: str
* retries: int
Example usages...
# Disable HTTP/2 on a single specific domain.
mounts = {
"all://": httpx.HTTPTransport(http2=True),
"all://*example.org": httpx.HTTPTransport()
}
# Using advanced httpcore configuration, with connection retries.
transport = httpx.HTTPTransport(retries=1)
client = httpx.Client(transport=transport)
# Using advanced httpcore configuration, with unix domain sockets.
transport = httpx.HTTPTransport(uds="socket.uds")
client = httpx.Client(transport=transport)
"""
from __future__ import annotations
import contextlib
import typing
from types import TracebackType
if typing.TYPE_CHECKING:
import ssl # pragma: no cover
import httpx # pragma: no cover
from .._config import DEFAULT_LIMITS, Limits, Proxy, create_ssl_context
from .._exceptions import (
ConnectError,
ConnectTimeout,
LocalProtocolError,
NetworkError,
PoolTimeout,
ProtocolError,
ProxyError,
ReadError,
ReadTimeout,
RemoteProtocolError,
TimeoutException,
UnsupportedProtocol,
WriteError,
WriteTimeout,
)
from .._models import Request, Response
from .._types import AsyncByteStream, CertTypes, ProxyTypes, SyncByteStream
from .._urls import URL
from .base import AsyncBaseTransport, BaseTransport
T = typing.TypeVar("T", bound="HTTPTransport")
A = typing.TypeVar("A", bound="AsyncHTTPTransport")
SOCKET_OPTION = typing.Union[
typing.Tuple[int, int, int],
typing.Tuple[int, int, typing.Union[bytes, bytearray]],
typing.Tuple[int, int, None, int],
]
__all__ = ["AsyncHTTPTransport", "HTTPTransport"]
HTTPCORE_EXC_MAP: dict[type[Exception], type[httpx.HTTPError]] = {}
def _load_httpcore_exceptions() -> dict[type[Exception], type[httpx.HTTPError]]:
import httpcore
return {
httpcore.TimeoutException: TimeoutException,
httpcore.ConnectTimeout: ConnectTimeout,
httpcore.ReadTimeout: ReadTimeout,
httpcore.WriteTimeout: WriteTimeout,
httpcore.PoolTimeout: PoolTimeout,
httpcore.NetworkError: NetworkError,
httpcore.ConnectError: ConnectError,
httpcore.ReadError: ReadError,
httpcore.WriteError: WriteError,
httpcore.ProxyError: ProxyError,
httpcore.UnsupportedProtocol: UnsupportedProtocol,
httpcore.ProtocolError: ProtocolError,
httpcore.LocalProtocolError: LocalProtocolError,
httpcore.RemoteProtocolError: RemoteProtocolError,
}
@contextlib.contextmanager
def map_httpcore_exceptions() -> typing.Iterator[None]:
global HTTPCORE_EXC_MAP
if len(HTTPCORE_EXC_MAP) == 0:
HTTPCORE_EXC_MAP = _load_httpcore_exceptions()
try:
yield
except Exception as exc:
mapped_exc = None
for from_exc, to_exc in HTTPCORE_EXC_MAP.items():
if not isinstance(exc, from_exc):
continue
# We want to map to the most specific exception we can find.
# Eg if `exc` is an `httpcore.ReadTimeout`, we want to map to
# `httpx.ReadTimeout`, not just `httpx.TimeoutException`.
if mapped_exc is None or issubclass(to_exc, mapped_exc):
mapped_exc = to_exc
if mapped_exc is None: # pragma: no cover
raise
message = str(exc)
raise mapped_exc(message) from exc
class ResponseStream(SyncByteStream):
def __init__(self, httpcore_stream: typing.Iterable[bytes]) -> None:
self._httpcore_stream = httpcore_stream
def __iter__(self) -> typing.Iterator[bytes]:
with map_httpcore_exceptions():
for part in self._httpcore_stream:
yield part
def close(self) -> None:
if hasattr(self._httpcore_stream, "close"):
self._httpcore_stream.close()
class HTTPTransport(BaseTransport):
def __init__(
self,
verify: ssl.SSLContext | str | bool = True,
cert: CertTypes | None = None,
trust_env: bool = True,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
proxy: ProxyTypes | None = None,
uds: str | None = None,
local_address: str | None = None,
retries: int = 0,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
) -> None:
import httpcore
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
if proxy is None:
self._pool = httpcore.ConnectionPool(
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
http1=http1,
http2=http2,
uds=uds,
local_address=local_address,
retries=retries,
socket_options=socket_options,
)
elif proxy.url.scheme in ("http", "https"):
self._pool = httpcore.HTTPProxy(
proxy_url=httpcore.URL(
scheme=proxy.url.raw_scheme,
host=proxy.url.raw_host,
port=proxy.url.port,
target=proxy.url.raw_path,
),
proxy_auth=proxy.raw_auth,
proxy_headers=proxy.headers.raw,
ssl_context=ssl_context,
proxy_ssl_context=proxy.ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
http1=http1,
http2=http2,
socket_options=socket_options,
)
elif proxy.url.scheme in ("socks5", "socks5h"):
try:
import socksio # noqa
except ImportError: # pragma: no cover
raise ImportError(
"Using SOCKS proxy, but the 'socksio' package is not installed. "
"Make sure to install httpx using `pip install httpx[socks]`."
) from None
self._pool = httpcore.SOCKSProxy(
proxy_url=httpcore.URL(
scheme=proxy.url.raw_scheme,
host=proxy.url.raw_host,
port=proxy.url.port,
target=proxy.url.raw_path,
),
proxy_auth=proxy.raw_auth,
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
http1=http1,
http2=http2,
)
else: # pragma: no cover
raise ValueError(
"Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h',"
f" but got {proxy.url.scheme!r}."
)
def __enter__(self: T) -> T: # Use generics for subclass support.
self._pool.__enter__()
return self
def __exit__(
self,
exc_type: type[BaseException] | None = None,
exc_value: BaseException | None = None,
traceback: TracebackType | None = None,
) -> None:
with map_httpcore_exceptions():
self._pool.__exit__(exc_type, exc_value, traceback)
def handle_request(
self,
request: Request,
) -> Response:
assert isinstance(request.stream, SyncByteStream)
import httpcore
req = httpcore.Request(
method=request.method,
url=httpcore.URL(
scheme=request.url.raw_scheme,
host=request.url.raw_host,
port=request.url.port,
target=request.url.raw_path,
),
headers=request.headers.raw,
content=request.stream,
extensions=request.extensions,
)
with map_httpcore_exceptions():
resp = self._pool.handle_request(req)
assert isinstance(resp.stream, typing.Iterable)
return Response(
status_code=resp.status,
headers=resp.headers,
stream=ResponseStream(resp.stream),
extensions=resp.extensions,
)
def close(self) -> None:
self._pool.close()
class AsyncResponseStream(AsyncByteStream):
def __init__(self, httpcore_stream: typing.AsyncIterable[bytes]) -> None:
self._httpcore_stream = httpcore_stream
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
with map_httpcore_exceptions():
async for part in self._httpcore_stream:
yield part
async def aclose(self) -> None:
if hasattr(self._httpcore_stream, "aclose"):
await self._httpcore_stream.aclose()
class AsyncHTTPTransport(AsyncBaseTransport):
def __init__(
self,
verify: ssl.SSLContext | str | bool = True,
cert: CertTypes | None = None,
trust_env: bool = True,
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
proxy: ProxyTypes | None = None,
uds: str | None = None,
local_address: str | None = None,
retries: int = 0,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
) -> None:
import httpcore
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)
if proxy is None:
self._pool = httpcore.AsyncConnectionPool(
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
http1=http1,
http2=http2,
uds=uds,
local_address=local_address,
retries=retries,
socket_options=socket_options,
)
elif proxy.url.scheme in ("http", "https"):
self._pool = httpcore.AsyncHTTPProxy(
proxy_url=httpcore.URL(
scheme=proxy.url.raw_scheme,
host=proxy.url.raw_host,
port=proxy.url.port,
target=proxy.url.raw_path,
),
proxy_auth=proxy.raw_auth,
proxy_headers=proxy.headers.raw,
proxy_ssl_context=proxy.ssl_context,
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
http1=http1,
http2=http2,
socket_options=socket_options,
)
elif proxy.url.scheme in ("socks5", "socks5h"):
try:
import socksio # noqa
except ImportError: # pragma: no cover
raise ImportError(
"Using SOCKS proxy, but the 'socksio' package is not installed. "
"Make sure to install httpx using `pip install httpx[socks]`."
) from None
self._pool = httpcore.AsyncSOCKSProxy(
proxy_url=httpcore.URL(
scheme=proxy.url.raw_scheme,
host=proxy.url.raw_host,
port=proxy.url.port,
target=proxy.url.raw_path,
),
proxy_auth=proxy.raw_auth,
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
http1=http1,
http2=http2,
)
else: # pragma: no cover
raise ValueError(
"Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h',"
f" but got {proxy.url.scheme!r}."
)
async def __aenter__(self: A) -> A: # Use generics for subclass support.
await self._pool.__aenter__()
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None = None,
exc_value: BaseException | None = None,
traceback: TracebackType | None = None,
) -> None:
with map_httpcore_exceptions():
await self._pool.__aexit__(exc_type, exc_value, traceback)
async def handle_async_request(
self,
request: Request,
) -> Response:
assert isinstance(request.stream, AsyncByteStream)
import httpcore
req = httpcore.Request(
method=request.method,
url=httpcore.URL(
scheme=request.url.raw_scheme,
host=request.url.raw_host,
port=request.url.port,
target=request.url.raw_path,
),
headers=request.headers.raw,
content=request.stream,
extensions=request.extensions,
)
with map_httpcore_exceptions():
resp = await self._pool.handle_async_request(req)
assert isinstance(resp.stream, typing.AsyncIterable)
return Response(
status_code=resp.status,
headers=resp.headers,
stream=AsyncResponseStream(resp.stream),
extensions=resp.extensions,
)
async def aclose(self) -> None:
await self._pool.aclose()

43
httpx/_transports/mock.py Normal file
View File

@ -0,0 +1,43 @@
from __future__ import annotations
import typing
from .._models import Request, Response
from .base import AsyncBaseTransport, BaseTransport
SyncHandler = typing.Callable[[Request], Response]
AsyncHandler = typing.Callable[[Request], typing.Coroutine[None, None, Response]]
__all__ = ["MockTransport"]
class MockTransport(AsyncBaseTransport, BaseTransport):
def __init__(self, handler: SyncHandler | AsyncHandler) -> None:
self.handler = handler
def handle_request(
self,
request: Request,
) -> Response:
request.read()
response = self.handler(request)
if not isinstance(response, Response): # pragma: no cover
raise TypeError("Cannot use an async handler in a sync Client")
return response
async def handle_async_request(
self,
request: Request,
) -> Response:
await request.aread()
response = self.handler(request)
# Allow handler to *optionally* be an `async` function.
# If it is, then the `response` variable need to be awaited to actually
# return the result.
if not isinstance(response, Response):
response = await response
return response

149
httpx/_transports/wsgi.py Normal file
View File

@ -0,0 +1,149 @@
from __future__ import annotations
import io
import itertools
import sys
import typing
from .._models import Request, Response
from .._types import SyncByteStream
from .base import BaseTransport
if typing.TYPE_CHECKING:
from _typeshed import OptExcInfo # pragma: no cover
from _typeshed.wsgi import WSGIApplication # pragma: no cover
_T = typing.TypeVar("_T")
__all__ = ["WSGITransport"]
def _skip_leading_empty_chunks(body: typing.Iterable[_T]) -> typing.Iterable[_T]:
body = iter(body)
for chunk in body:
if chunk:
return itertools.chain([chunk], body)
return []
class WSGIByteStream(SyncByteStream):
def __init__(self, result: typing.Iterable[bytes]) -> None:
self._close = getattr(result, "close", None)
self._result = _skip_leading_empty_chunks(result)
def __iter__(self) -> typing.Iterator[bytes]:
for part in self._result:
yield part
def close(self) -> None:
if self._close is not None:
self._close()
class WSGITransport(BaseTransport):
"""
A custom transport that handles sending requests directly to an WSGI app.
The simplest way to use this functionality is to use the `app` argument.
```
client = httpx.Client(app=app)
```
Alternatively, you can setup the transport instance explicitly.
This allows you to include any additional configuration arguments specific
to the WSGITransport class:
```
transport = httpx.WSGITransport(
app=app,
script_name="/submount",
remote_addr="1.2.3.4"
)
client = httpx.Client(transport=transport)
```
Arguments:
* `app` - The WSGI application.
* `raise_app_exceptions` - Boolean indicating if exceptions in the application
should be raised. Default to `True`. Can be set to `False` for use cases
such as testing the content of a client 500 response.
* `script_name` - The root path on which the WSGI application should be mounted.
* `remote_addr` - A string indicating the client IP of incoming requests.
```
"""
def __init__(
self,
app: WSGIApplication,
raise_app_exceptions: bool = True,
script_name: str = "",
remote_addr: str = "127.0.0.1",
wsgi_errors: typing.TextIO | None = None,
) -> None:
self.app = app
self.raise_app_exceptions = raise_app_exceptions
self.script_name = script_name
self.remote_addr = remote_addr
self.wsgi_errors = wsgi_errors
def handle_request(self, request: Request) -> Response:
request.read()
wsgi_input = io.BytesIO(request.content)
port = request.url.port or {"http": 80, "https": 443}[request.url.scheme]
environ = {
"wsgi.version": (1, 0),
"wsgi.url_scheme": request.url.scheme,
"wsgi.input": wsgi_input,
"wsgi.errors": self.wsgi_errors or sys.stderr,
"wsgi.multithread": True,
"wsgi.multiprocess": False,
"wsgi.run_once": False,
"REQUEST_METHOD": request.method,
"SCRIPT_NAME": self.script_name,
"PATH_INFO": request.url.path,
"QUERY_STRING": request.url.query.decode("ascii"),
"SERVER_NAME": request.url.host,
"SERVER_PORT": str(port),
"SERVER_PROTOCOL": "HTTP/1.1",
"REMOTE_ADDR": self.remote_addr,
}
for header_key, header_value in request.headers.raw:
key = header_key.decode("ascii").upper().replace("-", "_")
if key not in ("CONTENT_TYPE", "CONTENT_LENGTH"):
key = "HTTP_" + key
environ[key] = header_value.decode("ascii")
seen_status = None
seen_response_headers = None
seen_exc_info = None
def start_response(
status: str,
response_headers: list[tuple[str, str]],
exc_info: OptExcInfo | None = None,
) -> typing.Callable[[bytes], typing.Any]:
nonlocal seen_status, seen_response_headers, seen_exc_info
seen_status = status
seen_response_headers = response_headers
seen_exc_info = exc_info
return lambda _: None
result = self.app(environ, start_response)
stream = WSGIByteStream(result)
assert seen_status is not None
assert seen_response_headers is not None
if seen_exc_info and seen_exc_info[0] and self.raise_app_exceptions:
raise seen_exc_info[1]
status_code = int(seen_status.split()[0])
headers = [
(key.encode("ascii"), value.encode("ascii"))
for key, value in seen_response_headers
]
return Response(status_code, headers=headers, stream=stream)

114
httpx/_types.py Normal file
View File

@ -0,0 +1,114 @@
"""
Type definitions for type checking purposes.
"""
from http.cookiejar import CookieJar
from typing import (
IO,
TYPE_CHECKING,
Any,
AsyncIterable,
AsyncIterator,
Callable,
Dict,
Iterable,
Iterator,
List,
Mapping,
Optional,
Sequence,
Tuple,
Union,
)
if TYPE_CHECKING: # pragma: no cover
from ._auth import Auth # noqa: F401
from ._config import Proxy, Timeout # noqa: F401
from ._models import Cookies, Headers, Request # noqa: F401
from ._urls import URL, QueryParams # noqa: F401
PrimitiveData = Optional[Union[str, int, float, bool]]
URLTypes = Union["URL", str]
QueryParamTypes = Union[
"QueryParams",
Mapping[str, Union[PrimitiveData, Sequence[PrimitiveData]]],
List[Tuple[str, PrimitiveData]],
Tuple[Tuple[str, PrimitiveData], ...],
str,
bytes,
]
HeaderTypes = Union[
"Headers",
Mapping[str, str],
Mapping[bytes, bytes],
Sequence[Tuple[str, str]],
Sequence[Tuple[bytes, bytes]],
]
CookieTypes = Union["Cookies", CookieJar, Dict[str, str], List[Tuple[str, str]]]
TimeoutTypes = Union[
Optional[float],
Tuple[Optional[float], Optional[float], Optional[float], Optional[float]],
"Timeout",
]
ProxyTypes = Union["URL", str, "Proxy"]
CertTypes = Union[str, Tuple[str, str], Tuple[str, str, str]]
AuthTypes = Union[
Tuple[Union[str, bytes], Union[str, bytes]],
Callable[["Request"], "Request"],
"Auth",
]
RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
ResponseExtensions = Mapping[str, Any]
RequestData = Mapping[str, Any]
FileContent = Union[IO[bytes], bytes, str]
FileTypes = Union[
# file (or bytes)
FileContent,
# (filename, file (or bytes))
Tuple[Optional[str], FileContent],
# (filename, file (or bytes), content_type)
Tuple[Optional[str], FileContent, Optional[str]],
# (filename, file (or bytes), content_type, headers)
Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]],
]
RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]]
RequestExtensions = Mapping[str, Any]
__all__ = ["AsyncByteStream", "SyncByteStream"]
class SyncByteStream:
def __iter__(self) -> Iterator[bytes]:
raise NotImplementedError(
"The '__iter__' method must be implemented."
) # pragma: no cover
yield b"" # pragma: no cover
def close(self) -> None:
"""
Subclasses can override this method to release any network resources
after a request/response cycle is complete.
"""
class AsyncByteStream:
async def __aiter__(self) -> AsyncIterator[bytes]:
raise NotImplementedError(
"The '__aiter__' method must be implemented."
) # pragma: no cover
yield b"" # pragma: no cover
async def aclose(self) -> None:
pass

527
httpx/_urlparse.py Normal file
View File

@ -0,0 +1,527 @@
"""
An implementation of `urlparse` that provides URL validation and normalization
as described by RFC3986.
We rely on this implementation rather than the one in Python's stdlib, because:
* It provides more complete URL validation.
* It properly differentiates between an empty querystring and an absent querystring,
to distinguish URLs with a trailing '?'.
* It handles scheme, hostname, port, and path normalization.
* It supports IDNA hostnames, normalizing them to their encoded form.
* The API supports passing individual components, as well as the complete URL string.
Previously we relied on the excellent `rfc3986` package to handle URL parsing and
validation, but this module provides a simpler alternative, with less indirection
required.
"""
from __future__ import annotations
import ipaddress
import re
import typing
import idna
from ._exceptions import InvalidURL
MAX_URL_LENGTH = 65536
# https://datatracker.ietf.org/doc/html/rfc3986.html#section-2.3
UNRESERVED_CHARACTERS = (
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
)
SUB_DELIMS = "!$&'()*+,;="
PERCENT_ENCODED_REGEX = re.compile("%[A-Fa-f0-9]{2}")
# https://url.spec.whatwg.org/#percent-encoded-bytes
# The fragment percent-encode set is the C0 control percent-encode set
# and U+0020 SPACE, U+0022 ("), U+003C (<), U+003E (>), and U+0060 (`).
FRAG_SAFE = "".join(
[chr(i) for i in range(0x20, 0x7F) if i not in (0x20, 0x22, 0x3C, 0x3E, 0x60)]
)
# The query percent-encode set is the C0 control percent-encode set
# and U+0020 SPACE, U+0022 ("), U+0023 (#), U+003C (<), and U+003E (>).
QUERY_SAFE = "".join(
[chr(i) for i in range(0x20, 0x7F) if i not in (0x20, 0x22, 0x23, 0x3C, 0x3E)]
)
# The path percent-encode set is the query percent-encode set
# and U+003F (?), U+0060 (`), U+007B ({), and U+007D (}).
PATH_SAFE = "".join(
[
chr(i)
for i in range(0x20, 0x7F)
if i not in (0x20, 0x22, 0x23, 0x3C, 0x3E) + (0x3F, 0x60, 0x7B, 0x7D)
]
)
# The userinfo percent-encode set is the path percent-encode set
# and U+002F (/), U+003A (:), U+003B (;), U+003D (=), U+0040 (@),
# U+005B ([) to U+005E (^), inclusive, and U+007C (|).
USERNAME_SAFE = "".join(
[
chr(i)
for i in range(0x20, 0x7F)
if i
not in (0x20, 0x22, 0x23, 0x3C, 0x3E)
+ (0x3F, 0x60, 0x7B, 0x7D)
+ (0x2F, 0x3A, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C)
]
)
PASSWORD_SAFE = "".join(
[
chr(i)
for i in range(0x20, 0x7F)
if i
not in (0x20, 0x22, 0x23, 0x3C, 0x3E)
+ (0x3F, 0x60, 0x7B, 0x7D)
+ (0x2F, 0x3A, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C)
]
)
# Note... The terminology 'userinfo' percent-encode set in the WHATWG document
# is used for the username and password quoting. For the joint userinfo component
# we remove U+003A (:) from the safe set.
USERINFO_SAFE = "".join(
[
chr(i)
for i in range(0x20, 0x7F)
if i
not in (0x20, 0x22, 0x23, 0x3C, 0x3E)
+ (0x3F, 0x60, 0x7B, 0x7D)
+ (0x2F, 0x3B, 0x3D, 0x40, 0x5B, 0x5C, 0x5D, 0x5E, 0x7C)
]
)
# {scheme}: (optional)
# //{authority} (optional)
# {path}
# ?{query} (optional)
# #{fragment} (optional)
URL_REGEX = re.compile(
(
r"(?:(?P<scheme>{scheme}):)?"
r"(?://(?P<authority>{authority}))?"
r"(?P<path>{path})"
r"(?:\?(?P<query>{query}))?"
r"(?:#(?P<fragment>{fragment}))?"
).format(
scheme="([a-zA-Z][a-zA-Z0-9+.-]*)?",
authority="[^/?#]*",
path="[^?#]*",
query="[^#]*",
fragment=".*",
)
)
# {userinfo}@ (optional)
# {host}
# :{port} (optional)
AUTHORITY_REGEX = re.compile(
(
r"(?:(?P<userinfo>{userinfo})@)?" r"(?P<host>{host})" r":?(?P<port>{port})?"
).format(
userinfo=".*", # Any character sequence.
host="(\\[.*\\]|[^:@]*)", # Either any character sequence excluding ':' or '@',
# or an IPv6 address enclosed within square brackets.
port=".*", # Any character sequence.
)
)
# If we call urlparse with an individual component, then we need to regex
# validate that component individually.
# Note that we're duplicating the same strings as above. Shock! Horror!!
COMPONENT_REGEX = {
"scheme": re.compile("([a-zA-Z][a-zA-Z0-9+.-]*)?"),
"authority": re.compile("[^/?#]*"),
"path": re.compile("[^?#]*"),
"query": re.compile("[^#]*"),
"fragment": re.compile(".*"),
"userinfo": re.compile("[^@]*"),
"host": re.compile("(\\[.*\\]|[^:]*)"),
"port": re.compile(".*"),
}
# We use these simple regexs as a first pass before handing off to
# the stdlib 'ipaddress' module for IP address validation.
IPv4_STYLE_HOSTNAME = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$")
IPv6_STYLE_HOSTNAME = re.compile(r"^\[.*\]$")
class ParseResult(typing.NamedTuple):
scheme: str
userinfo: str
host: str
port: int | None
path: str
query: str | None
fragment: str | None
@property
def authority(self) -> str:
return "".join(
[
f"{self.userinfo}@" if self.userinfo else "",
f"[{self.host}]" if ":" in self.host else self.host,
f":{self.port}" if self.port is not None else "",
]
)
@property
def netloc(self) -> str:
return "".join(
[
f"[{self.host}]" if ":" in self.host else self.host,
f":{self.port}" if self.port is not None else "",
]
)
def copy_with(self, **kwargs: str | None) -> ParseResult:
if not kwargs:
return self
defaults = {
"scheme": self.scheme,
"authority": self.authority,
"path": self.path,
"query": self.query,
"fragment": self.fragment,
}
defaults.update(kwargs)
return urlparse("", **defaults)
def __str__(self) -> str:
authority = self.authority
return "".join(
[
f"{self.scheme}:" if self.scheme else "",
f"//{authority}" if authority else "",
self.path,
f"?{self.query}" if self.query is not None else "",
f"#{self.fragment}" if self.fragment is not None else "",
]
)
def urlparse(url: str = "", **kwargs: str | None) -> ParseResult:
# Initial basic checks on allowable URLs.
# ---------------------------------------
# Hard limit the maximum allowable URL length.
if len(url) > MAX_URL_LENGTH:
raise InvalidURL("URL too long")
# If a URL includes any ASCII control characters including \t, \r, \n,
# then treat it as invalid.
if any(char.isascii() and not char.isprintable() for char in url):
char = next(char for char in url if char.isascii() and not char.isprintable())
idx = url.find(char)
error = (
f"Invalid non-printable ASCII character in URL, {char!r} at position {idx}."
)
raise InvalidURL(error)
# Some keyword arguments require special handling.
# ------------------------------------------------
# Coerce "port" to a string, if it is provided as an integer.
if "port" in kwargs:
port = kwargs["port"]
kwargs["port"] = str(port) if isinstance(port, int) else port
# Replace "netloc" with "host and "port".
if "netloc" in kwargs:
netloc = kwargs.pop("netloc") or ""
kwargs["host"], _, kwargs["port"] = netloc.partition(":")
# Replace "username" and/or "password" with "userinfo".
if "username" in kwargs or "password" in kwargs:
username = quote(kwargs.pop("username", "") or "", safe=USERNAME_SAFE)
password = quote(kwargs.pop("password", "") or "", safe=PASSWORD_SAFE)
kwargs["userinfo"] = f"{username}:{password}" if password else username
# Replace "raw_path" with "path" and "query".
if "raw_path" in kwargs:
raw_path = kwargs.pop("raw_path") or ""
kwargs["path"], seperator, kwargs["query"] = raw_path.partition("?")
if not seperator:
kwargs["query"] = None
# Ensure that IPv6 "host" addresses are always escaped with "[...]".
if "host" in kwargs:
host = kwargs.get("host") or ""
if ":" in host and not (host.startswith("[") and host.endswith("]")):
kwargs["host"] = f"[{host}]"
# If any keyword arguments are provided, ensure they are valid.
# -------------------------------------------------------------
for key, value in kwargs.items():
if value is not None:
if len(value) > MAX_URL_LENGTH:
raise InvalidURL(f"URL component '{key}' too long")
# If a component includes any ASCII control characters including \t, \r, \n,
# then treat it as invalid.
if any(char.isascii() and not char.isprintable() for char in value):
char = next(
char for char in value if char.isascii() and not char.isprintable()
)
idx = value.find(char)
error = (
f"Invalid non-printable ASCII character in URL {key} component, "
f"{char!r} at position {idx}."
)
raise InvalidURL(error)
# Ensure that keyword arguments match as a valid regex.
if not COMPONENT_REGEX[key].fullmatch(value):
raise InvalidURL(f"Invalid URL component '{key}'")
# The URL_REGEX will always match, but may have empty components.
url_match = URL_REGEX.match(url)
assert url_match is not None
url_dict = url_match.groupdict()
# * 'scheme', 'authority', and 'path' may be empty strings.
# * 'query' may be 'None', indicating no trailing "?" portion.
# Any string including the empty string, indicates a trailing "?".
# * 'fragment' may be 'None', indicating no trailing "#" portion.
# Any string including the empty string, indicates a trailing "#".
scheme = kwargs.get("scheme", url_dict["scheme"]) or ""
authority = kwargs.get("authority", url_dict["authority"]) or ""
path = kwargs.get("path", url_dict["path"]) or ""
query = kwargs.get("query", url_dict["query"])
frag = kwargs.get("fragment", url_dict["fragment"])
# The AUTHORITY_REGEX will always match, but may have empty components.
authority_match = AUTHORITY_REGEX.match(authority)
assert authority_match is not None
authority_dict = authority_match.groupdict()
# * 'userinfo' and 'host' may be empty strings.
# * 'port' may be 'None'.
userinfo = kwargs.get("userinfo", authority_dict["userinfo"]) or ""
host = kwargs.get("host", authority_dict["host"]) or ""
port = kwargs.get("port", authority_dict["port"])
# Normalize and validate each component.
# We end up with a parsed representation of the URL,
# with components that are plain ASCII bytestrings.
parsed_scheme: str = scheme.lower()
parsed_userinfo: str = quote(userinfo, safe=USERINFO_SAFE)
parsed_host: str = encode_host(host)
parsed_port: int | None = normalize_port(port, scheme)
has_scheme = parsed_scheme != ""
has_authority = (
parsed_userinfo != "" or parsed_host != "" or parsed_port is not None
)
validate_path(path, has_scheme=has_scheme, has_authority=has_authority)
if has_scheme or has_authority:
path = normalize_path(path)
parsed_path: str = quote(path, safe=PATH_SAFE)
parsed_query: str | None = None if query is None else quote(query, safe=QUERY_SAFE)
parsed_frag: str | None = None if frag is None else quote(frag, safe=FRAG_SAFE)
# The parsed ASCII bytestrings are our canonical form.
# All properties of the URL are derived from these.
return ParseResult(
parsed_scheme,
parsed_userinfo,
parsed_host,
parsed_port,
parsed_path,
parsed_query,
parsed_frag,
)
def encode_host(host: str) -> str:
if not host:
return ""
elif IPv4_STYLE_HOSTNAME.match(host):
# Validate IPv4 hostnames like #.#.#.#
#
# From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2
#
# IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
try:
ipaddress.IPv4Address(host)
except ipaddress.AddressValueError:
raise InvalidURL(f"Invalid IPv4 address: {host!r}")
return host
elif IPv6_STYLE_HOSTNAME.match(host):
# Validate IPv6 hostnames like [...]
#
# From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2
#
# "A host identified by an Internet Protocol literal address, version 6
# [RFC3513] or later, is distinguished by enclosing the IP literal
# within square brackets ("[" and "]"). This is the only place where
# square bracket characters are allowed in the URI syntax."
try:
ipaddress.IPv6Address(host[1:-1])
except ipaddress.AddressValueError:
raise InvalidURL(f"Invalid IPv6 address: {host!r}")
return host[1:-1]
elif host.isascii():
# Regular ASCII hostnames
#
# From https://datatracker.ietf.org/doc/html/rfc3986/#section-3.2.2
#
# reg-name = *( unreserved / pct-encoded / sub-delims )
WHATWG_SAFE = '"`{}%|\\'
return quote(host.lower(), safe=SUB_DELIMS + WHATWG_SAFE)
# IDNA hostnames
try:
return idna.encode(host.lower()).decode("ascii")
except idna.IDNAError:
raise InvalidURL(f"Invalid IDNA hostname: {host!r}")
def normalize_port(port: str | int | None, scheme: str) -> int | None:
# From https://tools.ietf.org/html/rfc3986#section-3.2.3
#
# "A scheme may define a default port. For example, the "http" scheme
# defines a default port of "80", corresponding to its reserved TCP
# port number. The type of port designated by the port number (e.g.,
# TCP, UDP, SCTP) is defined by the URI scheme. URI producers and
# normalizers should omit the port component and its ":" delimiter if
# port is empty or if its value would be the same as that of the
# scheme's default."
if port is None or port == "":
return None
try:
port_as_int = int(port)
except ValueError:
raise InvalidURL(f"Invalid port: {port!r}")
# See https://url.spec.whatwg.org/#url-miscellaneous
default_port = {"ftp": 21, "http": 80, "https": 443, "ws": 80, "wss": 443}.get(
scheme
)
if port_as_int == default_port:
return None
return port_as_int
def validate_path(path: str, has_scheme: bool, has_authority: bool) -> None:
"""
Path validation rules that depend on if the URL contains
a scheme or authority component.
See https://datatracker.ietf.org/doc/html/rfc3986.html#section-3.3
"""
if has_authority:
# If a URI contains an authority component, then the path component
# must either be empty or begin with a slash ("/") character."
if path and not path.startswith("/"):
raise InvalidURL("For absolute URLs, path must be empty or begin with '/'")
if not has_scheme and not has_authority:
# If a URI does not contain an authority component, then the path cannot begin
# with two slash characters ("//").
if path.startswith("//"):
raise InvalidURL("Relative URLs cannot have a path starting with '//'")
# In addition, a URI reference (Section 4.1) may be a relative-path reference,
# in which case the first path segment cannot contain a colon (":") character.
if path.startswith(":"):
raise InvalidURL("Relative URLs cannot have a path starting with ':'")
def normalize_path(path: str) -> str:
"""
Drop "." and ".." segments from a URL path.
For example:
normalize_path("/path/./to/somewhere/..") == "/path/to"
"""
# Fast return when no '.' characters in the path.
if "." not in path:
return path
components = path.split("/")
# Fast return when no '.' or '..' components in the path.
if "." not in components and ".." not in components:
return path
# https://datatracker.ietf.org/doc/html/rfc3986#section-5.2.4
output: list[str] = []
for component in components:
if component == ".":
pass
elif component == "..":
if output and output != [""]:
output.pop()
else:
output.append(component)
return "/".join(output)
def PERCENT(string: str) -> str:
return "".join([f"%{byte:02X}" for byte in string.encode("utf-8")])
def percent_encoded(string: str, safe: str) -> str:
"""
Use percent-encoding to quote a string.
"""
NON_ESCAPED_CHARS = UNRESERVED_CHARACTERS + safe
# Fast path for strings that don't need escaping.
if not string.rstrip(NON_ESCAPED_CHARS):
return string
return "".join(
[char if char in NON_ESCAPED_CHARS else PERCENT(char) for char in string]
)
def quote(string: str, safe: str) -> str:
"""
Use percent-encoding to quote a string, omitting existing '%xx' escape sequences.
See: https://www.rfc-editor.org/rfc/rfc3986#section-2.1
* `string`: The string to be percent-escaped.
* `safe`: A string containing characters that may be treated as safe, and do not
need to be escaped. Unreserved characters are always treated as safe.
See: https://www.rfc-editor.org/rfc/rfc3986#section-2.3
"""
parts = []
current_position = 0
for match in re.finditer(PERCENT_ENCODED_REGEX, string):
start_position, end_position = match.start(), match.end()
matched_text = match.group(0)
# Add any text up to the '%xx' escape sequence.
if start_position != current_position:
leading_text = string[current_position:start_position]
parts.append(percent_encoded(leading_text, safe=safe))
# Add the '%xx' escape sequence.
parts.append(matched_text)
current_position = end_position
# Add any text after the final '%xx' escape sequence.
if current_position != len(string):
trailing_text = string[current_position:]
parts.append(percent_encoded(trailing_text, safe=safe))
return "".join(parts)

641
httpx/_urls.py Normal file
View File

@ -0,0 +1,641 @@
from __future__ import annotations
import typing
from urllib.parse import parse_qs, unquote, urlencode
import idna
from ._types import QueryParamTypes
from ._urlparse import urlparse
from ._utils import primitive_value_to_str
__all__ = ["URL", "QueryParams"]
class URL:
"""
url = httpx.URL("HTTPS://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink")
assert url.scheme == "https"
assert url.username == "jo@email.com"
assert url.password == "a secret"
assert url.userinfo == b"jo%40email.com:a%20secret"
assert url.host == "müller.de"
assert url.raw_host == b"xn--mller-kva.de"
assert url.port == 1234
assert url.netloc == b"xn--mller-kva.de:1234"
assert url.path == "/pa th"
assert url.query == b"?search=ab"
assert url.raw_path == b"/pa%20th?search=ab"
assert url.fragment == "anchorlink"
The components of a URL are broken down like this:
https://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink
[scheme] [ username ] [password] [ host ][port][ path ] [ query ] [fragment]
[ userinfo ] [ netloc ][ raw_path ]
Note that:
* `url.scheme` is normalized to always be lowercased.
* `url.host` is normalized to always be lowercased. Internationalized domain
names are represented in unicode, without IDNA encoding applied. For instance:
url = httpx.URL("http://中国.icom.museum")
assert url.host == "中国.icom.museum"
url = httpx.URL("http://xn--fiqs8s.icom.museum")
assert url.host == "中国.icom.museum"
* `url.raw_host` is normalized to always be lowercased, and is IDNA encoded.
url = httpx.URL("http://中国.icom.museum")
assert url.raw_host == b"xn--fiqs8s.icom.museum"
url = httpx.URL("http://xn--fiqs8s.icom.museum")
assert url.raw_host == b"xn--fiqs8s.icom.museum"
* `url.port` is either None or an integer. URLs that include the default port for
"http", "https", "ws", "wss", and "ftp" schemes have their port
normalized to `None`.
assert httpx.URL("http://example.com") == httpx.URL("http://example.com:80")
assert httpx.URL("http://example.com").port is None
assert httpx.URL("http://example.com:80").port is None
* `url.userinfo` is raw bytes, without URL escaping. Usually you'll want to work
with `url.username` and `url.password` instead, which handle the URL escaping.
* `url.raw_path` is raw bytes of both the path and query, without URL escaping.
This portion is used as the target when constructing HTTP requests. Usually you'll
want to work with `url.path` instead.
* `url.query` is raw bytes, without URL escaping. A URL query string portion can
only be properly URL escaped when decoding the parameter names and values
themselves.
"""
def __init__(self, url: URL | str = "", **kwargs: typing.Any) -> None:
if kwargs:
allowed = {
"scheme": str,
"username": str,
"password": str,
"userinfo": bytes,
"host": str,
"port": int,
"netloc": bytes,
"path": str,
"query": bytes,
"raw_path": bytes,
"fragment": str,
"params": object,
}
# Perform type checking for all supported keyword arguments.
for key, value in kwargs.items():
if key not in allowed:
message = f"{key!r} is an invalid keyword argument for URL()"
raise TypeError(message)
if value is not None and not isinstance(value, allowed[key]):
expected = allowed[key].__name__
seen = type(value).__name__
message = f"Argument {key!r} must be {expected} but got {seen}"
raise TypeError(message)
if isinstance(value, bytes):
kwargs[key] = value.decode("ascii")
if "params" in kwargs:
# Replace any "params" keyword with the raw "query" instead.
#
# Ensure that empty params use `kwargs["query"] = None` rather
# than `kwargs["query"] = ""`, so that generated URLs do not
# include an empty trailing "?".
params = kwargs.pop("params")
kwargs["query"] = None if not params else str(QueryParams(params))
if isinstance(url, str):
self._uri_reference = urlparse(url, **kwargs)
elif isinstance(url, URL):
self._uri_reference = url._uri_reference.copy_with(**kwargs)
else:
raise TypeError(
"Invalid type for url. Expected str or httpx.URL,"
f" got {type(url)}: {url!r}"
)
@property
def scheme(self) -> str:
"""
The URL scheme, such as "http", "https".
Always normalised to lowercase.
"""
return self._uri_reference.scheme
@property
def raw_scheme(self) -> bytes:
"""
The raw bytes representation of the URL scheme, such as b"http", b"https".
Always normalised to lowercase.
"""
return self._uri_reference.scheme.encode("ascii")
@property
def userinfo(self) -> bytes:
"""
The URL userinfo as a raw bytestring.
For example: b"jo%40email.com:a%20secret".
"""
return self._uri_reference.userinfo.encode("ascii")
@property
def username(self) -> str:
"""
The URL username as a string, with URL decoding applied.
For example: "jo@email.com"
"""
userinfo = self._uri_reference.userinfo
return unquote(userinfo.partition(":")[0])
@property
def password(self) -> str:
"""
The URL password as a string, with URL decoding applied.
For example: "a secret"
"""
userinfo = self._uri_reference.userinfo
return unquote(userinfo.partition(":")[2])
@property
def host(self) -> str:
"""
The URL host as a string.
Always normalized to lowercase, with IDNA hosts decoded into unicode.
Examples:
url = httpx.URL("http://www.EXAMPLE.org")
assert url.host == "www.example.org"
url = httpx.URL("http://中国.icom.museum")
assert url.host == "中国.icom.museum"
url = httpx.URL("http://xn--fiqs8s.icom.museum")
assert url.host == "中国.icom.museum"
url = httpx.URL("https://[::ffff:192.168.0.1]")
assert url.host == "::ffff:192.168.0.1"
"""
host: str = self._uri_reference.host
if host.startswith("xn--"):
host = idna.decode(host)
return host
@property
def raw_host(self) -> bytes:
"""
The raw bytes representation of the URL host.
Always normalized to lowercase, and IDNA encoded.
Examples:
url = httpx.URL("http://www.EXAMPLE.org")
assert url.raw_host == b"www.example.org"
url = httpx.URL("http://中国.icom.museum")
assert url.raw_host == b"xn--fiqs8s.icom.museum"
url = httpx.URL("http://xn--fiqs8s.icom.museum")
assert url.raw_host == b"xn--fiqs8s.icom.museum"
url = httpx.URL("https://[::ffff:192.168.0.1]")
assert url.raw_host == b"::ffff:192.168.0.1"
"""
return self._uri_reference.host.encode("ascii")
@property
def port(self) -> int | None:
"""
The URL port as an integer.
Note that the URL class performs port normalization as per the WHATWG spec.
Default ports for "http", "https", "ws", "wss", and "ftp" schemes are always
treated as `None`.
For example:
assert httpx.URL("http://www.example.com") == httpx.URL("http://www.example.com:80")
assert httpx.URL("http://www.example.com:80").port is None
"""
return self._uri_reference.port
@property
def netloc(self) -> bytes:
"""
Either `<host>` or `<host>:<port>` as bytes.
Always normalized to lowercase, and IDNA encoded.
This property may be used for generating the value of a request
"Host" header.
"""
return self._uri_reference.netloc.encode("ascii")
@property
def path(self) -> str:
"""
The URL path as a string. Excluding the query string, and URL decoded.
For example:
url = httpx.URL("https://example.com/pa%20th")
assert url.path == "/pa th"
"""
path = self._uri_reference.path or "/"
return unquote(path)
@property
def query(self) -> bytes:
"""
The URL query string, as raw bytes, excluding the leading b"?".
This is necessarily a bytewise interface, because we cannot
perform URL decoding of this representation until we've parsed
the keys and values into a QueryParams instance.
For example:
url = httpx.URL("https://example.com/?filter=some%20search%20terms")
assert url.query == b"filter=some%20search%20terms"
"""
query = self._uri_reference.query or ""
return query.encode("ascii")
@property
def params(self) -> QueryParams:
"""
The URL query parameters, neatly parsed and packaged into an immutable
multidict representation.
"""
return QueryParams(self._uri_reference.query)
@property
def raw_path(self) -> bytes:
"""
The complete URL path and query string as raw bytes.
Used as the target when constructing HTTP requests.
For example:
GET /users?search=some%20text HTTP/1.1
Host: www.example.org
Connection: close
"""
path = self._uri_reference.path or "/"
if self._uri_reference.query is not None:
path += "?" + self._uri_reference.query
return path.encode("ascii")
@property
def fragment(self) -> str:
"""
The URL fragments, as used in HTML anchors.
As a string, without the leading '#'.
"""
return unquote(self._uri_reference.fragment or "")
@property
def is_absolute_url(self) -> bool:
"""
Return `True` for absolute URLs such as 'http://example.com/path',
and `False` for relative URLs such as '/path'.
"""
# We don't use `.is_absolute` from `rfc3986` because it treats
# URLs with a fragment portion as not absolute.
# What we actually care about is if the URL provides
# a scheme and hostname to which connections should be made.
return bool(self._uri_reference.scheme and self._uri_reference.host)
@property
def is_relative_url(self) -> bool:
"""
Return `False` for absolute URLs such as 'http://example.com/path',
and `True` for relative URLs such as '/path'.
"""
return not self.is_absolute_url
def copy_with(self, **kwargs: typing.Any) -> URL:
"""
Copy this URL, returning a new URL with some components altered.
Accepts the same set of parameters as the components that are made
available via properties on the `URL` class.
For example:
url = httpx.URL("https://www.example.com").copy_with(
username="jo@gmail.com", password="a secret"
)
assert url == "https://jo%40email.com:a%20secret@www.example.com"
"""
return URL(self, **kwargs)
def copy_set_param(self, key: str, value: typing.Any = None) -> URL:
return self.copy_with(params=self.params.set(key, value))
def copy_add_param(self, key: str, value: typing.Any = None) -> URL:
return self.copy_with(params=self.params.add(key, value))
def copy_remove_param(self, key: str) -> URL:
return self.copy_with(params=self.params.remove(key))
def copy_merge_params(self, params: QueryParamTypes) -> URL:
return self.copy_with(params=self.params.merge(params))
def join(self, url: URL | str) -> URL:
"""
Return an absolute URL, using this URL as the base.
Eg.
url = httpx.URL("https://www.example.com/test")
url = url.join("/new/path")
assert url == "https://www.example.com/new/path"
"""
from urllib.parse import urljoin
return URL(urljoin(str(self), str(URL(url))))
def __hash__(self) -> int:
return hash(str(self))
def __eq__(self, other: typing.Any) -> bool:
return isinstance(other, (URL, str)) and str(self) == str(URL(other))
def __str__(self) -> str:
return str(self._uri_reference)
def __repr__(self) -> str:
scheme, userinfo, host, port, path, query, fragment = self._uri_reference
if ":" in userinfo:
# Mask any password component.
userinfo = f"{userinfo.split(':')[0]}:[secure]"
authority = "".join(
[
f"{userinfo}@" if userinfo else "",
f"[{host}]" if ":" in host else host,
f":{port}" if port is not None else "",
]
)
url = "".join(
[
f"{self.scheme}:" if scheme else "",
f"//{authority}" if authority else "",
path,
f"?{query}" if query is not None else "",
f"#{fragment}" if fragment is not None else "",
]
)
return f"{self.__class__.__name__}({url!r})"
@property
def raw(self) -> tuple[bytes, bytes, int, bytes]: # pragma: nocover
import collections
import warnings
warnings.warn("URL.raw is deprecated.")
RawURL = collections.namedtuple(
"RawURL", ["raw_scheme", "raw_host", "port", "raw_path"]
)
return RawURL(
raw_scheme=self.raw_scheme,
raw_host=self.raw_host,
port=self.port,
raw_path=self.raw_path,
)
class QueryParams(typing.Mapping[str, str]):
"""
URL query parameters, as a multi-dict.
"""
def __init__(self, *args: QueryParamTypes | None, **kwargs: typing.Any) -> None:
assert len(args) < 2, "Too many arguments."
assert not (args and kwargs), "Cannot mix named and unnamed arguments."
value = args[0] if args else kwargs
if value is None or isinstance(value, (str, bytes)):
value = value.decode("ascii") if isinstance(value, bytes) else value
self._dict = parse_qs(value, keep_blank_values=True)
elif isinstance(value, QueryParams):
self._dict = {k: list(v) for k, v in value._dict.items()}
else:
dict_value: dict[typing.Any, list[typing.Any]] = {}
if isinstance(value, (list, tuple)):
# Convert list inputs like:
# [("a", "123"), ("a", "456"), ("b", "789")]
# To a dict representation, like:
# {"a": ["123", "456"], "b": ["789"]}
for item in value:
dict_value.setdefault(item[0], []).append(item[1])
else:
# Convert dict inputs like:
# {"a": "123", "b": ["456", "789"]}
# To dict inputs where values are always lists, like:
# {"a": ["123"], "b": ["456", "789"]}
dict_value = {
k: list(v) if isinstance(v, (list, tuple)) else [v]
for k, v in value.items()
}
# Ensure that keys and values are neatly coerced to strings.
# We coerce values `True` and `False` to JSON-like "true" and "false"
# representations, and coerce `None` values to the empty string.
self._dict = {
str(k): [primitive_value_to_str(item) for item in v]
for k, v in dict_value.items()
}
def keys(self) -> typing.KeysView[str]:
"""
Return all the keys in the query params.
Usage:
q = httpx.QueryParams("a=123&a=456&b=789")
assert list(q.keys()) == ["a", "b"]
"""
return self._dict.keys()
def values(self) -> typing.ValuesView[str]:
"""
Return all the values in the query params. If a key occurs more than once
only the first item for that key is returned.
Usage:
q = httpx.QueryParams("a=123&a=456&b=789")
assert list(q.values()) == ["123", "789"]
"""
return {k: v[0] for k, v in self._dict.items()}.values()
def items(self) -> typing.ItemsView[str, str]:
"""
Return all items in the query params. If a key occurs more than once
only the first item for that key is returned.
Usage:
q = httpx.QueryParams("a=123&a=456&b=789")
assert list(q.items()) == [("a", "123"), ("b", "789")]
"""
return {k: v[0] for k, v in self._dict.items()}.items()
def multi_items(self) -> list[tuple[str, str]]:
"""
Return all items in the query params. Allow duplicate keys to occur.
Usage:
q = httpx.QueryParams("a=123&a=456&b=789")
assert list(q.multi_items()) == [("a", "123"), ("a", "456"), ("b", "789")]
"""
multi_items: list[tuple[str, str]] = []
for k, v in self._dict.items():
multi_items.extend([(k, i) for i in v])
return multi_items
def get(self, key: typing.Any, default: typing.Any = None) -> typing.Any:
"""
Get a value from the query param for a given key. If the key occurs
more than once, then only the first value is returned.
Usage:
q = httpx.QueryParams("a=123&a=456&b=789")
assert q.get("a") == "123"
"""
if key in self._dict:
return self._dict[str(key)][0]
return default
def get_list(self, key: str) -> list[str]:
"""
Get all values from the query param for a given key.
Usage:
q = httpx.QueryParams("a=123&a=456&b=789")
assert q.get_list("a") == ["123", "456"]
"""
return list(self._dict.get(str(key), []))
def set(self, key: str, value: typing.Any = None) -> QueryParams:
"""
Return a new QueryParams instance, setting the value of a key.
Usage:
q = httpx.QueryParams("a=123")
q = q.set("a", "456")
assert q == httpx.QueryParams("a=456")
"""
q = QueryParams()
q._dict = dict(self._dict)
q._dict[str(key)] = [primitive_value_to_str(value)]
return q
def add(self, key: str, value: typing.Any = None) -> QueryParams:
"""
Return a new QueryParams instance, setting or appending the value of a key.
Usage:
q = httpx.QueryParams("a=123")
q = q.add("a", "456")
assert q == httpx.QueryParams("a=123&a=456")
"""
q = QueryParams()
q._dict = dict(self._dict)
q._dict[str(key)] = q.get_list(key) + [primitive_value_to_str(value)]
return q
def remove(self, key: str) -> QueryParams:
"""
Return a new QueryParams instance, removing the value of a key.
Usage:
q = httpx.QueryParams("a=123")
q = q.remove("a")
assert q == httpx.QueryParams("")
"""
q = QueryParams()
q._dict = dict(self._dict)
q._dict.pop(str(key), None)
return q
def merge(self, params: QueryParamTypes | None = None) -> QueryParams:
"""
Return a new QueryParams instance, updated with.
Usage:
q = httpx.QueryParams("a=123")
q = q.merge({"b": "456"})
assert q == httpx.QueryParams("a=123&b=456")
q = httpx.QueryParams("a=123")
q = q.merge({"a": "456", "b": "789"})
assert q == httpx.QueryParams("a=456&b=789")
"""
q = QueryParams(params)
q._dict = {**self._dict, **q._dict}
return q
def __getitem__(self, key: typing.Any) -> str:
return self._dict[key][0]
def __contains__(self, key: typing.Any) -> bool:
return key in self._dict
def __iter__(self) -> typing.Iterator[typing.Any]:
return iter(self.keys())
def __len__(self) -> int:
return len(self._dict)
def __bool__(self) -> bool:
return bool(self._dict)
def __hash__(self) -> int:
return hash(str(self))
def __eq__(self, other: typing.Any) -> bool:
if not isinstance(other, self.__class__):
return False
return sorted(self.multi_items()) == sorted(other.multi_items())
def __str__(self) -> str:
return urlencode(self.multi_items())
def __repr__(self) -> str:
class_name = self.__class__.__name__
query_string = str(self)
return f"{class_name}({query_string!r})"
def update(self, params: QueryParamTypes | None = None) -> None:
raise RuntimeError(
"QueryParams are immutable since 0.18.0. "
"Use `q = q.merge(...)` to create an updated copy."
)
def __setitem__(self, key: str, value: str) -> None:
raise RuntimeError(
"QueryParams are immutable since 0.18.0. "
"Use `q = q.set(key, value)` to create an updated copy."
)

242
httpx/_utils.py Normal file
View File

@ -0,0 +1,242 @@
from __future__ import annotations
import ipaddress
import os
import re
import typing
from urllib.request import getproxies
from ._types import PrimitiveData
if typing.TYPE_CHECKING: # pragma: no cover
from ._urls import URL
def primitive_value_to_str(value: PrimitiveData) -> str:
"""
Coerce a primitive data type into a string value.
Note that we prefer JSON-style 'true'/'false' for boolean values here.
"""
if value is True:
return "true"
elif value is False:
return "false"
elif value is None:
return ""
return str(value)
def get_environment_proxies() -> dict[str, str | None]:
"""Gets proxy information from the environment"""
# urllib.request.getproxies() falls back on System
# Registry and Config for proxies on Windows and macOS.
# We don't want to propagate non-HTTP proxies into
# our configuration such as 'TRAVIS_APT_PROXY'.
proxy_info = getproxies()
mounts: dict[str, str | None] = {}
for scheme in ("http", "https", "all"):
if proxy_info.get(scheme):
hostname = proxy_info[scheme]
mounts[f"{scheme}://"] = (
hostname if "://" in hostname else f"http://{hostname}"
)
no_proxy_hosts = [host.strip() for host in proxy_info.get("no", "").split(",")]
for hostname in no_proxy_hosts:
# See https://curl.haxx.se/libcurl/c/CURLOPT_NOPROXY.html for details
# on how names in `NO_PROXY` are handled.
if hostname == "*":
# If NO_PROXY=* is used or if "*" occurs as any one of the comma
# separated hostnames, then we should just bypass any information
# from HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, and always ignore
# proxies.
return {}
elif hostname:
# NO_PROXY=.google.com is marked as "all://*.google.com,
# which disables "www.google.com" but not "google.com"
# NO_PROXY=google.com is marked as "all://*google.com,
# which disables "www.google.com" and "google.com".
# (But not "wwwgoogle.com")
# NO_PROXY can include domains, IPv6, IPv4 addresses and "localhost"
# NO_PROXY=example.com,::1,localhost,192.168.0.0/16
if "://" in hostname:
mounts[hostname] = None
elif is_ipv4_hostname(hostname):
mounts[f"all://{hostname}"] = None
elif is_ipv6_hostname(hostname):
mounts[f"all://[{hostname}]"] = None
elif hostname.lower() == "localhost":
mounts[f"all://{hostname}"] = None
else:
mounts[f"all://*{hostname}"] = None
return mounts
def to_bytes(value: str | bytes, encoding: str = "utf-8") -> bytes:
return value.encode(encoding) if isinstance(value, str) else value
def to_str(value: str | bytes, encoding: str = "utf-8") -> str:
return value if isinstance(value, str) else value.decode(encoding)
def to_bytes_or_str(value: str, match_type_of: typing.AnyStr) -> typing.AnyStr:
return value if isinstance(match_type_of, str) else value.encode()
def unquote(value: str) -> str:
return value[1:-1] if value[0] == value[-1] == '"' else value
def peek_filelike_length(stream: typing.Any) -> int | None:
"""
Given a file-like stream object, return its length in number of bytes
without reading it into memory.
"""
try:
# Is it an actual file?
fd = stream.fileno()
# Yup, seems to be an actual file.
length = os.fstat(fd).st_size
except (AttributeError, OSError):
# No... Maybe it's something that supports random access, like `io.BytesIO`?
try:
# Assuming so, go to end of stream to figure out its length,
# then put it back in place.
offset = stream.tell()
length = stream.seek(0, os.SEEK_END)
stream.seek(offset)
except (AttributeError, OSError):
# Not even that? Sorry, we're doomed...
return None
return length
class URLPattern:
"""
A utility class currently used for making lookups against proxy keys...
# Wildcard matching...
>>> pattern = URLPattern("all://")
>>> pattern.matches(httpx.URL("http://example.com"))
True
# Witch scheme matching...
>>> pattern = URLPattern("https://")
>>> pattern.matches(httpx.URL("https://example.com"))
True
>>> pattern.matches(httpx.URL("http://example.com"))
False
# With domain matching...
>>> pattern = URLPattern("https://example.com")
>>> pattern.matches(httpx.URL("https://example.com"))
True
>>> pattern.matches(httpx.URL("http://example.com"))
False
>>> pattern.matches(httpx.URL("https://other.com"))
False
# Wildcard scheme, with domain matching...
>>> pattern = URLPattern("all://example.com")
>>> pattern.matches(httpx.URL("https://example.com"))
True
>>> pattern.matches(httpx.URL("http://example.com"))
True
>>> pattern.matches(httpx.URL("https://other.com"))
False
# With port matching...
>>> pattern = URLPattern("https://example.com:1234")
>>> pattern.matches(httpx.URL("https://example.com:1234"))
True
>>> pattern.matches(httpx.URL("https://example.com"))
False
"""
def __init__(self, pattern: str) -> None:
from ._urls import URL
if pattern and ":" not in pattern:
raise ValueError(
f"Proxy keys should use proper URL forms rather "
f"than plain scheme strings. "
f'Instead of "{pattern}", use "{pattern}://"'
)
url = URL(pattern)
self.pattern = pattern
self.scheme = "" if url.scheme == "all" else url.scheme
self.host = "" if url.host == "*" else url.host
self.port = url.port
if not url.host or url.host == "*":
self.host_regex: typing.Pattern[str] | None = None
elif url.host.startswith("*."):
# *.example.com should match "www.example.com", but not "example.com"
domain = re.escape(url.host[2:])
self.host_regex = re.compile(f"^.+\\.{domain}$")
elif url.host.startswith("*"):
# *example.com should match "www.example.com" and "example.com"
domain = re.escape(url.host[1:])
self.host_regex = re.compile(f"^(.+\\.)?{domain}$")
else:
# example.com should match "example.com" but not "www.example.com"
domain = re.escape(url.host)
self.host_regex = re.compile(f"^{domain}$")
def matches(self, other: URL) -> bool:
if self.scheme and self.scheme != other.scheme:
return False
if (
self.host
and self.host_regex is not None
and not self.host_regex.match(other.host)
):
return False
if self.port is not None and self.port != other.port:
return False
return True
@property
def priority(self) -> tuple[int, int, int]:
"""
The priority allows URLPattern instances to be sortable, so that
we can match from most specific to least specific.
"""
# URLs with a port should take priority over URLs without a port.
port_priority = 0 if self.port is not None else 1
# Longer hostnames should match first.
host_priority = -len(self.host)
# Longer schemes should match first.
scheme_priority = -len(self.scheme)
return (port_priority, host_priority, scheme_priority)
def __hash__(self) -> int:
return hash(self.pattern)
def __lt__(self, other: URLPattern) -> bool:
return self.priority < other.priority
def __eq__(self, other: typing.Any) -> bool:
return isinstance(other, URLPattern) and self.pattern == other.pattern
def is_ipv4_hostname(hostname: str) -> bool:
try:
ipaddress.IPv4Address(hostname.split("/")[0])
except Exception:
return False
return True
def is_ipv6_hostname(hostname: str) -> bool:
try:
ipaddress.IPv6Address(hostname.split("/")[0])
except Exception:
return False
return True

View File

@ -1,295 +0,0 @@
import typing
from .client import Client
from .config import CertTypes, TimeoutTypes, VerifyTypes
from .models import (
AuthTypes,
CookieTypes,
HeaderTypes,
ProxiesTypes,
QueryParamTypes,
RequestData,
RequestFiles,
Response,
URLTypes,
)
def request(
method: str,
url: URLTypes,
*,
params: QueryParamTypes = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
auth: AuthTypes = None,
timeout: TimeoutTypes = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
stream: bool = False,
trust_env: bool = None,
proxies: ProxiesTypes = None,
) -> Response:
with Client(http_versions=["HTTP/1.1"]) as client:
return client.request(
method=method,
url=url,
data=data,
files=files,
json=json,
params=params,
headers=headers,
cookies=cookies,
stream=stream,
auth=auth,
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def get(
url: URLTypes,
*,
params: QueryParamTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
stream: bool = False,
auth: AuthTypes = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = None,
trust_env: bool = None,
proxies: ProxiesTypes = None,
) -> Response:
return request(
"GET",
url,
params=params,
headers=headers,
cookies=cookies,
stream=stream,
auth=auth,
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def options(
url: URLTypes,
*,
params: QueryParamTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
stream: bool = False,
auth: AuthTypes = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = None,
trust_env: bool = None,
proxies: ProxiesTypes = None,
) -> Response:
return request(
"OPTIONS",
url,
params=params,
headers=headers,
cookies=cookies,
stream=stream,
auth=auth,
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def head(
url: URLTypes,
*,
params: QueryParamTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
stream: bool = False,
auth: AuthTypes = None,
allow_redirects: bool = False, #  Note: Differs to usual default.
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = None,
trust_env: bool = None,
proxies: ProxiesTypes = None,
) -> Response:
return request(
"HEAD",
url,
params=params,
headers=headers,
cookies=cookies,
stream=stream,
auth=auth,
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def post(
url: URLTypes,
*,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
params: QueryParamTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
stream: bool = False,
auth: AuthTypes = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = None,
trust_env: bool = None,
proxies: ProxiesTypes = None,
) -> Response:
return request(
"POST",
url,
data=data,
files=files,
json=json,
params=params,
headers=headers,
cookies=cookies,
stream=stream,
auth=auth,
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def put(
url: URLTypes,
*,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
params: QueryParamTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
stream: bool = False,
auth: AuthTypes = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = None,
trust_env: bool = None,
proxies: ProxiesTypes = None,
) -> Response:
return request(
"PUT",
url,
data=data,
files=files,
json=json,
params=params,
headers=headers,
cookies=cookies,
stream=stream,
auth=auth,
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def patch(
url: URLTypes,
*,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
params: QueryParamTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
stream: bool = False,
auth: AuthTypes = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = None,
trust_env: bool = None,
proxies: ProxiesTypes = None,
) -> Response:
return request(
"PATCH",
url,
data=data,
files=files,
json=json,
params=params,
headers=headers,
cookies=cookies,
stream=stream,
auth=auth,
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)
def delete(
url: URLTypes,
*,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
params: QueryParamTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
stream: bool = False,
auth: AuthTypes = None,
allow_redirects: bool = True,
cert: CertTypes = None,
verify: VerifyTypes = True,
timeout: TimeoutTypes = None,
trust_env: bool = None,
proxies: ProxiesTypes = None,
) -> Response:
return request(
"DELETE",
url,
data=data,
files=files,
json=json,
params=params,
headers=headers,
cookies=cookies,
stream=stream,
auth=auth,
allow_redirects=allow_redirects,
cert=cert,
verify=verify,
timeout=timeout,
trust_env=trust_env,
)

File diff suppressed because it is too large Load Diff

View File

@ -1,298 +0,0 @@
import asyncio
import functools
import ssl
import typing
from types import TracebackType
from ..config import PoolLimits, TimeoutConfig
from ..exceptions import ConnectTimeout, PoolTimeout, ReadTimeout, WriteTimeout
from .base import (
BaseBackgroundManager,
BaseEvent,
BasePoolSemaphore,
BaseQueue,
BaseTCPStream,
ConcurrencyBackend,
TimeoutFlag,
)
SSL_MONKEY_PATCH_APPLIED = False
def ssl_monkey_patch() -> None:
"""
Monkey-patch for https://bugs.python.org/issue36709
This prevents console errors when outstanding HTTPS connections
still exist at the point of exiting.
Clients which have been opened using a `with` block, or which have
had `close()` closed, will not exhibit this issue in the first place.
"""
MonkeyPatch = asyncio.selector_events._SelectorSocketTransport # type: ignore
_write = MonkeyPatch.write
def _fixed_write(self, data: bytes) -> None: # type: ignore
if self._loop and not self._loop.is_closed():
_write(self, data)
MonkeyPatch.write = _fixed_write
class TCPStream(BaseTCPStream):
def __init__(
self,
stream_reader: asyncio.StreamReader,
stream_writer: asyncio.StreamWriter,
timeout: TimeoutConfig,
):
self.stream_reader = stream_reader
self.stream_writer = stream_writer
self.timeout = timeout
def get_http_version(self) -> str:
ssl_object = self.stream_writer.get_extra_info("ssl_object")
if ssl_object is None:
return "HTTP/1.1"
ident = ssl_object.selected_alpn_protocol()
if ident is None:
return "HTTP/1.1"
return "HTTP/2" if ident == "h2" else "HTTP/1.1"
async def read(
self, n: int, timeout: TimeoutConfig = None, flag: TimeoutFlag = None
) -> bytes:
if timeout is None:
timeout = self.timeout
while True:
# Check our flag at the first possible moment, and use a fine
# grained retry loop if we're not yet in read-timeout mode.
should_raise = flag is None or flag.raise_on_read_timeout
read_timeout = timeout.read_timeout if should_raise else 0.01
try:
data = await asyncio.wait_for(self.stream_reader.read(n), read_timeout)
break
except asyncio.TimeoutError:
if should_raise:
raise ReadTimeout() from None
# FIX(py3.6): yield control back to the event loop to give it a chance
# to cancel `.read(n)` before we retry.
# This prevents concurrent `.read()` calls, which asyncio
# doesn't seem to allow on 3.6.
# See: https://github.com/encode/httpx/issues/382
await asyncio.sleep(0)
return data
def write_no_block(self, data: bytes) -> None:
self.stream_writer.write(data) # pragma: nocover
async def write(
self, data: bytes, timeout: TimeoutConfig = None, flag: TimeoutFlag = None
) -> None:
if not data:
return
if timeout is None:
timeout = self.timeout
self.stream_writer.write(data)
while True:
try:
await asyncio.wait_for( # type: ignore
self.stream_writer.drain(), timeout.write_timeout
)
break
except asyncio.TimeoutError:
# We check our flag at the first possible moment, in order to
# allow us to suppress write timeouts, if we've since
# switched over to read-timeout mode.
should_raise = flag is None or flag.raise_on_write_timeout
if should_raise:
raise WriteTimeout() from None
def is_connection_dropped(self) -> bool:
# Counter-intuitively, what we really want to know here is whether the socket is
# *readable*, i.e. whether it would return immediately with empty bytes if we
# called `.recv()` on it, indicating that the other end has closed the socket.
# See: https://github.com/encode/httpx/pull/143#issuecomment-515181778
#
# As it turns out, asyncio checks for readability in the background
# (see: https://github.com/encode/httpx/pull/276#discussion_r322000402),
# so checking for EOF or readability here would yield the same result.
#
# At the cost of rigour, we check for EOF instead of readability because asyncio
# does not expose any public API to check for readability.
# (For a solution that uses private asyncio APIs, see:
# https://github.com/encode/httpx/pull/143#issuecomment-515202982)
return self.stream_reader.at_eof()
async def close(self) -> None:
self.stream_writer.close()
class PoolSemaphore(BasePoolSemaphore):
def __init__(self, pool_limits: PoolLimits):
self.pool_limits = pool_limits
@property
def semaphore(self) -> typing.Optional[asyncio.BoundedSemaphore]:
if not hasattr(self, "_semaphore"):
max_connections = self.pool_limits.hard_limit
if max_connections is None:
self._semaphore = None
else:
self._semaphore = asyncio.BoundedSemaphore(value=max_connections)
return self._semaphore
async def acquire(self) -> None:
if self.semaphore is None:
return
timeout = self.pool_limits.pool_timeout
try:
await asyncio.wait_for(self.semaphore.acquire(), timeout)
except asyncio.TimeoutError:
raise PoolTimeout()
def release(self) -> None:
if self.semaphore is None:
return
self.semaphore.release()
class AsyncioBackend(ConcurrencyBackend):
def __init__(self) -> None:
global SSL_MONKEY_PATCH_APPLIED
if not SSL_MONKEY_PATCH_APPLIED:
ssl_monkey_patch()
SSL_MONKEY_PATCH_APPLIED = True
@property
def loop(self) -> asyncio.AbstractEventLoop:
if not hasattr(self, "_loop"):
try:
self._loop = asyncio.get_event_loop()
except RuntimeError:
self._loop = asyncio.new_event_loop()
return self._loop
async def open_tcp_stream(
self,
hostname: str,
port: int,
ssl_context: typing.Optional[ssl.SSLContext],
timeout: TimeoutConfig,
) -> BaseTCPStream:
try:
stream_reader, stream_writer = await asyncio.wait_for( # type: ignore
asyncio.open_connection(hostname, port, ssl=ssl_context),
timeout.connect_timeout,
)
except asyncio.TimeoutError:
raise ConnectTimeout()
return TCPStream(
stream_reader=stream_reader, stream_writer=stream_writer, timeout=timeout
)
async def start_tls(
self,
stream: BaseTCPStream,
hostname: str,
ssl_context: ssl.SSLContext,
timeout: TimeoutConfig,
) -> BaseTCPStream:
loop = self.loop
if not hasattr(loop, "start_tls"): # pragma: no cover
raise NotImplementedError(
"asyncio.AbstractEventLoop.start_tls() is only available in Python 3.7+"
)
assert isinstance(stream, TCPStream)
stream_reader = asyncio.StreamReader()
protocol = asyncio.StreamReaderProtocol(stream_reader)
transport = stream.stream_writer.transport
loop_start_tls = loop.start_tls # type: ignore
transport = await asyncio.wait_for(
loop_start_tls(
transport=transport,
protocol=protocol,
sslcontext=ssl_context,
server_hostname=hostname,
),
timeout=timeout.connect_timeout,
)
stream_reader.set_transport(transport)
stream.stream_reader = stream_reader
stream.stream_writer = asyncio.StreamWriter(
transport=transport, protocol=protocol, reader=stream_reader, loop=loop
)
return stream
async def run_in_threadpool(
self, func: typing.Callable, *args: typing.Any, **kwargs: typing.Any
) -> typing.Any:
if kwargs:
# loop.run_in_executor doesn't accept 'kwargs', so bind them in here
func = functools.partial(func, **kwargs)
return await self.loop.run_in_executor(None, func, *args)
def run(
self, coroutine: typing.Callable, *args: typing.Any, **kwargs: typing.Any
) -> typing.Any:
loop = self.loop
if loop.is_running():
self._loop = asyncio.new_event_loop()
try:
return self.loop.run_until_complete(coroutine(*args, **kwargs))
finally:
self._loop = loop
def get_semaphore(self, limits: PoolLimits) -> BasePoolSemaphore:
return PoolSemaphore(limits)
def create_queue(self, max_size: int) -> BaseQueue:
return typing.cast(BaseQueue, asyncio.Queue(maxsize=max_size))
def create_event(self) -> BaseEvent:
return typing.cast(BaseEvent, asyncio.Event())
def background_manager(
self, coroutine: typing.Callable, *args: typing.Any
) -> "BackgroundManager":
return BackgroundManager(coroutine, args)
class BackgroundManager(BaseBackgroundManager):
def __init__(self, coroutine: typing.Callable, args: typing.Any) -> None:
self.coroutine = coroutine
self.args = args
async def __aenter__(self) -> "BackgroundManager":
loop = asyncio.get_event_loop()
self.task = loop.create_task(self.coroutine(*self.args))
return self
async def __aexit__(
self,
exc_type: typing.Type[BaseException] = None,
exc_value: BaseException = None,
traceback: TracebackType = None,
) -> None:
await self.task
if exc_type is None:
self.task.result()

View File

@ -1,196 +0,0 @@
import ssl
import typing
from types import TracebackType
from ..config import PoolLimits, TimeoutConfig
class TimeoutFlag:
"""
A timeout flag holds a state of either read-timeout or write-timeout mode.
We use this so that we can attempt both reads and writes concurrently, while
only enforcing timeouts in one direction.
During a request/response cycle we start in write-timeout mode.
Once we've sent a request fully, or once we start seeing a response,
then we switch to read-timeout mode instead.
"""
def __init__(self) -> None:
self.raise_on_read_timeout = False
self.raise_on_write_timeout = True
def set_read_timeouts(self) -> None:
"""
Set the flag to read-timeout mode.
"""
self.raise_on_read_timeout = True
self.raise_on_write_timeout = False
def set_write_timeouts(self) -> None:
"""
Set the flag to write-timeout mode.
"""
self.raise_on_read_timeout = False
self.raise_on_write_timeout = True
class BaseTCPStream:
"""
A TCP stream with read/write operations. Abstracts away any asyncio-specific
interfaces into a more generic base class, that we can use with alternate
backends, or for stand-alone test cases.
"""
def get_http_version(self) -> str:
raise NotImplementedError() # pragma: no cover
async def read(
self, n: int, timeout: TimeoutConfig = None, flag: typing.Any = None
) -> bytes:
raise NotImplementedError() # pragma: no cover
def write_no_block(self, data: bytes) -> None:
raise NotImplementedError() # pragma: no cover
async def write(self, data: bytes, timeout: TimeoutConfig = None) -> None:
raise NotImplementedError() # pragma: no cover
async def close(self) -> None:
raise NotImplementedError() # pragma: no cover
def is_connection_dropped(self) -> bool:
raise NotImplementedError() # pragma: no cover
class BaseQueue:
"""
A FIFO queue. Abstracts away any asyncio-specific interfaces.
"""
async def get(self) -> typing.Any:
raise NotImplementedError() # pragma: no cover
async def put(self, value: typing.Any) -> None:
raise NotImplementedError() # pragma: no cover
class BaseEvent:
"""
An event object. Abstracts away any asyncio-specific interfaces.
"""
def set(self) -> None:
raise NotImplementedError() # pragma: no cover
def is_set(self) -> bool:
raise NotImplementedError() # pragma: no cover
def clear(self) -> None:
raise NotImplementedError() # pragma: no cover
async def wait(self) -> None:
raise NotImplementedError() # pragma: no cover
class BasePoolSemaphore:
"""
A semaphore for use with connection pooling.
Abstracts away any asyncio-specific interfaces.
"""
async def acquire(self) -> None:
raise NotImplementedError() # pragma: no cover
def release(self) -> None:
raise NotImplementedError() # pragma: no cover
class ConcurrencyBackend:
async def open_tcp_stream(
self,
hostname: str,
port: int,
ssl_context: typing.Optional[ssl.SSLContext],
timeout: TimeoutConfig,
) -> BaseTCPStream:
raise NotImplementedError() # pragma: no cover
async def start_tls(
self,
stream: BaseTCPStream,
hostname: str,
ssl_context: ssl.SSLContext,
timeout: TimeoutConfig,
) -> BaseTCPStream:
raise NotImplementedError() # pragma: no cover
def get_semaphore(self, limits: PoolLimits) -> BasePoolSemaphore:
raise NotImplementedError() # pragma: no cover
async def run_in_threadpool(
self, func: typing.Callable, *args: typing.Any, **kwargs: typing.Any
) -> typing.Any:
raise NotImplementedError() # pragma: no cover
async def iterate_in_threadpool(self, iterator): # type: ignore
class IterationComplete(Exception):
pass
def next_wrapper(iterator): # type: ignore
try:
return next(iterator)
except StopIteration:
raise IterationComplete()
while True:
try:
yield await self.run_in_threadpool(next_wrapper, iterator)
except IterationComplete:
break
def run(
self, coroutine: typing.Callable, *args: typing.Any, **kwargs: typing.Any
) -> typing.Any:
raise NotImplementedError() # pragma: no cover
def iterate(self, async_iterator): # type: ignore
while True:
try:
yield self.run(async_iterator.__anext__)
except StopAsyncIteration:
break
def create_queue(self, max_size: int) -> BaseQueue:
raise NotImplementedError() # pragma: no cover
def create_event(self) -> BaseEvent:
raise NotImplementedError() # pragma: no cover
def background_manager(
self, coroutine: typing.Callable, *args: typing.Any
) -> "BaseBackgroundManager":
raise NotImplementedError() # pragma: no cover
class BaseBackgroundManager:
async def __aenter__(self) -> "BaseBackgroundManager":
raise NotImplementedError() # pragma: no cover
async def __aexit__(
self,
exc_type: typing.Type[BaseException] = None,
exc_value: BaseException = None,
traceback: TracebackType = None,
) -> None:
raise NotImplementedError() # pragma: no cover
async def close(self, exception: BaseException = None) -> None:
if exception is None:
await self.__aexit__(None, None, None)
else:
traceback = exception.__traceback__ # type: ignore
await self.__aexit__(type(exception), exception, traceback)

View File

@ -1,255 +0,0 @@
import functools
import math
import ssl
import typing
from types import TracebackType
import trio
from ..config import PoolLimits, TimeoutConfig
from ..exceptions import ConnectTimeout, PoolTimeout, ReadTimeout, WriteTimeout
from .base import (
BaseBackgroundManager,
BaseEvent,
BasePoolSemaphore,
BaseQueue,
BaseTCPStream,
ConcurrencyBackend,
TimeoutFlag,
)
def _or_inf(value: typing.Optional[float]) -> float:
return value if value is not None else float("inf")
class TCPStream(BaseTCPStream):
def __init__(
self,
stream: typing.Union[trio.SocketStream, trio.SSLStream],
timeout: TimeoutConfig,
) -> None:
self.stream = stream
self.timeout = timeout
self.write_buffer = b""
self.write_lock = trio.Lock()
def get_http_version(self) -> str:
if not isinstance(self.stream, trio.SSLStream):
return "HTTP/1.1"
ident = self.stream.selected_alpn_protocol()
if ident is None:
return "HTTP/1.1"
return "HTTP/2" if ident == "h2" else "HTTP/1.1"
async def read(
self, n: int, timeout: TimeoutConfig = None, flag: TimeoutFlag = None
) -> bytes:
if timeout is None:
timeout = self.timeout
while True:
# Check our flag at the first possible moment, and use a fine
# grained retry loop if we're not yet in read-timeout mode.
should_raise = flag is None or flag.raise_on_read_timeout
read_timeout = _or_inf(timeout.read_timeout if should_raise else 0.01)
with trio.move_on_after(read_timeout):
return await self.stream.receive_some(max_bytes=n)
if should_raise:
raise ReadTimeout() from None
def is_connection_dropped(self) -> bool:
# Adapted from: https://github.com/encode/httpx/pull/143#issuecomment-515202982
stream = self.stream
# Peek through any SSLStream wrappers to get the underlying SocketStream.
while hasattr(stream, "transport_stream"):
stream = stream.transport_stream
assert isinstance(stream, trio.SocketStream)
# Counter-intuitively, what we really want to know here is whether the socket is
# *readable*, i.e. whether it would return immediately with empty bytes if we
# called `.recv()` on it, indicating that the other end has closed the socket.
# See: https://github.com/encode/httpx/pull/143#issuecomment-515181778
return stream.socket.is_readable()
def write_no_block(self, data: bytes) -> None:
self.write_buffer += data
async def write(
self, data: bytes, timeout: TimeoutConfig = None, flag: TimeoutFlag = None
) -> None:
if self.write_buffer:
previous_data = self.write_buffer
# Reset before recursive call, otherwise we'll go through
# this branch indefinitely.
self.write_buffer = b""
try:
await self.write(previous_data, timeout=timeout, flag=flag)
except WriteTimeout:
self.writer_buffer = previous_data
raise
if not data:
return
if timeout is None:
timeout = self.timeout
write_timeout = _or_inf(timeout.write_timeout)
while True:
with trio.move_on_after(write_timeout):
async with self.write_lock:
await self.stream.send_all(data)
break
# We check our flag at the first possible moment, in order to
# allow us to suppress write timeouts, if we've since
# switched over to read-timeout mode.
should_raise = flag is None or flag.raise_on_write_timeout
if should_raise:
raise WriteTimeout() from None
async def close(self) -> None:
await self.stream.aclose()
class PoolSemaphore(BasePoolSemaphore):
def __init__(self, pool_limits: PoolLimits):
self.pool_limits = pool_limits
@property
def semaphore(self) -> typing.Optional[trio.Semaphore]:
if not hasattr(self, "_semaphore"):
max_connections = self.pool_limits.hard_limit
if max_connections is None:
self._semaphore = None
else:
self._semaphore = trio.Semaphore(
max_connections, max_value=max_connections
)
return self._semaphore
async def acquire(self) -> None:
if self.semaphore is None:
return
timeout = _or_inf(self.pool_limits.pool_timeout)
with trio.move_on_after(timeout):
await self.semaphore.acquire()
return
raise PoolTimeout()
def release(self) -> None:
if self.semaphore is None:
return
self.semaphore.release()
class TrioBackend(ConcurrencyBackend):
async def open_tcp_stream(
self,
hostname: str,
port: int,
ssl_context: typing.Optional[ssl.SSLContext],
timeout: TimeoutConfig,
) -> TCPStream:
connect_timeout = _or_inf(timeout.connect_timeout)
with trio.move_on_after(connect_timeout) as cancel_scope:
stream: trio.SocketStream = await trio.open_tcp_stream(hostname, port)
if ssl_context is not None:
stream = trio.SSLStream(stream, ssl_context, server_hostname=hostname)
await stream.do_handshake()
if cancel_scope.cancelled_caught:
raise ConnectTimeout()
return TCPStream(stream=stream, timeout=timeout)
async def run_in_threadpool(
self, func: typing.Callable, *args: typing.Any, **kwargs: typing.Any
) -> typing.Any:
return await trio.to_thread.run_sync(
functools.partial(func, **kwargs) if kwargs else func, *args
)
def run(
self, coroutine: typing.Callable, *args: typing.Any, **kwargs: typing.Any
) -> typing.Any:
return trio.run(
functools.partial(coroutine, **kwargs) if kwargs else coroutine, *args
)
def get_semaphore(self, limits: PoolLimits) -> BasePoolSemaphore:
return PoolSemaphore(limits)
def create_queue(self, max_size: int) -> BaseQueue:
return Queue(max_size=max_size)
def create_event(self) -> BaseEvent:
return Event()
def background_manager(
self, coroutine: typing.Callable, *args: typing.Any
) -> "BackgroundManager":
return BackgroundManager(coroutine, *args)
class Queue(BaseQueue):
def __init__(self, max_size: int) -> None:
self.send_channel, self.receive_channel = trio.open_memory_channel(math.inf)
async def get(self) -> typing.Any:
return await self.receive_channel.receive()
async def put(self, value: typing.Any) -> None:
await self.send_channel.send(value)
class Event(BaseEvent):
def __init__(self) -> None:
self._event = trio.Event()
def set(self) -> None:
self._event.set()
def is_set(self) -> bool:
return self._event.is_set()
async def wait(self) -> None:
await self._event.wait()
def clear(self) -> None:
# trio.Event.clear() was deprecated in Trio 0.12.
# https://github.com/python-trio/trio/issues/637
self._event = trio.Event()
class BackgroundManager(BaseBackgroundManager):
def __init__(self, coroutine: typing.Callable, *args: typing.Any) -> None:
self.coroutine = coroutine
self.args = args
self.nursery_manager = trio.open_nursery()
self.nursery: typing.Optional[trio.Nursery] = None
async def __aenter__(self) -> "BackgroundManager":
self.nursery = await self.nursery_manager.__aenter__()
self.nursery.start_soon(self.coroutine, *self.args)
return self
async def __aexit__(
self,
exc_type: typing.Type[BaseException] = None,
exc_value: BaseException = None,
traceback: TracebackType = None,
) -> None:
assert self.nursery is not None
await self.nursery_manager.__aexit__(exc_type, exc_value, traceback)

View File

@ -1,353 +0,0 @@
import os
import ssl
import typing
from pathlib import Path
import certifi
from .__version__ import __version__
from .utils import get_ca_bundle_from_env, get_logger
CertTypes = typing.Union[str, typing.Tuple[str, str], typing.Tuple[str, str, str]]
VerifyTypes = typing.Union[str, bool, ssl.SSLContext]
TimeoutTypes = typing.Union[float, typing.Tuple[float, float, float], "TimeoutConfig"]
HTTPVersionTypes = typing.Union[
str, typing.List[str], typing.Tuple[str], "HTTPVersionConfig"
]
USER_AGENT = f"python-httpx/{__version__}"
HTTP_VERSIONS_TO_ALPN_IDENTIFIERS = {"HTTP/1.1": "http/1.1", "HTTP/2": "h2"}
DEFAULT_CIPHERS = ":".join(
[
"ECDHE+AESGCM",
"ECDHE+CHACHA20",
"DHE+AESGCM",
"DHE+CHACHA20",
"ECDH+AESGCM",
"DH+AESGCM",
"ECDH+AES",
"DH+AES",
"RSA+AESGCM",
"RSA+AES",
"!aNULL",
"!eNULL",
"!MD5",
"!DSS",
]
)
logger = get_logger(__name__)
class SSLConfig:
"""
SSL Configuration.
"""
def __init__(
self,
*,
cert: CertTypes = None,
verify: VerifyTypes = True,
trust_env: bool = None,
):
self.cert = cert
# Allow passing in our own SSLContext object that's pre-configured.
# If you do this we assume that you want verify=True as well.
ssl_context = None
if isinstance(verify, ssl.SSLContext):
ssl_context = verify
verify = True
self._load_client_certs(ssl_context)
self.ssl_context: typing.Optional[ssl.SSLContext] = ssl_context
self.verify: typing.Union[str, bool] = verify
self.trust_env = trust_env
def __eq__(self, other: typing.Any) -> bool:
return (
isinstance(other, self.__class__)
and self.cert == other.cert
and self.verify == other.verify
)
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f"{class_name}(cert={self.cert}, verify={self.verify})"
def with_overrides(
self, cert: CertTypes = None, verify: VerifyTypes = None
) -> "SSLConfig":
cert = self.cert if cert is None else cert
verify = self.verify if verify is None else verify
if (cert == self.cert) and (verify == self.verify):
return self
return SSLConfig(cert=cert, verify=verify)
def load_ssl_context(
self, http_versions: "HTTPVersionConfig" = None
) -> ssl.SSLContext:
http_versions = HTTPVersionConfig() if http_versions is None else http_versions
logger.debug(
f"load_ssl_context "
f"verify={self.verify!r} "
f"cert={self.cert!r} "
f"trust_env={self.trust_env!r} "
f"http_versions={http_versions!r}"
)
if self.ssl_context is None:
self.ssl_context = (
self.load_ssl_context_verify(http_versions=http_versions)
if self.verify
else self.load_ssl_context_no_verify(http_versions=http_versions)
)
assert self.ssl_context is not None
return self.ssl_context
def load_ssl_context_no_verify(
self, http_versions: "HTTPVersionConfig"
) -> ssl.SSLContext:
"""
Return an SSL context for unverified connections.
"""
context = self._create_default_ssl_context(http_versions=http_versions)
context.verify_mode = ssl.CERT_NONE
context.check_hostname = False
return context
def load_ssl_context_verify(
self, http_versions: "HTTPVersionConfig"
) -> ssl.SSLContext:
"""
Return an SSL context for verified connections.
"""
if self.trust_env and self.verify is True:
ca_bundle = get_ca_bundle_from_env()
if ca_bundle is not None:
self.verify = ca_bundle # type: ignore
if isinstance(self.verify, bool):
ca_bundle_path = DEFAULT_CA_BUNDLE_PATH
elif Path(self.verify).exists():
ca_bundle_path = Path(self.verify)
else:
raise IOError(
"Could not find a suitable TLS CA certificate bundle, "
"invalid path: {}".format(self.verify)
)
context = self._create_default_ssl_context(http_versions=http_versions)
context.verify_mode = ssl.CERT_REQUIRED
context.check_hostname = True
# Signal to server support for PHA in TLS 1.3. Raises an
# AttributeError if only read-only access is implemented.
try:
context.post_handshake_auth = True # type: ignore
except AttributeError: # pragma: nocover
pass
# Disable using 'commonName' for SSLContext.check_hostname
# when the 'subjectAltName' extension isn't available.
try:
context.hostname_checks_common_name = False # type: ignore
except AttributeError: # pragma: nocover
pass
if ca_bundle_path.is_file():
logger.debug(f"load_verify_locations cafile={ca_bundle_path!s}")
context.load_verify_locations(cafile=str(ca_bundle_path))
elif ca_bundle_path.is_dir():
logger.debug(f"load_verify_locations capath={ca_bundle_path!s}")
context.load_verify_locations(capath=str(ca_bundle_path))
self._load_client_certs(context)
return context
def _create_default_ssl_context(
self, http_versions: "HTTPVersionConfig"
) -> ssl.SSLContext:
"""
Creates the default SSLContext object that's used for both verified
and unverified connections.
"""
context = ssl.SSLContext(ssl.PROTOCOL_TLS)
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3
context.options |= ssl.OP_NO_TLSv1
context.options |= ssl.OP_NO_TLSv1_1
context.options |= ssl.OP_NO_COMPRESSION
context.set_ciphers(DEFAULT_CIPHERS)
if ssl.HAS_ALPN:
context.set_alpn_protocols(http_versions.alpn_identifiers)
if hasattr(context, "keylog_filename"):
keylogfile = os.environ.get("SSLKEYLOGFILE")
if keylogfile and self.trust_env:
context.keylog_filename = keylogfile # type: ignore
return context
def _load_client_certs(self, ssl_context: ssl.SSLContext) -> None:
"""
Loads client certificates into our SSLContext object
"""
if self.cert is not None:
if isinstance(self.cert, str):
ssl_context.load_cert_chain(certfile=self.cert)
elif isinstance(self.cert, tuple) and len(self.cert) == 2:
ssl_context.load_cert_chain(certfile=self.cert[0], keyfile=self.cert[1])
elif isinstance(self.cert, tuple) and len(self.cert) == 3:
ssl_context.load_cert_chain(
certfile=self.cert[0],
keyfile=self.cert[1],
password=self.cert[2], # type: ignore
)
class TimeoutConfig:
"""
Timeout values.
"""
def __init__(
self,
timeout: TimeoutTypes = None,
*,
connect_timeout: float = None,
read_timeout: float = None,
write_timeout: float = None,
):
if timeout is None:
self.connect_timeout = connect_timeout
self.read_timeout = read_timeout
self.write_timeout = write_timeout
else:
# Specified as a single timeout value
assert connect_timeout is None
assert read_timeout is None
assert write_timeout is None
if isinstance(timeout, TimeoutConfig):
self.connect_timeout = timeout.connect_timeout
self.read_timeout = timeout.read_timeout
self.write_timeout = timeout.write_timeout
elif isinstance(timeout, tuple):
self.connect_timeout = timeout[0]
self.read_timeout = timeout[1]
self.write_timeout = timeout[2]
else:
self.connect_timeout = timeout
self.read_timeout = timeout
self.write_timeout = timeout
def __eq__(self, other: typing.Any) -> bool:
return (
isinstance(other, self.__class__)
and self.connect_timeout == other.connect_timeout
and self.read_timeout == other.read_timeout
and self.write_timeout == other.write_timeout
)
def __repr__(self) -> str:
class_name = self.__class__.__name__
if len({self.connect_timeout, self.read_timeout, self.write_timeout}) == 1:
return f"{class_name}(timeout={self.connect_timeout})"
return (
f"{class_name}(connect_timeout={self.connect_timeout}, "
f"read_timeout={self.read_timeout}, write_timeout={self.write_timeout})"
)
class HTTPVersionConfig:
"""
Configure which HTTP protocol versions are supported.
"""
def __init__(self, http_versions: HTTPVersionTypes = None):
if http_versions is None:
http_versions = ["HTTP/1.1", "HTTP/2"]
if isinstance(http_versions, str):
self.http_versions = {http_versions.upper()}
elif isinstance(http_versions, HTTPVersionConfig):
self.http_versions = http_versions.http_versions
elif isinstance(http_versions, typing.Iterable):
self.http_versions = {
version.upper() if isinstance(version, str) else version
for version in http_versions
}
else:
raise TypeError(
"HTTP version should be a string or list of strings, "
f"but got {type(http_versions)}"
)
for version in self.http_versions:
if version not in ("HTTP/1.1", "HTTP/2"):
raise ValueError(f"Unsupported HTTP version {version!r}.")
if not self.http_versions:
raise ValueError("HTTP versions cannot be an empty list.")
@property
def alpn_identifiers(self) -> typing.List[str]:
"""
Returns a list of supported ALPN identifiers. (One or more of "http/1.1", "h2").
"""
return [
HTTP_VERSIONS_TO_ALPN_IDENTIFIERS[version] for version in self.http_versions
]
def __repr__(self) -> str:
class_name = self.__class__.__name__
value = sorted(list(self.http_versions))
return f"{class_name}({value!r})"
class PoolLimits:
"""
Limits on the number of connections in a connection pool.
"""
def __init__(
self,
*,
soft_limit: int = None,
hard_limit: int = None,
pool_timeout: float = None,
):
self.soft_limit = soft_limit
self.hard_limit = hard_limit
self.pool_timeout = pool_timeout
def __eq__(self, other: typing.Any) -> bool:
return (
isinstance(other, self.__class__)
and self.soft_limit == other.soft_limit
and self.hard_limit == other.hard_limit
and self.pool_timeout == other.pool_timeout
)
def __repr__(self) -> str:
class_name = self.__class__.__name__
return (
f"{class_name}(soft_limit={self.soft_limit}, "
f"hard_limit={self.hard_limit}, pool_timeout={self.pool_timeout})"
)
DEFAULT_SSL_CONFIG = SSLConfig(cert=None, verify=True)
DEFAULT_TIMEOUT_CONFIG = TimeoutConfig(timeout=5.0)
DEFAULT_POOL_LIMITS = PoolLimits(soft_limit=10, hard_limit=100, pool_timeout=5.0)
DEFAULT_CA_BUNDLE_PATH = Path(certifi.where())
DEFAULT_MAX_REDIRECTS = 20

View File

@ -1,228 +0,0 @@
"""
Handlers for Content-Encoding.
See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
"""
import codecs
import typing
import zlib
import chardet
from .exceptions import DecodingError
try:
import brotli
except ImportError: # pragma: nocover
brotli = None
class Decoder:
def decode(self, data: bytes) -> bytes:
raise NotImplementedError() # pragma: nocover
def flush(self) -> bytes:
raise NotImplementedError() # pragma: nocover
class IdentityDecoder(Decoder):
"""
Handle unencoded data.
"""
def decode(self, data: bytes) -> bytes:
return data
def flush(self) -> bytes:
return b""
class DeflateDecoder(Decoder):
"""
Handle 'deflate' decoding.
See: https://stackoverflow.com/questions/1838699
"""
def __init__(self) -> None:
self.decompressor = zlib.decompressobj(-zlib.MAX_WBITS)
def decode(self, data: bytes) -> bytes:
try:
return self.decompressor.decompress(data)
except zlib.error as exc:
raise DecodingError from exc
def flush(self) -> bytes:
try:
return self.decompressor.flush()
except zlib.error as exc: # pragma: nocover
raise DecodingError from exc
class GZipDecoder(Decoder):
"""
Handle 'gzip' decoding.
See: https://stackoverflow.com/questions/1838699
"""
def __init__(self) -> None:
self.decompressor = zlib.decompressobj(zlib.MAX_WBITS | 16)
def decode(self, data: bytes) -> bytes:
try:
return self.decompressor.decompress(data)
except zlib.error as exc:
raise DecodingError from exc
def flush(self) -> bytes:
try:
return self.decompressor.flush()
except zlib.error as exc: # pragma: nocover
raise DecodingError from exc
class BrotliDecoder(Decoder):
"""
Handle 'brotli' decoding.
Requires `pip install brotlipy`. See: https://brotlipy.readthedocs.io/
or `pip install brotli`. See https://github.com/google/brotli
Supports both 'brotlipy' and 'Brotli' packages since they share an import
name. The top branches are for 'brotlipy' and bottom branches for 'Brotli'
"""
def __init__(self) -> None:
assert (
brotli is not None
), "The 'brotlipy' or 'brotli' library must be installed to use 'BrotliDecoder'"
self.decompressor = brotli.Decompressor()
self.seen_data = False
def decode(self, data: bytes) -> bytes:
if not data:
return b""
self.seen_data = True
try:
if hasattr(self.decompressor, "decompress"):
return self.decompressor.decompress(data)
return self.decompressor.process(data) # pragma: nocover
except brotli.error as exc:
raise DecodingError from exc
def flush(self) -> bytes:
if not self.seen_data:
return b""
try:
if hasattr(self.decompressor, "finish"):
self.decompressor.finish()
return b""
except brotli.error as exc: # pragma: nocover
raise DecodingError from exc
class MultiDecoder(Decoder):
"""
Handle the case where multiple encodings have been applied.
"""
def __init__(self, children: typing.Sequence[Decoder]) -> None:
"""
'children' should be a sequence of decoders in the order in which
each was applied.
"""
# Note that we reverse the order for decoding.
self.children = list(reversed(children))
def decode(self, data: bytes) -> bytes:
for child in self.children:
data = child.decode(data)
return data
def flush(self) -> bytes:
data = b""
for child in self.children:
data = child.decode(data) + child.flush()
return data
class TextDecoder:
"""
Handles incrementally decoding bytes into text
"""
def __init__(self, encoding: typing.Optional[str] = None):
self.decoder: typing.Optional[codecs.IncrementalDecoder] = (
None if encoding is None else codecs.getincrementaldecoder(encoding)()
)
self.detector = chardet.universaldetector.UniversalDetector()
# This buffer is only needed if 'decoder' is 'None'
# we want to trigger errors if data is getting added to
# our internal buffer for some silly reason while
# a decoder is discovered.
self.buffer: typing.Optional[bytearray] = None if self.decoder else bytearray()
def decode(self, data: bytes) -> str:
try:
if self.decoder is not None:
text = self.decoder.decode(data)
else:
assert self.buffer is not None
text = ""
self.detector.feed(data)
self.buffer += data
# Should be more than enough data to process, we don't
# want to buffer too long as chardet will wait until
# detector.close() is used to give back common
# encodings like 'utf-8'.
if len(self.buffer) >= 4096:
self.decoder = codecs.getincrementaldecoder(
self._detector_result()
)()
text = self.decoder.decode(bytes(self.buffer), False)
self.buffer = None
return text
except UnicodeDecodeError: # pragma: nocover
raise DecodingError() from None
def flush(self) -> str:
try:
if self.decoder is None:
# Empty string case as chardet is guaranteed to not have a guess.
assert self.buffer is not None
if len(self.buffer) == 0:
return ""
return bytes(self.buffer).decode(self._detector_result())
return self.decoder.decode(b"", True)
except UnicodeDecodeError: # pragma: nocover
raise DecodingError() from None
def _detector_result(self) -> str:
self.detector.close()
result = self.detector.result["encoding"]
if not result: # pragma: nocover
raise DecodingError("Unable to determine encoding of content")
return result
SUPPORTED_DECODERS = {
"identity": IdentityDecoder,
"gzip": GZipDecoder,
"deflate": DeflateDecoder,
"br": BrotliDecoder,
}
if brotli is None:
SUPPORTED_DECODERS.pop("br") # pragma: nocover
ACCEPT_ENCODING = ", ".join(
[key for key in SUPPORTED_DECODERS.keys() if key != "identity"]
)

View File

@ -1,4 +0,0 @@
"""
Dispatch classes handle the raw network connections and the implementation
details of making the HTTP request and receiving the response.
"""

View File

@ -1,198 +0,0 @@
import typing
from ..concurrency.asyncio import AsyncioBackend
from ..concurrency.base import ConcurrencyBackend
from ..config import CertTypes, TimeoutTypes, VerifyTypes
from ..models import AsyncRequest, AsyncResponse
from ..utils import MessageLoggerASGIMiddleware, get_logger
from .base import AsyncDispatcher
logger = get_logger(__name__)
class ASGIDispatch(AsyncDispatcher):
"""
A custom dispatcher that handles sending requests directly to an ASGI app.
The simplest way to use this functionality is to use the `app` argument.
This will automatically infer if 'app' is a WSGI or an ASGI application,
and will setup an appropriate dispatch class:
```
client = httpx.Client(app=app)
```
Alternatively, you can setup the dispatch instance explicitly.
This allows you to include any additional configuration arguments specific
to the ASGIDispatch class:
```
dispatch = httpx.ASGIDispatch(
app=app,
root_path="/submount",
client=("1.2.3.4", 123)
)
client = httpx.Client(dispatch=dispatch)
```
Arguments:
* `app` - The ASGI application.
* `raise_app_exceptions` - Boolean indicating if exceptions in the application
should be raised. Default to `True`. Can be set to `False` for use cases
such as testing the content of a client 500 response.
* `root_path` - The root path on which the ASGI application should be mounted.
* `client` - A two-tuple indicating the client IP and port of incoming requests.
```
"""
def __init__(
self,
app: typing.Callable,
raise_app_exceptions: bool = True,
root_path: str = "",
client: typing.Tuple[str, int] = ("127.0.0.1", 123),
backend: ConcurrencyBackend = None,
) -> None:
self.app = app
self.raise_app_exceptions = raise_app_exceptions
self.root_path = root_path
self.client = client
self.backend = AsyncioBackend() if backend is None else backend
async def send(
self,
request: AsyncRequest,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> AsyncResponse:
scope = {
"type": "http",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"method": request.method,
"headers": request.headers.raw,
"scheme": request.url.scheme,
"path": request.url.path,
"query_string": request.url.query.encode("ascii"),
"server": request.url.host,
"client": self.client,
"root_path": self.root_path,
}
app = MessageLoggerASGIMiddleware(self.app, logger=logger)
app_exc = None
status_code = None
headers = None
response_started_or_failed = self.backend.create_event()
response_body = BodyIterator(self.backend)
request_stream = request.stream()
async def receive() -> dict:
try:
body = await request_stream.__anext__()
except StopAsyncIteration:
return {"type": "http.request", "body": b"", "more_body": False}
return {"type": "http.request", "body": body, "more_body": True}
async def send(message: dict) -> None:
nonlocal status_code, headers
if message["type"] == "http.response.start":
status_code = message["status"]
headers = message.get("headers", [])
response_started_or_failed.set()
elif message["type"] == "http.response.body":
body = message.get("body", b"")
more_body = message.get("more_body", False)
if body and request.method != "HEAD":
await response_body.put(body)
if not more_body:
await response_body.mark_as_done()
async def run_app() -> None:
nonlocal app_exc
try:
await app(scope, receive, send)
except Exception as exc:
app_exc = exc
finally:
await response_body.mark_as_done()
response_started_or_failed.set()
# Using the background manager here *works*, but it is weak design because
# the background task isn't strictly context-managed.
# We could consider refactoring the other uses of this abstraction
# (mainly sending/receiving request/response data in h11 and h2 dispatchers),
# and see if that allows us to come back here and refactor things out.
background = await self.backend.background_manager(run_app).__aenter__()
await response_started_or_failed.wait()
if app_exc is not None and self.raise_app_exceptions:
await background.close(app_exc)
raise app_exc
assert status_code is not None, "application did not return a response."
assert headers is not None
async def on_close() -> None:
await response_body.drain()
await background.close(app_exc)
if app_exc is not None and self.raise_app_exceptions:
raise app_exc
return AsyncResponse(
status_code=status_code,
http_version="HTTP/1.1",
headers=headers,
content=response_body.iterate(),
on_close=on_close,
request=request,
)
class BodyIterator:
"""
Provides a byte-iterator interface that the client can use to
ingest the response content from.
"""
def __init__(self, backend: ConcurrencyBackend) -> None:
self._queue = backend.create_queue(max_size=1)
self._done = object()
async def iterate(self) -> typing.AsyncIterator[bytes]:
"""
A byte-iterator, used by the client to consume the response body.
"""
while True:
data = await self._queue.get()
if data is self._done:
break
assert isinstance(data, bytes)
yield data
async def drain(self) -> None:
"""
Drain any remaining body, in order to allow any blocked `put()` calls
to complete.
"""
async for chunk in self.iterate():
pass # pragma: no cover
async def put(self, data: bytes) -> None:
"""
Used by the server to add data to the response body.
"""
await self._queue.put(data)
async def mark_as_done(self) -> None:
"""
Used by the server to signal the end of the response body.
"""
await self._queue.put(self._done)

View File

@ -1,111 +0,0 @@
import typing
from types import TracebackType
from ..config import CertTypes, TimeoutTypes, VerifyTypes
from ..models import (
AsyncRequest,
AsyncRequestData,
AsyncResponse,
HeaderTypes,
QueryParamTypes,
Request,
RequestData,
Response,
URLTypes,
)
class AsyncDispatcher:
"""
Base class for async dispatcher classes, that handle sending the request.
Stubs out the interface, as well as providing a `.request()` convenience
implementation, to make it easy to use or test stand-alone dispatchers,
without requiring a complete `Client` instance.
"""
async def request(
self,
method: str,
url: URLTypes,
*,
data: AsyncRequestData = b"",
params: QueryParamTypes = None,
headers: HeaderTypes = None,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> AsyncResponse:
request = AsyncRequest(method, url, data=data, params=params, headers=headers)
return await self.send(request, verify=verify, cert=cert, timeout=timeout)
async def send(
self,
request: AsyncRequest,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> AsyncResponse:
raise NotImplementedError() # pragma: nocover
async def close(self) -> None:
pass # pragma: nocover
async def __aenter__(self) -> "AsyncDispatcher":
return self
async def __aexit__(
self,
exc_type: typing.Type[BaseException] = None,
exc_value: BaseException = None,
traceback: TracebackType = None,
) -> None:
await self.close()
class Dispatcher:
"""
Base class for synchronous dispatcher classes, that handle sending the request.
Stubs out the interface, as well as providing a `.request()` convenience
implementation, to make it easy to use or test stand-alone dispatchers,
without requiring a complete `Client` instance.
"""
def request(
self,
method: str,
url: URLTypes,
*,
data: RequestData = b"",
params: QueryParamTypes = None,
headers: HeaderTypes = None,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> Response:
request = Request(method, url, data=data, params=params, headers=headers)
return self.send(request, verify=verify, cert=cert, timeout=timeout)
def send(
self,
request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> Response:
raise NotImplementedError() # pragma: nocover
def close(self) -> None:
pass # pragma: nocover
def __enter__(self) -> "Dispatcher":
return self
def __exit__(
self,
exc_type: typing.Type[BaseException] = None,
exc_value: BaseException = None,
traceback: TracebackType = None,
) -> None:
self.close()

View File

@ -1,139 +0,0 @@
import functools
import ssl
import typing
from ..concurrency.asyncio import AsyncioBackend
from ..concurrency.base import ConcurrencyBackend
from ..config import (
DEFAULT_TIMEOUT_CONFIG,
CertTypes,
HTTPVersionConfig,
HTTPVersionTypes,
SSLConfig,
TimeoutConfig,
TimeoutTypes,
VerifyTypes,
)
from ..models import AsyncRequest, AsyncResponse, Origin
from ..utils import get_logger
from .base import AsyncDispatcher
from .http2 import HTTP2Connection
from .http11 import HTTP11Connection
# Callback signature: async def callback(conn: HTTPConnection) -> None
ReleaseCallback = typing.Callable[["HTTPConnection"], typing.Awaitable[None]]
logger = get_logger(__name__)
class HTTPConnection(AsyncDispatcher):
def __init__(
self,
origin: typing.Union[str, Origin],
verify: VerifyTypes = True,
cert: CertTypes = None,
trust_env: bool = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
http_versions: HTTPVersionTypes = None,
backend: ConcurrencyBackend = None,
release_func: typing.Optional[ReleaseCallback] = None,
):
self.origin = Origin(origin) if isinstance(origin, str) else origin
self.ssl = SSLConfig(cert=cert, verify=verify, trust_env=trust_env)
self.timeout = TimeoutConfig(timeout)
self.http_versions = HTTPVersionConfig(http_versions)
self.backend = AsyncioBackend() if backend is None else backend
self.release_func = release_func
self.h11_connection = None # type: typing.Optional[HTTP11Connection]
self.h2_connection = None # type: typing.Optional[HTTP2Connection]
async def send(
self,
request: AsyncRequest,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> AsyncResponse:
if self.h11_connection is None and self.h2_connection is None:
await self.connect(verify=verify, cert=cert, timeout=timeout)
if self.h2_connection is not None:
response = await self.h2_connection.send(request, timeout=timeout)
else:
assert self.h11_connection is not None
response = await self.h11_connection.send(request, timeout=timeout)
return response
async def connect(
self,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> None:
ssl = self.ssl.with_overrides(verify=verify, cert=cert)
timeout = self.timeout if timeout is None else TimeoutConfig(timeout)
host = self.origin.host
port = self.origin.port
ssl_context = await self.get_ssl_context(ssl)
if self.release_func is None:
on_release = None
else:
on_release = functools.partial(self.release_func, self)
logger.debug(f"start_connect host={host!r} port={port!r} timeout={timeout!r}")
stream = await self.backend.open_tcp_stream(host, port, ssl_context, timeout)
http_version = stream.get_http_version()
logger.debug(f"connected http_version={http_version!r}")
if http_version == "HTTP/2":
self.h2_connection = HTTP2Connection(
stream, self.backend, on_release=on_release
)
else:
assert http_version == "HTTP/1.1"
self.h11_connection = HTTP11Connection(
stream, self.backend, on_release=on_release
)
async def get_ssl_context(self, ssl: SSLConfig) -> typing.Optional[ssl.SSLContext]:
if not self.origin.is_ssl:
return None
# Run the SSL loading in a threadpool, since it may make disk accesses.
return await self.backend.run_in_threadpool(
ssl.load_ssl_context, self.http_versions
)
async def close(self) -> None:
logger.debug("close_connection")
if self.h2_connection is not None:
await self.h2_connection.close()
elif self.h11_connection is not None:
await self.h11_connection.close()
@property
def is_http2(self) -> bool:
return self.h2_connection is not None
@property
def is_closed(self) -> bool:
if self.h2_connection is not None:
return self.h2_connection.is_closed
else:
assert self.h11_connection is not None
return self.h11_connection.is_closed
def is_connection_dropped(self) -> bool:
if self.h2_connection is not None:
return self.h2_connection.is_connection_dropped()
else:
assert self.h11_connection is not None
return self.h11_connection.is_connection_dropped()
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f"{class_name}(origin={self.origin!r})"

View File

@ -1,186 +0,0 @@
import typing
from ..concurrency.asyncio import AsyncioBackend
from ..concurrency.base import ConcurrencyBackend
from ..config import (
DEFAULT_POOL_LIMITS,
DEFAULT_TIMEOUT_CONFIG,
CertTypes,
HTTPVersionTypes,
PoolLimits,
TimeoutTypes,
VerifyTypes,
)
from ..models import AsyncRequest, AsyncResponse, Origin
from ..utils import get_logger
from .base import AsyncDispatcher
from .connection import HTTPConnection
CONNECTIONS_DICT = typing.Dict[Origin, typing.List[HTTPConnection]]
logger = get_logger(__name__)
class ConnectionStore:
"""
We need to maintain collections of connections in a way that allows us to:
* Lookup connections by origin.
* Iterate over connections by insertion time.
* Return the total number of connections.
"""
def __init__(self) -> None:
self.all: typing.Dict[HTTPConnection, float] = {}
self.by_origin: typing.Dict[Origin, typing.Dict[HTTPConnection, float]] = {}
def pop_by_origin(
self, origin: Origin, http2_only: bool = False
) -> typing.Optional[HTTPConnection]:
try:
connections = self.by_origin[origin]
except KeyError:
return None
connection = next(reversed(list(connections.keys())))
if http2_only and not connection.is_http2:
return None
del connections[connection]
if not connections:
del self.by_origin[origin]
del self.all[connection]
return connection
def add(self, connection: HTTPConnection) -> None:
self.all[connection] = 0.0
try:
self.by_origin[connection.origin][connection] = 0.0
except KeyError:
self.by_origin[connection.origin] = {connection: 0.0}
def remove(self, connection: HTTPConnection) -> None:
del self.all[connection]
del self.by_origin[connection.origin][connection]
if not self.by_origin[connection.origin]:
del self.by_origin[connection.origin]
def clear(self) -> None:
self.all.clear()
self.by_origin.clear()
def __iter__(self) -> typing.Iterator[HTTPConnection]:
return iter(self.all.keys())
def __len__(self) -> int:
return len(self.all)
class ConnectionPool(AsyncDispatcher):
def __init__(
self,
*,
verify: VerifyTypes = True,
cert: CertTypes = None,
trust_env: bool = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
http_versions: HTTPVersionTypes = None,
backend: ConcurrencyBackend = None,
):
self.verify = verify
self.cert = cert
self.timeout = timeout
self.pool_limits = pool_limits
self.http_versions = http_versions
self.is_closed = False
self.trust_env = trust_env
self.keepalive_connections = ConnectionStore()
self.active_connections = ConnectionStore()
self.backend = AsyncioBackend() if backend is None else backend
self.max_connections = self.backend.get_semaphore(pool_limits)
@property
def num_connections(self) -> int:
return len(self.keepalive_connections) + len(self.active_connections)
async def send(
self,
request: AsyncRequest,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> AsyncResponse:
connection = await self.acquire_connection(origin=request.url.origin)
try:
response = await connection.send(
request, verify=verify, cert=cert, timeout=timeout
)
except BaseException as exc:
self.active_connections.remove(connection)
self.max_connections.release()
raise exc
return response
async def acquire_connection(self, origin: Origin) -> HTTPConnection:
logger.debug(f"acquire_connection origin={origin!r}")
connection = self.pop_connection(origin)
if connection is None:
await self.max_connections.acquire()
connection = HTTPConnection(
origin,
verify=self.verify,
cert=self.cert,
timeout=self.timeout,
http_versions=self.http_versions,
backend=self.backend,
release_func=self.release_connection,
trust_env=self.trust_env,
)
logger.debug(f"new_connection connection={connection!r}")
else:
logger.debug(f"reuse_connection connection={connection!r}")
self.active_connections.add(connection)
return connection
async def release_connection(self, connection: HTTPConnection) -> None:
logger.debug(f"release_connection connection={connection!r}")
if connection.is_closed:
self.active_connections.remove(connection)
self.max_connections.release()
elif (
self.pool_limits.soft_limit is not None
and self.num_connections > self.pool_limits.soft_limit
):
self.active_connections.remove(connection)
self.max_connections.release()
await connection.close()
else:
self.active_connections.remove(connection)
self.keepalive_connections.add(connection)
async def close(self) -> None:
self.is_closed = True
connections = list(self.keepalive_connections)
self.keepalive_connections.clear()
for connection in connections:
await connection.close()
def pop_connection(self, origin: Origin) -> typing.Optional[HTTPConnection]:
connection = self.active_connections.pop_by_origin(origin, http2_only=True)
if connection is None:
connection = self.keepalive_connections.pop_by_origin(origin)
if connection is not None and connection.is_connection_dropped():
self.max_connections.release()
connection = None
return connection

View File

@ -1,208 +0,0 @@
import typing
import h11
from ..concurrency.base import BaseTCPStream, ConcurrencyBackend, TimeoutFlag
from ..config import TimeoutConfig, TimeoutTypes
from ..models import AsyncRequest, AsyncResponse
from ..utils import get_logger
H11Event = typing.Union[
h11.Request,
h11.Response,
h11.InformationalResponse,
h11.Data,
h11.EndOfMessage,
h11.ConnectionClosed,
]
# Callback signature: async def callback() -> None
# In practice the callback will be a functools partial, which binds
# the `ConnectionPool.release_connection(conn: HTTPConnection)` method.
OnReleaseCallback = typing.Callable[[], typing.Awaitable[None]]
logger = get_logger(__name__)
class HTTP11Connection:
READ_NUM_BYTES = 4096
def __init__(
self,
stream: BaseTCPStream,
backend: ConcurrencyBackend,
on_release: typing.Optional[OnReleaseCallback] = None,
):
self.stream = stream
self.backend = backend
self.on_release = on_release
self.h11_state = h11.Connection(our_role=h11.CLIENT)
self.timeout_flag = TimeoutFlag()
async def send(
self, request: AsyncRequest, timeout: TimeoutTypes = None
) -> AsyncResponse:
timeout = None if timeout is None else TimeoutConfig(timeout)
await self._send_request(request, timeout)
task, args = self._send_request_data, [request.stream(), timeout]
async with self.backend.background_manager(task, *args):
http_version, status_code, headers = await self._receive_response(timeout)
content = self._receive_response_data(timeout)
return AsyncResponse(
status_code=status_code,
http_version=http_version,
headers=headers,
content=content,
on_close=self.response_closed,
request=request,
)
async def close(self) -> None:
event = h11.ConnectionClosed()
try:
logger.debug(f"send_event event={event!r}")
self.h11_state.send(event)
except h11.LocalProtocolError: # pragma: no cover
# Premature client disconnect
pass
await self.stream.close()
async def _send_request(
self, request: AsyncRequest, timeout: TimeoutConfig = None
) -> None:
"""
Send the request method, URL, and headers to the network.
"""
logger.debug(
f"send_headers method={request.method!r} "
f"target={request.url.full_path!r} "
f"headers={request.headers!r}"
)
method = request.method.encode("ascii")
target = request.url.full_path.encode("ascii")
headers = request.headers.raw
event = h11.Request(method=method, target=target, headers=headers)
await self._send_event(event, timeout)
async def _send_request_data(
self, data: typing.AsyncIterator[bytes], timeout: TimeoutConfig = None
) -> None:
"""
Send the request body to the network.
"""
try:
# Send the request body.
async for chunk in data:
logger.debug(f"send_data data=Data(<{len(chunk)} bytes>)")
event = h11.Data(data=chunk)
await self._send_event(event, timeout)
# Finalize sending the request.
event = h11.EndOfMessage()
await self._send_event(event, timeout)
except OSError: # pragma: nocover
# Once we've sent the initial part of the request we don't actually
# care about connection errors that occur when sending the body.
# Ignore these, and defer to any exceptions on reading the response.
self.h11_state.send_failed()
finally:
# Once we've sent the request, we enable read timeouts.
self.timeout_flag.set_read_timeouts()
async def _send_event(self, event: H11Event, timeout: TimeoutConfig = None) -> None:
"""
Send a single `h11` event to the network, waiting for the data to
drain before returning.
"""
bytes_to_send = self.h11_state.send(event)
await self.stream.write(bytes_to_send, timeout)
async def _receive_response(
self, timeout: TimeoutConfig = None
) -> typing.Tuple[str, int, typing.List[typing.Tuple[bytes, bytes]]]:
"""
Read the response status and headers from the network.
"""
while True:
event = await self._receive_event(timeout)
# As soon as we start seeing response events, we should enable
# read timeouts, if we haven't already.
self.timeout_flag.set_read_timeouts()
if isinstance(event, h11.InformationalResponse):
continue
else:
assert isinstance(event, h11.Response)
break # pragma: no cover
http_version = "HTTP/%s" % event.http_version.decode("latin-1", errors="ignore")
return http_version, event.status_code, event.headers
async def _receive_response_data(
self, timeout: TimeoutConfig = None
) -> typing.AsyncIterator[bytes]:
"""
Read the response data from the network.
"""
while True:
event = await self._receive_event(timeout)
if isinstance(event, h11.Data):
yield bytes(event.data)
else:
assert isinstance(event, h11.EndOfMessage) or event is h11.PAUSED
break # pragma: no cover
async def _receive_event(self, timeout: TimeoutConfig = None) -> H11Event:
"""
Read a single `h11` event, reading more data from the network if needed.
"""
while True:
event = self.h11_state.next_event()
if isinstance(event, h11.Data):
logger.debug(f"receive_event event=Data(<{len(event.data)} bytes>)")
else:
logger.debug(f"receive_event event={event!r}")
if event is h11.NEED_DATA:
try:
data = await self.stream.read(
self.READ_NUM_BYTES, timeout, flag=self.timeout_flag
)
except OSError: # pragma: nocover
data = b""
self.h11_state.receive_data(data)
else:
assert event is not h11.NEED_DATA
break # pragma: no cover
return event
async def response_closed(self) -> None:
logger.debug(
f"response_closed "
f"our_state={self.h11_state.our_state!r} "
f"their_state={self.h11_state.their_state}"
)
if (
self.h11_state.our_state is h11.DONE
and self.h11_state.their_state is h11.DONE
):
# Get ready for another request/response cycle.
self.h11_state.start_next_cycle()
self.timeout_flag.set_write_timeouts()
else:
await self.close()
if self.on_release is not None:
await self.on_release()
@property
def is_closed(self) -> bool:
return self.h11_state.our_state in (h11.CLOSED, h11.ERROR)
def is_connection_dropped(self) -> bool:
return self.stream.is_connection_dropped()

View File

@ -1,251 +0,0 @@
import functools
import typing
import h2.connection
import h2.events
from h2.settings import SettingCodes, Settings
from ..concurrency.base import BaseEvent, BaseTCPStream, ConcurrencyBackend, TimeoutFlag
from ..config import TimeoutConfig, TimeoutTypes
from ..exceptions import ProtocolError
from ..models import AsyncRequest, AsyncResponse
from ..utils import get_logger
logger = get_logger(__name__)
class HTTP2Connection:
READ_NUM_BYTES = 4096
def __init__(
self,
stream: BaseTCPStream,
backend: ConcurrencyBackend,
on_release: typing.Callable = None,
):
self.stream = stream
self.backend = backend
self.on_release = on_release
self.h2_state = h2.connection.H2Connection()
self.events = {} # type: typing.Dict[int, typing.List[h2.events.Event]]
self.timeout_flags = {} # type: typing.Dict[int, TimeoutFlag]
self.initialized = False
self.window_update_received = {} # type: typing.Dict[int, BaseEvent]
async def send(
self, request: AsyncRequest, timeout: TimeoutTypes = None
) -> AsyncResponse:
timeout = None if timeout is None else TimeoutConfig(timeout)
# Start sending the request.
if not self.initialized:
self.initiate_connection()
stream_id = await self.send_headers(request, timeout)
self.events[stream_id] = []
self.timeout_flags[stream_id] = TimeoutFlag()
self.window_update_received[stream_id] = self.backend.create_event()
task, args = self.send_request_data, [stream_id, request.stream(), timeout]
async with self.backend.background_manager(task, *args):
status_code, headers = await self.receive_response(stream_id, timeout)
content = self.body_iter(stream_id, timeout)
on_close = functools.partial(self.response_closed, stream_id=stream_id)
return AsyncResponse(
status_code=status_code,
http_version="HTTP/2",
headers=headers,
content=content,
on_close=on_close,
request=request,
)
async def close(self) -> None:
await self.stream.close()
def initiate_connection(self) -> None:
# Need to set these manually here instead of manipulating via
# __setitem__() otherwise the H2Connection will emit SettingsUpdate
# frames in addition to sending the undesired defaults.
self.h2_state.local_settings = Settings(
client=True,
initial_values={
# Disable PUSH_PROMISE frames from the server since we don't do anything
# with them for now. Maybe when we support caching?
SettingCodes.ENABLE_PUSH: 0,
# These two are taken from h2 for safe defaults
SettingCodes.MAX_CONCURRENT_STREAMS: 100,
SettingCodes.MAX_HEADER_LIST_SIZE: 65536,
},
)
# Some websites (*cough* Yahoo *cough*) balk at this setting being
# present in the initial handshake since it's not defined in the original
# RFC despite the RFC mandating ignoring settings you don't know about.
del self.h2_state.local_settings[
h2.settings.SettingCodes.ENABLE_CONNECT_PROTOCOL
]
self.h2_state.initiate_connection()
data_to_send = self.h2_state.data_to_send()
self.stream.write_no_block(data_to_send)
self.initialized = True
async def send_headers(
self, request: AsyncRequest, timeout: TimeoutConfig = None
) -> int:
stream_id = self.h2_state.get_next_available_stream_id()
headers = [
(b":method", request.method.encode("ascii")),
(b":authority", request.url.authority.encode("ascii")),
(b":scheme", request.url.scheme.encode("ascii")),
(b":path", request.url.full_path.encode("ascii")),
] + [(k, v) for k, v in request.headers.raw if k != b"host"]
logger.debug(
f"send_headers "
f"stream_id={stream_id} "
f"method={request.method!r} "
f"target={request.url.full_path!r} "
f"headers={headers!r}"
)
self.h2_state.send_headers(stream_id, headers)
data_to_send = self.h2_state.data_to_send()
await self.stream.write(data_to_send, timeout)
return stream_id
async def send_request_data(
self,
stream_id: int,
stream: typing.AsyncIterator[bytes],
timeout: TimeoutConfig = None,
) -> None:
try:
async for data in stream:
await self.send_data(stream_id, data, timeout)
await self.end_stream(stream_id, timeout)
finally:
# Once we've sent the request we should enable read timeouts.
self.timeout_flags[stream_id].set_read_timeouts()
async def send_data(
self, stream_id: int, data: bytes, timeout: TimeoutConfig = None
) -> None:
while data:
# The data will be divided into frames to send based on the flow control
# window and the maximum frame size. Because the flow control window
# can decrease in size, even possibly to zero, this will loop until all the
# data is sent. In http2 specification:
# https://tools.ietf.org/html/rfc7540#section-6.9
flow_control = self.h2_state.local_flow_control_window(stream_id)
chunk_size = min(
len(data), flow_control, self.h2_state.max_outbound_frame_size
)
if chunk_size == 0:
# this means that the flow control window is 0 (either for the stream
# or the connection one), and no data can be sent until the flow control
# window is updated.
await self.window_update_received[stream_id].wait()
self.window_update_received[stream_id].clear()
else:
chunk, data = data[:chunk_size], data[chunk_size:]
self.h2_state.send_data(stream_id, chunk)
data_to_send = self.h2_state.data_to_send()
await self.stream.write(data_to_send, timeout)
async def end_stream(self, stream_id: int, timeout: TimeoutConfig = None) -> None:
logger.debug(f"end_stream stream_id={stream_id}")
self.h2_state.end_stream(stream_id)
data_to_send = self.h2_state.data_to_send()
await self.stream.write(data_to_send, timeout)
async def receive_response(
self, stream_id: int, timeout: TimeoutConfig = None
) -> typing.Tuple[int, typing.List[typing.Tuple[bytes, bytes]]]:
"""
Read the response status and headers from the network.
"""
while True:
event = await self.receive_event(stream_id, timeout)
# As soon as we start seeing response events, we should enable
# read timeouts, if we haven't already.
self.timeout_flags[stream_id].set_read_timeouts()
if isinstance(event, h2.events.ResponseReceived):
break
status_code = 200
headers = []
for k, v in event.headers:
if k == b":status":
status_code = int(v.decode("ascii", errors="ignore"))
elif not k.startswith(b":"):
headers.append((k, v))
return (status_code, headers)
async def body_iter(
self, stream_id: int, timeout: TimeoutConfig = None
) -> typing.AsyncIterator[bytes]:
while True:
event = await self.receive_event(stream_id, timeout)
if isinstance(event, h2.events.DataReceived):
self.h2_state.acknowledge_received_data(
event.flow_controlled_length, stream_id
)
yield event.data
elif isinstance(event, (h2.events.StreamEnded, h2.events.StreamReset)):
break
async def receive_event(
self, stream_id: int, timeout: TimeoutConfig = None
) -> h2.events.Event:
while not self.events[stream_id]:
flag = self.timeout_flags[stream_id]
data = await self.stream.read(self.READ_NUM_BYTES, timeout, flag=flag)
events = self.h2_state.receive_data(data)
for event in events:
event_stream_id = getattr(event, "stream_id", 0)
logger.debug(
f"receive_event stream_id={event_stream_id} event={event!r}"
)
if hasattr(event, "error_code"):
raise ProtocolError(event)
if isinstance(event, h2.events.WindowUpdated):
if event_stream_id == 0:
for window_update_event in self.window_update_received.values():
window_update_event.set()
else:
try:
self.window_update_received[event_stream_id].set()
except KeyError:
# the window_update_received dictionary is only relevant
# when sending data, which should never raise a KeyError
# here.
pass
if event_stream_id:
self.events[event.stream_id].append(event)
data_to_send = self.h2_state.data_to_send()
await self.stream.write(data_to_send, timeout)
return self.events[stream_id].pop(0)
async def response_closed(self, stream_id: int) -> None:
del self.events[stream_id]
del self.timeout_flags[stream_id]
del self.window_update_received[stream_id]
if not self.events and self.on_release is not None:
await self.on_release()
@property
def is_closed(self) -> bool:
return False
def is_connection_dropped(self) -> bool:
return self.stream.is_connection_dropped()

View File

@ -1,256 +0,0 @@
import enum
import h11
from ..concurrency.base import ConcurrencyBackend
from ..config import (
DEFAULT_POOL_LIMITS,
DEFAULT_TIMEOUT_CONFIG,
CertTypes,
HTTPVersionTypes,
PoolLimits,
SSLConfig,
TimeoutTypes,
VerifyTypes,
)
from ..exceptions import ProxyError
from ..middleware.basic_auth import build_basic_auth_header
from ..models import (
URL,
AsyncRequest,
AsyncResponse,
Headers,
HeaderTypes,
Origin,
URLTypes,
)
from ..utils import get_logger
from .connection import HTTPConnection
from .connection_pool import ConnectionPool
from .http2 import HTTP2Connection
from .http11 import HTTP11Connection
logger = get_logger(__name__)
class HTTPProxyMode(enum.Enum):
DEFAULT = "DEFAULT"
FORWARD_ONLY = "FORWARD_ONLY"
TUNNEL_ONLY = "TUNNEL_ONLY"
class HTTPProxy(ConnectionPool):
"""A proxy that sends requests to the recipient server
on behalf of the connecting client.
"""
def __init__(
self,
proxy_url: URLTypes,
*,
proxy_headers: HeaderTypes = None,
proxy_mode: HTTPProxyMode = HTTPProxyMode.DEFAULT,
verify: VerifyTypes = True,
cert: CertTypes = None,
trust_env: bool = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
http_versions: HTTPVersionTypes = None,
backend: ConcurrencyBackend = None,
):
super(HTTPProxy, self).__init__(
verify=verify,
cert=cert,
timeout=timeout,
pool_limits=pool_limits,
backend=backend,
trust_env=trust_env,
http_versions=http_versions,
)
self.proxy_url = URL(proxy_url)
self.proxy_mode = proxy_mode
self.proxy_headers = Headers(proxy_headers)
url = self.proxy_url
if url.username or url.password:
self.proxy_headers.setdefault(
"Proxy-Authorization",
build_basic_auth_header(url.username, url.password),
)
# Remove userinfo from the URL authority, e.g.:
# 'username:password@proxy_host:proxy_port' -> 'proxy_host:proxy_port'
credentials, _, authority = url.authority.rpartition("@")
self.proxy_url = url.copy_with(authority=authority)
async def acquire_connection(self, origin: Origin) -> HTTPConnection:
if self.should_forward_origin(origin):
logger.debug(
f"forward_connection proxy_url={self.proxy_url!r} origin={origin!r}"
)
return await super().acquire_connection(self.proxy_url.origin)
else:
logger.debug(
f"tunnel_connection proxy_url={self.proxy_url!r} origin={origin!r}"
)
return await self.tunnel_connection(origin)
async def tunnel_connection(self, origin: Origin) -> HTTPConnection:
"""Creates a new HTTPConnection via the CONNECT method
usually reserved for proxying HTTPS connections.
"""
connection = self.pop_connection(origin)
if connection is None:
connection = await self.request_tunnel_proxy_connection(origin)
# After we receive the 2XX response from the proxy that our
# tunnel is open we switch the connection's origin
# to the original so the tunnel can be re-used.
self.active_connections.remove(connection)
connection.origin = origin
self.active_connections.add(connection)
await self.tunnel_start_tls(origin, connection)
else:
self.active_connections.add(connection)
return connection
async def request_tunnel_proxy_connection(self, origin: Origin) -> HTTPConnection:
"""Creates an HTTPConnection by setting up a TCP tunnel"""
proxy_headers = self.proxy_headers.copy()
proxy_headers.setdefault("Accept", "*/*")
proxy_request = AsyncRequest(
method="CONNECT", url=self.proxy_url.copy_with(), headers=proxy_headers
)
proxy_request.url.full_path = f"{origin.host}:{origin.port}"
await self.max_connections.acquire()
connection = HTTPConnection(
self.proxy_url.origin,
verify=self.verify,
cert=self.cert,
timeout=self.timeout,
backend=self.backend,
http_versions=["HTTP/1.1"], # Short-lived 'connection'
trust_env=self.trust_env,
release_func=self.release_connection,
)
self.active_connections.add(connection)
# See if our tunnel has been opened successfully
proxy_response = await connection.send(proxy_request)
logger.debug(
f"tunnel_response "
f"proxy_url={self.proxy_url!r} "
f"origin={origin!r} "
f"response={proxy_response!r}"
)
if not (200 <= proxy_response.status_code <= 299):
await proxy_response.read()
raise ProxyError(
f"Non-2XX response received from HTTP proxy "
f"({proxy_response.status_code})",
request=proxy_request,
response=proxy_response,
)
else:
proxy_response.on_close = None
await proxy_response.read()
return connection
async def tunnel_start_tls(
self, origin: Origin, connection: HTTPConnection
) -> None:
"""Runs start_tls() on a TCP-tunneled connection"""
# Store this information here so that we can transfer
# it to the new internal connection object after
# the old one goes to 'SWITCHED_PROTOCOL'.
http_version = "HTTP/1.1"
http_connection = connection.h11_connection
assert http_connection is not None
assert http_connection.h11_state.our_state == h11.SWITCHED_PROTOCOL
on_release = http_connection.on_release
stream = http_connection.stream
# If we need to start TLS again for the target server
# we need to pull the TCP stream off the internal
# HTTP connection object and run start_tls()
if origin.is_ssl:
ssl_config = SSLConfig(cert=self.cert, verify=self.verify)
timeout = connection.timeout
ssl_context = await connection.get_ssl_context(ssl_config)
assert ssl_context is not None
logger.debug(
f"tunnel_start_tls "
f"proxy_url={self.proxy_url!r} "
f"origin={origin!r}"
)
stream = await self.backend.start_tls(
stream=stream,
hostname=origin.host,
ssl_context=ssl_context,
timeout=timeout,
)
http_version = stream.get_http_version()
logger.debug(
f"tunnel_tls_complete "
f"proxy_url={self.proxy_url!r} "
f"origin={origin!r} "
f"http_version={http_version!r}"
)
if http_version == "HTTP/2":
connection.h2_connection = HTTP2Connection(
stream, self.backend, on_release=on_release
)
else:
assert http_version == "HTTP/1.1"
connection.h11_connection = HTTP11Connection(
stream, self.backend, on_release=on_release
)
def should_forward_origin(self, origin: Origin) -> bool:
"""Determines if the given origin should
be forwarded or tunneled. If 'proxy_mode' is 'DEFAULT'
then the proxy will forward all 'HTTP' requests and
tunnel all 'HTTPS' requests.
"""
return (
self.proxy_mode == HTTPProxyMode.DEFAULT and not origin.is_ssl
) or self.proxy_mode == HTTPProxyMode.FORWARD_ONLY
async def send(
self,
request: AsyncRequest,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> AsyncResponse:
if self.should_forward_origin(request.url.origin):
# Change the request to have the target URL
# as its full_path and switch the proxy URL
# for where the request will be sent.
target_url = str(request.url)
request.url = self.proxy_url.copy_with()
request.url.full_path = target_url
for name, value in self.proxy_headers.items():
request.headers.setdefault(name, value)
return await super().send(
request=request, verify=verify, cert=cert, timeout=timeout
)
def __repr__(self) -> str:
return (
f"HTTPProxy(proxy_url={self.proxy_url!r} "
f"proxy_headers={self.proxy_headers!r} "
f"proxy_mode={self.proxy_mode!r})"
)

View File

@ -1,97 +0,0 @@
from ..concurrency.base import ConcurrencyBackend
from ..config import CertTypes, TimeoutTypes, VerifyTypes
from ..models import (
AsyncRequest,
AsyncRequestData,
AsyncResponse,
AsyncResponseContent,
Request,
RequestData,
Response,
ResponseContent,
)
from .base import AsyncDispatcher, Dispatcher
class ThreadedDispatcher(AsyncDispatcher):
"""
The ThreadedDispatcher class is used to mediate between the Client
(which always uses async under the hood), and a synchronous `Dispatch`
class.
"""
def __init__(self, dispatch: Dispatcher, backend: ConcurrencyBackend) -> None:
self.sync_dispatcher = dispatch
self.backend = backend
async def send(
self,
request: AsyncRequest,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> AsyncResponse:
concurrency_backend = self.backend
data = getattr(request, "content", getattr(request, "content_aiter", None))
sync_data = self._sync_request_data(data)
sync_request = Request(
method=request.method,
url=request.url,
headers=request.headers,
data=sync_data,
)
func = self.sync_dispatcher.send
kwargs = {
"request": sync_request,
"verify": verify,
"cert": cert,
"timeout": timeout,
}
sync_response = await self.backend.run_in_threadpool(func, **kwargs)
assert isinstance(sync_response, Response)
content = getattr(
sync_response, "_raw_content", getattr(sync_response, "_raw_stream", None)
)
async_content = self._async_response_content(content)
async def async_on_close() -> None:
nonlocal concurrency_backend, sync_response
await concurrency_backend.run_in_threadpool(sync_response.close)
return AsyncResponse(
status_code=sync_response.status_code,
http_version=sync_response.http_version,
headers=sync_response.headers,
content=async_content,
on_close=async_on_close,
request=request,
history=sync_response.history,
)
async def close(self) -> None:
"""
The `.close()` method runs the `Dispatcher.close()` within a threadpool,
so as not to block the async event loop.
"""
func = self.sync_dispatcher.close
await self.backend.run_in_threadpool(func)
def _async_response_content(self, content: ResponseContent) -> AsyncResponseContent:
if isinstance(content, bytes):
return content
# Coerce an async iterator into an iterator, with each item in the
# iteration run within the event loop.
assert hasattr(content, "__iter__")
return self.backend.iterate_in_threadpool(content)
def _sync_request_data(self, data: AsyncRequestData) -> RequestData:
if isinstance(data, bytes):
return data
return self.backend.iterate(data)

View File

@ -1,164 +0,0 @@
import io
import typing
from ..config import CertTypes, TimeoutTypes, VerifyTypes
from ..models import Request, Response
from .base import Dispatcher
class WSGIDispatch(Dispatcher):
"""
A custom dispatcher that handles sending requests directly to an ASGI app.
The simplest way to use this functionality is to use the `app`argument.
This will automatically infer if 'app' is a WSGI or an ASGI application,
and will setup an appropriate dispatch class:
```
client = httpx.Client(app=app)
```
Alternatively, you can setup the dispatch instance explicitly.
This allows you to include any additional configuration arguments specific
to the WSGIDispatch class:
```
dispatch = httpx.WSGIDispatch(
app=app,
script_name="/submount",
remote_addr="1.2.3.4"
)
client = httpx.Client(dispatch=dispatch)
Arguments:
* `app` - The ASGI application.
* `raise_app_exceptions` - Boolean indicating if exceptions in the application
should be raised. Default to `True`. Can be set to `False` for use cases
such as testing the content of a client 500 response.
* `script_name` - The root path on which the ASGI application should be mounted.
* `remote_addr` - A string indicating the client IP of incoming requests.
```
"""
def __init__(
self,
app: typing.Callable,
raise_app_exceptions: bool = True,
script_name: str = "",
remote_addr: str = "127.0.0.1",
) -> None:
self.app = app
self.raise_app_exceptions = raise_app_exceptions
self.script_name = script_name
self.remote_addr = remote_addr
def send(
self,
request: Request,
verify: VerifyTypes = None,
cert: CertTypes = None,
timeout: TimeoutTypes = None,
) -> Response:
environ = {
"wsgi.version": (1, 0),
"wsgi.url_scheme": request.url.scheme,
"wsgi.input": BodyStream(request.stream()),
"wsgi.errors": io.BytesIO(),
"wsgi.multithread": True,
"wsgi.multiprocess": False,
"wsgi.run_once": False,
"REQUEST_METHOD": request.method,
"SCRIPT_NAME": self.script_name,
"PATH_INFO": request.url.path,
"QUERY_STRING": request.url.query,
"SERVER_NAME": request.url.host,
"SERVER_PORT": str(request.url.port),
"REMOTE_ADDR": self.remote_addr,
}
for key, value in request.headers.items():
key = key.upper().replace("-", "_")
if key not in ("CONTENT_TYPE", "CONTENT_LENGTH"):
key = "HTTP_" + key
environ[key] = value
seen_status = None
seen_response_headers = None
seen_exc_info = None
def start_response(
status: str, response_headers: list, exc_info: typing.Any = None
) -> None:
nonlocal seen_status, seen_response_headers, seen_exc_info
seen_status = status
seen_response_headers = response_headers
seen_exc_info = exc_info
result = self.app(environ, start_response)
assert seen_status is not None
assert seen_response_headers is not None
if seen_exc_info and self.raise_app_exceptions:
raise seen_exc_info[1]
return Response(
status_code=int(seen_status.split()[0]),
http_version="HTTP/1.1",
headers=seen_response_headers,
content=(chunk for chunk in result),
on_close=getattr(result, "close", None),
)
class BodyStream(io.RawIOBase):
def __init__(self, iterator: typing.Iterator[bytes]) -> None:
self._iterator = iterator
self._buffer = b""
self._closed = False
def read(self, size: int = -1) -> bytes:
if self._closed:
return b""
if size == -1:
return self.readall()
try:
while len(self._buffer) < size:
self._buffer += next(self._iterator)
except StopIteration:
self._closed = True
return self._buffer
output = self._buffer[:size]
self._buffer = self._buffer[size:]
return output
def readall(self) -> bytes:
if self._closed:
raise OSError("Stream closed") # pragma: nocover
for chunk in self._iterator:
self._buffer += chunk
self._closed = True
return self._buffer
def readinto(self, b: bytearray) -> typing.Optional[int]: # pragma: nocover
output = self.read(len(b))
count = len(output)
b[:count] = output
return count
def write(self, b: bytes) -> int:
raise OSError("Operation not supported") # pragma: nocover
def fileno(self) -> int:
raise OSError("Operation not supported") # pragma: nocover
def seek(self, offset: int, whence: int = 0) -> int:
raise OSError("Operation not supported") # pragma: nocover
def truncate(self, size: int = None) -> int:
raise OSError("Operation not supported") # pragma: nocover

View File

@ -1,156 +0,0 @@
import typing
if typing.TYPE_CHECKING:
from .models import BaseRequest, BaseResponse # pragma: nocover
class HTTPError(Exception):
"""
Base class for Httpx exception
"""
def __init__(
self,
*args: typing.Any,
request: "BaseRequest" = None,
response: "BaseResponse" = None,
) -> None:
self.response = response
self.request = request or getattr(self.response, "request", None)
super().__init__(*args)
# Timeout exceptions...
class Timeout(HTTPError):
"""
A base class for all timeouts.
"""
class ConnectTimeout(Timeout):
"""
Timeout while establishing a connection.
"""
class ReadTimeout(Timeout):
"""
Timeout while reading response data.
"""
class WriteTimeout(Timeout):
"""
Timeout while writing request data.
"""
class PoolTimeout(Timeout):
"""
Timeout while waiting to acquire a connection from the pool.
"""
class ProxyError(HTTPError):
"""
Error from within a proxy
"""
# HTTP exceptions...
class ProtocolError(HTTPError):
"""
Malformed HTTP.
"""
class DecodingError(HTTPError):
"""
Decoding of the response failed.
"""
# Redirect exceptions...
class RedirectError(HTTPError):
"""
Base class for HTTP redirect errors.
"""
class TooManyRedirects(RedirectError):
"""
Too many redirects.
"""
class RedirectBodyUnavailable(RedirectError):
"""
Got a redirect response, but the request body was streaming, and is
no longer available.
"""
class RedirectLoop(RedirectError):
"""
Infinite redirect loop.
"""
class NotRedirectResponse(RedirectError):
"""
Response was not a redirect response.
"""
# Stream exceptions...
class StreamError(HTTPError):
"""
The base class for stream exceptions.
The developer made an error in accessing the request stream in
an invalid way.
"""
class StreamConsumed(StreamError):
"""
Attempted to read or stream response content, but the content has already
been streamed.
"""
class ResponseNotRead(StreamError):
"""
Attempted to access response content, without having called `read()`
after a streaming response.
"""
class ResponseClosed(StreamError):
"""
Attempted to read or stream response content, but the request has been
closed.
"""
# Other cases...
class InvalidURL(HTTPError):
"""
URL was missing a hostname, or was not one of HTTP/HTTPS.
"""
class CookieConflict(HTTPError):
"""
Attempted to lookup a cookie by name, but multiple cookies existed.
"""

View File

@ -1,10 +0,0 @@
import typing
from ..models import AsyncRequest, AsyncResponse
class BaseMiddleware:
async def __call__(
self, request: AsyncRequest, get_response: typing.Callable
) -> AsyncResponse:
raise NotImplementedError # pragma: no cover

View File

@ -1,27 +0,0 @@
import typing
from base64 import b64encode
from ..models import AsyncRequest, AsyncResponse
from ..utils import to_bytes
from .base import BaseMiddleware
class BasicAuthMiddleware(BaseMiddleware):
def __init__(
self, username: typing.Union[str, bytes], password: typing.Union[str, bytes]
):
self.authorization_header = build_basic_auth_header(username, password)
async def __call__(
self, request: AsyncRequest, get_response: typing.Callable
) -> AsyncResponse:
request.headers["Authorization"] = self.authorization_header
return await get_response(request)
def build_basic_auth_header(
username: typing.Union[str, bytes], password: typing.Union[str, bytes]
) -> str:
userpass = b":".join((to_bytes(username), to_bytes(password)))
token = b64encode(userpass).decode().strip()
return f"Basic {token}"

View File

@ -1,15 +0,0 @@
import typing
from ..models import AsyncRequest, AsyncResponse
from .base import BaseMiddleware
class CustomAuthMiddleware(BaseMiddleware):
def __init__(self, auth: typing.Callable[[AsyncRequest], AsyncRequest]):
self.auth = auth
async def __call__(
self, request: AsyncRequest, get_response: typing.Callable
) -> AsyncResponse:
request = self.auth(request)
return await get_response(request)

View File

@ -1,181 +0,0 @@
import hashlib
import os
import re
import time
import typing
from urllib.request import parse_http_list
from ..exceptions import ProtocolError
from ..models import AsyncRequest, AsyncResponse, StatusCode
from ..utils import to_bytes, to_str, unquote
from .base import BaseMiddleware
class DigestAuth(BaseMiddleware):
ALGORITHM_TO_HASH_FUNCTION: typing.Dict[str, typing.Callable] = {
"MD5": hashlib.md5,
"MD5-SESS": hashlib.md5,
"SHA": hashlib.sha1,
"SHA-SESS": hashlib.sha1,
"SHA-256": hashlib.sha256,
"SHA-256-SESS": hashlib.sha256,
"SHA-512": hashlib.sha512,
"SHA-512-SESS": hashlib.sha512,
}
def __init__(
self, username: typing.Union[str, bytes], password: typing.Union[str, bytes]
) -> None:
self.username = to_bytes(username)
self.password = to_bytes(password)
async def __call__(
self, request: AsyncRequest, get_response: typing.Callable
) -> AsyncResponse:
response = await get_response(request)
if not (
StatusCode.is_client_error(response.status_code)
and "www-authenticate" in response.headers
):
return response
header = response.headers["www-authenticate"]
try:
challenge = DigestAuthChallenge.from_header(header)
except ValueError:
raise ProtocolError("Malformed Digest authentication header")
request.headers["Authorization"] = self._build_auth_header(request, challenge)
return await get_response(request)
def _build_auth_header(
self, request: AsyncRequest, challenge: "DigestAuthChallenge"
) -> str:
hash_func = self.ALGORITHM_TO_HASH_FUNCTION[challenge.algorithm]
def digest(data: bytes) -> bytes:
return hash_func(data).hexdigest().encode()
A1 = b":".join((self.username, challenge.realm, self.password))
path = request.url.full_path.encode("utf-8")
A2 = b":".join((request.method.encode(), path))
# TODO: implement auth-int
HA2 = digest(A2)
nonce_count = 1 # TODO: implement nonce counting
nc_value = b"%08x" % nonce_count
cnonce = self._get_client_nonce(nonce_count, challenge.nonce)
HA1 = digest(A1)
if challenge.algorithm.lower().endswith("-sess"):
HA1 = digest(b":".join((HA1, challenge.nonce, cnonce)))
qop = self._resolve_qop(challenge.qop)
if qop is None:
digest_data = [HA1, challenge.nonce, HA2]
else:
digest_data = [challenge.nonce, nc_value, cnonce, qop, HA2]
key_digest = b":".join(digest_data)
format_args = {
"username": self.username,
"realm": challenge.realm,
"nonce": challenge.nonce,
"uri": path,
"response": digest(b":".join((HA1, key_digest))),
"algorithm": challenge.algorithm.encode(),
}
if challenge.opaque:
format_args["opaque"] = challenge.opaque
if qop:
format_args["qop"] = b"auth"
format_args["nc"] = nc_value
format_args["cnonce"] = cnonce
return "Digest " + self._get_header_value(format_args)
def _get_client_nonce(self, nonce_count: int, nonce: bytes) -> bytes:
s = str(nonce_count).encode()
s += nonce
s += time.ctime().encode()
s += os.urandom(8)
return hashlib.sha1(s).hexdigest()[:16].encode()
def _get_header_value(self, header_fields: typing.Dict[str, bytes]) -> str:
NON_QUOTED_FIELDS = ("algorithm", "qop", "nc")
QUOTED_TEMPLATE = '{}="{}"'
NON_QUOTED_TEMPLATE = "{}={}"
header_value = ""
for i, (field, value) in enumerate(header_fields.items()):
if i > 0:
header_value += ", "
template = (
QUOTED_TEMPLATE
if field not in NON_QUOTED_FIELDS
else NON_QUOTED_TEMPLATE
)
header_value += template.format(field, to_str(value))
return header_value
def _resolve_qop(self, qop: typing.Optional[bytes]) -> typing.Optional[bytes]:
if qop is None:
return None
qops = re.split(b", ?", qop)
if b"auth" in qops:
return b"auth"
if qops == [b"auth-int"]:
raise NotImplementedError("Digest auth-int support is not yet implemented")
raise ProtocolError(f'Unexpected qop value "{qop!r}" in digest auth')
class DigestAuthChallenge:
def __init__(
self,
realm: bytes,
nonce: bytes,
algorithm: str = None,
opaque: typing.Optional[bytes] = None,
qop: typing.Optional[bytes] = None,
) -> None:
self.realm = realm
self.nonce = nonce
self.algorithm = algorithm or "MD5"
self.opaque = opaque
self.qop = qop
@classmethod
def from_header(cls, header: str) -> "DigestAuthChallenge":
"""Returns a challenge from a Digest WWW-Authenticate header.
These take the form of:
`Digest realm="realm@host.com",qop="auth,auth-int",nonce="abc",opaque="xyz"`
"""
scheme, _, fields = header.partition(" ")
if scheme.lower() != "digest":
raise ValueError("Header does not start with 'Digest'")
header_dict: typing.Dict[str, str] = {}
for field in parse_http_list(fields):
key, value = field.strip().split("=", 1)
header_dict[key] = unquote(value)
try:
return cls.from_header_dict(header_dict)
except KeyError as exc:
raise ValueError("Malformed Digest WWW-Authenticate header") from exc
@classmethod
def from_header_dict(cls, header_dict: dict) -> "DigestAuthChallenge":
realm = header_dict["realm"].encode()
nonce = header_dict["nonce"].encode()
qop = header_dict["qop"].encode() if "qop" in header_dict else None
opaque = header_dict["opaque"].encode() if "opaque" in header_dict else None
algorithm = header_dict.get("algorithm")
return cls(
realm=realm, nonce=nonce, qop=qop, opaque=opaque, algorithm=algorithm
)

View File

@ -1,128 +0,0 @@
import functools
import typing
from ..config import DEFAULT_MAX_REDIRECTS
from ..exceptions import RedirectBodyUnavailable, RedirectLoop, TooManyRedirects
from ..models import URL, AsyncRequest, AsyncResponse, Cookies, Headers
from ..status_codes import codes
from .base import BaseMiddleware
class RedirectMiddleware(BaseMiddleware):
def __init__(
self,
allow_redirects: bool = True,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
cookies: typing.Optional[Cookies] = None,
):
self.allow_redirects = allow_redirects
self.max_redirects = max_redirects
self.cookies = cookies
self.history: typing.List[AsyncResponse] = []
async def __call__(
self, request: AsyncRequest, get_response: typing.Callable
) -> AsyncResponse:
if len(self.history) > self.max_redirects:
raise TooManyRedirects()
if request.url in (response.url for response in self.history):
raise RedirectLoop()
response = await get_response(request)
response.history = list(self.history)
if not response.is_redirect:
return response
self.history.append(response)
next_request = self.build_redirect_request(request, response)
if self.allow_redirects:
return await self(next_request, get_response)
response.call_next = functools.partial(self, next_request, get_response)
return response
def build_redirect_request(
self, request: AsyncRequest, response: AsyncResponse
) -> AsyncRequest:
method = self.redirect_method(request, response)
url = self.redirect_url(request, response)
headers = self.redirect_headers(request, url, method) # TODO: merge headers?
content = self.redirect_content(request, method)
cookies = Cookies(self.cookies)
cookies.update(request.cookies)
return AsyncRequest(
method=method, url=url, headers=headers, data=content, cookies=cookies
)
def redirect_method(self, request: AsyncRequest, response: AsyncResponse) -> str:
"""
When being redirected we may want to change the method of the request
based on certain specs or browser behavior.
"""
method = request.method
# https://tools.ietf.org/html/rfc7231#section-6.4.4
if response.status_code == codes.SEE_OTHER and method != "HEAD":
method = "GET"
# Do what the browsers do, despite standards...
# Turn 302s into GETs.
if response.status_code == codes.FOUND and method != "HEAD":
method = "GET"
# If a POST is responded to with a 301, turn it into a GET.
# This bizarre behaviour is explained in 'requests' issue 1704.
if response.status_code == codes.MOVED_PERMANENTLY and method == "POST":
method = "GET"
return method
def redirect_url(self, request: AsyncRequest, response: AsyncResponse) -> URL:
"""
Return the URL for the redirect to follow.
"""
location = response.headers["Location"]
url = URL(location, allow_relative=True)
# Facilitate relative 'Location' headers, as allowed by RFC 7231.
# (e.g. '/path/to/resource' instead of 'http://domain.tld/path/to/resource')
if url.is_relative_url:
url = request.url.join(url)
# Attach previous fragment if needed (RFC 7231 7.1.2)
if request.url.fragment and not url.fragment:
url = url.copy_with(fragment=request.url.fragment)
return url
def redirect_headers(self, request: AsyncRequest, url: URL, method: str) -> Headers:
"""
Return the headers that should be used for the redirect request.
"""
headers = Headers(request.headers)
if url.origin != request.url.origin:
# Strip Authorization headers when responses are redirected away from
# the origin.
headers.pop("Authorization", None)
headers["Host"] = url.authority
if method != request.method and method == "GET":
# If we've switch to a 'GET' request, then strip any headers which
# are only relevant to the request body.
headers.pop("Content-Length", None)
headers.pop("Transfer-Encoding", None)
return headers
def redirect_content(self, request: AsyncRequest, method: str) -> bytes:
"""
Return the body that should be used for the redirect request.
"""
if method != request.method and method == "GET":
return b""
if request.is_streaming:
raise RedirectBodyUnavailable()
return request.content

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More