Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,44 @@ Dataset generation happens in the background, and the generated dataset is downl
dataset_type=ModelTargetDatasetType.STANDARD,
)

HTTP transports
---------------

By default, the synchronous clients make requests with `requests`_ and the asynchronous clients make requests with `httpx`_.
To use another HTTP library, pass a transport from :mod:`vws.transports` to a client.

Transports are available for `requests`_, `httpx`_ and `HTTPX2`_.
``httpx`` and ``httpx2`` are separate packages with separate client, response and exception classes.
``HTTPXTransport`` and ``AsyncHTTPXTransport`` use ``httpx`` and raise ``httpx`` exceptions.
``HTTPX2Transport`` and ``AsyncHTTPX2Transport`` use ``httpx2`` and raise ``httpx2`` exceptions.

.. clear-namespace

.. code-block:: python

"""List targets using HTTPX2."""

import os

from vws import VWS
from vws.transports import HTTPX2Transport

server_access_key = os.environ["VWS_SERVER_ACCESS_KEY"]
server_secret_key = os.environ["VWS_SERVER_SECRET_KEY"]

vws_client = VWS(
server_access_key=server_access_key,
server_secret_key=server_secret_key,
transport=HTTPX2Transport(),
)

# This database has no targets.
assert not vws_client.list_targets()

.. _requests: https://pypi.org/project/requests/
.. _httpx: https://pypi.org/project/httpx/
.. _HTTPX2: https://httpx2.pydantic.dev/

Testing
-------

