From 652bac8e67d489e905e3aacbe46280451522dac1 Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Fri, 11 Sep 2026 15:18:36 +0800 Subject: [PATCH] chore: migrate HTTP client dependency from httpx to httpx2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Streamable HTTP transport now builds on `httpx2` instead of `httpx`. httpx2 keeps the same public surface for the pieces we rely on (`AsyncClient`, `ASGITransport`, `MockTransport`, `Request`, `Response`, `AsyncByteStream`, `Timeout`), so the transport logic is unchanged — only the module name and the dependency move. - pyproject.toml: `httpx[http2]>=0.27` -> `httpx2[http2]>=2.12` in both the dev group and the `http` extra - src/acp/http/client.py: import and type/annotation references now point at httpx2 - src/acp/_cookies.py, src/acp/http/__init__.py: docstring references updated - tests/http/*: updated to httpx2 - docs/web-transport.md: dependency note updated - uv.lock: drop httpx 0.28.1 + httpcore 1.0.9, add httpx2 2.12.0 + httpcore2 2.12.0 (+ truststore) Note: starlette.testclient prefers httpx2 and only falls back to httpx with a deprecation warning, so this also removes that warning path. Verified: `uv lock --locked`, `prek run -a`, `ty check`, `deptry src`, and `pytest --doctest-modules` (256 passed, 1 skipped). --- docs/web-transport.md | 2 +- pyproject.toml | 4 +- src/acp/_cookies.py | 2 +- src/acp/http/__init__.py | 2 +- src/acp/http/client.py | 14 +++---- tests/http/test_asgi.py | 12 +++--- tests/http/test_fixes.py | 14 +++---- tests/http/test_http_client.py | 40 +++++++++--------- tests/http/test_loopback.py | 2 +- uv.lock | 77 +++++++++++++++++++++------------- 10 files changed, 95 insertions(+), 74 deletions(-) diff --git a/docs/web-transport.md b/docs/web-transport.md index 20c000b..ee87930 100644 --- a/docs/web-transport.md +++ b/docs/web-transport.md @@ -21,7 +21,7 @@ Both reuse the existing JSON-RPC message format and ACP lifecycle pip install "agent-client-protocol[http]" ``` -This pulls in `httpx[http2]` (HTTP/2 + SSE consumption), `websockets`, and +This pulls in `httpx2[http2]` (HTTP/2 + SSE consumption), `websockets`, and `starlette` (the server application). The core SDK and stdio transport do not require these optional dependencies. diff --git a/pyproject.toml b/pyproject.toml index 5722c3a..4438e7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ dev = [ "mkdocstrings[python]>=0.26.1", "python-dotenv>=1.1.1", "prek>=0.2.17", - "httpx[http2]>=0.27", + "httpx2[http2]>=2.12", "websockets>=12.0", "uvicorn>=0.30", "starlette>=0.49.3", @@ -54,7 +54,7 @@ dev = [ [project.optional-dependencies] logfire = ["logfire>=0.14", "opentelemetry-sdk>=1.28.0"] # Experimental remote transports (Streamable HTTP + WebSocket), client + server. -http = ["httpx[http2]>=0.27", "websockets>=12.0", "starlette>=0.49.3"] +http = ["httpx2[http2]>=2.12", "websockets>=12.0", "starlette>=0.49.3"] [build-system] requires = ["pdm-backend"] diff --git a/src/acp/_cookies.py b/src/acp/_cookies.py index a4b05d6..1203369 100644 --- a/src/acp/_cookies.py +++ b/src/acp/_cookies.py @@ -1,6 +1,6 @@ """In-memory cookie store for the WebSocket handshake. -The HTTP client relies on ``httpx``'s built-in cookie jar for session affinity, +The HTTP client relies on ``httpx2``'s built-in cookie jar for session affinity, but the WebSocket handshake needs a small, explicit store to collect ``Set-Cookie`` headers from the upgrade response and echo them back as a ``Cookie`` request header for the socket lifetime. diff --git a/src/acp/http/__init__.py b/src/acp/http/__init__.py index dedcbd3..095c88c 100644 --- a/src/acp/http/__init__.py +++ b/src/acp/http/__init__.py @@ -1,7 +1,7 @@ """Streamable HTTP transport for ACP (experimental). Public exports are import-guarded: the heavy client/server implementations pull -in optional dependencies (``httpx[http2]`` and ``starlette``). Importing a symbol without the +in optional dependencies (``httpx2[http2]`` and ``starlette``). Importing a symbol without the extra installed raises a friendly ``ImportError`` pointing at ``pip install agent-client-protocol[http]``. """ diff --git a/src/acp/http/client.py b/src/acp/http/client.py index 77381f8..cee5949 100644 --- a/src/acp/http/client.py +++ b/src/acp/http/client.py @@ -38,7 +38,7 @@ ) try: - import httpx + import httpx2 except ImportError as exc: # pragma: no cover - exercised via import guard message msg = "The Streamable HTTP transport requires the 'http' extra: pip install agent-client-protocol[http]" raise ImportError(msg) from exc @@ -68,7 +68,7 @@ def __init__( self, url: str, *, - client: httpx.AsyncClient, + client: httpx2.AsyncClient, owns_client: bool, headers: dict[str, str] | None = None, ) -> None: @@ -191,7 +191,7 @@ async def _consume_stream(self, *, session_id: str | None) -> None: return async for event in parse_sse_stream(_aiter_raw(response)): self._handle_incoming(event) - except (httpx.HTTPError, asyncio.CancelledError): + except (httpx2.HTTPError, asyncio.CancelledError): return finally: self._on_stream_closed(session_id) @@ -229,7 +229,7 @@ def _handle_incoming(self, message: dict[str, Any]) -> None: self._inbox.put_nowait(message) -async def _aiter_raw(response: httpx.Response) -> AsyncIterator[bytes]: +async def _aiter_raw(response: httpx2.Response) -> AsyncIterator[bytes]: async for chunk in response.aiter_bytes(): yield chunk @@ -237,14 +237,14 @@ async def _aiter_raw(response: httpx.Response) -> AsyncIterator[bytes]: def create_http_stream( url: str, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, headers: dict[str, str] | None = None, ) -> Transport: """Create a Streamable HTTP client :class:`Transport`. Args: url: The ACP endpoint URL (e.g. ``https://host/acp``). - client: An optional pre-configured ``httpx.AsyncClient``. If omitted, an + client: An optional pre-configured ``httpx2.AsyncClient``. If omitted, an HTTP/2-enabled client with a cookie jar is created and owned by the transport (closed on ``close()``). headers: Extra headers sent on every request. @@ -255,5 +255,5 @@ def create_http_stream( owns_client = client is None if client is None: # SSE GET streams are long-lived, so disable read timeouts by default. - client = httpx.AsyncClient(http2=True, timeout=httpx.Timeout(None)) + client = httpx2.AsyncClient(http2=True, timeout=httpx2.Timeout(None)) return _HttpStreamTransport(url, client=client, owns_client=owns_client, headers=headers) diff --git a/tests/http/test_asgi.py b/tests/http/test_asgi.py index b91940e..b9ee3e4 100644 --- a/tests/http/test_asgi.py +++ b/tests/http/test_asgi.py @@ -7,7 +7,7 @@ from contextlib import asynccontextmanager from typing import Any -import httpx +import httpx2 import pytest from starlette.applications import Starlette from starlette.routing import Mount @@ -40,7 +40,7 @@ async def test_http_errors(method: str, headers: dict[str, str], body: str, stat app = create_asgi_app(lambda conn: TestAgent()) async with ( app.router.lifespan_context(app), - httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client, + httpx2.AsyncClient(transport=httpx2.ASGITransport(app=app), base_url="http://test") as client, ): response = await client.request(method, "/acp", headers=headers, content=body) assert response.status_code == status @@ -53,7 +53,7 @@ async def test_unsupported_methods_do_not_open_a_stream(method: str) -> None: app = create_asgi_app(lambda conn: TestAgent()) async with ( app.router.lifespan_context(app), - httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client, + httpx2.AsyncClient(transport=httpx2.ASGITransport(app=app), base_url="http://test") as client, ): response = await client.request(method, "/acp") assert response.status_code == 405 @@ -82,7 +82,7 @@ async def lifespan(app: Starlette): prefix = "/agents" if mounted else "" path = prefix + (endpoint or "/acp") server = await serve_asgi(app) - async with httpx.AsyncClient(base_url=f"http://{server.host}:{server.port}") as client: + async with httpx2.AsyncClient(base_url=f"http://{server.host}:{server.port}") as client: response = await client.post(path, json=INITIALIZE) assert response.status_code == 200 assert response.json()["id"] == 0 @@ -111,7 +111,7 @@ async def lifespan(app: Starlette): @pytest.mark.asyncio async def test_lifespan_closes_http_connections() -> None: app = create_asgi_app(lambda conn: TestAgent()) - async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client: + async with httpx2.AsyncClient(transport=httpx2.ASGITransport(app=app), base_url="http://test") as client: async with app.router.lifespan_context(app): response = await client.post("/acp", json=INITIALIZE) connection_id = response.headers[CONNECTION_ID_HEADER] @@ -130,7 +130,7 @@ def factory(conn): app = create_asgi_app(factory, path=path) async with app.router.lifespan_context(app): - async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client: + async with httpx2.AsyncClient(transport=httpx2.ASGITransport(app=app), base_url="http://test") as client: response = await client.post(path, json=INITIALIZE) connection_id = response.headers[CONNECTION_ID_HEADER] scope = { diff --git a/tests/http/test_fixes.py b/tests/http/test_fixes.py index 1439934..6cd63a6 100644 --- a/tests/http/test_fixes.py +++ b/tests/http/test_fixes.py @@ -14,7 +14,7 @@ import json from typing import Any -import httpx +import httpx2 import pytest import acp.http.server as server_mod @@ -177,22 +177,22 @@ async def test_http_client_surfaces_eof_when_connection_stream_ends() -> None: """When the connection-scoped SSE stream ends, receive() must return None.""" conn_id = "conn-eof" - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.method == "POST": body = json.loads(request.content) if body.get("method") == "initialize": - return httpx.Response( + return httpx2.Response( 200, headers={CONNECTION_ID_HEADER: conn_id, "Content-Type": CONTENT_TYPE_JSON}, json={"jsonrpc": "2.0", "id": body["id"], "result": {}}, ) - return httpx.Response(202) + return httpx2.Response(202) if request.method == "GET": # SSE stream that immediately ends (empty body -> EOF). - return httpx.Response(200, headers={"Content-Type": "text/event-stream"}, content=b"") - return httpx.Response(202) + return httpx2.Response(200, headers={"Content-Type": "text/event-stream"}, content=b"") + return httpx2.Response(202) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) transport = create_http_stream("http://testserver/acp", client=client) try: await transport.send({"jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {}}) diff --git a/tests/http/test_http_client.py b/tests/http/test_http_client.py index 18578f6..2cad6df 100644 --- a/tests/http/test_http_client.py +++ b/tests/http/test_http_client.py @@ -6,7 +6,7 @@ import json from typing import Any -import httpx +import httpx2 import pytest from acp._sse import serialize_sse_event @@ -17,7 +17,7 @@ class FakeServer: - """A minimal in-memory Streamable HTTP server backed by httpx.MockTransport.""" + """A minimal in-memory Streamable HTTP server backed by httpx2.MockTransport.""" def __init__(self) -> None: self.posts: list[dict[str, Any]] = [] @@ -26,28 +26,28 @@ def __init__(self) -> None: self.conn_stream: asyncio.Queue[bytes | None] = asyncio.Queue() self.session_streams: dict[str, asyncio.Queue[bytes | None]] = {} - def handler(self, request: httpx.Request) -> httpx.Response: + def handler(self, request: httpx2.Request) -> httpx2.Response: if request.method == "POST": return self._handle_post(request) if request.method == "GET": return self._handle_get(request) if request.method == "DELETE": self.deleted = True - return httpx.Response(202) - return httpx.Response(405) + return httpx2.Response(202) + return httpx2.Response(405) - def _handle_post(self, request: httpx.Request) -> httpx.Response: + def _handle_post(self, request: httpx2.Request) -> httpx2.Response: body = json.loads(request.content) self.posts.append(body) if body.get("method") == "initialize": - return httpx.Response( + return httpx2.Response( 200, headers={CONNECTION_ID_HEADER: CONN_ID, "Content-Type": CONTENT_TYPE_JSON}, json={"jsonrpc": "2.0", "id": body["id"], "result": {"protocolVersion": 1}}, ) - return httpx.Response(202) + return httpx2.Response(202) - def _handle_get(self, request: httpx.Request) -> httpx.Response: + def _handle_get(self, request: httpx2.Request) -> httpx2.Response: session_id = request.headers.get(SESSION_ID_HEADER) if session_id is not None: queue = self.session_streams.setdefault(session_id, asyncio.Queue()) @@ -61,7 +61,7 @@ async def body() -> Any: return yield chunk - return httpx.Response(200, headers={"Content-Type": "text/event-stream"}, stream=_AsyncByteStream(body())) + return httpx2.Response(200, headers={"Content-Type": "text/event-stream"}, stream=_AsyncByteStream(body())) def push_conn(self, message: dict[str, Any]) -> None: self.conn_stream.put_nowait(serialize_sse_event(message)) @@ -71,7 +71,7 @@ def push_session(self, session_id: str, message: dict[str, Any]) -> None: queue.put_nowait(serialize_sse_event(message)) -class _AsyncByteStream(httpx.AsyncByteStream): +class _AsyncByteStream(httpx2.AsyncByteStream): def __init__(self, iterator: Any) -> None: self._iterator = iterator @@ -81,7 +81,7 @@ async def __aiter__(self) -> Any: def _make_transport(server: FakeServer): - client = httpx.AsyncClient(transport=httpx.MockTransport(server.handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(server.handler)) return create_http_stream("http://testserver/acp", client=client), client @@ -102,10 +102,10 @@ async def test_initialize_posts_and_reads_connection_id() -> None: @pytest.mark.asyncio async def test_initialize_failure_raises() -> None: - def handler(request: httpx.Request) -> httpx.Response: - return httpx.Response(500) + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(500) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) transport = create_http_stream("http://testserver/acp", client=client) try: with pytest.raises(AcpHttpStatusError) as exc: @@ -188,19 +188,19 @@ async def test_close_deletes_connection() -> None: @pytest.mark.asyncio async def test_post_error_status_raises() -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.method == "POST": body = json.loads(request.content) if body.get("method") == "initialize": - return httpx.Response( + return httpx2.Response( 200, headers={CONNECTION_ID_HEADER: CONN_ID}, json={"jsonrpc": "2.0", "id": body["id"], "result": {}}, ) - return httpx.Response(404) - return httpx.Response(200, headers={"Content-Type": "text/event-stream"}) + return httpx2.Response(404) + return httpx2.Response(200, headers={"Content-Type": "text/event-stream"}) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) transport = create_http_stream("http://testserver/acp", client=client) try: await transport.send({"jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {}}) diff --git a/tests/http/test_loopback.py b/tests/http/test_loopback.py index 7d8b8fe..4b25def 100644 --- a/tests/http/test_loopback.py +++ b/tests/http/test_loopback.py @@ -1,6 +1,6 @@ """End-to-end in-process loopback tests: Python client transport <-> ASGI server. -Boots the ASGI app under a real uvicorn server (httpx's ASGITransport buffers +Boots the ASGI app under a real uvicorn server (httpx2's ASGITransport buffers whole responses and cannot consume infinite SSE streams), then drives the full ACP flow over both the Streamable HTTP and WebSocket transports. """ diff --git a/uv.lock b/uv.lock index 18190d3..3138ae5 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,8 @@ revision = 3 requires-python = ">=3.10, <3.15" resolution-markers = [ "python_full_version >= '3.14'", - "python_full_version < '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "(python_full_version < '3.14' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')", ] [[package]] @@ -17,7 +18,7 @@ dependencies = [ [package.optional-dependencies] http = [ - { name = "httpx", extra = ["http2"] }, + { name = "httpx2", extra = ["http2"] }, { name = "starlette" }, { name = "websockets" }, ] @@ -30,7 +31,7 @@ logfire = [ dev = [ { name = "datamodel-code-generator" }, { name = "deptry" }, - { name = "httpx", extra = ["http2"] }, + { name = "httpx2", extra = ["http2"] }, { name = "mkdocs" }, { name = "mkdocs-material" }, { name = "mkdocstrings", extra = ["python"] }, @@ -48,7 +49,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "httpx", extras = ["http2"], marker = "extra == 'http'", specifier = ">=0.27" }, + { name = "httpx2", extras = ["http2"], marker = "extra == 'http'", specifier = ">=2.12" }, { name = "logfire", marker = "extra == 'logfire'", specifier = ">=0.14" }, { name = "opentelemetry-sdk", marker = "extra == 'logfire'", specifier = ">=1.28.0" }, { name = "pydantic", specifier = ">=2.7" }, @@ -62,7 +63,7 @@ provides-extras = ["logfire", "http"] dev = [ { name = "datamodel-code-generator", specifier = "==0.71.0" }, { name = "deptry", specifier = ">=0.23.0" }, - { name = "httpx", extras = ["http2"], specifier = ">=0.27" }, + { name = "httpx2", extras = ["http2"], specifier = ">=2.12" }, { name = "mkdocs", specifier = ">=1.4.2" }, { name = "mkdocs-material", specifier = ">=8.5.10" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.26.1" }, @@ -147,12 +148,12 @@ name = "black" version = "26.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, + { name = "click", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "mypy-extensions", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "packaging", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "pathspec", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "platformdirs", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "pytokens", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] @@ -387,7 +388,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "(python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version < '3.12' and sys_platform == 'emscripten')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -489,31 +490,33 @@ wheels = [ ] [[package]] -name = "httpcore" -version = "1.0.9" +name = "httpcore2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "h11" }, + { name = "h11", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" }, + { name = "truststore", marker = "python_full_version < '3.12' or python_full_version >= '3.14' or sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, ] [[package]] -name = "httpx" -version = "0.28.1" +name = "httpx2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, ] [package.optional-dependencies] @@ -521,6 +524,15 @@ http2 = [ { name = "h2" }, ] +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + [[package]] name = "hyperframe" version = "6.1.0" @@ -532,11 +544,11 @@ wheels = [ [[package]] name = "idna" -version = "3.15" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -1564,6 +1576,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/17/221d62937c4130b044bb437caac4181e7e13d5536bbede65264db1f0ac9f/tox_uv-1.29.0-py3-none-any.whl", hash = "sha256:b1d251286edeeb4bc4af1e24c8acfdd9404700143c2199ccdbb4ea195f7de6cc", size = 17254, upload-time = "2025-10-09T20:40:25.885Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "ty" version = "0.0.1a25"