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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## Unreleased

***Changed:***

- HTTP requests verify certificates against the operating system trust store by default
- The `http` feature available to commands now provides the `httpx2` dependency and will remove the `httpx` dependency in a future minor release

## 0.37.0 - 2026-07-21

***Added:***
Expand Down
10 changes: 5 additions & 5 deletions docs/tutorials/cli/create-command.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Agent release data

## Requiring dependencies

Fetching the Agent's [`release.json`](https://github.com/DataDog/datadog-agent/blob/main/release.json) file requires using an HTTP client. Add the `http` [feature][dda.cli.base.DynamicCommand] to the command to make sure dependencies such as `httpx` are available:
Fetching the Agent's [`release.json`](https://github.com/DataDog/datadog-agent/blob/main/release.json) file requires using an HTTP client. Add the `http` [feature][dda.cli.base.DynamicCommand] to the command to make sure dependencies such as `httpx2` are available:

/// tab | :octicons-file-code-16: src/dda/cli/agent_release/data/\_\_init\_\_.py
```python hl_lines="13 20-30"
Expand All @@ -107,14 +107,14 @@ def cmd(app: Application) -> None:
"""
Show Agent release data.
"""
import httpx
import httpx2

base = "https://raw.githubusercontent.com"
repo = "DataDog/datadog-agent"
branch = "main"
path = "release.json"
with app.status("Fetching Agent release data"):
response = httpx.get(f"{base}/{repo}/{branch}/{path}")
response = httpx2.get(f"{base}/{repo}/{branch}/{path}")

response.raise_for_status()
app.display_table(response.json())
Expand Down Expand Up @@ -174,14 +174,14 @@ def cmd(app: Application) -> None:
app.display_warning("This command is currently disabled by feature flag.")
return

import httpx
import httpx2

base = "https://raw.githubusercontent.com"
repo = "DataDog/datadog-agent"
branch = "main"
path = "release.json"
with app.status("Fetching Agent release data"):
response = httpx.get(f"{base}/{repo}/{branch}/{path}")
response = httpx2.get(f"{base}/{repo}/{branch}/{path}")

response.raise_for_status()
app.display_table(response.json())
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ dependencies = [
"dep-sync~=0.1",
"filelock~=3.18",
"find-exe~=0.1",
# Retained only for local commands in other repositories that import it.
"httpx[http2]~=0.28.1",
"httpx2[http2]~=2.9.1",
"hvac~=2.3.0",
"keyring~=25.6.0",
"msgspec~=0.18",
Expand Down Expand Up @@ -64,6 +66,7 @@ dotslash = [
]
http = [
"httpx[zstd]",
"httpx2[zstd]",
]
gcp = [
"google-api-python-client~=2.160.0",
Expand Down
4 changes: 2 additions & 2 deletions src/dda/cli/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def ready(self) -> bool:
return now - last_check >= self.__app.config.update.check.get_period_seconds()

def new_release(self) -> tuple[str, str] | None:
import httpx
import httpx2
from packaging.version import Version

from dda._version import __version__
Expand All @@ -205,7 +205,7 @@ def new_release(self) -> tuple[str, str] | None:
with self.__app.github.http.client(timeout=5) as client:
try:
response = client.get("https://api.github.com/repos/DataDog/datadog-agent-dev/releases/latest")
except httpx.HTTPStatusError as e:
except httpx2.HTTPStatusError as e:
# Rate limiting
if e.response.headers.get("Retry-After") is not None:
github_auth = self.__app.config.github.auth
Expand Down
6 changes: 3 additions & 3 deletions src/dda/feature_flags/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ def _fetch_flags(
Dictionary containing the flag configuration response

Raises:
httpx.HTTPError: If the request fails
httpx2.HTTPError: If the request fails
RuntimeError: If an unexpected error occurs
"""
if not self.__client_token:
return {}

from httpx import HTTPError
from httpx2 import HTTPError

# Build headers
headers = {
Expand Down Expand Up @@ -103,7 +103,7 @@ def get_flag_value(self, flag: str, targeting_key: str, targeting_attributes: di
The flag value or None if the flag is not found

Raises:
httpx.HTTPError: If the request fails
httpx2.HTTPError: If the request fails
ValueError: If the flag is not found
RuntimeError: If an unexpected error occurs
"""
Expand Down
18 changes: 9 additions & 9 deletions src/dda/utils/network/http/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import time
from typing import TYPE_CHECKING, Any

import httpx
import httpx2

from dda.utils.retry import DelayedError, FailFastError, wait_for

Expand Down Expand Up @@ -46,9 +46,9 @@ def get_http_client(**kwargs: Any) -> HTTPClient:
return HTTPClient(**kwargs)


class HTTPClient(httpx.Client):
class HTTPClient(httpx2.Client):
"""
A subclass of [`httpx.Client`](https://www.python-httpx.org/api/#client) that intelligently retries requests.
A subclass of [`httpx2.Client`](https://httpx2.pydantic.dev/api/#client) that intelligently retries requests.

/// warning
This class should never be used directly. Instead, use the
Expand All @@ -64,21 +64,21 @@ def __init__(self, **kwargs: Any) -> None:
# connection errors
self.timeout.connect = None

def send(self, *args: Any, **kwargs: Any) -> httpx.Response:
def send(self, *args: Any, **kwargs: Any) -> httpx2.Response:
return wait_for(
lambda: _get_response(lambda: super(HTTPClient, self).send(*args, **kwargs)),
timeout=self.__timeout,
)


def _get_response(sender: Callable[[], httpx.Response]) -> httpx.Response:
def _get_response(sender: Callable[[], httpx2.Response]) -> httpx2.Response:
try:
response = sender()
except httpx.ConnectError as e:
except httpx2.ConnectError as e:
if (cause := getattr(e, "__cause__", None)) is not None:
import httpcore
import httpcore2

if isinstance(cause, httpcore.ConnectError):
if isinstance(cause, httpcore2.ConnectError):
import ssl

internal_error = cause.args[0]
Expand All @@ -90,7 +90,7 @@ def _get_response(sender: Callable[[], httpx.Response]) -> httpx.Response:

try:
response.raise_for_status()
except httpx.HTTPStatusError as e:
except httpx2.HTTPStatusError as e:
# Not idempotent
if e.request.method == "POST":
raise FailFastError(e) from None
Expand Down
2 changes: 1 addition & 1 deletion tests/utils/git/test_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from datetime import UTC, datetime

from httpx import Response
from httpx2 import Response

from dda.utils.fs import Path
from dda.utils.git.commit import Commit, GitPersonDetails
Expand Down
2 changes: 1 addition & 1 deletion tests/utils/git/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime

import pytest
from httpx import Response
from httpx2 import Response

from dda.utils.fs import Path
from dda.utils.git.changeset import ChangedFile, ChangeSet
Expand Down
4 changes: 2 additions & 2 deletions tests/utils/network/http/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import ssl

import httpx
import httpx2
import truststore

from dda.utils.network.http.client import DEFAULT_TIMEOUT, HTTPClient, get_http_client
Expand All @@ -15,7 +15,7 @@ class TestGetHTTPClient:
def test_types(self):
client = get_http_client()
assert isinstance(client, HTTPClient)
assert isinstance(client, httpx.Client)
assert isinstance(client, httpx2.Client)

def test_defaults(self, mocker):
truststore_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
Expand Down
67 changes: 50 additions & 17 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading