diff --git a/README.md b/README.md
index a5a2de57..6ac6c0b8 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@
```
[Impit](https://github.com/apify/impit) is the default HTTP client and is installed automatically. To use the
- built-in [HTTPX](https://github.com/pydantic/httpx2) client instead, install the optional `httpx2` extra, which
+ built-in [HTTPX2](https://github.com/pydantic/httpx2) client instead, install the optional `httpx2` extra, which
provides Pydantic's maintained continuation of HTTPX, and pass `http_client=Httpx2HttpClient()` to
`ApifyClient.with_custom_http_client()`:
@@ -135,7 +135,7 @@ For a guided walkthrough — authenticating, running an Actor, and reading its r
- **Tiered timeouts** — short / medium / long tiers picked per endpoint, overridable per call ([Timeouts](https://docs.apify.com/api/client/python/docs/concepts/timeouts)).
- **Pagination and streaming** — iterate datasets, key-value store keys, or live logs without manual paging or buffering ([Pagination](https://docs.apify.com/api/client/python/docs/concepts/pagination), [Streaming](https://docs.apify.com/api/client/python/docs/concepts/streaming-resources)).
- **Convenience methods** — `call()`, `wait_for_finish()`, nested resource access, and other shortcuts that hide platform quirks ([Convenience methods](https://docs.apify.com/api/client/python/docs/concepts/convenience-methods)).
-- **Pluggable HTTP layer** — use the default [Impit](https://github.com/apify/impit)-based client, opt in to the built-in [HTTPX](https://github.com/pydantic/httpx2) client, or plug in any custom implementation ([HTTP clients](https://docs.apify.com/api/client/python/docs/concepts/custom-http-clients)).
+- **Pluggable HTTP layer** — use the default [Impit](https://github.com/apify/impit)-based client, opt in to the built-in [HTTPX2](https://github.com/pydantic/httpx2) client, or plug in any custom implementation ([HTTP clients](https://docs.apify.com/api/client/python/docs/concepts/custom-http-clients)).
- **Structured errors** — every API error surfaces as an [`ApifyApiError`](https://docs.apify.com/api/client/python/reference/class/ApifyApiError) with HTTP-specific subclasses for precise handling ([Error handling](https://docs.apify.com/api/client/python/docs/concepts/error-handling)).
- **Debug logging** — opt-in structured logging on the `apify_client` logger captures request URLs, status codes, retry attempts, and more ([Logging](https://docs.apify.com/api/client/python/docs/concepts/logging)).
diff --git a/docs/01_introduction/index.mdx b/docs/01_introduction/index.mdx
index 1a530baf..44b3ac1f 100644
--- a/docs/01_introduction/index.mdx
+++ b/docs/01_introduction/index.mdx
@@ -63,7 +63,7 @@ For better request-body compression, opt in to `brotli`, which compresses better
For details, see [HTTP compression](../02_concepts/13_http_compression.mdx).
-The client uses [Impit](https://github.com/apify/impit) as its default HTTP transport. To use the built-in [HTTPX](https://github.com/pydantic/httpx2) transport, install the optional `httpx2` extra, which provides Pydantic's maintained continuation of HTTPX:
+The client uses [Impit](https://github.com/apify/impit) as its default HTTP transport. To use the built-in [HTTPX2](https://github.com/pydantic/httpx2) transport, install the optional `httpx2` extra, which provides Pydantic's maintained continuation of HTTPX:
diff --git a/docs/02_concepts/10_custom_http_clients.mdx b/docs/02_concepts/10_custom_http_clients.mdx
index a254281a..7d4289d6 100644
--- a/docs/02_concepts/10_custom_http_clients.mdx
+++ b/docs/02_concepts/10_custom_http_clients.mdx
@@ -19,7 +19,7 @@ import ArchitectureImportsExample from '!!raw-loader!./code/10_architecture_impo
import PluggingInAsyncExample from '!!raw-loader!./code/10_plugging_in_async.py';
import PluggingInSyncExample from '!!raw-loader!./code/10_plugging_in_sync.py';
-The Apify API client uses a pluggable HTTP layer. It ships with an [Impit](https://github.com/apify/impit)-based default, offers [HTTPX](https://github.com/pydantic/httpx2) as an optional built-in alternative, and accepts custom synchronous or asynchronous implementations.
+The Apify API client uses a pluggable HTTP layer. It ships with an [Impit](https://github.com/apify/impit)-based default, offers [HTTPX2](https://github.com/pydantic/httpx2) as an optional built-in alternative, and accepts custom synchronous or asynchronous implementations.
## Default HTTP client
@@ -45,11 +45,11 @@ You can configure the default client through the
-## Built-in HTTPX client
+## Built-in HTTPX2 client
-The package also provides `Httpx2HttpClient` and `Httpx2HttpClientAsync`. They use the same request preparation, compression, retry policy, timeout tiers and growth, error handling, logging, and statistics as the default Impit clients, with [HTTPX](https://github.com/pydantic/httpx2) as the transport.
+The package also provides `Httpx2HttpClient` and `Httpx2HttpClientAsync`. They use the same request preparation, compression, retry policy, timeout tiers and growth, error handling, logging, and statistics as the default Impit clients, with [HTTPX2](https://github.com/pydantic/httpx2) as the transport.
-HTTPX is an optional dependency provided by the `httpx2` package, Pydantic's maintained continuation of HTTPX. Install `apify-client[httpx2]`, then pass the appropriate client to `ApifyClient.with_custom_http_client`. Impit remains the default even when the extra is installed.
+The `httpx2` package, Pydantic's maintained continuation of HTTPX, is an optional dependency. Install `apify-client[httpx2]`, then pass the appropriate client to `ApifyClient.with_custom_http_client`. Impit remains the default even when the extra is installed.
```bash
pip install "apify-client[httpx2]"
@@ -70,9 +70,9 @@ uv add "apify-client[httpx2]"
-Configure retries, timeout tiers, default headers, and compression on the HTTPX client instance. The token passed to `with_custom_http_client` is applied automatically unless the HTTP client already has an `Authorization` header. The examples use the clients as context managers so their connection pools are closed deterministically. If a context manager doesn't fit your application's lifecycle, call `close()` on `Httpx2HttpClient` or `await aclose()` on `Httpx2HttpClientAsync` during shutdown.
+Configure retries, timeout tiers, default headers, and compression on the HTTPX2 client instance. The token passed to `with_custom_http_client` is applied automatically unless the HTTP client already has an `Authorization` header. The examples use the clients as context managers so their connection pools are closed deterministically. If a context manager doesn't fit your application's lifecycle, call `close()` on `Httpx2HttpClient` or `await aclose()` on `Httpx2HttpClientAsync` during shutdown.
-Timeout values are passed to the selected transport. Impit enforces them as a deadline for the whole request, body included. HTTPX applies them to each socket operation instead, so a response whose body arrives slowly keeps resetting the timeout and can outlast both the requested timeout and `timeout_max`. The `no_timeout` option disables HTTPX's timeouts.
+Timeout values are passed to the selected transport. Impit enforces them as a deadline for the whole request, body included. HTTPX2 applies them to each socket operation instead, so a response whose body arrives slowly keeps resetting the timeout and can outlast both the requested timeout and `timeout_max`. The `no_timeout` option disables HTTPX2's timeouts.
## Architecture
@@ -80,7 +80,7 @@ Internally, the HTTP client hierarchy has three layers:
- A common internal base contains configuration and utilities shared by synchronous and asynchronous clients, including headers, request-body preparation, parameters, compression, and timeout tiers. It isn't a public extension point.
- `HttpClient` and `HttpClientAsync` add the synchronous or asynchronous request pipeline, retry loop, transport hooks, and lifecycle interface.
-- The built-in Impit and HTTPX classes inherit directly from the corresponding sync or async class and adapt the underlying transport.
+- The built-in Impit and HTTPX2 classes inherit directly from the corresponding sync or async class and adapt the underlying transport.
`HttpClient.is_timeout_error(exc)` and `HttpClientAsync.is_timeout_error(exc)` are the public, transport-neutral way to tell whether an exception is a timeout, so code built on the client, such as streamed logs, doesn't need to know which transport raised it.
@@ -103,7 +103,7 @@ The public `call` method provides the shared request pipeline. A concrete transp
- `is_timeout_error(exc)` identifies transport-specific timeout exceptions for higher-level client features. The default recognizes Python's `TimeoutError`. Timeout classification is independent of retryability, so a timeout the retry loop should retry has to be listed in `is_retryable_transport_error` too.
- `close()` or `aclose()` closes resources owned by the transport. The default does nothing, which is correct for a transport that owns no pool or session.
-Decorate your implementations with `@override`, as the built-in Impit and HTTPX adapters do, so a type checker catches a misspelled or incompatible override.
+Decorate your implementations with `@override`, as the built-in Impit and HTTPX2 adapters do, so a type checker catches a misspelled or incompatible override.
### The HTTP response protocol
@@ -124,9 +124,9 @@ Decorate your implementations with `@override`, as the built-in Impit and HTTPX
| `aiter_bytes() -> AsyncIterator[bytes]` | Iterate body in chunks (async) |
:::note
-Many HTTP libraries, including our default [Impit](https://github.com/apify/impit) or for example [HTTPX](https://github.com/pydantic/httpx2) already satisfy this protocol out of the box.
+Many HTTP libraries, including our default [Impit](https://github.com/apify/impit) or for example [HTTPX2](https://github.com/pydantic/httpx2) already satisfy this protocol out of the box.
-For a streamed response, consume the body inside the streaming context manager with `iter_bytes()` / `aiter_bytes()`, or call `read()` / `aread()` before accessing `content`. Some transports, including HTTPX, intentionally reject `content` on an unread streamed response.
+For a streamed response, consume the body inside the streaming context manager with `iter_bytes()` / `aiter_bytes()`, or call `read()` / `aread()` before accessing `content`. Some transports, including HTTPX2, intentionally reject `content` on an unread streamed response.
:::
### Plugging it in
@@ -154,7 +154,7 @@ If you override `call` itself, your implementation becomes responsible for reque
## Use cases
-Custom HTTP clients might be useful when the built-in Impit and HTTPX clients don't cover your requirements, for example when you need to:
+Custom HTTP clients might be useful when the built-in Impit and HTTPX2 clients don't cover your requirements, for example when you need to:
- **Use a different HTTP library** - Integrate [requests](https://requests.readthedocs.io/), [aiohttp](https://docs.aiohttp.org/), or another transport.
- **Route through a proxy** - Add proxy support or request routing.
diff --git a/docs/03_guides/05_custom_http_client.mdx b/docs/03_guides/05_custom_http_client.mdx
index 52a02848..05f1c05e 100644
--- a/docs/03_guides/05_custom_http_client.mdx
+++ b/docs/03_guides/05_custom_http_client.mdx
@@ -14,7 +14,7 @@ import CustomHttpClientSyncExample from '!!raw-loader!./code/05_custom_http_clie
This guide implements a custom `HttpClientAsync` with [aiohttp](https://docs.aiohttp.org/) and a custom `HttpClient` with [requests](https://requests.readthedocs.io/). Neither library satisfies the `HttpResponse` protocol, so both examples also show how to adapt a foreign response API.
-For an overview of the architecture and the built-in Impit and HTTPX implementations, see [HTTP clients](../02_concepts/10_custom_http_clients.mdx).
+For an overview of the architecture and the built-in Impit and HTTPX2 implementations, see [HTTP clients](../02_concepts/10_custom_http_clients.mdx).
## Installation
@@ -47,5 +47,5 @@ Each example has three parts:
:::warning
-These examples are compact integrations, not a replacement for all built-in client behavior. A production custom client should account for transport-specific details such as proxy configuration, TLS settings, redirects, and response resource cleanup. Timeout semantics differ per transport too: the aiohttp example passes the value as a budget for the whole request, while `requests` applies it to each socket read. Both example sessions also keep a shared cookie jar, which replays server cookies on later API requests. The built-in HTTPX client clears it instead.
+These examples are compact integrations, not a replacement for all built-in client behavior. A production custom client should account for transport-specific details such as proxy configuration, TLS settings, redirects, and response resource cleanup. Timeout semantics differ per transport too: the aiohttp example passes the value as a budget for the whole request, while `requests` applies it to each socket read. Both example sessions also keep a shared cookie jar, which replays server cookies on later API requests. The built-in HTTPX2 client clears it instead.
:::
diff --git a/docs/04_upgrading/upgrading_to_v2.mdx b/docs/04_upgrading/upgrading_to_v2.mdx
index c794503f..ed02202b 100644
--- a/docs/04_upgrading/upgrading_to_v2.mdx
+++ b/docs/04_upgrading/upgrading_to_v2.mdx
@@ -14,7 +14,7 @@ Support for Python 3.9 has been dropped. The Apify Python API Client v2.x now re
## New underlying HTTP library
-In v2.0, the Apify Python API client switched from using [`httpx`](https://github.com/pydantic/httpx2) to [`impit`](https://github.com/apify/impit) as the underlying HTTP library. However, this change shouldn't have much impact on the end user.
+In v2.0, the Apify Python API client switched from using [`httpx`](https://www.python-httpx.org/) to [`impit`](https://github.com/apify/impit) as the underlying HTTP library. However, this change shouldn't have much impact on the end user.
## API method changes
diff --git a/src/apify_client/http_clients/__init__.py b/src/apify_client/http_clients/__init__.py
index 399726f5..6bfea628 100644
--- a/src/apify_client/http_clients/__init__.py
+++ b/src/apify_client/http_clients/__init__.py
@@ -5,7 +5,7 @@
_install_import_hook(__name__)
-# `httpx2` is an optional extra, so the import is wrapped in try_import. Accessing the HTTPX clients without the
+# `httpx2` is an optional extra, so the import is wrapped in try_import. Accessing the HTTPX2 clients without the
# extra installed raises a clear ImportError instead of failing at package import time.
with _try_import(
__name__,
diff --git a/src/apify_client/http_clients/_httpx2.py b/src/apify_client/http_clients/_httpx2.py
index f2b155b4..429fda78 100644
--- a/src/apify_client/http_clients/_httpx2.py
+++ b/src/apify_client/http_clients/_httpx2.py
@@ -2,7 +2,7 @@
from typing import TYPE_CHECKING
-import httpx2 as httpx
+import httpx2
from typing_extensions import override
from apify_client._consts import (
@@ -24,32 +24,32 @@
_PERMANENT_ERRORS = (
- # A request HTTPX rejects before sending it, e.g. one carrying an invalid header value.
- httpx.LocalProtocolError,
- # A URL scheme HTTPX refuses to speak, which repeating the request cannot change.
- httpx.UnsupportedProtocol,
+ # A request HTTPX2 rejects before sending it, e.g. one carrying an invalid header value.
+ httpx2.LocalProtocolError,
+ # A URL scheme HTTPX2 refuses to speak, which repeating the request cannot change.
+ httpx2.UnsupportedProtocol,
# An over-long redirect chain is a routing loop, which repeating the request cannot break.
- httpx.TooManyRedirects,
+ httpx2.TooManyRedirects,
# Only `Response.raise_for_status()` raises this, and the client never calls it - the shared pipeline decides on
# status codes from the response itself.
- httpx.HTTPStatusError,
+ httpx2.HTTPStatusError,
)
-"""HTTPX errors that a retry cannot fix. Everything else in the `httpx.HTTPError` tree counts as transient."""
+"""HTTPX2 errors that a retry cannot fix. Everything else in the `httpx2.HTTPError` tree counts as transient."""
@docs_group('HTTP clients')
class Httpx2HttpClient(HttpClient):
- """Synchronous HTTP client for the Apify API built on top of [HTTPX](https://github.com/pydantic/httpx2).
+ """Synchronous HTTP client for the Apify API built on top of [HTTPX2](https://github.com/pydantic/httpx2).
- This client wraps `httpx.Client` and adds automatic retries with exponential backoff for rate-limited
+ This client wraps `httpx2.Client` and adds automatic retries with exponential backoff for rate-limited
(HTTP 429) and server error (HTTP 5xx) responses.
- HTTPX applies a request timeout to each socket operation rather than to the request as a whole, so a response
+ HTTPX2 applies a request timeout to each socket operation rather than to the request as a whole, so a response
whose body arrives slowly keeps resetting it and can outlast both the requested timeout and `timeout_max`. The
default Impit client enforces the same value as a deadline for the whole request, body included.
Requires the `httpx2` extra: `pip install "apify-client[httpx2]"`. The `httpx2` package is Pydantic's maintained
- continuation of HTTPX, which this module imports under the `httpx` name.
+ continuation of HTTPX.
"""
def __init__(
@@ -66,7 +66,7 @@ def __init__(
headers: dict[str, str] | None = None,
http_compressor: HttpCompressor | None = None,
) -> None:
- """Initialize the HTTPX-based synchronous HTTP client.
+ """Initialize the HTTPX2-based synchronous HTTP client.
Args:
token: Apify API token for authentication.
@@ -93,27 +93,27 @@ def __init__(
http_compressor=http_compressor,
)
- self._httpx_client = httpx.Client(
+ self._httpx2_client = httpx2.Client(
follow_redirects=True,
event_hooks={'response': [self._clear_response_cookies]},
)
@override
def is_timeout_error(self, exc: Exception) -> bool:
- return super().is_timeout_error(exc) or isinstance(exc, httpx.TimeoutException)
+ return super().is_timeout_error(exc) or isinstance(exc, httpx2.TimeoutException)
@override
def is_retryable_transport_error(self, exc: Exception) -> bool:
- # Every error from HTTPX's own hierarchy counts as transient except the permanently-failing types listed in
- # `_PERMANENT_ERRORS`. Retrying is the default so a subclass HTTPX adds later is retried rather than
+ # Every error from HTTPX2's own hierarchy counts as transient except the permanently-failing types listed in
+ # `_PERMANENT_ERRORS`. Retrying is the default so a subclass HTTPX2 adds later is retried rather than
# silently treated as fatal. HTTP status code errors are handled by the shared pipeline based on the
# response status code, not here.
- return isinstance(exc, httpx.HTTPError) and not isinstance(exc, _PERMANENT_ERRORS)
+ return isinstance(exc, httpx2.HTTPError) and not isinstance(exc, _PERMANENT_ERRORS)
@override
def close(self) -> None:
- """Close the underlying HTTPX connection pool."""
- self._httpx_client.close()
+ """Close the underlying HTTPX2 connection pool."""
+ self._httpx2_client.close()
@override
def send_request(
@@ -125,8 +125,8 @@ def send_request(
content: bytes | None,
timeout: float | None,
stream: bool,
- ) -> httpx.Response:
- request = self._httpx_client.build_request(
+ ) -> httpx2.Response:
+ request = self._httpx2_client.build_request(
method=method,
url=url,
headers=headers,
@@ -134,26 +134,26 @@ def send_request(
timeout=timeout,
)
_restore_explicit_cookie_header(request, headers)
- return self._httpx_client.send(request, stream=stream)
+ return self._httpx2_client.send(request, stream=stream)
- def _clear_response_cookies(self, _response: httpx.Response) -> None:
- """Prevent HTTPX's shared cookie jar from leaking server cookies into later API requests."""
- self._httpx_client.cookies.clear()
+ def _clear_response_cookies(self, _response: httpx2.Response) -> None:
+ """Prevent HTTPX2's shared cookie jar from leaking server cookies into later API requests."""
+ self._httpx2_client.cookies.clear()
@docs_group('HTTP clients')
class Httpx2HttpClientAsync(HttpClientAsync):
- """Asynchronous HTTP client for the Apify API built on top of [HTTPX](https://github.com/pydantic/httpx2).
+ """Asynchronous HTTP client for the Apify API built on top of [HTTPX2](https://github.com/pydantic/httpx2).
- This client wraps `httpx.AsyncClient` and adds automatic retries with exponential backoff for rate-limited
+ This client wraps `httpx2.AsyncClient` and adds automatic retries with exponential backoff for rate-limited
(HTTP 429) and server error (HTTP 5xx) responses.
- HTTPX applies a request timeout to each socket operation rather than to the request as a whole, so a response
+ HTTPX2 applies a request timeout to each socket operation rather than to the request as a whole, so a response
whose body arrives slowly keeps resetting it and can outlast both the requested timeout and `timeout_max`. The
default Impit client enforces the same value as a deadline for the whole request, body included.
Requires the `httpx2` extra: `pip install "apify-client[httpx2]"`. The `httpx2` package is Pydantic's maintained
- continuation of HTTPX, which this module imports under the `httpx` name.
+ continuation of HTTPX.
"""
def __init__(
@@ -170,7 +170,7 @@ def __init__(
headers: dict[str, str] | None = None,
http_compressor: HttpCompressor | None = None,
) -> None:
- """Initialize the HTTPX-based asynchronous HTTP client.
+ """Initialize the HTTPX2-based asynchronous HTTP client.
Args:
token: Apify API token for authentication.
@@ -197,27 +197,27 @@ def __init__(
http_compressor=http_compressor,
)
- self._httpx_async_client = httpx.AsyncClient(
+ self._httpx2_async_client = httpx2.AsyncClient(
follow_redirects=True,
event_hooks={'response': [self._clear_response_cookies]},
)
@override
def is_timeout_error(self, exc: Exception) -> bool:
- return super().is_timeout_error(exc) or isinstance(exc, httpx.TimeoutException)
+ return super().is_timeout_error(exc) or isinstance(exc, httpx2.TimeoutException)
@override
def is_retryable_transport_error(self, exc: Exception) -> bool:
- # Every error from HTTPX's own hierarchy counts as transient except the permanently-failing types listed in
- # `_PERMANENT_ERRORS`. Retrying is the default so a subclass HTTPX adds later is retried rather than
+ # Every error from HTTPX2's own hierarchy counts as transient except the permanently-failing types listed in
+ # `_PERMANENT_ERRORS`. Retrying is the default so a subclass HTTPX2 adds later is retried rather than
# silently treated as fatal. HTTP status code errors are handled by the shared pipeline based on the
# response status code, not here.
- return isinstance(exc, httpx.HTTPError) and not isinstance(exc, _PERMANENT_ERRORS)
+ return isinstance(exc, httpx2.HTTPError) and not isinstance(exc, _PERMANENT_ERRORS)
@override
async def aclose(self) -> None:
- """Close the underlying asynchronous HTTPX connection pool."""
- await self._httpx_async_client.aclose()
+ """Close the underlying asynchronous HTTPX2 connection pool."""
+ await self._httpx2_async_client.aclose()
@override
async def send_request(
@@ -229,8 +229,8 @@ async def send_request(
content: bytes | None,
timeout: float | None,
stream: bool,
- ) -> httpx.Response:
- request = self._httpx_async_client.build_request(
+ ) -> httpx2.Response:
+ request = self._httpx2_async_client.build_request(
method=method,
url=url,
headers=headers,
@@ -238,17 +238,17 @@ async def send_request(
timeout=timeout,
)
_restore_explicit_cookie_header(request, headers)
- return await self._httpx_async_client.send(request, stream=stream)
+ return await self._httpx2_async_client.send(request, stream=stream)
- async def _clear_response_cookies(self, _response: httpx.Response) -> None:
- """Prevent HTTPX's shared cookie jar from leaking server cookies into later API requests."""
- self._httpx_async_client.cookies.clear()
+ async def _clear_response_cookies(self, _response: httpx2.Response) -> None:
+ """Prevent HTTPX2's shared cookie jar from leaking server cookies into later API requests."""
+ self._httpx2_async_client.cookies.clear()
-def _restore_explicit_cookie_header(request: httpx.Request, headers: dict[str, str]) -> None:
- """Keep only cookies explicitly supplied for this request, never cookies from HTTPX's shared jar.
+def _restore_explicit_cookie_header(request: httpx2.Request, headers: dict[str, str]) -> None:
+ """Keep only cookies explicitly supplied for this request, never cookies from HTTPX2's shared jar.
- HTTPX drops the `Cookie` header when it builds a redirect request and rebuilds it from the jar, so an explicit
+ HTTPX2 drops the `Cookie` header when it builds a redirect request and rebuilds it from the jar, so an explicit
cookie only reaches the first hop of a redirected request.
"""
explicit_cookie = next((value for key, value in headers.items() if key.lower() == 'cookie'), None)
diff --git a/tests/unit/test_client_headers.py b/tests/unit/test_client_headers.py
index e3e02d43..8706c31b 100644
--- a/tests/unit/test_client_headers.py
+++ b/tests/unit/test_client_headers.py
@@ -6,7 +6,7 @@
from importlib import metadata
from typing import TYPE_CHECKING
-import httpx2 as httpx
+import httpx2
from werkzeug import Request, Response
from apify_client.http_clients import Httpx2HttpClient, Httpx2HttpClientAsync, ImpitHttpClient, ImpitHttpClientAsync
@@ -28,9 +28,9 @@ def _transport_wire_headers(
"""Return the headers the transport adds on its own and the content encodings it advertises."""
if issubclass(client_class, (ImpitHttpClient, ImpitHttpClientAsync)):
return {}, {'zstd', 'gzip', 'deflate', 'br'}
- # HTTPX advertises whichever decoders happen to be installed alongside it, so read the set off the client
+ # HTTPX2 advertises whichever decoders happen to be installed alongside it, so read the set off the client
# itself rather than hard-coding it and breaking whenever the environment gains or loses a codec.
- with httpx.Client() as probe:
+ with httpx2.Client() as probe:
return {'Connection': 'keep-alive'}, _parse_accept_encoding(probe.headers['accept-encoding'])
@@ -187,56 +187,56 @@ def _echo_cookie_handler(request: Request) -> Response:
return Response(json.dumps({'cookie': request.headers.get('Cookie')}), content_type='application/json')
-def test_httpx_does_not_reuse_server_cookies(httpserver: HTTPServer) -> None:
- """A Set-Cookie response must not enter HTTPX's shared cookie jar, nor leak into a later API request."""
+def test_httpx2_does_not_reuse_server_cookies(httpserver: HTTPServer) -> None:
+ """A Set-Cookie response must not enter HTTPX2's shared cookie jar, nor leak into a later API request."""
httpserver.expect_request('/set-cookie').respond_with_data('ok', headers={'Set-Cookie': 'session=secret'})
httpserver.expect_request('/echo-cookie').respond_with_handler(_echo_cookie_handler)
with Httpx2HttpClient() as client:
client.call(method='GET', url=httpserver.url_for('/set-cookie'))
- assert len(client._httpx_client.cookies) == 0
+ assert len(client._httpx2_client.cookies) == 0
response = client.call(method='GET', url=httpserver.url_for('/echo-cookie'))
assert response.json() == {'cookie': None}
-async def test_httpx_async_does_not_reuse_server_cookies(httpserver: HTTPServer) -> None:
- """The asynchronous HTTPX pool also remains stateless between API calls."""
+async def test_httpx2_async_does_not_reuse_server_cookies(httpserver: HTTPServer) -> None:
+ """The asynchronous HTTPX2 pool also remains stateless between API calls."""
httpserver.expect_request('/set-cookie').respond_with_data('ok', headers={'Set-Cookie': 'session=secret'})
httpserver.expect_request('/echo-cookie').respond_with_handler(_echo_cookie_handler)
async with Httpx2HttpClientAsync() as client:
await client.call(method='GET', url=httpserver.url_for('/set-cookie'))
- assert len(client._httpx_async_client.cookies) == 0
+ assert len(client._httpx2_async_client.cookies) == 0
response = await client.call(method='GET', url=httpserver.url_for('/echo-cookie'))
assert response.json() == {'cookie': None}
-def test_httpx_drops_cookies_left_in_the_shared_jar(httpserver: HTTPServer) -> None:
+def test_httpx2_drops_cookies_left_in_the_shared_jar(httpserver: HTTPServer) -> None:
"""A cookie another in-flight request left in the shared jar must not ride along on the next request."""
httpserver.expect_request('/echo-cookie').respond_with_handler(_echo_cookie_handler)
with Httpx2HttpClient() as client:
- client._httpx_client.cookies.set('session', 'secret', domain=httpserver.host)
+ client._httpx2_client.cookies.set('session', 'secret', domain=httpserver.host)
response = client.call(method='GET', url=httpserver.url_for('/echo-cookie'))
assert response.json() == {'cookie': None}
-async def test_httpx_async_drops_cookies_left_in_the_shared_jar(httpserver: HTTPServer) -> None:
+async def test_httpx2_async_drops_cookies_left_in_the_shared_jar(httpserver: HTTPServer) -> None:
"""The asynchronous pool, where concurrent requests really do share one jar, drops leftover cookies too."""
httpserver.expect_request('/echo-cookie').respond_with_handler(_echo_cookie_handler)
async with Httpx2HttpClientAsync() as client:
- client._httpx_async_client.cookies.set('session', 'secret', domain=httpserver.host)
+ client._httpx2_async_client.cookies.set('session', 'secret', domain=httpserver.host)
response = await client.call(method='GET', url=httpserver.url_for('/echo-cookie'))
assert response.json() == {'cookie': None}
-def test_httpx_does_not_carry_server_cookies_across_a_redirect(httpserver: HTTPServer) -> None:
- """A cookie set by a redirecting response must not ride along on the next hop, which HTTPX builds from its jar."""
+def test_httpx2_does_not_carry_server_cookies_across_a_redirect(httpserver: HTTPServer) -> None:
+ """A cookie set by a redirecting response must not ride along on the next hop, which HTTPX2 builds from its jar."""
httpserver.expect_request('/redirect').respond_with_data(
'',
status=302,
@@ -250,7 +250,7 @@ def test_httpx_does_not_carry_server_cookies_across_a_redirect(httpserver: HTTPS
assert response.json() == {'cookie': None}
-async def test_httpx_async_does_not_carry_server_cookies_across_a_redirect(httpserver: HTTPServer) -> None:
+async def test_httpx2_async_does_not_carry_server_cookies_across_a_redirect(httpserver: HTTPServer) -> None:
"""The asynchronous pool keeps a redirecting response's cookie off the next hop too."""
httpserver.expect_request('/redirect').respond_with_data(
'',
@@ -265,7 +265,7 @@ async def test_httpx_async_does_not_carry_server_cookies_across_a_redirect(https
assert response.json() == {'cookie': None}
-def test_httpx_keeps_explicit_cookie_header(httpserver: HTTPServer) -> None:
+def test_httpx2_keeps_explicit_cookie_header(httpserver: HTTPServer) -> None:
"""Disabling the shared cookie jar must not remove a Cookie header explicitly supplied by the caller."""
httpserver.expect_request('/echo-explicit-cookie').respond_with_handler(_echo_cookie_handler)
@@ -279,7 +279,7 @@ def test_httpx_keeps_explicit_cookie_header(httpserver: HTTPServer) -> None:
assert response.json() == {'cookie': 'explicit=value'}
-async def test_httpx_async_keeps_explicit_cookie_header(httpserver: HTTPServer) -> None:
+async def test_httpx2_async_keeps_explicit_cookie_header(httpserver: HTTPServer) -> None:
"""The asynchronous pool forwards an explicitly supplied Cookie header as well."""
httpserver.expect_request('/echo-explicit-cookie').respond_with_handler(_echo_cookie_handler)
diff --git a/tests/unit/test_client_timeouts.py b/tests/unit/test_client_timeouts.py
index 046ae905..f2ce6bb4 100644
--- a/tests/unit/test_client_timeouts.py
+++ b/tests/unit/test_client_timeouts.py
@@ -5,7 +5,7 @@
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, Mock
-import httpx2 as httpx
+import httpx2
import impit
import pytest
@@ -23,8 +23,8 @@
if TYPE_CHECKING:
from _pytest.logging import LogCaptureFixture
-UNSET_HTTPX_TIMEOUT = {'connect': None, 'read': None, 'write': None, 'pool': None}
-"""What HTTPX stores on a request built with `timeout=None`: every sub-timeout unset, not the client default."""
+UNSET_HTTPX2_TIMEOUT = {'connect': None, 'read': None, 'write': None, 'pool': None}
+"""What HTTPX2 stores on a request built with `timeout=None`: every sub-timeout unset, not the client default."""
@pytest.fixture
@@ -40,7 +40,7 @@ def successful_response() -> Mock:
def retryable_error(client: HttpClient | HttpClientAsync) -> Exception:
if isinstance(client, (ImpitHttpClient, ImpitHttpClientAsync)):
return impit.TimeoutException('timeout')
- return httpx.ReadTimeout('timeout', request=httpx.Request('GET', 'https://example.com'))
+ return httpx2.ReadTimeout('timeout', request=httpx2.Request('GET', 'https://example.com'))
@pytest.mark.parametrize(
@@ -235,29 +235,29 @@ async def test_no_timeout_mapping_for_async_impit_adapter() -> None:
assert client._impit_async_client.request.call_args.kwargs['timeout'] == 86_400
-def test_no_timeout_mapping_for_sync_httpx_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
- """The synchronous HTTPX adapter maps no-timeout to every HTTPX sub-timeout being unset."""
- # Only the transport call is stubbed, so the real `build_request` decides what `None` means to HTTPX.
+def test_no_timeout_mapping_for_sync_httpx2_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
+ """The synchronous HTTPX2 adapter maps no-timeout to every HTTPX2 sub-timeout being unset."""
+ # Only the transport call is stubbed, so the real `build_request` decides what `None` means to HTTPX2.
with Httpx2HttpClient() as client:
send = Mock(return_value=successful_response())
- monkeypatch.setattr(client._httpx_client, 'send', send)
+ monkeypatch.setattr(client._httpx2_client, 'send', send)
client.send_request(
method='GET', url='https://example.com', headers={}, content=None, timeout=None, stream=False
)
- assert send.call_args.args[0].extensions['timeout'] == UNSET_HTTPX_TIMEOUT
+ assert send.call_args.args[0].extensions['timeout'] == UNSET_HTTPX2_TIMEOUT
-async def test_no_timeout_mapping_for_async_httpx_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
- """The asynchronous HTTPX adapter maps no-timeout to every HTTPX sub-timeout being unset."""
- # Only the transport call is stubbed, so the real `build_request` decides what `None` means to HTTPX.
+async def test_no_timeout_mapping_for_async_httpx2_adapter(monkeypatch: pytest.MonkeyPatch) -> None:
+ """The asynchronous HTTPX2 adapter maps no-timeout to every HTTPX2 sub-timeout being unset."""
+ # Only the transport call is stubbed, so the real `build_request` decides what `None` means to HTTPX2.
async with Httpx2HttpClientAsync() as client:
send = AsyncMock(return_value=successful_response())
- monkeypatch.setattr(client._httpx_async_client, 'send', send)
+ monkeypatch.setattr(client._httpx2_async_client, 'send', send)
await client.send_request(
method='GET', url='https://example.com', headers={}, content=None, timeout=None, stream=False
)
- assert send.call_args.args[0].extensions['timeout'] == UNSET_HTTPX_TIMEOUT
+ assert send.call_args.args[0].extensions['timeout'] == UNSET_HTTPX2_TIMEOUT
diff --git a/tests/unit/test_http_clients.py b/tests/unit/test_http_clients.py
index 4e55cb8b..3e441189 100644
--- a/tests/unit/test_http_clients.py
+++ b/tests/unit/test_http_clients.py
@@ -11,7 +11,7 @@
from unittest.mock import AsyncMock, Mock
import brotli
-import httpx2 as httpx
+import httpx2
import impit
import pytest
@@ -267,22 +267,22 @@ async def test_http_client_async_creates_async_impit_client() -> None:
await client.aclose()
-def test_http_client_creates_sync_httpx_client() -> None:
- """The synchronous HTTPX adapter creates the underlying HTTPX client, and the close hook closes its pool."""
+def test_http_client_creates_sync_httpx2_client() -> None:
+ """The synchronous HTTPX2 adapter creates the underlying HTTPX2 client, and the close hook closes its pool."""
client = Httpx2HttpClient(token='test_token_123')
- assert isinstance(client._httpx_client, httpx.Client)
+ assert isinstance(client._httpx2_client, httpx2.Client)
client.close()
- assert client._httpx_client.is_closed
+ assert client._httpx2_client.is_closed
-async def test_http_client_async_creates_async_httpx_client() -> None:
- """The asynchronous HTTPX adapter creates the underlying HTTPX client, and the close hook closes its pool."""
+async def test_http_client_async_creates_async_httpx2_client() -> None:
+ """The asynchronous HTTPX2 adapter creates the underlying HTTPX2 client, and the close hook closes its pool."""
client = Httpx2HttpClientAsync(token='test_token_123')
- assert isinstance(client._httpx_async_client, httpx.AsyncClient)
+ assert isinstance(client._httpx2_async_client, httpx2.AsyncClient)
await client.aclose()
- assert client._httpx_async_client.is_closed
+ assert client._httpx2_async_client.is_closed
def test_parse_params_none() -> None:
@@ -416,20 +416,20 @@ async def test_async_http_client_classifies_timeout_errors() -> None:
@pytest.mark.parametrize(
'exc',
[
- # Even the generic base class is transient: HTTPX subclasses it for every failure mode, so an
+ # Even the generic base class is transient: HTTPX2 subclasses it for every failure mode, so an
# unclassified failure is safer to retry.
- pytest.param(httpx.HTTPError('unclassified failure'), id='bare HTTPError'),
- pytest.param(httpx.TimeoutException('timeout'), id='TimeoutException'),
- pytest.param(httpx.NetworkError('network error'), id='NetworkError'),
- pytest.param(httpx.RemoteProtocolError('remote protocol error'), id='RemoteProtocolError'),
- pytest.param(httpx.DecodingError('decoding error'), id='DecodingError'),
+ pytest.param(httpx2.HTTPError('unclassified failure'), id='bare HTTPError'),
+ pytest.param(httpx2.TimeoutException('timeout'), id='TimeoutException'),
+ pytest.param(httpx2.NetworkError('network error'), id='NetworkError'),
+ pytest.param(httpx2.RemoteProtocolError('remote protocol error'), id='RemoteProtocolError'),
+ pytest.param(httpx2.DecodingError('decoding error'), id='DecodingError'),
# One `ProxyError` covers both a proxy rejecting the CONNECT tunnel and a 407, so a transient case cannot
# be told from a permanent one - retrying is the safer default.
- pytest.param(httpx.ProxyError('proxy error'), id='ProxyError'),
+ pytest.param(httpx2.ProxyError('proxy error'), id='ProxyError'),
],
)
-def test_httpx_is_retryable_transport_error(exc: Exception) -> None:
- """A transient HTTPX transport failure is classified as retryable."""
+def test_httpx2_is_retryable_transport_error(exc: Exception) -> None:
+ """A transient HTTPX2 transport failure is classified as retryable."""
with Httpx2HttpClient() as client:
assert client.is_retryable_transport_error(exc)
@@ -437,43 +437,43 @@ def test_httpx_is_retryable_transport_error(exc: Exception) -> None:
@pytest.mark.parametrize(
'exc',
[
- pytest.param(httpx.LocalProtocolError('invalid header value'), id='LocalProtocolError'),
- pytest.param(httpx.UnsupportedProtocol('unsupported scheme'), id='UnsupportedProtocol'),
- pytest.param(httpx.TooManyRedirects('too many redirects'), id='TooManyRedirects'),
+ pytest.param(httpx2.LocalProtocolError('invalid header value'), id='LocalProtocolError'),
+ pytest.param(httpx2.UnsupportedProtocol('unsupported scheme'), id='UnsupportedProtocol'),
+ pytest.param(httpx2.TooManyRedirects('too many redirects'), id='TooManyRedirects'),
pytest.param(
- httpx.HTTPStatusError(
+ httpx2.HTTPStatusError(
'status error',
- request=httpx.Request('GET', 'https://example.com'),
- response=httpx.Response(500),
+ request=httpx2.Request('GET', 'https://example.com'),
+ response=httpx2.Response(500),
),
id='HTTPStatusError',
),
- # HTTPX reports a bad URL outside the `httpx.HTTPError` tree entirely.
- pytest.param(httpx.InvalidURL('unsupported scheme'), id='InvalidURL'),
+ # HTTPX2 reports a bad URL outside the `httpx2.HTTPError` tree entirely.
+ pytest.param(httpx2.InvalidURL('unsupported scheme'), id='InvalidURL'),
pytest.param(ValueError('value error'), id='ValueError'),
pytest.param(RuntimeError('runtime error'), id='RuntimeError'),
pytest.param(Exception('generic exception'), id='Exception'),
],
)
-def test_httpx_is_not_retryable_transport_error(exc: Exception) -> None:
- """A transport failure a retry cannot fix, and anything outside HTTPX's hierarchy, is not retried."""
+def test_httpx2_is_not_retryable_transport_error(exc: Exception) -> None:
+ """A transport failure a retry cannot fix, and anything outside HTTPX2's hierarchy, is not retried."""
with Httpx2HttpClient() as client:
assert not client.is_retryable_transport_error(exc)
-def test_sync_httpx_client_classifies_timeout_errors() -> None:
- """The built-in synchronous HTTPX client exposes transport-neutral timeout classification."""
+def test_sync_httpx2_client_classifies_timeout_errors() -> None:
+ """The built-in synchronous HTTPX2 client exposes transport-neutral timeout classification."""
with Httpx2HttpClient() as client:
assert client.is_timeout_error(TimeoutError('test'))
- assert client.is_timeout_error(httpx.TimeoutException('test'))
+ assert client.is_timeout_error(httpx2.TimeoutException('test'))
assert not client.is_timeout_error(ValueError('test'))
-async def test_async_httpx_client_classifies_timeout_errors() -> None:
- """The built-in asynchronous HTTPX client exposes transport-neutral timeout classification."""
+async def test_async_httpx2_client_classifies_timeout_errors() -> None:
+ """The built-in asynchronous HTTPX2 client exposes transport-neutral timeout classification."""
async with Httpx2HttpClientAsync() as client:
assert client.is_timeout_error(TimeoutError('test'))
- assert client.is_timeout_error(httpx.TimeoutException('test'))
+ assert client.is_timeout_error(httpx2.TimeoutException('test'))
assert not client.is_timeout_error(ValueError('test'))
@@ -514,52 +514,52 @@ def test_transient_transport_error_is_retried() -> None:
assert request.call_count == 3
-def test_httpx_permanent_transport_error_is_not_retried(monkeypatch: pytest.MonkeyPatch) -> None:
- """The HTTPX adapter feeds the same fail-fast classification into the shared pipeline."""
+def test_httpx2_permanent_transport_error_is_not_retried(monkeypatch: pytest.MonkeyPatch) -> None:
+ """The HTTPX2 adapter feeds the same fail-fast classification into the shared pipeline."""
with Httpx2HttpClient(token='test_token', min_delay_between_retries=timedelta(0)) as client:
- send = Mock(side_effect=httpx.UnsupportedProtocol('unsupported scheme'))
- monkeypatch.setattr(client._httpx_client, 'send', send)
+ send = Mock(side_effect=httpx2.UnsupportedProtocol('unsupported scheme'))
+ monkeypatch.setattr(client._httpx2_client, 'send', send)
- with pytest.raises(httpx.UnsupportedProtocol):
+ with pytest.raises(httpx2.UnsupportedProtocol):
client.call(method='GET', url='https://api.test.com/endpoint')
send.assert_called_once()
-async def test_httpx_permanent_transport_error_is_not_retried_async(monkeypatch: pytest.MonkeyPatch) -> None:
- """The asynchronous HTTPX adapter applies the same policy, failing on the first attempt."""
+async def test_httpx2_permanent_transport_error_is_not_retried_async(monkeypatch: pytest.MonkeyPatch) -> None:
+ """The asynchronous HTTPX2 adapter applies the same policy, failing on the first attempt."""
async with Httpx2HttpClientAsync(token='test_token', min_delay_between_retries=timedelta(0)) as client:
- send = AsyncMock(side_effect=httpx.UnsupportedProtocol('unsupported scheme'))
- monkeypatch.setattr(client._httpx_async_client, 'send', send)
+ send = AsyncMock(side_effect=httpx2.UnsupportedProtocol('unsupported scheme'))
+ monkeypatch.setattr(client._httpx2_async_client, 'send', send)
- with pytest.raises(httpx.UnsupportedProtocol):
+ with pytest.raises(httpx2.UnsupportedProtocol):
await client.call(method='GET', url='https://api.test.com/endpoint')
send.assert_awaited_once()
-def test_httpx_transient_transport_error_is_retried(monkeypatch: pytest.MonkeyPatch) -> None:
- """The HTTPX adapter keeps a transient transport failure inside the shared retry loop."""
+def test_httpx2_transient_transport_error_is_retried(monkeypatch: pytest.MonkeyPatch) -> None:
+ """The HTTPX2 adapter keeps a transient transport failure inside the shared retry loop."""
with Httpx2HttpClient(token='test_token', max_retries=2, min_delay_between_retries=timedelta(0)) as client:
- send = Mock(side_effect=httpx.TimeoutException('timeout'))
- monkeypatch.setattr(client._httpx_client, 'send', send)
+ send = Mock(side_effect=httpx2.TimeoutException('timeout'))
+ monkeypatch.setattr(client._httpx2_client, 'send', send)
- with pytest.raises(httpx.TimeoutException):
+ with pytest.raises(httpx2.TimeoutException):
client.call(method='GET', url='https://api.test.com/endpoint')
# `max_retries` attempts inside the backoff loop, plus the final one it makes after the last delay.
assert send.call_count == 3
-async def test_httpx_transient_transport_error_is_retried_async(monkeypatch: pytest.MonkeyPatch) -> None:
- """The asynchronous HTTPX adapter keeps a transient transport failure inside the shared retry loop too."""
+async def test_httpx2_transient_transport_error_is_retried_async(monkeypatch: pytest.MonkeyPatch) -> None:
+ """The asynchronous HTTPX2 adapter keeps a transient transport failure inside the shared retry loop too."""
async with Httpx2HttpClientAsync(
token='test_token', max_retries=2, min_delay_between_retries=timedelta(0)
) as client:
- send = AsyncMock(side_effect=httpx.TimeoutException('timeout'))
- monkeypatch.setattr(client._httpx_async_client, 'send', send)
+ send = AsyncMock(side_effect=httpx2.TimeoutException('timeout'))
+ monkeypatch.setattr(client._httpx2_async_client, 'send', send)
- with pytest.raises(httpx.TimeoutException):
+ with pytest.raises(httpx2.TimeoutException):
await client.call(method='GET', url='https://api.test.com/endpoint')
# `max_retries` attempts inside the backoff loop, plus the final one it makes after the last delay.
diff --git a/tests/unit/test_pluggable_http_client.py b/tests/unit/test_pluggable_http_client.py
index ab7448f9..563f65b8 100644
--- a/tests/unit/test_pluggable_http_client.py
+++ b/tests/unit/test_pluggable_http_client.py
@@ -367,8 +367,8 @@ def test_public_exports() -> None:
assert not hasattr(http_clients_module, 'HttpClientBase')
-def test_httpx_clients_raise_clear_error_when_extra_missing() -> None:
- """Missing HTTPX keeps normal and star imports usable while explicit HTTPX access raises a clear error."""
+def test_httpx2_clients_raise_clear_error_when_extra_missing() -> None:
+ """Missing HTTPX2 keeps normal and star imports usable while explicit HTTPX2 access raises a clear error."""
script = dedent(
"""
import sys