Skip to content

Commit a65e582

Browse files
committed
refactor: migrate from httpx to httpx2
Replace legacy httpx with httpx2 across PDP resolvers (opa, authzen), tools (integrity, catalog), and their tests. No dependency in the locked tree requires httpx 1.x -- mcp 2.0.0 already depends on httpx2. - Drop httpx and unused httpx[http2] extra; declare httpx2>=2.0.0 - Relock: removes httpx, httpcore, h2, hpack, hyperframe - Fix stale pre-migration httpx.AsyncClient refs in test_client_coverage.py (missing import would NameError) - Keep PLUGINS_HTTPX_* setting names for config compatibility
1 parent d24ea80 commit a65e582

12 files changed

Lines changed: 67 additions & 132 deletions

File tree

cpex/framework/external/mcp/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ def _tls_httpx_client_factory(
326326
auth: Optional authentication handler for HTTP requests.
327327
328328
Returns:
329-
Configured httpx AsyncClient with TLS settings applied.
329+
Configured httpx2 AsyncClient with TLS settings applied.
330330
331331
Raises:
332332
PluginError: If TLS configuration fails.

cpex/framework/external/mcp/tls_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def create_ssl_context(tls_config: MCPClientTLSConfig, plugin_name: str) -> ssl.
103103
plugin_name: Name of the plugin (for error messages)
104104
105105
Returns:
106-
Configured SSLContext ready for use with httpx or other SSL connections
106+
Configured SSLContext ready for use with httpx2 or other SSL connections
107107
108108
Raises:
109109
PluginError: If SSL context configuration fails

cpex/framework/pdp/authzen.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
import time
4343
from typing import Any
4444

45-
import httpx
45+
import httpx2
4646

4747
from cpex.framework.pdp.base import PdpError, PdpResolver, PdpResult
4848

@@ -95,8 +95,8 @@ def __init__(
9595
"""
9696
self._endpoint_template = endpoint
9797
self.fail_open = fail_open
98-
self._client = httpx.AsyncClient(
99-
timeout=httpx.Timeout(timeout_ms / 1000.0),
98+
self._client = httpx2.AsyncClient(
99+
timeout=httpx2.Timeout(timeout_ms / 1000.0),
100100
headers=headers or {},
101101
)
102102

@@ -134,7 +134,7 @@ async def resolve(self, input_data: dict[str, Any]) -> PdpResult:
134134
response.raise_for_status()
135135
return self._parse_response(response.json(), latency_ms)
136136

137-
except httpx.TimeoutException as e:
137+
except httpx2.TimeoutException as e:
138138
latency_ms = (time.monotonic() - start) * 1000
139139
logger.warning("AuthZen timeout after %.1fms: %s", latency_ms, endpoint)
140140
if self.fail_open:
@@ -149,7 +149,7 @@ async def resolve(self, input_data: dict[str, Any]) -> PdpResult:
149149
cause=e,
150150
)
151151

152-
except httpx.HTTPStatusError as e:
152+
except httpx2.HTTPStatusError as e:
153153
latency_ms = (time.monotonic() - start) * 1000
154154
logger.error(
155155
"AuthZen HTTP %d from %s: %s",
@@ -169,7 +169,7 @@ async def resolve(self, input_data: dict[str, Any]) -> PdpResult:
169169
cause=e,
170170
)
171171

172-
except httpx.HTTPError as e:
172+
except httpx2.HTTPError as e:
173173
latency_ms = (time.monotonic() - start) * 1000
174174
logger.error("AuthZen connection error: %s", e)
175175
if self.fail_open:

cpex/framework/pdp/opa.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
import time
3939
from typing import Any
4040

41-
import httpx
41+
import httpx2
4242

4343
from cpex.framework.pdp.base import PdpError, PdpResolver, PdpResult
4444

@@ -92,8 +92,8 @@ def __init__(
9292
"""
9393
self._endpoint_template = endpoint
9494
self.fail_open = fail_open
95-
self._client = httpx.AsyncClient(
96-
timeout=httpx.Timeout(timeout_ms / 1000.0),
95+
self._client = httpx2.AsyncClient(
96+
timeout=httpx2.Timeout(timeout_ms / 1000.0),
9797
headers=headers or {},
9898
)
9999

@@ -133,7 +133,7 @@ async def resolve(self, input_data: dict[str, Any]) -> PdpResult:
133133
response.raise_for_status()
134134
return self._parse_response(response.json(), latency_ms)
135135

136-
except httpx.TimeoutException as e:
136+
except httpx2.TimeoutException as e:
137137
latency_ms = (time.monotonic() - start) * 1000
138138
logger.warning("OPA timeout after %.1fms: %s", latency_ms, endpoint)
139139
if self.fail_open:
@@ -148,7 +148,7 @@ async def resolve(self, input_data: dict[str, Any]) -> PdpResult:
148148
cause=e,
149149
)
150150

151-
except httpx.HTTPStatusError as e:
151+
except httpx2.HTTPStatusError as e:
152152
latency_ms = (time.monotonic() - start) * 1000
153153
logger.error("OPA HTTP %d from %s", e.response.status_code, endpoint)
154154
if self.fail_open:
@@ -163,7 +163,7 @@ async def resolve(self, input_data: dict[str, Any]) -> PdpResult:
163163
cause=e,
164164
)
165165

166-
except httpx.HTTPError as e:
166+
except httpx2.HTTPError as e:
167167
latency_ms = (time.monotonic() - start) * 1000
168168
logger.error("OPA connection error: %s", e)
169169
if self.fail_open:

cpex/framework/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ def empty_string_to_none(cls, value: Any) -> Any:
349349

350350

351351
class PluginsHttpClientSettings(BaseSettings):
352-
"""Lightweight settings for HTTP client (httpx) configuration."""
352+
"""Lightweight settings for HTTP client (httpx2) configuration."""
353353

354354
skip_ssl_verify: bool = False
355355
httpx_max_connections: int = 200

cpex/tools/catalog.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from pathlib import Path
2424
from typing import Any, Optional
2525

26-
import httpx
26+
import httpx2
2727
import yaml
2828
from github import Auth, Github
2929
from packaging.version import InvalidVersion, Version
@@ -292,7 +292,7 @@ def update_plugin_version_registry(self, manifest: PluginManifest, relpath: Path
292292
encoding="utf-8",
293293
)
294294

295-
def save_manifest_content(self, content: str, path, repo_url: httpx.URL):
295+
def save_manifest_content(self, content: str, path, repo_url: httpx2.URL):
296296
"""
297297
write the manifest content to the supplied path relative to the ouptut folder,
298298
injecting the monorepo.package_source value before saving the file.
@@ -333,11 +333,11 @@ def save_catalog_content(self, content: str, path):
333333
"""
334334
self.save_content(self.catalog_folder, content, path)
335335

336-
def download_contents(self, git_url: str, headers, path: str, repo_url: httpx.URL):
336+
def download_contents(self, git_url: str, headers, path: str, repo_url: httpx2.URL):
337337
"""
338338
Download the contents of the file using the github REST API.
339339
"""
340-
result = httpx.get(git_url, headers=headers, timeout=30.0)
340+
result = httpx2.get(git_url, headers=headers, timeout=30.0)
341341
if result.status_code == 200:
342342
js = result.json()
343343
b64_content = js["content"]
@@ -456,7 +456,7 @@ def _search_github_code(self, repo_path: str, member: str | None, headers) -> li
456456
return None
457457

458458
def _transform_manifest_data(
459-
self, manifest_content: dict, name: str, member: str | None, repo_url: httpx.URL
459+
self, manifest_content: dict, name: str, member: str | None, repo_url: httpx2.URL
460460
) -> dict:
461461
"""Apply standard transformations to manifest data.
462462
@@ -507,7 +507,7 @@ def _process_manifest_item(
507507
item: dict,
508508
name: str,
509509
member: str,
510-
repo_url: httpx.URL,
510+
repo_url: httpx2.URL,
511511
headers,
512512
relpath: Path,
513513
repo_path: str,
@@ -548,7 +548,7 @@ def _process_manifest_item(
548548
return True
549549

550550
def _process_version_item(
551-
self, item: dict, member: str, name: str, repo_url: httpx.URL, headers, relpath, repo_path, gh_repo
551+
self, item: dict, member: str, name: str, repo_url: httpx2.URL, headers, relpath, repo_path, gh_repo
552552
) -> None:
553553
"""Find plugin-versions.json files relative to the supplied member folder,
554554
download and save the manifest, updating the monorepo's package_folder, package_source and repo_url attributes
@@ -566,7 +566,7 @@ def _process_version_item(
566566
return
567567
relpath.write_text(version_data, encoding="utf-8")
568568

569-
def find_and_save_plugin_versions_json(self, member: str, name: str, repo_url: httpx.URL, headers, gh_repo) -> None:
569+
def find_and_save_plugin_versions_json(self, member: str, name: str, repo_url: httpx2.URL, headers, gh_repo) -> None:
570570
"""Find plugin-versions.json files relative to the supplied member folder,
571571
download and save the manifest, updating the monorepo's package_folder, package_source and repo_url attributes
572572
Args:
@@ -590,7 +590,7 @@ def find_and_save_plugin_versions_json(self, member: str, name: str, repo_url: h
590590
self._process_version_item(item, member, name, repo_url, headers, relpath, repo_path, gh_repo)
591591

592592
def find_and_save_plugin_manifest(
593-
self, member: str, name: str, repo_url: httpx.URL, headers, gh_repo
593+
self, member: str, name: str, repo_url: httpx2.URL, headers, gh_repo
594594
) -> PluginManifest | None:
595595
"""Find plugin-manifest*.yaml files relative to the supplied member folder,
596596
download and save the manifest, updating the monorepo's package_folder, package_source and repo_url attributes
@@ -620,7 +620,7 @@ def find_and_save_plugin_manifest(
620620

621621
return None
622622

623-
def _process_pyproject(self, gh_repo, item, repo_url: httpx.URL, headers) -> None:
623+
def _process_pyproject(self, gh_repo, item, repo_url: httpx2.URL, headers) -> None:
624624
"""Process a single pyproject.toml file.
625625
626626
Args:
@@ -672,7 +672,7 @@ def update_catalog_with_pyproject(self) -> bool:
672672
repo_cache: dict[str, Any] = {}
673673

674674
for repo in self.monorepos:
675-
repo_url = httpx.URL(repo.strip())
675+
repo_url = httpx2.URL(repo.strip())
676676
repo_path = repo_url.path.removeprefix("/")
677677

678678
try:

cpex/tools/integrity.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from pathlib import Path
3737
from typing import Optional
3838

39-
import httpx
39+
import httpx2
4040

4141
logger = logging.getLogger(__name__)
4242

@@ -162,16 +162,16 @@ def fetch_pypi_package_hashes(
162162
logger.debug("Fetching package hashes from: %s", url)
163163

164164
try:
165-
with httpx.Client(timeout=timeout) as client:
165+
with httpx2.Client(timeout=timeout) as client:
166166
response = client.get(url)
167167
response.raise_for_status()
168168
data = response.json()
169169

170-
except httpx.HTTPStatusError as e:
170+
except httpx2.HTTPStatusError as e:
171171
if e.response.status_code == 404:
172172
raise RuntimeError(f"Package '{package_name}' not found on {'test.' if use_test else ''}PyPI") from e
173173
raise RuntimeError(f"Failed to fetch package metadata: {e}") from e
174-
except httpx.RequestError as e:
174+
except httpx2.RequestError as e:
175175
raise RuntimeError(f"Network error fetching package metadata: {e}") from e
176176
except Exception as e:
177177
raise RuntimeError(f"Unexpected error fetching package metadata: {e}") from e

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ maintainers = [
2323
requires-python = ">=3.11"
2424
dependencies = [
2525
"fastapi>=0.133.1",
26-
"httpx>=0.28.1",
27-
"httpx[http2]>=0.28.1",
26+
"httpx2>=2.0.0",
2827
"jinja2>=3.1.6",
2928
"mcp>=2.0.0",
3029
"mcp-types>=2.0.0",

tests/unit/cpex/framework/external/mcp/test_client_coverage.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from unittest.mock import AsyncMock, MagicMock, patch
88

99
# Third-Party
10+
import httpx2
1011
import orjson
1112
import pytest
1213
from mcp_types import TextContent
@@ -238,7 +239,7 @@ def mock_streamable(*args, **kwargs):
238239

239240
@pytest.mark.asyncio
240241
async def test_streamable_client_called_with_prebuilt_client_and_terminate_on_close(self):
241-
"""v2 SDK contract: streamable_http_client receives a pre-built httpx client and
242+
"""v2 SDK contract: streamable_http_client receives a pre-built httpx2 client and
242243
terminate_on_close=True (replacing the removed manual __terminate_http_session)."""
243244
plugin = _make_plugin()
244245

@@ -255,12 +256,12 @@ async def __aexit__(self, *args):
255256
list_tools_result.tools = []
256257
mock_session.list_tools = AsyncMock(return_value=list_tools_result)
257258

258-
prebuilt_client = AsyncMock(spec=httpx.AsyncClient)
259+
prebuilt_client = AsyncMock(spec=httpx2.AsyncClient)
259260

260261
with (
261262
patch("cpex.framework.external.mcp.client.streamable_http_client", return_value=OkCtx()) as mock_streamable,
262263
patch("cpex.framework.external.mcp.client.ClientSession", return_value=mock_session),
263-
patch("cpex.framework.external.mcp.client.httpx.AsyncClient", return_value=prebuilt_client),
264+
patch("cpex.framework.external.mcp.client.httpx2.AsyncClient", return_value=prebuilt_client),
264265
):
265266
plugin._exit_stack = AsyncExitStack()
266267
await plugin._ExternalPlugin__connect_to_http_server("http://localhost:9999/mcp")

0 commit comments

Comments
 (0)