Compare commits

..

2 Commits

Author SHA1 Message Date
Kar Petrosyan
f88fa846d9 changelog 2025-06-10 23:13:35 +04:00
Kar Petrosyan
2f737cadc1 feat: add socket_options to Client and AsyncClient classes 2025-06-10 23:03:04 +04:00
25 changed files with 77 additions and 91 deletions

View File

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

View File

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

View File

@ -4,15 +4,9 @@ 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/).
## [UNRELEASED]
## Development
### Removed
* Drop support for Python 3.8
### Added
* Expose `FunctionAuth` from the public API. (#3699)
* Add `socket_options` to `Client` and `AsyncClient` classes. (#3587)
## 0.28.1 (6th December, 2024)

View File

@ -101,7 +101,7 @@ Or, to include the optional HTTP/2 support, use:
$ pip install httpx[http2]
```
HTTPX requires Python 3.9+.
HTTPX requires Python 3.8+.
## Documentation

View File

@ -29,7 +29,7 @@ import certifi
import httpx
import ssl
# This SSL context is equivalent to the default `verify=True`.
# This SSL context is equivelent to the default `verify=True`.
ctx = ssl.create_default_context(cafile=certifi.where())
client = httpx.Client(verify=ctx)
```
@ -71,7 +71,19 @@ 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).
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). If you want to use these they need to be enabled explicitly.
For example...
```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

View File

@ -23,7 +23,7 @@ To make asynchronous requests, you'll need an `AsyncClient`.
```
!!! 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

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.
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('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]
```
HTTPX requires Python 3.9+
HTTPX requires Python 3.8+
[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...
```
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: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
@ -78,4 +80,4 @@ 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.
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

@ -191,7 +191,7 @@ You can also explicitly set the filename and content type, by using a tuple
of items for the file value:
```pycon
>>> with open('report.xls', 'rb') as report_file:
>>> with open('report.xls', 'rb') 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)

View File

@ -24,12 +24,6 @@ Provides authentication classes to be used with HTTPX's [authentication paramete
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)

View File

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

View File

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

View File

@ -29,7 +29,7 @@ from ._exceptions import (
from ._models import Cookies, Headers, Request, Response
from ._status_codes import codes
from ._transports.base import AsyncBaseTransport, BaseTransport
from ._transports.default import AsyncHTTPTransport, HTTPTransport
from ._transports.default import SOCKET_OPTION, AsyncHTTPTransport, HTTPTransport
from ._types import (
AsyncByteStream,
AuthTypes,
@ -653,6 +653,7 @@ class Client(BaseClient):
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
base_url: URL | str = "",
@ -693,6 +694,7 @@ class Client(BaseClient):
http2=http2,
limits=limits,
transport=transport,
socket_options=socket_options,
)
self._mounts: dict[URLPattern, BaseTransport | None] = {
URLPattern(key): None
@ -705,6 +707,7 @@ class Client(BaseClient):
http1=http1,
http2=http2,
limits=limits,
socket_options=socket_options,
)
for key, proxy in proxy_map.items()
}
@ -723,6 +726,7 @@ class Client(BaseClient):
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
transport: BaseTransport | None = None,
) -> BaseTransport:
if transport is not None:
@ -735,6 +739,7 @@ class Client(BaseClient):
http1=http1,
http2=http2,
limits=limits,
socket_options=socket_options,
)
def _init_proxy_transport(
@ -746,6 +751,7 @@ class Client(BaseClient):
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
) -> BaseTransport:
return HTTPTransport(
verify=verify,
@ -755,6 +761,7 @@ class Client(BaseClient):
http2=http2,
limits=limits,
proxy=proxy,
socket_options=socket_options,
)
def _transport_for_url(self, url: URL) -> BaseTransport:
@ -1366,6 +1373,7 @@ class AsyncClient(BaseClient):
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
follow_redirects: bool = False,
limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None,
base_url: URL | str = "",
@ -1407,6 +1415,7 @@ class AsyncClient(BaseClient):
http2=http2,
limits=limits,
transport=transport,
socket_options=socket_options,
)
self._mounts: dict[URLPattern, AsyncBaseTransport | None] = {
@ -1420,6 +1429,7 @@ class AsyncClient(BaseClient):
http1=http1,
http2=http2,
limits=limits,
socket_options=socket_options,
)
for key, proxy in proxy_map.items()
}
@ -1437,6 +1447,7 @@ class AsyncClient(BaseClient):
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
transport: AsyncBaseTransport | None = None,
) -> AsyncBaseTransport:
if transport is not None:
@ -1449,6 +1460,7 @@ class AsyncClient(BaseClient):
http1=http1,
http2=http2,
limits=limits,
socket_options=socket_options,
)
def _init_proxy_transport(
@ -1460,6 +1472,7 @@ class AsyncClient(BaseClient):
http1: bool = True,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
) -> AsyncBaseTransport:
return AsyncHTTPTransport(
verify=verify,
@ -1469,6 +1482,7 @@ class AsyncClient(BaseClient):
http2=http2,
limits=limits,
proxy=proxy,
socket_options=socket_options,
)
def _transport_for_url(self, url: URL) -> AsyncBaseTransport:

View File

@ -331,7 +331,9 @@ class StreamClosed(StreamError):
"""
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)

View File

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

View File

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

View File

@ -11,19 +11,20 @@ chardet==5.2.0
# Documentation
mkdocs==1.6.1
mkautodoc==0.2.0
mkdocs-material==9.6.18
mkdocs-material==9.5.47
# Packaging
build==1.3.0
twine==6.1.0
build==1.2.2.post1
twine==6.0.1
# Tests & Linting
coverage[toml]==7.10.6
cryptography==45.0.7
mypy==1.17.1
pytest==8.4.1
ruff==0.12.11
trio==0.31.0
coverage[toml]==7.6.1
cryptography==44.0.1
mypy==1.13.0
pytest==8.3.4
ruff==0.8.1
trio==0.27.0
trio-typing==0.10.0
trustme==1.2.1
uvicorn==0.35.0
trustme==1.1.0; python_version < '3.9'
trustme==1.2.0; python_version >= '3.9'
uvicorn==0.32.1

View File

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

View File

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

View File

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

View File

@ -1011,10 +1011,7 @@ def test_response_decode_text_using_autodetect():
assert response.status_code == 200
assert response.reason_phrase == "OK"
# The encoded byte string is consistent with either ISO-8859-1 or
# 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.encoding == "ISO-8859-1"
assert response.text == text

View File

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