Compare commits

..

21 Commits

Author SHA1 Message Date
Tom Christie
ad5234f3ac
Update docs/advanced/ssl.md 2024-11-28 11:40:58 +00:00
Tom Christie
6c483c9e07
Merge branch 'master' into tomchristie-patch-1 2024-11-22 11:43:18 +00:00
Tom Christie
4e1c691636
Update CHANGELOG.md 2024-11-21 13:54:13 +00:00
Tom Christie
2d2eab7fa8
Update CHANGELOG.md 2024-11-21 13:50:08 +00:00
Tom Christie
92d92956a5
Update CHANGELOG.md 2024-11-21 13:49:10 +00:00
Tom Christie
5560472f7d
Update docs/advanced/ssl.md 2024-11-21 13:42:26 +00:00
Tom Christie
670a156b56
Update README.md 2024-11-21 13:40:48 +00:00
Tom Christie
218db9b6db
Update ssl.md 2024-11-21 13:36:31 +00:00
Tom Christie
e49cc07bc9
Update ssl.md 2024-11-21 13:35:10 +00:00
Tom Christie
4321f008ed
Update ssl.md 2024-11-21 13:33:30 +00:00
Tom Christie
8bff380cec
Update ssl.md 2024-11-21 13:31:24 +00:00
Tom Christie
2f220ec3f5
Update ssl.md 2024-11-21 13:18:14 +00:00
Tom Christie
3933a88c2b
Update test_config.py
Drop unneeded code.
2024-11-21 11:47:42 +00:00
Tom Christie
cc72d348f8
Update test_config.py 2024-11-21 11:44:32 +00:00
Tom Christie
15ffaf9e1a
Update test_config.py 2024-11-21 11:38:53 +00:00
Tom Christie
c1a88bc873
Update README.md 2024-11-21 11:35:17 +00:00
Tom Christie
83fbd71063
Update _config.py 2024-11-21 11:31:57 +00:00
Tom Christie
b413005684
Python versions 2024-11-21 11:30:33 +00:00
Tom Christie
d861e9c179
Update pyproject.toml 2024-11-21 11:25:02 +00:00
Tom Christie
9bcb36a634
Requirements 2024-11-21 11:18:56 +00:00
Tom Christie
3769c569ed
Truststore 2024-11-21 11:15:26 +00:00
35 changed files with 187 additions and 373 deletions

View File

@ -15,9 +15,9 @@ jobs:
steps: steps:
- uses: "actions/checkout@v4" - uses: "actions/checkout@v4"
- uses: "actions/setup-python@v6" - uses: "actions/setup-python@v5"
with: with:
python-version: 3.9 python-version: 3.8
- name: "Install dependencies" - name: "Install dependencies"
run: "scripts/install" run: "scripts/install"
- name: "Build package & docs" - name: "Build package & docs"

View File

@ -14,11 +14,11 @@ jobs:
strategy: strategy:
matrix: matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.10", "3.11", "3.12", "3.13"]
steps: steps:
- uses: "actions/checkout@v4" - uses: "actions/checkout@v4"
- uses: "actions/setup-python@v6" - uses: "actions/setup-python@v5"
with: with:
python-version: "${{ matrix.python-version }}" python-version: "${{ matrix.python-version }}"
allow-prereleases: true allow-prereleases: true

View File