Expand Down
1 change: 1 addition & 0 deletions newsfragments/3174.change
Original file line number Diff line number Diff line change
@@ -0,0 +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.
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dynamic = [
dependencies = [
"beartype>=0.22.9",
"httpx>=0.28.0",
"httpx2>=2.12",
"requests>=2.32.3",
"urllib3>=2.2.3",
"vws-auth-tools>=2024.7.12",
Expand Down Expand Up @@ -86,7 +87,7 @@ optional-dependencies.dev = [
"types-requests==2.33.0.20260712",
"vale==3.19.0.0",
"vulture==2.16",
"vws-python-mock==2026.8.26.1",
"vws-python-mock==2026.9.6",
"vws-test-fixtures==2026.8.26",
"yamlfix==1.19.1",
"zizmor==1.30.0",
Expand Down Expand Up @@ -330,6 +331,7 @@ ignore_names = [
"ALWAYS",
"AR_CONTROLLER",
# Public API classes imported by users from vws.transports
"AsyncHTTPX2Transport",
"AsyncHTTPXTransport",
"AUTO",
# Sphinx
Expand All @@ -353,6 +355,7 @@ ignore_names = [
"html_theme_options",
"html_title",
"htmlhelp_basename",
"HTTPX2Transport",
"HTTPXTransport",
"IGES",
"intersphinx_mapping",
Expand Down
3 changes: 3 additions & 0 deletions spelling_private_dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ BadImage
ConnectionErrorPossiblyImageTooLarge
DateRangeError
Falsy
HTTPX2
ImageTooLarge
InactiveProject
JSONDecodeError
Expand All @@ -17,6 +18,7 @@ OopsAnErrorOccurredPossiblyBadNameError
ProjectHasNoApiAccess
ProjectInactive
ProjectSuspended
Pydantic
QuotaExceeded
RequestQuotaReached
RequestTimeTooSkewed
Expand Down Expand Up @@ -65,6 +67,7 @@ html
http
https
httpx
httpx2
iff
io
issuecomment
Expand Down
193 changes: 192 additions & 1 deletion src/vws/transports.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
"""HTTP transport implementations for VWS clients."""
"""HTTP transport implementations for VWS clients.

Three transport families are available:

* ``RequestsTransport`` uses ``requests``. It is synchronous only, and
is the default transport for the synchronous clients.
* ``HTTPXTransport`` and ``AsyncHTTPXTransport`` use ``httpx``.
``AsyncHTTPXTransport`` is the default transport for the
asynchronous clients.
* ``HTTPX2Transport`` and ``AsyncHTTPX2Transport`` use ``httpx2``, the
continuation of ``httpx`` maintained by Pydantic.

``httpx`` and ``httpx2`` are separate packages with separate client,
request, response, timeout and exception classes. Each transport uses
exactly one of them: the ``httpx`` transports raise ``httpx``
exceptions, and the ``httpx2`` transports raise ``httpx2`` exceptions.
"""

from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable

import httpx
import httpx2
import requests
from beartype import BeartypeConf, beartype

Expand Down Expand Up @@ -191,6 +208,120 @@ def __call__(
)


@beartype(conf=BeartypeConf(is_pep484_tower=True))
def _httpx2_timeout(
*,
request_timeout: float | tuple[float, float],
) -> httpx2.Timeout:
"""The ``httpx2`` timeout for a request timeout.

Args:
request_timeout: The timeout for the request. A float sets
both the connect and read timeouts. A (connect, read)
tuple sets them individually.

Returns:
The equivalent ``httpx2`` timeout.
"""
match request_timeout:
case tuple() as timeout:
connect_timeout, read_timeout = timeout
case timeout:
connect_timeout = timeout
read_timeout = timeout

return httpx2.Timeout(
connect=connect_timeout,
read=read_timeout,
write=None,
pool=None,
)


@beartype(conf=BeartypeConf(is_pep484_tower=True))
def _response_from_httpx2(*, httpx2_response: httpx2.Response) -> Response:
"""Convert an ``httpx2`` response to a ``Response``.

Args:
httpx2_response: The response to convert.

Returns:
A Response populated from the ``httpx2`` response.
"""
content = bytes(httpx2_response.content)
request_content = httpx2_response.request.content

return Response(
text=httpx2_response.text,
url=str(object=httpx2_response.url),
status_code=httpx2_response.status_code,
headers=dict(httpx2_response.headers),
request_body=bytes(request_content) or None,
tell_position=len(content),
content=content,
)


@beartype(conf=BeartypeConf(is_pep484_tower=True))
class HTTPX2Transport:
"""HTTP transport using the ``httpx2`` library.

``httpx2`` is the continuation of ``httpx`` maintained by
Pydantic. Its client, request, response, timeout and exception
classes are distinct from the ``httpx`` ones, so this transport
raises ``httpx2`` exceptions, not ``httpx`` ones.
A single ``httpx2.Client`` is reused across requests
for connection pooling.
"""

def __init__(self) -> None:
"""Create an ``HTTPX2Transport``."""
self._client = httpx2.Client()

def close(self) -> None:
"""Close the underlying ``httpx2.Client``."""
self._client.close()

def __enter__(self) -> Self:
"""Enter the context manager."""
return self

def __exit__(self, *_args: object) -> None:
"""Exit the context manager and close the client."""
self.close()

def __call__(
self,
*,
method: str,
url: str,
headers: dict[str, str],
data: bytes,
request_timeout: float | tuple[float, float],
) -> Response:
"""Make an HTTP request using ``httpx2``.

Args:
method: The HTTP method.
url: The full URL.
headers: Request headers.
data: The request body.
request_timeout: The request timeout.

Returns:
A Response populated from the ``httpx2`` response.
"""
httpx2_response = self._client.request(
method=method,
url=url,
headers=headers,
content=data,
timeout=_httpx2_timeout(request_timeout=request_timeout),
follow_redirects=True,
)
return _response_from_httpx2(httpx2_response=httpx2_response)


@runtime_checkable
class AsyncTransport(Protocol):
"""Protocol for async HTTP transports used by VWS clients.
Expand Down Expand Up @@ -313,3 +444,63 @@ async def __call__(
tell_position=len(content),
content=content,
)


@beartype(conf=BeartypeConf(is_pep484_tower=True))
class AsyncHTTPX2Transport:
"""Async HTTP transport using the ``httpx2`` library.

``httpx2`` is the continuation of ``httpx`` maintained by
Pydantic. Its client, request, response, timeout and exception
classes are distinct from the ``httpx`` ones, so this transport
raises ``httpx2`` exceptions, not ``httpx`` ones.
A single ``httpx2.AsyncClient`` is reused across requests
for connection pooling.
"""

def __init__(self) -> None:
"""Create an ``AsyncHTTPX2Transport``."""
self._client = httpx2.AsyncClient()

async def aclose(self) -> None:
"""Close the underlying ``httpx2.AsyncClient``."""
await self._client.aclose()

async def __aenter__(self) -> Self:
"""Enter the async context manager."""
return self

async def __aexit__(self, *_args: object) -> None:
"""Exit the async context manager and close the client."""
await self.aclose()

async def __call__(
self,
*,
method: str,
url: str,
headers: dict[str, str],
data: bytes,
request_timeout: float | tuple[float, float],
) -> Response:
"""Make an async HTTP request using ``httpx2``.

Args:
method: The HTTP method.
url: The full URL.
headers: Request headers.
data: The request body.
request_timeout: The request timeout.

Returns:
A Response populated from the ``httpx2`` response.
"""
httpx2_response = await self._client.request(
method=method,
url=url,
headers=headers,
content=data,
timeout=_httpx2_timeout(request_timeout=request_timeout),
follow_redirects=True,
)
return _response_from_httpx2(httpx2_response=httpx2_response)
Loading
Loading