Skip to content

Commit a45df11

Browse files
adamtheturtleclaude
andcommitted
Add HTTPX2 transports
Add HTTPX2Transport and AsyncHTTPX2Transport, which make requests with httpx2, the continuation of httpx maintained by Pydantic. httpx and httpx2 are separate packages with separate client, request, response, timeout and exception classes. The new transports use only httpx2 objects, so they raise httpx2 exceptions, and the existing requests and httpx transports are unchanged. The transports are covered by unit tests against an httpx2 mock transport, and by tests which drive the VWS, Cloud Reco, VuMark and Model Target clients through them against VWS Python Mock, which gained httpx2 interception in 2026.9.6. Closes #3174. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent fbcdb72 commit a45df11

6 files changed

Lines changed: 943 additions & 3 deletions

File tree

docs/source/index.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,44 @@ Dataset generation happens in the background, and the generated dataset is downl
186186
dataset_type=ModelTargetDatasetType.STANDARD,
187187
)
188188
189+
HTTP transports
190+
---------------
191+
192+
By default, the synchronous clients make requests with `requests`_ and the asynchronous clients make requests with `httpx`_.
193+
To use another HTTP library, pass a transport from :mod:`vws.transports` to a client.
194+
195+
Transports are available for `requests`_, `httpx`_ and `HTTPX2`_.
196+
``httpx`` and ``httpx2`` are separate packages with separate client, response and exception classes.
197+
``HTTPXTransport`` and ``AsyncHTTPXTransport`` use ``httpx`` and raise ``httpx`` exceptions.
198+
``HTTPX2Transport`` and ``AsyncHTTPX2Transport`` use ``httpx2`` and raise ``httpx2`` exceptions.
199+
200+
.. clear-namespace
201+
202+
.. code-block:: python
203+
204+
"""List targets using HTTPX2."""
205+
206+
import os
207+
208+
from vws import VWS
209+
from vws.transports import HTTPX2Transport
210+
211+
server_access_key = os.environ["VWS_SERVER_ACCESS_KEY"]
212+
server_secret_key = os.environ["VWS_SERVER_SECRET_KEY"]
213+
214+
vws_client = VWS(
215+
server_access_key=server_access_key,
216+
server_secret_key=server_secret_key,
217+
transport=HTTPX2Transport(),
218+
)
219+
220+
# This database has no targets.
221+
assert not vws_client.list_targets()
222+
223+
.. _requests: https://pypi.org/project/requests/
224+
.. _httpx: https://pypi.org/project/httpx/
225+
.. _HTTPX2: https://httpx2.pydantic.dev/
226+
189227
Testing
190228
-------
191229