@ -4,47 +4,34 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [UNRELEASED] ## 0.28.0 (...)
### Removed TODO... writeup `truststore` switch & 3.10+ requirement.
* Drop support for Python 3.8 The 0.28 release includes a limited set of backwards incompatible changes.
### Added **Backwards incompatible changes**:
* Expose `FunctionAuth` from the public API. (#3699) SSL configuration has been significantly simplified.
## 0.28.1 (6th December, 2024) * The `verify` argument no longer accepts string arguments. Explicitly specified certificate stores can still be enabled through the SSL configuration API.
* The `cert` argument has now been removed. Client side certificates can still be enabled through the SSL configuration API.
* The `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables are no longer used. These environment variables can be enabled manually although should be obsoleted by our switch to `truststore`.
* Fix SSL case where `verify=False` together with client side certificates. For users of the standard `verify=True` or `verify=False` cases this should require no changes.
## 0.28.0 (28th November, 2024) For information on configuring more complex SSL cases, please see the [SSL documentation](docs/advanced/ssl.md).
Be aware that the default *JSON request bodies now use a more compact representation*. This is generally considered a prefered style, tho may require updates to test suites.
The 0.28 release includes a limited set of deprecations...
**Deprecations**:
We are working towards a simplified SSL configuration API.
*For users of the standard `verify=True` or `verify=False` cases, or `verify=<ssl_context>` case this should require no changes. The following cases have been deprecated...*
* The `verify` argument as a string argument is now deprecated and will raise warnings.
* The `cert` argument is now deprecated and will raise warnings.
Our revised [SSL documentation](docs/advanced/ssl.md) covers how to implement the same behaviour with a more constrained API.
**The following changes are also included**: **The following changes are also included**:
* The undocumented `URL.raw` property has now been deprecated, and will raise warnings.
* The deprecated `proxies` argument has now been removed. * The deprecated `proxies` argument has now been removed.
* The deprecated `app` argument has now been removed. * The deprecated `app` argument has now been removed.
* JSON request bodies use a compact representation. (#3363) * Ensure JSON request bodies are compact. (#3363)
* Review URL percent escape sets, based on WHATWG spec. (#3371, #3373) * Review URL percent escape sets, based on WHATWG spec. (#3371, #3373)
* Ensure `certifi` and `httpcore` are only imported if required. (#3377) * Ensure `certifi` and `httpcore` are only imported if required. (#3377)
* Treat `socks5h` as a valid proxy scheme. (#3178) * Treat `socks5h` as a valid proxy scheme. (#3178)
* Cleanup `Request()` method signature in line with `client.request()` and `httpx.request()`. (#3378) * Cleanup `Request()` method signature in line with `client.request()` and `httpx.request()`. (#3378)
* Bugfix: When passing `params={}`, always strictly update rather than merge with an existing querystring. (#3364)
## 0.27.2 (27th August, 2024) ## 0.27.2 (27th August, 2024)

View File

@ -101,7 +101,7 @@ Or, to include the optional HTTP/2 support, use:
$ pip install httpx[http2] $ pip install httpx[http2]
``` ```
HTTPX requires Python 3.9+. HTTPX requires Python 3.10+.
## Documentation ## Documentation
@ -125,7 +125,7 @@ The HTTPX project relies on these excellent libraries:
* `httpcore` - The underlying transport implementation for `httpx`. * `httpcore` - The underlying transport implementation for `httpx`.
* `h11` - HTTP/1.1 support. * `h11` - HTTP/1.1 support.
* `certifi` - SSL certificates. * `truststore` - System SSL certificates.
* `idna` - Internationalized domain name support. * `idna` - Internationalized domain name support.
* `sniffio` - Async library autodetection. * `sniffio` - Async library autodetection.

View File

@ -270,9 +270,8 @@ 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. name of the payloads as keys and either tuple of elements or a file-like object or a string as values.
```pycon ```pycon
>>> with open('report.xls', 'rb') as report_file: >>> files = {'upload-file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel')}
... files = {'upload-file': ('report.xls', report_file, 'application/vnd.ms-excel')} >>> r = httpx.post("https://httpbin.org/post", files=files)
... r = httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text) >>> print(r.text)
{ {
... ...
@ -319,10 +318,7 @@ To do that, pass a list of `(field, <file>)` items instead of a dictionary, allo
For instance this request sends 2 files, `foo.png` and `bar.png` in one request on the `images` form field: For instance this request sends 2 files, `foo.png` and `bar.png` in one request on the `images` form field:
```pycon ```pycon
>>> with open('foo.png', 'rb') as foo_file, open('bar.png', 'rb') as bar_file: >>> files = [('images', ('foo.png', open('foo.png', 'rb'), 'image/png')),
... files = [ ('images', ('bar.png', open('bar.png', 'rb'), 'image/png'))]
... ('images', ('foo.png', foo_file, 'image/png')), >>> r = httpx.post("https://httpbin.org/post", files=files)
... ('images', ('bar.png', bar_file, 'image/png')),
... ]
... r = httpx.post("https://httpbin.org/post", files=files)
``` ```

View File

@ -1,26 +1,28 @@
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). When making a request over HTTPS we need to verify the identity of the requested host. We rely on the [`truststore`](https://truststore.readthedocs.io/en/latest/) package to load the system certificates, ensuring that `httpx` has the same behaviour on SSL sites as your browser.
### Enabling and disabling verification ### SSL verification
By default httpx will verify HTTPS connections, and raise an error for invalid SSL cases... By default httpx will verify HTTPS connections, and raise an error for invalid SSL cases...
```pycon ```python
>>> httpx.get("https://expired.badssl.com/") >>> httpx.get("https://expired.badssl.com/")
httpx.ConnectError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:997) 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... If you're confident that you want to visit a site with an invalid certificate you can disable SSL verification completely...
```pycon ```python
>>> httpx.get("https://expired.badssl.com/", verify=False) >>> httpx.get("https://expired.badssl.com/", verify=False)
<Response [200 OK]> <Response [200 OK]>
``` ```
### Configuring client instances ### Custom SSL configurations
If you're using a `Client()` instance you should pass any `verify=<...>` configuration when instantiating the client. If you're using a `Client()` instance you can pass the `verify=<...>` configuration when instantiating the client.
By default the [certifi CA bundle](https://certifiio.readthedocs.io/en/latest/) is used for SSL verification. ```python
>>> client = httpx.Client(verify=True)
```
For more complex configurations you can pass an [SSL Context](https://docs.python.org/3/library/ssl.html) instance... For more complex configurations you can pass an [SSL Context](https://docs.python.org/3/library/ssl.html) instance...
@ -28,35 +30,13 @@ For more complex configurations you can pass an [SSL Context](https://docs.pytho
import certifi import certifi
import httpx import httpx
import ssl import ssl
import certifi
# This SSL context is equivalent to the default `verify=True`. # Use certifi for certificate validation, rather than the system truststore.
ctx = ssl.create_default_context(cafile=certifi.where()) ctx = ssl.create_default_context(cafile=certifi.where())
client = httpx.Client(verify=ctx) 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
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. 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.
@ -71,11 +51,23 @@ client = httpx.Client(verify=ctx)
### Working with `SSL_CERT_FILE` and `SSL_CERT_DIR` ### 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). Unlike `requests`, the `httpx` package does not automatically pull in [the environment variables `SSL_CERT_FILE` or `SSL_CERT_DIR`](https://www.openssl.org/docs/manmaster/man3/SSL_CTX_set_default_verify_paths.html).
These environment variables shouldn't be necessary since they're obsoleted by `truststore`. They can be enabled if required like so...
```python
# Use `SSL_CERT_FILE` or `SSL_CERT_DIR` if configured.
# Otherwise default to certifi.
ctx = ssl.create_default_context(
cafile=os.environ.get("SSL_CERT_FILE", certifi.where()),
capath=os.environ.get("SSL_CERT_DIR"),
)
client = httpx.Client(verify=ctx)
```
### Making HTTPS requests to a local server ### 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. 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... 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...

View File

@ -159,18 +159,3 @@ what gets sent over the wire.*
* `def delete(name, [domain], [path])` * `def delete(name, [domain], [path])`
* `def clear([domain], [path])` * `def clear([domain], [path])`
* *Standard mutable mapping interface* * *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

@ -23,7 +23,7 @@ To make asynchronous requests, you'll need an `AsyncClient`.
``` ```
!!! tip !!! tip
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. Use [IPython](https://ipython.readthedocs.io/en/stable/) or Python 3.8+ with `python -m asyncio` to try this code interactively, as they support executing `async`/`await` expressions in the console.
## API Differences ## API Differences

View File

@ -226,7 +226,3 @@ For both query params (`params=`) and form data (`data=`), `requests` supports s
In HTTPX, event hooks may access properties of requests and responses, but event hook callbacks cannot mutate the original request/response. 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). 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

@ -51,29 +51,3 @@ python -c "import httpx; httpx.get('http://example.com')"
python -c "import httpx; httpx.get('http://127.0.0.1:5000/my-api')" 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')" python -c "import httpx; httpx.get('https://www.python-httpx.org')"
``` ```
## `SSL_CERT_FILE`
Valid values: a filename
If this environment variable is set then HTTPX will load
CA certificate from the specified file instead of the default
location.
Example:
```console
SSL_CERT_FILE=/path/to/ca-certs/ca-bundle.crt python -c "import httpx; httpx.get('https://example.com')"
```
## `SSL_CERT_DIR`
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 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')"
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -145,6 +145,6 @@ To include the optional brotli and zstandard decoders support, use:
$ pip install httpx[brotli,zstd] $ pip install httpx[brotli,zstd]
``` ```
HTTPX requires Python 3.9+ HTTPX requires Python 3.8+
[sync-support]: https://github.com/encode/httpx/issues/572 [sync-support]: https://github.com/encode/httpx/issues/572

View File

@ -20,6 +20,8 @@ httpx.get("https://www.example.com")
Will send debug level output to the console, or wherever `stdout` is directed too... Will send debug level output to the console, or wherever `stdout` is directed too...
``` ```
DEBUG [2024-09-28 17:27:40] httpx - load_ssl_context verify=True cert=None
DEBUG [2024-09-28 17:27:40] httpx - load_verify_locations cafile='/Users/karenpetrosyan/oss/karhttpx/.venv/lib/python3.9/site-packages/certifi/cacert.pem'
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: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 - 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.started ssl_context=SSLContext(verify=True) server_hostname='www.example.com' timeout=5.0

View File

@ -174,9 +174,8 @@ Form encoded data can also include multiple values from a given key.
You can also upload files, using HTTP multipart encoding: You can also upload files, using HTTP multipart encoding:
```pycon ```pycon
>>> with open('report.xls', 'rb') as report_file: >>> files = {'upload-file': open('report.xls', 'rb')}
... files = {'upload-file': report_file} >>> r = httpx.post("https://httpbin.org/post", files=files)
... r = httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text) >>> print(r.text)
{ {
... ...
@ -191,9 +190,8 @@ You can also explicitly set the filename and content type, by using a tuple
of items for the file value: of items for the file value:
```pycon ```pycon
>>> with open('report.xls', 'rb') as report_file: >>> files = {'upload-file': ('report.xls', open('report.xls', 'rb'), 'application/vnd.ms-excel')}
... files = {'upload-file': ('report.xls', report_file, 'application/vnd.ms-excel')} >>> r = httpx.post("https://httpbin.org/post", files=files)
... r = httpx.post("https://httpbin.org/post", files=files)
>>> print(r.text) >>> print(r.text)
{ {
... ...
@ -208,9 +206,8 @@ If you need to include non-file data fields in the multipart form, use the `data
```pycon ```pycon
>>> data = {'message': 'Hello, world!'} >>> data = {'message': 'Hello, world!'}
>>> with open('report.xls', 'rb') as report_file: >>> files = {'file': open('report.xls', 'rb')}
... files = {'file': report_file} >>> r = httpx.post("https://httpbin.org/post", data=data, files=files)
... r = httpx.post("https://httpbin.org/post", data=data, files=files)
>>> print(r.text) >>> print(r.text)
{ {
... ...

View File

@ -2,83 +2,31 @@
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. 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 ## 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 ### httpx-ws
[GitHub](https://github.com/frankie567/httpx-ws) - [Documentation](https://frankie567.github.io/httpx-ws/) [GitHub](https://github.com/frankie567/httpx-ws) - [Documentation](https://frankie567.github.io/httpx-ws/)
WebSocket support for HTTPX. WebSocket support for HTTPX.
### pytest-HTTPX ### httpx-socks
[GitHub](https://github.com/Colin-b/pytest_httpx) - [Documentation](https://colin-b.github.io/pytest_httpx/) [GitHub](https://github.com/romis2012/httpx-socks)
Provides a [pytest](https://docs.pytest.org/en/latest/) fixture to mock HTTPX within test cases. Proxy (HTTP, SOCKS) transports for httpx.
### RESPX ### Hishel
[GitHub](https://github.com/lundberg/respx) - [Documentation](https://lundberg.github.io/respx/) [GitHub](https://github.com/karpetrosyan/hishel) - [Documentation](https://hishel.com/)
A utility for mocking out HTTPX. An elegant HTTP Cache implementation for HTTPX and HTTP Core.
### 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 ### Authlib
[GitHub](https://github.com/lepture/authlib) - [Documentation](https://docs.authlib.org/en/latest/) [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). The ultimate Python library in building OAuth and OpenID Connect clients and servers. Includes an [OAuth HTTPX client](https://docs.authlib.org/en/latest/client/httpx.html).
### Gidgethub ### Gidgethub
@ -86,20 +34,58 @@ A python library for building OAuth and OpenID Connect clients and servers. Incl
An asynchronous GitHub API library. Includes [HTTPX support](https://gidgethub.readthedocs.io/en/latest/httpx.html). An asynchronous GitHub API library. Includes [HTTPX support](https://gidgethub.readthedocs.io/en/latest/httpx.html).
### httpdbg ### HTTPX-Auth
[GitHub](https://github.com/cle-b/httpdbg) - [Documentation](https://httpdbg.readthedocs.io/) [GitHub](https://github.com/Colin-b/httpx_auth) - [Documentation](https://colin-b.github.io/httpx_auth/)
A tool for python developers to easily debug the HTTP(S) client requests in a python program. Provides authentication classes to be used with HTTPX [authentication parameter](advanced/authentication.md#customizing-authentication).
### pytest-HTTPX
[GitHub](https://github.com/Colin-b/pytest_httpx) - [Documentation](https://colin-b.github.io/pytest_httpx/)
Provides `httpx_mock` [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 the Python HTTPX library.
### rpc.py
[Github](https://github.com/abersheeran/rpc.py) - [Documentation](https://github.com/abersheeran/rpc.py#rpcpy)
An fast and powerful RPC framework based on ASGI/WSGI. Use HTTPX as the client of the RPC service.
### VCR.py ### VCR.py
[GitHub](https://github.com/kevin1024/vcrpy) - [Documentation](https://vcrpy.readthedocs.io/) [GitHub](https://github.com/kevin1024/vcrpy) - [Documentation](https://vcrpy.readthedocs.io/)
Record and repeat requests. A utility for record and repeat an http request.
### httpx-caching
[Github](https://github.com/johtso/httpx-caching)
This package adds caching functionality to HTTPX
### httpx-sse
[GitHub](https://github.com/florimondmanca/httpx-sse)
Allows consuming Server-Sent Events (SSE) with HTTPX.
### robox
[Github](https://github.com/danclaudiupop/robox)
A library for scraping the web built on top of HTTPX.
## Gists ## Gists
<!-- NOTE: this list is in alphabetical order. -->
### urllib3-transport ### urllib3-transport
[GitHub](https://gist.github.com/florimondmanca/d56764d78d748eb9f73165da388e546e) [GitHub](https://gist.github.com/florimondmanca/d56764d78d748eb9f73165da388e546e)

View File

@ -50,7 +50,6 @@ __all__ = [
"DecodingError", "DecodingError",
"delete", "delete",
"DigestAuth", "DigestAuth",
"FunctionAuth",
"get", "get",
"head", "head",
"Headers", "Headers",

View File

@ -1,3 +1,3 @@
__title__ = "httpx" __title__ = "httpx"
__description__ = "A next generation HTTP client, for Python 3." __description__ = "A next generation HTTP client, for Python 3."
__version__ = "0.28.1" __version__ = "0.28.0"

View File

@ -51,7 +51,7 @@ def request(
proxy: ProxyTypes | None = None, proxy: ProxyTypes | None = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False, follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
trust_env: bool = True, trust_env: bool = True,
) -> Response: ) -> Response:
""" """
@ -136,7 +136,7 @@ def stream(
proxy: ProxyTypes | None = None, proxy: ProxyTypes | None = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False, follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
trust_env: bool = True, trust_env: bool = True,
) -> typing.Iterator[Response]: ) -> typing.Iterator[Response]:
""" """
@ -180,7 +180,7 @@ def get(
auth: AuthTypes | None = None, auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None, proxy: ProxyTypes | None = None,
follow_redirects: bool = False, follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True, trust_env: bool = True,
) -> Response: ) -> Response:
@ -216,7 +216,7 @@ def options(
auth: AuthTypes | None = None, auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None, proxy: ProxyTypes | None = None,
follow_redirects: bool = False, follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True, trust_env: bool = True,
) -> Response: ) -> Response:
@ -252,7 +252,7 @@ def head(
auth: AuthTypes | None = None, auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None, proxy: ProxyTypes | None = None,
follow_redirects: bool = False, follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True, trust_env: bool = True,
) -> Response: ) -> Response:
@ -292,7 +292,7 @@ def post(
auth: AuthTypes | None = None, auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None, proxy: ProxyTypes | None = None,
follow_redirects: bool = False, follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True, trust_env: bool = True,
) -> Response: ) -> Response:
@ -333,7 +333,7 @@ def put(
auth: AuthTypes | None = None, auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None, proxy: ProxyTypes | None = None,
follow_redirects: bool = False, follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True, trust_env: bool = True,
) -> Response: ) -> Response:
@ -374,7 +374,7 @@ def patch(
auth: AuthTypes | None = None, auth: AuthTypes | None = None,
proxy: ProxyTypes | None = None, proxy: ProxyTypes | None = None,
follow_redirects: bool = False, follow_redirects: bool = False,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
trust_env: bool = True, trust_env: bool = True,
) -> Response: ) -> Response:
@ -412,7 +412,7 @@ def delete(
proxy: ProxyTypes | None = None, proxy: ProxyTypes | None = None,
follow_redirects: bool = False, follow_redirects: bool = False,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
trust_env: bool = True, trust_env: bool = True,
) -> Response: ) -> Response:
""" """

View File

@ -16,7 +16,7 @@ if typing.TYPE_CHECKING: # pragma: no cover
from hashlib import _Hash from hashlib import _Hash
__all__ = ["Auth", "BasicAuth", "DigestAuth", "FunctionAuth", "NetRCAuth"] __all__ = ["Auth", "BasicAuth", "DigestAuth", "NetRCAuth"]
class Auth: class Auth:

View File

@ -33,7 +33,6 @@ from ._transports.default import AsyncHTTPTransport, HTTPTransport
from ._types import ( from ._types import (
AsyncByteStream, AsyncByteStream,
AuthTypes, AuthTypes,
CertTypes,
CookieTypes, CookieTypes,
HeaderTypes, HeaderTypes,
ProxyTypes, ProxyTypes,
@ -620,6 +619,8 @@ class Client(BaseClient):
* **http2** - *(optional)* A boolean indicating if HTTP/2 support should be * **http2** - *(optional)* A boolean indicating if HTTP/2 support should be
enabled. Defaults to `False`. enabled. Defaults to `False`.
* **proxy** - *(optional)* A proxy URL where all the traffic should be routed. * **proxy** - *(optional)* A proxy URL where all the traffic should be routed.
* **proxies** - *(optional)* A dictionary mapping proxy keys to proxy
URLs.
* **timeout** - *(optional)* The timeout configuration to use when sending * **timeout** - *(optional)* The timeout configuration to use when sending
requests. requests.
* **limits** - *(optional)* The limits configuration to use. * **limits** - *(optional)* The limits configuration to use.
@ -643,9 +644,7 @@ class Client(BaseClient):
params: QueryParamTypes | None = None, params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None, headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None, cookies: CookieTypes | None = None,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
cert: CertTypes | None = None,
trust_env: bool = True,
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
proxy: ProxyTypes | None = None, proxy: ProxyTypes | None = None,
@ -657,6 +656,7 @@ class Client(BaseClient):
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
base_url: URL | str = "", base_url: URL | str = "",
transport: BaseTransport | None = None, transport: BaseTransport | None = None,
trust_env: bool = True,
default_encoding: str | typing.Callable[[bytes], str] = "utf-8", default_encoding: str | typing.Callable[[bytes], str] = "utf-8",
) -> None: ) -> None:
super().__init__( super().__init__(
@ -687,8 +687,6 @@ class Client(BaseClient):
self._transport = self._init_transport( self._transport = self._init_transport(
verify=verify, verify=verify,
cert=cert,
trust_env=trust_env,
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
@ -700,8 +698,6 @@ class Client(BaseClient):
else self._init_proxy_transport( else self._init_proxy_transport(
proxy, proxy,
verify=verify, verify=verify,
cert=cert,
trust_env=trust_env,
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
@ -717,9 +713,7 @@ class Client(BaseClient):
def _init_transport( def _init_transport(
self, self,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
cert: CertTypes | None = None,
trust_env: bool = True,
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
@ -730,8 +724,6 @@ class Client(BaseClient):
return HTTPTransport( return HTTPTransport(
verify=verify, verify=verify,
cert=cert,
trust_env=trust_env,
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
@ -740,17 +732,13 @@ class Client(BaseClient):
def _init_proxy_transport( def _init_proxy_transport(
self, self,
proxy: Proxy, proxy: Proxy,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
cert: CertTypes | None = None,
trust_env: bool = True,
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
) -> BaseTransport: ) -> BaseTransport:
return HTTPTransport( return HTTPTransport(
verify=verify, verify=verify,
cert=cert,
trust_env=trust_env,
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
@ -1357,8 +1345,7 @@ class AsyncClient(BaseClient):
params: QueryParamTypes | None = None, params: QueryParamTypes | None = None,
headers: HeaderTypes | None = None, headers: HeaderTypes | None = None,
cookies: CookieTypes | None = None, cookies: CookieTypes | None = None,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
cert: CertTypes | None = None,
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
proxy: ProxyTypes | None = None, proxy: ProxyTypes | None = None,
@ -1401,8 +1388,6 @@ class AsyncClient(BaseClient):
self._transport = self._init_transport( self._transport = self._init_transport(
verify=verify, verify=verify,
cert=cert,
trust_env=trust_env,
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
@ -1415,8 +1400,6 @@ class AsyncClient(BaseClient):
else self._init_proxy_transport( else self._init_proxy_transport(
proxy, proxy,
verify=verify, verify=verify,
cert=cert,
trust_env=trust_env,
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
@ -1431,9 +1414,7 @@ class AsyncClient(BaseClient):
def _init_transport( def _init_transport(
self, self,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
cert: CertTypes | None = None,
trust_env: bool = True,
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
@ -1444,8 +1425,6 @@ class AsyncClient(BaseClient):
return AsyncHTTPTransport( return AsyncHTTPTransport(
verify=verify, verify=verify,
cert=cert,
trust_env=trust_env,
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
@ -1454,17 +1433,13 @@ class AsyncClient(BaseClient):
def _init_proxy_transport( def _init_proxy_transport(
self, self,
proxy: Proxy, proxy: Proxy,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
cert: CertTypes | None = None,
trust_env: bool = True,
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
) -> AsyncBaseTransport: ) -> AsyncBaseTransport:
return AsyncHTTPTransport( return AsyncHTTPTransport(
verify=verify, verify=verify,
cert=cert,
trust_env=trust_env,
http1=http1, http1=http1,
http2=http2, http2=http2,
limits=limits, limits=limits,
@ -1723,7 +1698,7 @@ class AsyncClient(BaseClient):
if not isinstance(request.stream, AsyncByteStream): if not isinstance(request.stream, AsyncByteStream):
raise RuntimeError( raise RuntimeError(
"Attempted to send a sync request with an AsyncClient instance." "Attempted to send an sync request with an AsyncClient instance."
) )
with request_context(request=request): with request_context(request=request):

View File

@ -1,10 +1,9 @@
from __future__ import annotations from __future__ import annotations
import os
import typing import typing
from ._models import Headers from ._models import Headers
from ._types import CertTypes, HeaderTypes, TimeoutTypes from ._types import HeaderTypes, TimeoutTypes
from ._urls import URL from ._urls import URL
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
@ -20,53 +19,20 @@ class UnsetType:
UNSET = UnsetType() UNSET = UnsetType()
def create_ssl_context( def create_ssl_context(verify: ssl.SSLContext | bool = True) -> ssl.SSLContext:
verify: ssl.SSLContext | str | bool = True,
cert: CertTypes | None = None,
trust_env: bool = True,
) -> ssl.SSLContext:
import ssl import ssl
import warnings
import certifi import truststore
if verify is True: if verify is True:
if trust_env and os.environ.get("SSL_CERT_FILE"): # pragma: nocover return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
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: elif verify is False:
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.check_hostname = False ssl_context.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE ssl_context.verify_mode = ssl.CERT_NONE
elif isinstance(verify, str): # pragma: nocover return ssl_context
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 return verify
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: class Timeout:

View File

@ -331,7 +331,9 @@ class StreamClosed(StreamError):
""" """
def __init__(self) -> None: def __init__(self) -> None:
message = "Attempted to read or stream content, but the stream has been closed." message = (
"Attempted to read or stream content, but the stream has " "been closed."
)
super().__init__(message) super().__init__(message)

View File

@ -398,7 +398,7 @@ class Request:
self.method = method.upper() self.method = method.upper()
self.url = URL(url) if params is None else URL(url, params=params) self.url = URL(url) if params is None else URL(url, params=params)
self.headers = Headers(headers) self.headers = Headers(headers)
self.extensions = {} if extensions is None else dict(extensions) self.extensions = {} if extensions is None else extensions
if cookies: if cookies:
Cookies(cookies).set_cookie_header(self) Cookies(cookies).set_cookie_header(self)
@ -537,7 +537,7 @@ class Response:
# the client will set `response.next_request`. # the client will set `response.next_request`.
self.next_request: Request | None = None self.next_request: Request | None = None
self.extensions = {} if extensions is None else dict(extensions) self.extensions: ResponseExtensions = {} if extensions is None else extensions
self.history = [] if history is None else list(history) self.history = [] if history is None else list(history)
self.is_closed = False self.is_closed = False
@ -964,7 +964,7 @@ class Response:
Automatically called if the response body is read to completion. Automatically called if the response body is read to completion.
""" """
if not isinstance(self.stream, SyncByteStream): if not isinstance(self.stream, SyncByteStream):
raise RuntimeError("Attempted to call a sync close on an async stream.") raise RuntimeError("Attempted to call an sync close on an async stream.")
if not self.is_closed: if not self.is_closed:
self.is_closed = True self.is_closed = True
@ -1045,7 +1045,7 @@ class Response:
if self.is_closed: if self.is_closed:
raise StreamClosed() raise StreamClosed()
if not isinstance(self.stream, AsyncByteStream): if not isinstance(self.stream, AsyncByteStream):
raise RuntimeError("Attempted to call an async iterator on a sync stream.") raise RuntimeError("Attempted to call an async iterator on an sync stream.")
self.is_stream_consumed = True self.is_stream_consumed = True
self._num_bytes_downloaded = 0 self._num_bytes_downloaded = 0
@ -1068,7 +1068,7 @@ class Response:
Automatically called if the response body is read to completion. Automatically called if the response body is read to completion.
""" """
if not isinstance(self.stream, AsyncByteStream): if not isinstance(self.stream, AsyncByteStream):
raise RuntimeError("Attempted to call an async close on a sync stream.") raise RuntimeError("Attempted to call an async close on an sync stream.")
if not self.is_closed: if not self.is_closed:
self.is_closed = True self.is_closed = True

View File

@ -53,7 +53,7 @@ from .._exceptions import (
WriteTimeout, WriteTimeout,
) )
from .._models import Request, Response from .._models import Request, Response
from .._types import AsyncByteStream, CertTypes, ProxyTypes, SyncByteStream from .._types import AsyncByteStream, ProxyTypes, SyncByteStream
from .._urls import URL from .._urls import URL
from .base import AsyncBaseTransport, BaseTransport from .base import AsyncBaseTransport, BaseTransport
@ -135,9 +135,7 @@ class ResponseStream(SyncByteStream):
class HTTPTransport(BaseTransport): class HTTPTransport(BaseTransport):
def __init__( def __init__(
self, self,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
cert: CertTypes | None = None,
trust_env: bool = True,
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
@ -150,7 +148,7 @@ class HTTPTransport(BaseTransport):
import httpcore import httpcore
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) ssl_context = create_ssl_context(verify=verify)
if proxy is None: if proxy is None:
self._pool = httpcore.ConnectionPool( self._pool = httpcore.ConnectionPool(
@ -279,9 +277,7 @@ class AsyncResponseStream(AsyncByteStream):
class AsyncHTTPTransport(AsyncBaseTransport): class AsyncHTTPTransport(AsyncBaseTransport):
def __init__( def __init__(
self, self,
verify: ssl.SSLContext | str | bool = True, verify: ssl.SSLContext | bool = True,
cert: CertTypes | None = None,
trust_env: bool = True,
http1: bool = True, http1: bool = True,
http2: bool = False, http2: bool = False,
limits: Limits = DEFAULT_LIMITS, limits: Limits = DEFAULT_LIMITS,
@ -294,7 +290,7 @@ class AsyncHTTPTransport(AsyncBaseTransport):
import httpcore import httpcore
proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy proxy = Proxy(url=proxy) if isinstance(proxy, (str, URL)) else proxy
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env) ssl_context = create_ssl_context(verify=verify)
if proxy is None: if proxy is None:
self._pool = httpcore.AsyncConnectionPool( self._pool = httpcore.AsyncConnectionPool(
@ -355,7 +351,7 @@ class AsyncHTTPTransport(AsyncBaseTransport):
else: # pragma: no cover else: # pragma: no cover
raise ValueError( raise ValueError(
"Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h'," "Proxy protocol must be either 'http', 'https', 'socks5', or 'socks5h',"
f" but got {proxy.url.scheme!r}." " but got {proxy.url.scheme!r}."
) )
async def __aenter__(self: A) -> A: # Use generics for subclass support. async def __aenter__(self: A) -> A: # Use generics for subclass support.

View File

@ -15,6 +15,7 @@ from typing import (
Iterator, Iterator,
List, List,
Mapping, Mapping,
MutableMapping,
Optional, Optional,
Sequence, Sequence,
Tuple, Tuple,
@ -57,7 +58,6 @@ TimeoutTypes = Union[
"Timeout", "Timeout",
] ]
ProxyTypes = Union["URL", str, "Proxy"] ProxyTypes = Union["URL", str, "Proxy"]
CertTypes = Union[str, Tuple[str, str], Tuple[str, str, str]]
AuthTypes = Union[ AuthTypes = Union[
Tuple[Union[str, bytes], Union[str, bytes]], Tuple[Union[str, bytes], Union[str, bytes]],
@ -67,7 +67,7 @@ AuthTypes = Union[
RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
ResponseExtensions = Mapping[str, Any] ResponseExtensions = MutableMapping[str, Any]
RequestData = Mapping[str, Any] RequestData = Mapping[str, Any]
@ -84,7 +84,7 @@ FileTypes = Union[
] ]
RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]]
RequestExtensions = Mapping[str, Any] RequestExtensions = MutableMapping[str, Any]
__all__ = ["AsyncByteStream", "SyncByteStream"] __all__ = ["AsyncByteStream", "SyncByteStream"]

View File

@ -379,7 +379,7 @@ class URL:
if ":" in userinfo: if ":" in userinfo:
# Mask any password component. # Mask any password component.
userinfo = f"{userinfo.split(':')[0]}:[secure]" userinfo = f'{userinfo.split(":")[0]}:[secure]'
authority = "".join( authority = "".join(
[ [

View File

@ -6,7 +6,7 @@ build-backend = "hatchling.build"
name = "httpx" name = "httpx"
description = "The next generation HTTP client." description = "The next generation HTTP client."
license = "BSD-3-Clause" license = "BSD-3-Clause"
requires-python = ">=3.9" requires-python = ">=3.8"
authors = [ authors = [
{ name = "Tom Christie", email = "tom@tomchristie.com" }, { name = "Tom Christie", email = "tom@tomchristie.com" },
] ]
@ -20,15 +20,15 @@ classifiers = [
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP",
] ]
dependencies = [ dependencies = [
"certifi", "truststore==0.10.0",
"httpcore==1.*", "httpcore==1.*",
"anyio", "anyio",
"idna", "idna",
@ -43,7 +43,7 @@ brotli = [
cli = [ cli = [
"click==8.*", "click==8.*",
"pygments==2.*", "pygments==2.*",
"rich>=10,<15", "rich>=10,<14",
] ]
http2 = [ http2 = [
"h2>=3,<5", "h2>=3,<5",

View File

@ -11,19 +11,19 @@ chardet==5.2.0
# Documentation # Documentation
mkdocs==1.6.1 mkdocs==1.6.1
mkautodoc==0.2.0 mkautodoc==0.2.0
mkdocs-material==9.6.18 mkdocs-material==9.5.39
# Packaging # Packaging
build==1.3.0 build==1.2.2
twine==6.1.0 twine==5.1.1
# Tests & Linting # Tests & Linting
coverage[toml]==7.10.6 coverage[toml]==7.6.1
cryptography==45.0.7 cryptography==43.0.1
mypy==1.17.1 mypy==1.11.2
pytest==8.4.1 pytest==8.3.3
ruff==0.12.11 ruff==0.6.8
trio==0.31.0 trio==0.26.2
trio-typing==0.10.0 trio-typing==0.10.0
trustme==1.2.1 trustme==1.1.0
uvicorn==0.35.0 uvicorn==0.31.0

View File

@ -326,7 +326,7 @@ async def test_auth_property() -> None:
async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client: async with httpx.AsyncClient(transport=httpx.MockTransport(app)) as client:
assert client.auth is None assert client.auth is None
client.auth = ("user", "password123") client.auth = ("user", "password123") # type: ignore
assert isinstance(client.auth, httpx.BasicAuth) assert isinstance(client.auth, httpx.BasicAuth)
url = "https://example.org/" url = "https://example.org/"

View File

@ -3,35 +3,35 @@ import httpx
def test_client_base_url(): def test_client_base_url():
client = httpx.Client() client = httpx.Client()
client.base_url = "https://www.example.org/" client.base_url = "https://www.example.org/" # type: ignore
assert isinstance(client.base_url, httpx.URL) assert isinstance(client.base_url, httpx.URL)
assert client.base_url == "https://www.example.org/" assert client.base_url == "https://www.example.org/"
def test_client_base_url_without_trailing_slash(): def test_client_base_url_without_trailing_slash():
client = httpx.Client() client = httpx.Client()
client.base_url = "https://www.example.org/path" client.base_url = "https://www.example.org/path" # type: ignore
assert isinstance(client.base_url, httpx.URL) assert isinstance(client.base_url, httpx.URL)
assert client.base_url == "https://www.example.org/path/" assert client.base_url == "https://www.example.org/path/"
def test_client_base_url_with_trailing_slash(): def test_client_base_url_with_trailing_slash():
client = httpx.Client() client = httpx.Client()
client.base_url = "https://www.example.org/path/" client.base_url = "https://www.example.org/path/" # type: ignore
assert isinstance(client.base_url, httpx.URL) assert isinstance(client.base_url, httpx.URL)
assert client.base_url == "https://www.example.org/path/" assert client.base_url == "https://www.example.org/path/"
def test_client_headers(): def test_client_headers():
client = httpx.Client() client = httpx.Client()
client.headers = {"a": "b"} client.headers = {"a": "b"} # type: ignore
assert isinstance(client.headers, httpx.Headers) assert isinstance(client.headers, httpx.Headers)
assert client.headers["A"] == "b" assert client.headers["A"] == "b"
def test_client_cookies(): def test_client_cookies():
client = httpx.Client() client = httpx.Client()
client.cookies = {"a": "b"} client.cookies = {"a": "b"} # type: ignore
assert isinstance(client.cookies, httpx.Cookies) assert isinstance(client.cookies, httpx.Cookies)
mycookies = list(client.cookies.jar) mycookies = list(client.cookies.jar)
assert len(mycookies) == 1 assert len(mycookies) == 1
@ -42,7 +42,7 @@ def test_client_timeout():
expected_timeout = 12.0 expected_timeout = 12.0
client = httpx.Client() client = httpx.Client()
client.timeout = expected_timeout client.timeout = expected_timeout # type: ignore
assert isinstance(client.timeout, httpx.Timeout) assert isinstance(client.timeout, httpx.Timeout)
assert client.timeout.connect == expected_timeout assert client.timeout.connect == expected_timeout

View File

@ -17,7 +17,7 @@ def test_client_queryparams_string():
assert client.params["a"] == "b" assert client.params["a"] == "b"
client = httpx.Client() client = httpx.Client()
client.params = "a=b" client.params = "a=b" # type: ignore
assert isinstance(client.params, httpx.QueryParams) assert isinstance(client.params, httpx.QueryParams)
assert client.params["a"] == "b" assert client.params["a"] == "b"

View File

@ -226,16 +226,3 @@ def test_request_generator_content_picklable():
request.read() request.read()
pickle_request = pickle.loads(pickle.dumps(request)) pickle_request = pickle.loads(pickle.dumps(request))
assert pickle_request.content == b"test 123" assert pickle_request.content == b"test 123"
def test_request_params():
request = httpx.Request("GET", "http://example.com", params={})
assert str(request.url) == "http://example.com"
request = httpx.Request(
"GET", "http://example.com?c=3", params={"a": "1", "b": "2"}
)
assert str(request.url) == "http://example.com?a=1&b=2"
request = httpx.Request("GET", "http://example.com?a=1", params={})
assert str(request.url) == "http://example.com"

View File

@ -1011,10 +1011,7 @@ def test_response_decode_text_using_autodetect():
assert response.status_code == 200 assert response.status_code == 200
assert response.reason_phrase == "OK" assert response.reason_phrase == "OK"
# The encoded byte string is consistent with either ISO-8859-1 or assert response.encoding == "ISO-8859-1"
# WINDOWS-1252. Versions <6.0 of chardet claim the former, while chardet
# 6.0 detects the latter.
assert response.encoding in ("ISO-8859-1", "WINDOWS-1252")
assert response.text == text assert response.text == text

View File

@ -1,8 +1,5 @@
import ssl import ssl
import typing
from pathlib import Path
import certifi
import pytest import pytest
import httpx import httpx
@ -20,26 +17,6 @@ def test_load_ssl_config_verify_non_existing_file():
context.load_verify_locations(cafile="/path/to/nowhere") context.load_verify_locations(cafile="/path/to/nowhere")
def test_load_ssl_with_keylog(monkeypatch: typing.Any) -> None:
monkeypatch.setenv("SSLKEYLOGFILE", "test")
context = httpx.create_ssl_context()
assert context.keylog_filename == "test"
def test_load_ssl_config_verify_existing_file():
context = httpx.create_ssl_context()
context.load_verify_locations(capath=certifi.where())
assert context.verify_mode == ssl.VerifyMode.CERT_REQUIRED
assert context.check_hostname is True
def test_load_ssl_config_verify_directory():
context = httpx.create_ssl_context()
context.load_verify_locations(capath=Path(certifi.where()).parent)
assert context.verify_mode == ssl.VerifyMode.CERT_REQUIRED
assert context.check_hostname is True
def test_load_ssl_config_cert_and_key(cert_pem_file, cert_private_key_file): def test_load_ssl_config_cert_and_key(cert_pem_file, cert_private_key_file):
context = httpx.create_ssl_context() context = httpx.create_ssl_context()
context.load_cert_chain(cert_pem_file, cert_private_key_file) context.load_cert_chain(cert_pem_file, cert_private_key_file)

View File

@ -489,18 +489,18 @@ def test_response_invalid_argument():
def test_ensure_ascii_false_with_french_characters(): def test_ensure_ascii_false_with_french_characters():
data = {"greeting": "Bonjour, ça va ?"} data = {"greeting": "Bonjour, ça va ?"}
response = httpx.Response(200, json=data) response = httpx.Response(200, json=data)
assert "ça va" in response.text, ( assert (
"ensure_ascii=False should preserve French accented characters" "ça va" in response.text
) ), "ensure_ascii=False should preserve French accented characters"
assert response.headers["Content-Type"] == "application/json" assert response.headers["Content-Type"] == "application/json"
def test_separators_for_compact_json(): def test_separators_for_compact_json():
data = {"clé": "valeur", "liste": [1, 2, 3]} data = {"clé": "valeur", "liste": [1, 2, 3]}
response = httpx.Response(200, json=data) response = httpx.Response(200, json=data)
assert response.text == '{"clé":"valeur","liste":[1,2,3]}', ( assert (
"separators=(',', ':') should produce a compact representation" response.text == '{"clé":"valeur","liste":[1,2,3]}'
) ), "separators=(',', ':') should produce a compact representation"
assert response.headers["Content-Type"] == "application/json" assert response.headers["Content-Type"] == "application/json"