Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/default_to_httpx2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: major
---

# Default to using httpx2 and drop support for httpx entirely
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from unittest.mock import MagicMock

import httpx
import httpx2

from end_to_end_tests.functional_tests.helpers import (
with_generated_client_fixture,
Expand Down Expand Up @@ -44,8 +44,8 @@ class TestPathParameterEncoding:

def test_path_params_with_normal_chars_work(self, sync_detailed, Client):
"""Test that normal alphanumeric path parameters still work correctly"""
mock_httpx_client = MagicMock(spec=httpx.Client)
mock_response = MagicMock(spec=httpx.Response)
mock_httpx_client = MagicMock(spec=httpx2.Client)
mock_response = MagicMock(spec=httpx2.Response)
mock_response.status_code = 200
mock_response.json.return_value = {"id": "test"}
mock_response.content = b'{"id": "test"}'
Expand All @@ -70,16 +70,16 @@ def test_path_params_with_normal_chars_work(self, sync_detailed, Client):

def test_path_params_with_reserved_chars_are_encoded(self, sync_detailed, Client):
"""Test that path parameters with reserved characters are properly URL-encoded"""
# Create a mock httpx client
mock_httpx_client = MagicMock(spec=httpx.Client)
mock_response = MagicMock(spec=httpx.Response)
# Create a mock httpx2 client
mock_httpx_client = MagicMock(spec=httpx2.Client)
mock_response = MagicMock(spec=httpx2.Response)
mock_response.status_code = 200
mock_response.json.return_value = {"id": "test"}
mock_response.content = b'{"id": "test"}'
mock_response.headers = {}
mock_httpx_client.request.return_value = mock_response

# Create a client with the mock httpx client
# Create a client with the mock httpx2 client
client = Client(base_url="https://api.example.com")
client.set_httpx_client(mock_httpx_client)

Expand All @@ -100,8 +100,8 @@ def test_path_params_with_reserved_chars_are_encoded(self, sync_detailed, Client

def test_path_params_with_spaces_are_encoded(self, sync_detailed, Client):
"""Test that path parameters with spaces are properly URL-encoded"""
mock_httpx_client = MagicMock(spec=httpx.Client)
mock_response = MagicMock(spec=httpx.Response)
mock_httpx_client = MagicMock(spec=httpx2.Client)
mock_response = MagicMock(spec=httpx2.Response)
mock_response.status_code = 200
mock_response.json.return_value = {"id": "test"}
mock_response.content = b'{"id": "test"}'
Expand All @@ -126,8 +126,8 @@ def test_path_params_with_spaces_are_encoded(self, sync_detailed, Client):

def test_path_params_with_hash_are_encoded(self, sync_detailed, Client):
"""Test that path parameters with hash/fragment characters are properly URL-encoded"""
mock_httpx_client = MagicMock(spec=httpx.Client)
mock_response = MagicMock(spec=httpx.Response)
mock_httpx_client = MagicMock(spec=httpx2.Client)
mock_response = MagicMock(spec=httpx2.Response)
mock_response.status_code = 200
mock_response.json.return_value = {"id": "test"}
mock_response.content = b'{"id": "test"}'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Things to know:

## Advanced customizations

There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx.Client` or `httpx.AsyncClient` (depending on your use-case):
There are more settings on the generated `Client` class which let you control more runtime behavior, check out the docstring on that class for more info. You can also customize the underlying `httpx2.Client` or `httpx2.AsyncClient` (depending on your use-case):

```python
from my_test_api_client import Client
Expand All @@ -90,23 +90,23 @@ def log_response(response):

client = Client(
base_url="https://api.example.com",
httpx_args={"event_hooks": {"request": [log_request], "response": [log_response]}},
httpx2_args={"event_hooks": {"request": [log_request], "response": [log_response]}},
)

# Or get the underlying httpx client to modify directly with client.get_httpx_client() or client.get_async_httpx_client()
# Or get the underlying httpx2 client to modify directly with client.get_httpx_client() or client.get_async_httpx_client()
```

You can even set the httpx client directly, but beware that this will override any existing settings (e.g., base_url):
You can even set the httpx2 client directly, but beware that this will override any existing settings (e.g., base_url):

```python
import httpx
import httpx2
from my_test_api_client import Client

client = Client(
base_url="https://api.example.com",
)
# Note that base_url needs to be re-set, as would any shared cookies, headers, etc.
client.set_httpx_client(httpx.Client(base_url="https://api.example.com", proxies="http://localhost:8030"))
client.set_httpx_client(httpx2.Client(base_url="https://api.example.com", proxy="http://localhost:8030"))
```

## Building / publishing this package
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import ssl
from typing import Any, Self

import httpx
import httpx2
from attrs import define, evolve, field


@define
class Client:
"""A class for keeping track of data related to the API

The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally:

``base_url``: The base URL for the API, all requests are made to a relative path to this URL

Expand All @@ -18,14 +18,14 @@ class Client:
``headers``: A dictionary of headers to be sent with every request

``timeout``: The maximum amount of a time a request can take. API functions will raise
httpx.TimeoutException if this is exceeded.
httpx2.TimeoutException if this is exceeded.

``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
but can be set to False for testing purposes.

``follow_redirects``: Whether or not to follow redirects. Default value is False.

``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
``httpx2_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor.

"""

Expand All @@ -34,12 +34,12 @@ class Client:
_base_url: str = field(alias="base_url")
_cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
_headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
_timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
_timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout")
_verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl")
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
_httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
_client: httpx.Client | None = field(default=None, init=False)
_async_client: httpx.AsyncClient | None = field(default=None, init=False)
_httpx2_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx2_args")
_client: httpx2.Client | None = field(default=None, init=False)
_async_client: httpx2.AsyncClient | None = field(default=None, init=False)

def with_headers(self, headers: dict[str, str]) -> "Client":
"""Get a new client matching this one with additional headers"""
Expand All @@ -57,82 +57,82 @@ def with_cookies(self, cookies: dict[str, str]) -> "Client":
self._async_client.cookies.update(cookies)
return evolve(self, cookies={**self._cookies, **cookies})

def with_timeout(self, timeout: httpx.Timeout) -> "Client":
def with_timeout(self, timeout: httpx2.Timeout) -> "Client":
"""Get a new client matching this one with a new timeout configuration"""
if self._client is not None:
self._client.timeout = timeout
if self._async_client is not None:
self._async_client.timeout = timeout
return evolve(self, timeout=timeout)

def set_httpx_client(self, client: httpx.Client) -> Self:
"""Manually set the underlying httpx.Client
def set_httpx_client(self, client: httpx2.Client) -> Self:
"""Manually set the underlying httpx2.Client

**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._client = client
return self

def get_httpx_client(self) -> httpx.Client:
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
def get_httpx_client(self) -> httpx2.Client:
"""Get the underlying httpx2.Client, constructing a new one if not previously set"""
if self._client is None:
self._client = httpx.Client(
self._client = httpx2.Client(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
**self._httpx2_args,
)
return self._client

def __enter__(self) -> Self:
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
"""Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)"""
self.get_httpx_client().__enter__()
return self

def __exit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
"""Exit a context manager for internal httpx2.Client (see httpx2 docs)"""
self.get_httpx_client().__exit__(*args, **kwargs)

def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> Self:
"""Manually set the underlying httpx.AsyncClient
def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self:
"""Manually set the underlying httpx2.AsyncClient

**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._async_client = async_client
return self

def get_async_httpx_client(self) -> httpx.AsyncClient:
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
def get_async_httpx_client(self) -> httpx2.AsyncClient:
"""Get the underlying httpx2.AsyncClient, constructing a new one if not previously set"""
if self._async_client is None:
self._async_client = httpx.AsyncClient(
self._async_client = httpx2.AsyncClient(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
**self._httpx2_args,
)
return self._async_client

async def __aenter__(self) -> Self:
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
"""Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)"""
await self.get_async_httpx_client().__aenter__()
return self

async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
"""Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)"""
await self.get_async_httpx_client().__aexit__(*args, **kwargs)


@define
class AuthenticatedClient:
"""A Client which has been authenticated for use on secured endpoints

The following are accepted as keyword arguments and will be used to construct httpx Clients internally:
The following are accepted as keyword arguments and will be used to construct httpx2 Clients internally:

``base_url``: The base URL for the API, all requests are made to a relative path to this URL

Expand All @@ -141,14 +141,14 @@ class AuthenticatedClient:
``headers``: A dictionary of headers to be sent with every request

``timeout``: The maximum amount of a time a request can take. API functions will raise
httpx.TimeoutException if this is exceeded.
httpx2.TimeoutException if this is exceeded.

``verify_ssl``: Whether or not to verify the SSL certificate of the API server. This should be True in production,
but can be set to False for testing purposes.

``follow_redirects``: Whether or not to follow redirects. Default value is False.

``httpx_args``: A dictionary of additional arguments to be passed to the ``httpx.Client`` and ``httpx.AsyncClient`` constructor.
``httpx2_args``: A dictionary of additional arguments to be passed to the ``httpx2.Client`` and ``httpx2.AsyncClient`` constructor.

"""

Expand All @@ -157,12 +157,12 @@ class AuthenticatedClient:
_base_url: str = field(alias="base_url")
_cookies: dict[str, str] = field(factory=dict, kw_only=True, alias="cookies")
_headers: dict[str, str] = field(factory=dict, kw_only=True, alias="headers")
_timeout: httpx.Timeout | None = field(default=None, kw_only=True, alias="timeout")
_timeout: httpx2.Timeout | None = field(default=None, kw_only=True, alias="timeout")
_verify_ssl: str | bool | ssl.SSLContext = field(default=True, kw_only=True, alias="verify_ssl")
_follow_redirects: bool = field(default=False, kw_only=True, alias="follow_redirects")
_httpx_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx_args")
_client: httpx.Client | None = field(default=None, init=False)
_async_client: httpx.AsyncClient | None = field(default=None, init=False)
_httpx2_args: dict[str, Any] = field(factory=dict, kw_only=True, alias="httpx2_args")
_client: httpx2.Client | None = field(default=None, init=False)
_async_client: httpx2.AsyncClient | None = field(default=None, init=False)

token: str
"""The token to use for authentication"""
Expand All @@ -187,74 +187,74 @@ def with_cookies(self, cookies: dict[str, str]) -> "AuthenticatedClient":
self._async_client.cookies.update(cookies)
return evolve(self, cookies={**self._cookies, **cookies})

def with_timeout(self, timeout: httpx.Timeout) -> "AuthenticatedClient":
def with_timeout(self, timeout: httpx2.Timeout) -> "AuthenticatedClient":
"""Get a new client matching this one with a new timeout configuration"""
if self._client is not None:
self._client.timeout = timeout
if self._async_client is not None:
self._async_client.timeout = timeout
return evolve(self, timeout=timeout)

def set_httpx_client(self, client: httpx.Client) -> Self:
"""Manually set the underlying httpx.Client
def set_httpx_client(self, client: httpx2.Client) -> Self:
"""Manually set the underlying httpx2.Client

**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._client = client
return self

def get_httpx_client(self) -> httpx.Client:
"""Get the underlying httpx.Client, constructing a new one if not previously set"""
def get_httpx_client(self) -> httpx2.Client:
"""Get the underlying httpx2.Client, constructing a new one if not previously set"""
if self._client is None:
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
self._client = httpx.Client(
self._client = httpx2.Client(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
**self._httpx2_args,
)
return self._client

def __enter__(self) -> Self:
"""Enter a context manager for self.client—you cannot enter twice (see httpx docs)"""
"""Enter a context manager for self.client—you cannot enter twice (see httpx2 docs)"""
self.get_httpx_client().__enter__()
return self

def __exit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for internal httpx.Client (see httpx docs)"""
"""Exit a context manager for internal httpx2.Client (see httpx2 docs)"""
self.get_httpx_client().__exit__(*args, **kwargs)

def set_async_httpx_client(self, async_client: httpx.AsyncClient) -> Self:
"""Manually set the underlying httpx.AsyncClient
def set_async_httpx_client(self, async_client: httpx2.AsyncClient) -> Self:
"""Manually set the underlying httpx2.AsyncClient

**NOTE**: This will override any other settings on the client, including cookies, headers, and timeout.
"""
self._async_client = async_client
return self

def get_async_httpx_client(self) -> httpx.AsyncClient:
"""Get the underlying httpx.AsyncClient, constructing a new one if not previously set"""
def get_async_httpx_client(self) -> httpx2.AsyncClient:
"""Get the underlying httpx2.AsyncClient, constructing a new one if not previously set"""
if self._async_client is None:
self._headers[self.auth_header_name] = f"{self.prefix} {self.token}" if self.prefix else self.token
self._async_client = httpx.AsyncClient(
self._async_client = httpx2.AsyncClient(
base_url=self._base_url,
cookies=self._cookies,
headers=self._headers,
timeout=self._timeout,
verify=self._verify_ssl,
follow_redirects=self._follow_redirects,
**self._httpx_args,
**self._httpx2_args,
)
return self._async_client

async def __aenter__(self) -> Self:
"""Enter a context manager for underlying httpx.AsyncClient—you cannot enter twice (see httpx docs)"""
"""Enter a context manager for underlying httpx2.AsyncClient—you cannot enter twice (see httpx2 docs)"""
await self.get_async_httpx_client().__aenter__()
return self

async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
"""Exit a context manager for underlying httpx.AsyncClient (see httpx docs)"""
"""Exit a context manager for underlying httpx2.AsyncClient (see httpx2 docs)"""
await self.get_async_httpx_client().__aexit__(*args, **kwargs)
Loading
Loading