|
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 | +""" |
2 | 18 |
|
3 | 19 | from typing import TYPE_CHECKING, Protocol, Self, runtime_checkable |
4 | 20 |
|
5 | 21 | import httpx |
| 22 | +import httpx2 |
6 | 23 | import requests |
7 | 24 | from beartype import BeartypeConf, beartype |
8 | 25 |
|
@@ -191,6 +208,120 @@ def __call__( |
191 | 208 | ) |
192 | 209 |
|
193 | 210 |
|
| 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 | + |
194 | 325 | @runtime_checkable |
195 | 326 | class AsyncTransport(Protocol): |
196 | 327 | """Protocol for async HTTP transports used by VWS clients. |
@@ -313,3 +444,63 @@ async def __call__( |
313 | 444 | tell_position=len(content), |
314 | 445 | content=content, |
315 | 446 | ) |
| 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