newsfragments/3174.change

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add ``HTTPX2Transport`` and ``AsyncHTTPX2Transport``, which make requests with ``httpx2``, the continuation of ``httpx`` maintained by Pydantic. The ``requests`` and ``httpx`` transports are unchanged. ``httpx`` and ``httpx2`` objects are never mixed: the ``httpx`` transports raise ``httpx`` exceptions and the ``httpx2`` transports raise ``httpx2`` exceptions.

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dynamic = [
3333
dependencies = [
3434
"beartype>=0.22.9",
3535
"httpx>=0.28.0",
36+
"httpx2>=2.12",
3637
"requests>=2.32.3",
3738
"urllib3>=2.2.3",
3839
"vws-auth-tools>=2024.7.12",
@@ -86,7 +87,7 @@ optional-dependencies.dev = [
8687
"types-requests==2.33.0.20260712",
8788
"vale==3.20.0.0",
8889
"vulture==2.16",
89-
"vws-python-mock==2026.8.26.1",
90+
"vws-python-mock==2026.9.6",
9091
"vws-test-fixtures==2026.8.26",
9192
"yamlfix==1.19.1",
9293
"zizmor==1.30.0",
@@ -330,6 +331,7 @@ ignore_names = [
330331
"ALWAYS",
331332
"AR_CONTROLLER",
332333
# Public API classes imported by users from vws.transports
334+
"AsyncHTTPX2Transport",
333335
"AsyncHTTPXTransport",
334336
"AUTO",
335337
# Sphinx
@@ -353,6 +355,7 @@ ignore_names = [
353355
"html_theme_options",
354356
"html_title",
355357
"htmlhelp_basename",
358+
"HTTPX2Transport",
356359
"HTTPXTransport",
357360
"IGES",
358361
"intersphinx_mapping",

spelling_private_dict.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ BadImage
44
ConnectionErrorPossiblyImageTooLarge
55
DateRangeError
66
Falsy
7+
HTTPX2
78
ImageTooLarge
89
InactiveProject
910
JSONDecodeError
@@ -17,6 +18,7 @@ OopsAnErrorOccurredPossiblyBadNameError
1718
ProjectHasNoApiAccess
1819
ProjectInactive
1920
ProjectSuspended
21+
Pydantic
2022
QuotaExceeded
2123
RequestQuotaReached
2224
RequestTimeTooSkewed
@@ -65,6 +67,7 @@ html
6567
http
6668
https
6769
httpx
70+
httpx2
6871
iff
6972
io
7073
issuecomment

src/vws/transports.py

Lines changed: 192 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
1-
"""HTTP transport implementations for VWS clients."""
1+
"""HTTP transport implementations for VWS clients.
2+
3+
Three transport families are available:
4+
5+
* ``RequestsTransport`` uses ``requests``. It is synchronous only, and
6+
is the default transport for the synchronous clients.
7+
* ``HTTPXTransport`` and ``AsyncHTTPXTransport`` use ``httpx``.
8+
``AsyncHTTPXTransport`` is the default transport for the
9+
asynchronous clients.
10+
* ``HTTPX2Transport`` and ``AsyncHTTPX2Transport`` use ``httpx2``, the
11+
continuation of ``httpx`` maintained by Pydantic.
12+
13+
``httpx`` and ``httpx2`` are separate packages with separate client,
14+
request, response, timeout and exception classes. Each transport uses
15+
exactly one of them: the ``httpx`` transports raise ``httpx``
16+
exceptions, and the ``httpx2`` transports raise ``httpx2`` exceptions.
17+
"""
218

319
from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable
420

521
import httpx
22+
import httpx2
623
import requests
724
from beartype import BeartypeConf, beartype
825

@@ -191,6 +208,120 @@ def __call__(
191208
)
192209

193210

211+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
212+
def _httpx2_timeout(
213+
*,
214+
request_timeout: float | tuple[float, float],
215+
) -> httpx2.Timeout:
216+
"""The ``httpx2`` timeout for a request timeout.
217+
218+
Args:
219+
request_timeout: The timeout for the request. A float sets
220+
both the connect and read timeouts. A (connect, read)
221+
tuple sets them individually.
222+
223+
Returns:
224+
The equivalent ``httpx2`` timeout.
225+
"""
226+
match request_timeout:
227+
case tuple() as timeout:
228+
connect_timeout, read_timeout = timeout
229+
case timeout:
230+
connect_timeout = timeout
231+
read_timeout = timeout
232+
233+
return httpx2.Timeout(
234+
connect=connect_timeout,
235+
read=read_timeout,
236+
write=None,
237+
pool=None,
238+
)
239+
240+
241+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
242+
def _response_from_httpx2(*, httpx2_response: httpx2.Response) -> Response:
243+
"""Convert an ``httpx2`` response to a ``Response``.
244+
245+
Args:
246+
httpx2_response: The response to convert.
247+
248+
Returns:
249+
A Response populated from the ``httpx2`` response.
250+
"""
251+
content = bytes(httpx2_response.content)
252+
request_content = httpx2_response.request.content
253+
254+
return Response(
255+
text=httpx2_response.text,
256+
url=str(object=httpx2_response.url),
257+
status_code=httpx2_response.status_code,
258+
headers=dict(httpx2_response.headers),
259+
request_body=bytes(request_content) or None,
260+
tell_position=len(content),
261+
content=content,
262+
)
263+
264+
265+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
266+
class HTTPX2Transport:
267+
"""HTTP transport using the ``httpx2`` library.
268+
269+
``httpx2`` is the continuation of ``httpx`` maintained by
270+
Pydantic. Its client, request, response, timeout and exception
271+
classes are distinct from the ``httpx`` ones, so this transport
272+
raises ``httpx2`` exceptions, not ``httpx`` ones.
273+
A single ``httpx2.Client`` is reused across requests
274+
for connection pooling.
275+
"""
276+
277+
def __init__(self) -> None:
278+
"""Create an ``HTTPX2Transport``."""
279+
self._client = httpx2.Client()
280+
281+
def close(self) -> None:
282+
"""Close the underlying ``httpx2.Client``."""
283+
self._client.close()
284+
285+
def __enter__(self) -> Self:
286+
"""Enter the context manager."""
287+
return self
288+
289+
def __exit__(self, *_args: object) -> None:
290+
"""Exit the context manager and close the client."""
291+
self.close()
292+
293+
def __call__(
294+
self,
295+
*,
296+
method: str,
297+
url: str,
298+
headers: dict[str, str],
299+
data: bytes,
300+
request_timeout: float | tuple[float, float],
301+
) -> Response:
302+
"""Make an HTTP request using ``httpx2``.
303+
304+
Args:
305+
method: The HTTP method.
306+
url: The full URL.
307+
headers: Request headers.
308+
data: The request body.
309+
request_timeout: The request timeout.
310+
311+
Returns:
312+
A Response populated from the ``httpx2`` response.
313+
"""
314+
httpx2_response = self._client.request(
315+
method=method,
316+
url=url,
317+
headers=headers,
318+
content=data,
319+
timeout=_httpx2_timeout(request_timeout=request_timeout),
320+
follow_redirects=True,
321+
)
322+
return _response_from_httpx2(httpx2_response=httpx2_response)
323+
324+
194325
@runtime_checkable
195326
class AsyncTransport(Protocol):
196327
"""Protocol for async HTTP transports used by VWS clients.
@@ -313,3 +444,63 @@ async def __call__(
313444
tell_position=len(content),
314445
content=content,
315446
)
447+
448+
449+
@beartype(conf=BeartypeConf(is_pep484_tower=True))
450+
class AsyncHTTPX2Transport:
451+
"""Async HTTP transport using the ``httpx2`` library.
452+
453+
``httpx2`` is the continuation of ``httpx`` maintained by
454+
Pydantic. Its client, request, response, timeout and exception
455+
classes are distinct from the ``httpx`` ones, so this transport
456+
raises ``httpx2`` exceptions, not ``httpx`` ones.
457+
A single ``httpx2.AsyncClient`` is reused across requests
458+
for connection pooling.
459+
"""
460+
461+
def __init__(self) -> None:
462+
"""Create an ``AsyncHTTPX2Transport``."""
463+
self._client = httpx2.AsyncClient()
464+
465+
async def aclose(self) -> None:
466+
"""Close the underlying ``httpx2.AsyncClient``."""
467+
await self._client.aclose()
468+
469+
async def __aenter__(self) -> Self:
470+
"""Enter the async context manager."""
471+
return self
472+
473+
async def __aexit__(self, *_args: object) -> None:
474+
"""Exit the async context manager and close the client."""
475+
await self.aclose()
476+
477+
async def __call__(
478+
self,
479+
*,
480+
method: str,
481+
url: str,
482+
headers: dict[str, str],
483+
data: bytes,
484+
request_timeout: float | tuple[float, float],
485+
) -> Response:
486+
"""Make an async HTTP request using ``httpx2``.
487+
488+
Args:
489+
method: The HTTP method.
490+
url: The full URL.
491+
headers: Request headers.
492+
data: The request body.
493+
request_timeout: The request timeout.
494+
495+
Returns:
496+
A Response populated from the ``httpx2`` response.
497+
"""
498+
httpx2_response = await self._client.request(
499+
method=method,
500+
url=url,
501+
headers=headers,
502+
content=data,
503+
timeout=_httpx2_timeout(request_timeout=request_timeout),
504+
follow_redirects=True,
505+
)
506+
return _response_from_httpx2(httpx2_response=httpx2_response)

0 commit comments

Comments
 (0)