diff --git a/ci/pyodide-e2e/e2e.py b/ci/pyodide-e2e/e2e.py index b37eacca6..562e1884e 100644 --- a/ci/pyodide-e2e/e2e.py +++ b/ci/pyodide-e2e/e2e.py @@ -10,7 +10,7 @@ import weaviate_client_web # bootstraps the grpc shim + fetch transport under Emscripten import grpc -import httpx +import httpx2 import weaviate import weaviate.classes as wvc from weaviate.classes.config import DataType, Property, ReferenceProperty @@ -37,8 +37,8 @@ async def main() -> None: # REST must use weaviate-client-web's fetch transport, not Pyodide's bundled one. assert weaviate_client_web.is_fetch_transport_installed(), "fetch transport not installed" assert getattr( - httpx.AsyncHTTPTransport.handle_async_request, "__weaviate_fetch_shim__", False - ), "httpx.AsyncHTTPTransport is not the package's fetch transport" + httpx2.AsyncHTTPTransport.handle_async_request, "__weaviate_fetch_shim__", False + ), "httpx2.AsyncHTTPTransport is not the package's fetch transport" ok("self-check: package fetch transport is the active httpx transport") host = os.environ.get("WEAVIATE_HOST", "localhost") diff --git a/ci/pyodide-e2e/units.mjs b/ci/pyodide-e2e/units.mjs index 469a3ccee..f7c7778c2 100644 --- a/ci/pyodide-e2e/units.mjs +++ b/ci/pyodide-e2e/units.mjs @@ -54,12 +54,12 @@ import weaviate # the ONLY weaviate-side import: must bootstrap the companion assert "weaviate_client_web" in sys.modules, "hook did not import the companion" import weaviate_client_web import grpc -import httpx +import httpx2 assert weaviate_client_web.is_installed() assert weaviate_client_web.is_fetch_transport_installed() assert getattr(grpc, "__weaviate_client_web_shim__", False) is True assert getattr( - httpx.AsyncHTTPTransport.handle_async_request, "__weaviate_fetch_shim__", False + httpx2.AsyncHTTPTransport.handle_async_request, "__weaviate_fetch_shim__", False ) is True `); console.log("OK scenario: bare 'import weaviate' bootstraps the companion"); diff --git a/integration/test_auth.py b/integration/test_auth.py index f4aea1999..ee936dfe2 100644 --- a/integration/test_auth.py +++ b/integration/test_auth.py @@ -4,7 +4,7 @@ from typing import Callable, Dict, Optional import grpc -import httpx +import httpx2 import pytest from _pytest.fixtures import SubRequest @@ -24,7 +24,7 @@ def is_auth_enabled(url: str) -> bool: - response = httpx.get("http://" + url + "/v1/.well-known/openid-configuration") + response = httpx2.get("http://" + url + "/v1/.well-known/openid-configuration") return response.status_code == 200 @@ -139,12 +139,12 @@ def test_client_with_authentication_with_anon_weaviate() -> None: assert any(str(warn.message).startswith("Auth001") for warn in recwarn.list) -def _request_with_backoff(send: Callable[[], httpx.Response]) -> httpx.Response: +def _request_with_backoff(send: Callable[[], httpx2.Response]) -> httpx2.Response: """Send an httpx request, retrying with backoff for errors, or 429.""" for delay in (2, 4, 8): try: response = send() - except httpx.TransportError: + except httpx2.TransportError: time.sleep(delay) continue if response.status_code != 429: @@ -155,14 +155,14 @@ def _request_with_backoff(send: Callable[[], httpx.Response]) -> httpx.Response: def _get_access_token(url: str, user: str, pw: str) -> Dict[str, str]: # get an access token with WCS user and pw. - weaviate_open_id_config = httpx.get("http://" + url + "/v1/.well-known/openid-configuration") + weaviate_open_id_config = httpx2.get("http://" + url + "/v1/.well-known/openid-configuration") response_json = weaviate_open_id_config.json() client_id = response_json["clientId"] href = response_json["href"] # Get the token issuer's OIDC configuration # Try with backoff; may fail due to rate limit errors in CICD pipeline - response_auth = _request_with_backoff(lambda: httpx.get(href)) + response_auth = _request_with_backoff(lambda: httpx2.get(href)) # Construct the POST request to send to 'token_endpoint' auth_body = { @@ -173,7 +173,7 @@ def _get_access_token(url: str, user: str, pw: str) -> Dict[str, str]: "scope": "openid offline_access", } token_endpoint = response_auth.json()["token_endpoint"] - response_post = _request_with_backoff(lambda: httpx.post(url=token_endpoint, data=auth_body)) + response_post = _request_with_backoff(lambda: httpx2.post(url=token_endpoint, data=auth_body)) resp_typed = util._decode_json_response_dict(response_post, "test") assert resp_typed is not None return resp_typed diff --git a/journey_tests/gunicorn/test.py b/journey_tests/gunicorn/test.py index 7a3ad9e39..cbe80b46a 100644 --- a/journey_tests/gunicorn/test.py +++ b/journey_tests/gunicorn/test.py @@ -1,19 +1,19 @@ -import httpx +import httpx2 def test_sync_in_sync() -> None: - res = httpx.get("http://localhost:8000/sync-in-sync") + res = httpx2.get("http://localhost:8000/sync-in-sync") assert res.status_code == 200 assert len(res.json()) == 100 def test_sync_in_async() -> None: - res = httpx.get("http://localhost:8000/sync-in-async") + res = httpx2.get("http://localhost:8000/sync-in-async") assert res.status_code == 200 assert len(res.json()) == 100 def test_async_in_async() -> None: - res = httpx.get("http://localhost:8000/async-in-async") + res = httpx2.get("http://localhost:8000/async-in-async") assert res.status_code == 200 assert len(res.json()) == 100 diff --git a/mock_tests/test_auth.py b/mock_tests/test_auth.py index cc4480c93..312e782c0 100644 --- a/mock_tests/test_auth.py +++ b/mock_tests/test_auth.py @@ -148,7 +148,7 @@ async def test_token_refresh_survives_failures_async( ) -> None: """A failing refresh must not kill the refresher: warn, retry, stay alive. - A 400 invalid_grant makes authlib raise OAuthError, which is not an httpx.HTTPError. + A 400 invalid_grant makes authlib raise OAuthError, which is not an httpx2.HTTPError. """ hits = _reject_refreshes(weaviate_auth_mock) @@ -179,7 +179,7 @@ def test_token_refresh_survives_failures( ) -> None: """Sync twin of the test above. - The daemon thread used to die silently on anything but an httpx.HTTPError. + The daemon thread used to die silently on anything but an httpx2.HTTPError. """ hits = _reject_refreshes(weaviate_auth_mock) diff --git a/packages/web/README.md b/packages/web/README.md index 2331127b2..3466a272c 100644 --- a/packages/web/README.md +++ b/packages/web/README.md @@ -50,8 +50,8 @@ client = WeaviateAsyncClient( ) ``` -REST goes through the package's own `fetch`-based httpx transport, including on Pyodide -builds whose httpx has one (that one fails on body-less HEAD/204 responses). +REST goes through the package's own `fetch`-based httpx2 transport, in place of the +`httpx2-jsfetch` one httpx2 uses under Pyodide. ## Usage diff --git a/packages/web/setup.py b/packages/web/setup.py index 181715c9e..0eb0316ae 100644 --- a/packages/web/setup.py +++ b/packages/web/setup.py @@ -13,7 +13,7 @@ setup( install_requires=[ f"weaviate-client=={version}", - # Pyodide's bundled httpx build omits anyio, but authlib imports it directly. + # httpx2 skips anyio under Emscripten, but authlib imports it directly. 'anyio ; sys_platform == "emscripten"', ] ) diff --git a/packages/web/src/weaviate_client_web/_channel.py b/packages/web/src/weaviate_client_web/_channel.py index e5fbbe852..289b63b26 100644 --- a/packages/web/src/weaviate_client_web/_channel.py +++ b/packages/web/src/weaviate_client_web/_channel.py @@ -220,7 +220,7 @@ async def _unary( details=f"grpc-web request to {path} timed out after {timeout}s", ) from exc except Exception as exc: # network/transport failure -> retryable UNAVAILABLE - # str() of transport errors can be empty (e.g. httpx.ConnectError) — always + # str() of transport errors can be empty (e.g. httpx2.ConnectError) — always # include the exception type so failures stay diagnosable detail = f"{type(exc).__name__}: {exc}" if str(exc) else repr(exc) details = f"grpc-web transport error for {path}: {detail}" diff --git a/packages/web/src/weaviate_client_web/_httpx_fetch.py b/packages/web/src/weaviate_client_web/_httpx_fetch.py index dcf337f7f..0cde2d1cb 100644 --- a/packages/web/src/weaviate_client_web/_httpx_fetch.py +++ b/packages/web/src/weaviate_client_web/_httpx_fetch.py @@ -1,15 +1,16 @@ -"""fetch-based httpx.AsyncHTTPTransport for Pyodide, where httpcore has no sockets. +"""fetch-based httpx2.AsyncHTTPTransport for Pyodide, where httpcore2 has no sockets. -Replaces Pyodide's bundled jsfetch transport, which fails on body-less HEAD/204 responses -and ignores read timeouts. Differs from native httpx: fetch follows redirects itself, -multi-value headers are folded, and bodies are fully buffered. +Replaces the transport httpx2 uses under Emscripten (httpx2-jsfetch), so body-less +HEAD/204 responses and read timeouts behave as tested here. Differs from native httpx2: +fetch follows redirects itself, multi-value headers are folded, and bodies are fully +buffered. """ import math import sys from typing import Callable, Dict, Optional -import httpx +import httpx2 from pyodide.http import pyfetch # type: ignore[import-not-found] _installed = False @@ -40,14 +41,14 @@ _MAX_ABORT_SIGNAL_MS = 2**31 - 1 -async def _read_request_body(request: httpx.Request) -> bytes: +async def _read_request_body(request: httpx2.Request) -> bytes: try: return request.content - except httpx.RequestNotRead: + except httpx2.RequestNotRead: return await request.aread() -def _pick_timeout(request: httpx.Request) -> Optional[float]: +def _pick_timeout(request: httpx2.Request) -> Optional[float]: """The request deadline: httpx's ``read`` timeout only (None = no deadline). ``connect`` and ``pool`` have no meaning under fetch. @@ -71,27 +72,27 @@ def _abort_signal_ms(timeout: Optional[float]) -> Optional[int]: def _map_fetch_error( - e: BaseException, request: httpx.Request, deadline_set: bool -) -> httpx.TransportError: + e: BaseException, request: httpx2.Request, deadline_set: bool +) -> httpx2.TransportError: """Map a pyfetch OSError (network, DNS, CORS, CSP, abort) to an httpx error. - httpx.ReadTimeout if our deadline fired, else httpx.ConnectError. + httpx2.ReadTimeout if our deadline fired, else httpx2.ConnectError. """ msg = str(e) or repr(e) if deadline_set and any(hint in msg.lower() for hint in _TIMEOUT_HINTS): - return httpx.ReadTimeout(msg, request=request) - return httpx.ConnectError(msg, request=request) + return httpx2.ReadTimeout(msg, request=request) + return httpx2.ConnectError(msg, request=request) def _validate_header(name: str, value: str) -> None: # h11 normally rejects CR/LF/NUL in headers; this transport bypasses h11. if any(c in name or c in value for c in ("\r", "\n", "\0")): - raise httpx.LocalProtocolError(f"Illegal character in header {name!r}") + raise httpx2.LocalProtocolError(f"Illegal character in header {name!r}") async def _fetch_handle_async_request( - self: httpx.AsyncHTTPTransport, request: httpx.Request -) -> httpx.Response: + self: httpx2.AsyncHTTPTransport, request: httpx2.Request +) -> httpx2.Response: headers: Dict[str, str] = {} for k, v in request.headers.items(): if k.lower() in _FETCH_MANAGED_HEADERS: @@ -133,10 +134,10 @@ async def _fetch_handle_async_request( resp_headers = {} # Hand httpx an unread stream, as its own transports do: the client reads it and # only then stamps ``response.elapsed``, which the batch-references path relies on. - return httpx.Response( + return httpx2.Response( status_code=int(response.status), headers=resp_headers, - stream=httpx.ByteStream(data), + stream=httpx2.ByteStream(data), request=request, ) @@ -146,9 +147,9 @@ async def _fetch_handle_async_request( def install_fetch_transport() -> None: - """Patch ``httpx.AsyncHTTPTransport`` to send requests through ``fetch``. + """Patch ``httpx2.AsyncHTTPTransport`` to send requests through ``fetch``. - Installs only under Emscripten (elsewhere httpx's own socket transports work and + Installs only under Emscripten (elsewhere httpx2's own socket transports work and must be left in place). Idempotent. """ global _installed, _original_handle_async_request @@ -156,18 +157,18 @@ def install_fetch_transport() -> None: return if sys.platform != "emscripten": return - _original_handle_async_request = httpx.AsyncHTTPTransport.handle_async_request - httpx.AsyncHTTPTransport.handle_async_request = _fetch_handle_async_request # type: ignore[method-assign] + _original_handle_async_request = httpx2.AsyncHTTPTransport.handle_async_request + httpx2.AsyncHTTPTransport.handle_async_request = _fetch_handle_async_request # type: ignore[method-assign] _installed = True def uninstall_fetch_transport() -> None: - """Restore the original ``httpx.AsyncHTTPTransport`` behaviour. No-op if not installed.""" + """Restore the original ``httpx2.AsyncHTTPTransport`` behaviour. No-op if not installed.""" global _installed, _original_handle_async_request if not _installed: return assert _original_handle_async_request is not None - httpx.AsyncHTTPTransport.handle_async_request = _original_handle_async_request # type: ignore[method-assign] + httpx2.AsyncHTTPTransport.handle_async_request = _original_handle_async_request # type: ignore[method-assign] _original_handle_async_request = None _installed = False diff --git a/packages/web/tests/test_httpx_fetch.py b/packages/web/tests/test_httpx_fetch.py index fe6adb2be..af8d46d7b 100644 --- a/packages/web/tests/test_httpx_fetch.py +++ b/packages/web/tests/test_httpx_fetch.py @@ -7,7 +7,7 @@ import types from typing import Any, Dict, List, Optional -import httpx +import httpx2 import pytest import weaviate_client_web @@ -70,23 +70,23 @@ def missing_js(monkeypatch): monkeypatch.setitem(sys.modules, "js", None) -async def _handle(request: httpx.Request) -> httpx.Response: +async def _handle(request: httpx2.Request) -> httpx2.Response: # self is unused by the handler implementation; a bare transport instance suffices - transport = httpx.AsyncHTTPTransport.__new__(httpx.AsyncHTTPTransport) + transport = httpx2.AsyncHTTPTransport.__new__(httpx2.AsyncHTTPTransport) response = await _httpx_fetch._fetch_handle_async_request(transport, request) - await response.aread() # httpx.AsyncClient reads non-streamed responses the same way + await response.aread() # httpx2.AsyncClient reads non-streamed responses the same way return response -class _FetchTransport(httpx.AsyncBaseTransport): - """Route an ``httpx.AsyncClient`` through the handler without touching global state.""" +class _FetchTransport(httpx2.AsyncBaseTransport): + """Route an ``httpx2.AsyncClient`` through the handler without touching global state.""" - async def handle_async_request(self, request: httpx.Request) -> httpx.Response: + async def handle_async_request(self, request: httpx2.Request) -> httpx2.Response: return await _httpx_fetch._fetch_handle_async_request(self, request) # type: ignore[arg-type] -async def _via_client(method: str, url: str, **kwargs: Any) -> httpx.Response: - async with httpx.AsyncClient(transport=_FetchTransport()) as client: +async def _via_client(method: str, url: str, **kwargs: Any) -> httpx2.Response: + async with httpx2.AsyncClient(transport=_FetchTransport()) as client: return await client.request(method, url, **kwargs) @@ -94,7 +94,7 @@ async def test_basic_get_round_trip(fake_pyfetch): fake_pyfetch.response = FakeFetchResponse( status=200, headers={"content-type": "application/json"}, body=b'{"version": "1.30.0"}' ) - response = await _handle(httpx.Request("GET", "http://h:8080/v1/meta")) + response = await _handle(httpx2.Request("GET", "http://h:8080/v1/meta")) assert response.status_code == 200 assert response.json() == {"version": "1.30.0"} @@ -106,8 +106,8 @@ async def test_basic_get_round_trip(fake_pyfetch): async def test_response_has_request_attached_for_raise_for_status(fake_pyfetch): fake_pyfetch.response = FakeFetchResponse(status=404, body=b"") - response = await _handle(httpx.Request("GET", "http://h:8080/v1/schema/Nope")) - with pytest.raises(httpx.HTTPStatusError): + response = await _handle(httpx2.Request("GET", "http://h:8080/v1/schema/Nope")) + with pytest.raises(httpx2.HTTPStatusError): response.raise_for_status() @@ -116,7 +116,7 @@ async def test_head_response_without_body_yields_empty_content(fake_pyfetch, sta # data.exists() / tenants.exists() are HEAD requests answered 204/404 with a null # body; the transport must hand httpx an empty response, not fail on the missing body fake_pyfetch.response = FakeFetchResponse(status=status, body=None) - response = await _handle(httpx.Request("HEAD", "http://h:8080/v1/objects/A/uuid")) + response = await _handle(httpx2.Request("HEAD", "http://h:8080/v1/objects/A/uuid")) assert response.status_code == status assert response.content == b"" assert "body" not in fake_pyfetch.calls[0] @@ -125,13 +125,13 @@ async def test_head_response_without_body_yields_empty_content(fake_pyfetch, sta async def test_delete_204_without_body_yields_empty_content(fake_pyfetch): # data.delete_by_id() / reference_delete() are answered 204 with a null body fake_pyfetch.response = FakeFetchResponse(status=204, body=None) - response = await _handle(httpx.Request("DELETE", "http://h:8080/v1/objects/A/uuid")) + response = await _handle(httpx2.Request("DELETE", "http://h:8080/v1/objects/A/uuid")) assert response.status_code == 204 assert response.content == b"" async def test_body_less_response_through_async_client(fake_pyfetch): - # the full httpx.AsyncClient path (stream wrapping + read) on a body-less response + # the full httpx2.AsyncClient path (stream wrapping + read) on a body-less response fake_pyfetch.response = FakeFetchResponse(status=204, body=None) response = await _via_client("HEAD", "http://h:8080/v1/objects/A/uuid") assert response.status_code == 204 @@ -150,7 +150,7 @@ async def test_response_through_async_client_exposes_elapsed_and_content(fake_py async def test_fetch_managed_request_headers_stripped(fake_pyfetch): - request = httpx.Request( + request = httpx2.Request( "POST", "http://h:8080/v1/objects", headers={ @@ -173,25 +173,25 @@ async def test_fetch_managed_request_headers_stripped(fake_pyfetch): async def test_get_without_body_omits_body_kwarg(fake_pyfetch): # fetch rejects GET/HEAD requests that carry a body, so the kwarg must be absent - await _handle(httpx.Request("GET", "http://h:8080/v1/.well-known/ready")) + await _handle(httpx2.Request("GET", "http://h:8080/v1/.well-known/ready")) assert "body" not in fake_pyfetch.calls[0] async def test_post_body_passed(fake_pyfetch): - await _handle(httpx.Request("POST", "http://h:8080/v1/graphql", content=b'{"query": "x"}')) + await _handle(httpx2.Request("POST", "http://h:8080/v1/graphql", content=b'{"query": "x"}')) assert fake_pyfetch.calls[0]["body"] == b'{"query": "x"}' async def test_delete_with_body_passed(fake_pyfetch): # the REST batch-delete path sends DELETE with a JSON body await _handle( - httpx.Request("DELETE", "http://h:8080/v1/batch/objects", content=b'{"match": {}}') + httpx2.Request("DELETE", "http://h:8080/v1/batch/objects", content=b'{"match": {}}') ) assert fake_pyfetch.calls[0]["body"] == b'{"match": {}}' async def test_query_string_preserved_in_url(fake_pyfetch): - await _handle(httpx.Request("GET", "http://h:8080/v1/objects?class=A&limit=10&after=a%20b")) + await _handle(httpx2.Request("GET", "http://h:8080/v1/objects?class=A&limit=10&after=a%20b")) assert fake_pyfetch.calls[0]["url"] == "http://h:8080/v1/objects?class=A&limit=10&after=a%20b" @@ -202,7 +202,7 @@ async def test_content_encoding_stripped_from_response(fake_pyfetch): headers={"content-encoding": "gzip", "content-length": "23", "x-other": "kept"}, body=b'{"version": "1.30.0"}', ) - response = await _handle(httpx.Request("GET", "http://h:8080/v1/meta")) + response = await _handle(httpx2.Request("GET", "http://h:8080/v1/meta")) assert response.json() == {"version": "1.30.0"} assert "content-encoding" not in response.headers assert response.headers["x-other"] == "kept" @@ -215,13 +215,13 @@ def keys(self): fake_pyfetch.response = FakeFetchResponse(status=200, body=b"ok") fake_pyfetch.response.headers = BadHeaders() - response = await _handle(httpx.Request("GET", "http://h:8080/v1/meta")) + response = await _handle(httpx2.Request("GET", "http://h:8080/v1/meta")) assert response.status_code == 200 assert response.content == b"ok" -def _request_with_timeout(timeouts: Dict[str, Optional[float]]) -> httpx.Request: - request = httpx.Request("GET", "http://h:8080/v1/meta") +def _request_with_timeout(timeouts: Dict[str, Optional[float]]) -> httpx2.Request: + request = httpx2.Request("GET", "http://h:8080/v1/meta") request.extensions["timeout"] = timeouts return request @@ -250,7 +250,7 @@ async def test_read_timeout_alone_sets_the_deadline(fake_pyfetch, fake_abort_sig async def test_no_timeout_extension_sends_no_signal(fake_pyfetch, fake_abort_signal): - await _handle(httpx.Request("GET", "http://h:8080/v1/meta")) + await _handle(httpx2.Request("GET", "http://h:8080/v1/meta")) assert fake_abort_signal.timeouts == [] assert "signal" not in fake_pyfetch.calls[0] @@ -324,8 +324,8 @@ async def test_fetch_failure_maps_to_httpx_connect_error(monkeypatch): # pyodide surfaces JS fetch rejections as OSError; the base client can only classify # httpx exceptions, so the transport maps OSError to httpx errors _install_raising_pyfetch(monkeypatch, OSError("TypeError: Failed to fetch")) - with pytest.raises(httpx.ConnectError, match="Failed to fetch") as excinfo: - await _handle(httpx.Request("GET", "http://h:8080/v1/meta")) + with pytest.raises(httpx2.ConnectError, match="Failed to fetch") as excinfo: + await _handle(httpx2.Request("GET", "http://h:8080/v1/meta")) assert isinstance(excinfo.value.__cause__, OSError) @@ -333,7 +333,7 @@ async def test_fetch_abort_with_deadline_maps_to_read_timeout(monkeypatch, fake_ # AbortSignal.timeout firing surfaces as an OSError subclass mentioning the abort; # with a deadline set this must classify as a timeout, not a connection error _install_raising_pyfetch(monkeypatch, OSError("AbortError: signal timed out")) - with pytest.raises(httpx.ReadTimeout, match="signal timed out"): + with pytest.raises(httpx2.ReadTimeout, match="signal timed out"): await _handle( _request_with_timeout({"connect": None, "read": 0.5, "write": None, "pool": None}) ) @@ -345,7 +345,7 @@ async def test_fetch_failure_with_deadline_but_no_timeout_message_stays_connect_ # nearly every weaviate request sets a read deadline; a plain network failure on # such a request must remain a connection error, not become a timeout _install_raising_pyfetch(monkeypatch, OSError("TypeError: Failed to fetch")) - with pytest.raises(httpx.ConnectError, match="Failed to fetch"): + with pytest.raises(httpx2.ConnectError, match="Failed to fetch"): await _handle( _request_with_timeout({"connect": None, "read": 30.0, "write": None, "pool": None}) ) @@ -354,7 +354,7 @@ async def test_fetch_failure_with_deadline_but_no_timeout_message_stays_connect_ async def test_fetch_abort_without_deadline_stays_connect_error(monkeypatch, missing_js): # without our deadline (no js bridge -> no signal), an abort is a connection error _install_raising_pyfetch(monkeypatch, OSError("AbortError: signal timed out")) - with pytest.raises(httpx.ConnectError): + with pytest.raises(httpx2.ConnectError): await _handle( _request_with_timeout({"connect": None, "read": 0.5, "write": None, "pool": None}) ) @@ -362,16 +362,16 @@ async def test_fetch_abort_without_deadline_stays_connect_error(monkeypatch, mis async def test_empty_oserror_str_keeps_repr_detail(monkeypatch): _install_raising_pyfetch(monkeypatch, OSError()) - with pytest.raises(httpx.ConnectError) as excinfo: - await _handle(httpx.Request("GET", "http://h:8080/v1/meta")) + with pytest.raises(httpx2.ConnectError) as excinfo: + await _handle(httpx2.Request("GET", "http://h:8080/v1/meta")) assert "OSError" in str(excinfo.value) async def test_crlf_in_header_value_rejected(fake_pyfetch): - request = httpx.Request( + request = httpx2.Request( "GET", "http://h:8080/v1/meta", headers={"x-key": "val\r\nx-injected: evil"} ) - with pytest.raises(httpx.LocalProtocolError): + with pytest.raises(httpx2.LocalProtocolError): await _handle(request) assert fake_pyfetch.calls == [] @@ -385,19 +385,19 @@ def test_bootstrap_installed_fetch_transport(): # the import at the top ran the real bootstrap, which replaces Pyodide's jsfetch transport assert weaviate_client_web.is_fetch_transport_installed() assert ( - getattr(httpx.AsyncHTTPTransport.handle_async_request, "__weaviate_fetch_shim__", False) + getattr(httpx2.AsyncHTTPTransport.handle_async_request, "__weaviate_fetch_shim__", False) is True ) def test_install_fetch_transport_is_idempotent(): - patched_method = httpx.AsyncHTTPTransport.handle_async_request + patched_method = httpx2.AsyncHTTPTransport.handle_async_request weaviate_client_web.install_fetch_transport() - assert httpx.AsyncHTTPTransport.handle_async_request is patched_method + assert httpx2.AsyncHTTPTransport.handle_async_request is patched_method def test_sync_transport_left_untouched(): - assert not getattr(httpx.HTTPTransport.handle_request, "__weaviate_fetch_shim__", False) + assert not getattr(httpx2.HTTPTransport.handle_request, "__weaviate_fetch_shim__", False) def test_uninstall_restores_original_transport(): @@ -405,19 +405,19 @@ def test_uninstall_restores_original_transport(): # checked by identity even though the install happened at import time original = _httpx_fetch._original_handle_async_request assert original is not None - patched_method = httpx.AsyncHTTPTransport.handle_async_request + patched_method = httpx2.AsyncHTTPTransport.handle_async_request try: weaviate_client_web.uninstall_fetch_transport() assert not weaviate_client_web.is_fetch_transport_installed() - assert httpx.AsyncHTTPTransport.handle_async_request is original - assert httpx.AsyncHTTPTransport.handle_async_request is not patched_method + assert httpx2.AsyncHTTPTransport.handle_async_request is original + assert httpx2.AsyncHTTPTransport.handle_async_request is not patched_method weaviate_client_web.uninstall_fetch_transport() # no-op when not installed - assert httpx.AsyncHTTPTransport.handle_async_request is original + assert httpx2.AsyncHTTPTransport.handle_async_request is original finally: weaviate_client_web.install_fetch_transport() assert weaviate_client_web.is_fetch_transport_installed() assert ( - getattr(httpx.AsyncHTTPTransport.handle_async_request, "__weaviate_fetch_shim__", False) + getattr(httpx2.AsyncHTTPTransport.handle_async_request, "__weaviate_fetch_shim__", False) is True ) @@ -427,7 +427,7 @@ async def test_installed_transport_routes_async_client_through_pyfetch(fake_pyfe fake_pyfetch.response = FakeFetchResponse( status=200, headers={"content-type": "application/json"}, body=b'{"ok": true}' ) - async with httpx.AsyncClient() as client: + async with httpx2.AsyncClient() as client: response = await client.get("http://h:8080/v1/meta") assert response.status_code == 200 assert response.json() == {"ok": True} diff --git a/requirements-devel.txt b/requirements-devel.txt index 340f43093..a7bca65f2 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,6 +1,6 @@ -httpx==0.26.0 +httpx2==2.12.0 validators==0.34.0 -authlib==1.7.2 +authlib==1.8.0 grpcio==1.75.1 grpcio-tools==1.75.1 grpcio-health-checking==1.75.1 diff --git a/setup.cfg b/setup.cfg index b964b7ef3..89fb152b9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,12 +34,14 @@ packages = platforms = any include_package_data = True install_requires = - httpx>=0.26.0,<0.29.0 - # CA bundle for both REST (via httpx) and gRPC. 2026.10.19 drops the emSign ECC + # httpx2 2.15+ needs Python 3.11; on 3.10 pip resolves 2.12-2.14. + httpx2>=2.12.0,<3.0.0 + # CA bundle for both REST (via httpx2) and gRPC. 2026.10.19 drops the emSign ECC # Root CA - G3, whose intermediates' private keys were compromised in October 2026. certifi>=2026.10.19 validators>=0.34.0,<1.0.0 - authlib>=1.7.1,<2.0.0 + # 1.8 is the first authlib whose httpx_client integration is built on httpx2. + authlib>=1.8.0,<2.0.0 pydantic>=2.12.0,<3.0.0 grpcio>=1.59.5,<1.80.0; sys_platform != "emscripten" protobuf>=4.21.6,<7.0.0 diff --git a/test/test_embedded_cache.py b/test/test_embedded_cache.py index 196540e51..b49d6096e 100644 --- a/test/test_embedded_cache.py +++ b/test/test_embedded_cache.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import List -import httpx +import httpx2 import pytest from weaviate import embedded @@ -106,7 +106,9 @@ def test_known_version_reuses_existing_cache( tmp_path: Path, downloads: List[str], monkeypatch: pytest.MonkeyPatch, version: str ) -> None: monkeypatch.setattr( - embedded.httpx, "get", lambda url: httpx.Response(200, json={"tag_name": "v1.39.7"}) + embedded.httpx2, + "get", + lambda url, **kwargs: httpx2.Response(200, json={"tag_name": "v1.39.7"}), ) instance = _instance(version, tmp_path) cached = tmp_path / "bin" / f"weaviate-v1.39.7-{hashlib.sha256(b'v1.39.7').hexdigest()}" @@ -124,7 +126,9 @@ def test_latest_resolves_current_version_before_reusing_cache( ) -> None: tags = iter(["v1.39.7", "v1.40.0"]) monkeypatch.setattr( - embedded.httpx, "get", lambda url: httpx.Response(200, json={"tag_name": next(tags)}) + embedded.httpx2, + "get", + lambda url, **kwargs: httpx2.Response(200, json={"tag_name": next(tags)}), ) (tmp_path / "bin").mkdir() for tag in ["v1.39.7", "v1.40.0"]: diff --git a/test/test_wasm_compat.py b/test/test_wasm_compat.py index bcb4b9e8e..8c984a045 100644 --- a/test/test_wasm_compat.py +++ b/test/test_wasm_compat.py @@ -330,6 +330,10 @@ def test_bare_import_with_grpc_present_falls_through_silently() -> None: result = _run_hook_scenario( prelude=_PRIME_SYSCONFIG, body=""" + # httpx2 picks its transports from sys.platform at import time; under a faked + # emscripten it would look for httpx2-jsfetch, so import it for real first + import httpx2 + sys.platform = "emscripten" sys.modules["weaviate_client_web"] = None # makes its import raise ImportError diff --git a/weaviate/aliases/executor.py b/weaviate/aliases/executor.py index e08b80ca2..727b8c4b7 100644 --- a/weaviate/aliases/executor.py +++ b/weaviate/aliases/executor.py @@ -1,6 +1,6 @@ from typing import Dict, Generic, List, Optional, cast -from httpx import Response +from httpx2 import Response from weaviate.aliases.alias import AliasReturn, _WeaviateAlias from weaviate.connect import executor diff --git a/weaviate/backup/executor.py b/weaviate/backup/executor.py index da7005143..a008494d9 100644 --- a/weaviate/backup/executor.py +++ b/weaviate/backup/executor.py @@ -4,7 +4,7 @@ import time from typing import Dict, Generic, List, Literal, Optional, Tuple, Union -from httpx import Response +from httpx2 import Response from weaviate.backup.backup import ( STORAGE_NAMES, diff --git a/weaviate/client_executor.py b/weaviate/client_executor.py index 94742f17d..e5c55da81 100644 --- a/weaviate/client_executor.py +++ b/weaviate/client_executor.py @@ -12,7 +12,7 @@ cast, ) -from httpx import Response +from httpx2 import Response from weaviate.collections.classes.internal import _GQLEntryReturnType, _RawGQLReturn from weaviate.integrations import _Integrations diff --git a/weaviate/cluster/base.py b/weaviate/cluster/base.py index 1fcdcf370..197eee8fd 100644 --- a/weaviate/cluster/base.py +++ b/weaviate/cluster/base.py @@ -1,7 +1,7 @@ import uuid from typing import Generic, List, Optional, Union -from httpx import Response +from httpx2 import Response from weaviate.cluster.models import ( ClusterStatistics, diff --git a/weaviate/cluster/replicate/executor.py b/weaviate/cluster/replicate/executor.py index 5bb91fd64..1fd3dfd0b 100644 --- a/weaviate/cluster/replicate/executor.py +++ b/weaviate/cluster/replicate/executor.py @@ -1,6 +1,6 @@ from typing import Generic, Literal, Optional, overload -from httpx import Response +from httpx2 import Response from weaviate.cluster.models import ( ReplicateOperation, diff --git a/weaviate/collections/aggregations/base_executor.py b/weaviate/collections/aggregations/base_executor.py index 90cc72089..a42eed612 100644 --- a/weaviate/collections/aggregations/base_executor.py +++ b/weaviate/collections/aggregations/base_executor.py @@ -3,7 +3,7 @@ import pathlib from typing import Generic, List, Optional, TypeVar, Union, cast -from httpx import Response +from httpx2 import Response from typing_extensions import ParamSpec from weaviate.collections.classes.aggregate import ( diff --git a/weaviate/collections/batch/rest.py b/weaviate/collections/batch/rest.py index 40f1d66f3..a93c713e5 100644 --- a/weaviate/collections/batch/rest.py +++ b/weaviate/collections/batch/rest.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional -from httpx import Response +from httpx2 import Response from weaviate.collections.classes.batch import ( BatchReference, diff --git a/weaviate/collections/collections/executor.py b/weaviate/collections/collections/executor.py index da8b3ca17..f77f453fe 100644 --- a/weaviate/collections/collections/executor.py +++ b/weaviate/collections/collections/executor.py @@ -11,7 +11,7 @@ Union, ) -from httpx import Response +from httpx2 import Response from pydantic import ValidationError from weaviate.collections.classes.config import ( diff --git a/weaviate/collections/config/executor.py b/weaviate/collections/config/executor.py index dd32efc49..12f9d1fb1 100644 --- a/weaviate/collections/config/executor.py +++ b/weaviate/collections/config/executor.py @@ -13,7 +13,7 @@ overload, ) -from httpx import Response +from httpx2 import Response from pydantic_core import ValidationError from typing_extensions import deprecated diff --git a/weaviate/collections/data/executor.py b/weaviate/collections/data/executor.py index 688d2cc98..f6783444f 100644 --- a/weaviate/collections/data/executor.py +++ b/weaviate/collections/data/executor.py @@ -18,7 +18,7 @@ overload, ) -from httpx import Response +from httpx2 import Response from weaviate.collections.batch.base import _BatchDataWrapper from weaviate.collections.batch.collection import ( diff --git a/weaviate/collections/tenants/executor.py b/weaviate/collections/tenants/executor.py index 7feb2124b..6b850fc0b 100644 --- a/weaviate/collections/tenants/executor.py +++ b/weaviate/collections/tenants/executor.py @@ -2,7 +2,7 @@ from math import ceil from typing import Any, Dict, Generic, List, Optional, Sequence, Union -from httpx import Response +from httpx2 import Response from weaviate.collections.classes.tenants import ( Tenant, diff --git a/weaviate/connect/authentication.py b/weaviate/connect/authentication.py index b7ca53996..c3e16fe9d 100644 --- a/weaviate/connect/authentication.py +++ b/weaviate/connect/authentication.py @@ -2,7 +2,7 @@ from typing import Awaitable, Callable, Dict, List, Optional, Union -import httpx +import httpx2 from authlib.integrations.httpx_client import ( # type: ignore AsyncOAuth2Client, OAuth2Client, @@ -19,14 +19,15 @@ from ..util import _decode_json_response_dict from ..warnings import _Warnings from . import executor +from .base import _default_ssl_context AUTH_DEFAULT_TIMEOUT = 5 OIDC_CONFIG = Dict[str, Union[str, List[str]]] Result = Union[OAuth2Client, Awaitable[AsyncOAuth2Client]] MountsMaker = Union[ - Callable[[], Dict[str, httpx.AsyncHTTPTransport]], - Callable[[], Dict[str, httpx.HTTPTransport]], + Callable[[], Dict[str, httpx2.AsyncHTTPTransport]], + Callable[[], Dict[str, httpx2.HTTPTransport]], ] @@ -111,7 +112,7 @@ def _get_token_endpoint(self) -> executor.Result[str]: if self._token_endpoint is not None: return executor.return_(self._token_endpoint, self.__colour) - def resp(res: httpx.Response) -> str: + def resp(res: httpx2.Response) -> str: data = _decode_json_response_dict(res, "Get token endpoint") assert data is not None token_endpoint = data["token_endpoint"] @@ -121,19 +122,21 @@ def resp(res: httpx.Response) -> str: if self.__colour == "async": async def _execute() -> str: - mounts: Dict[str, httpx.AsyncBaseTransport] = {} + mounts: Dict[str, httpx2.AsyncBaseTransport] = {} for key, mount in self.__make_mounts().items(): - assert isinstance(mount, httpx.AsyncHTTPTransport) + assert isinstance(mount, httpx2.AsyncHTTPTransport) mounts[key] = mount - async with httpx.AsyncClient(mounts=mounts) as client: + async with httpx2.AsyncClient( + mounts=mounts, verify=_default_ssl_context() + ) as client: return resp(await client.get(self._open_id_config_url)) return _execute() - mounts: Dict[str, httpx.BaseTransport] = {} + mounts: Dict[str, httpx2.BaseTransport] = {} for key, mount in self.__make_mounts().items(): - assert isinstance(mount, httpx.BaseTransport) + assert isinstance(mount, httpx2.BaseTransport) mounts[key] = mount - with httpx.Client(mounts=mounts) as client: + with httpx2.Client(mounts=mounts, verify=_default_ssl_context()) as client: return resp(client.get(self._open_id_config_url)) def get_auth_session(self) -> Result: @@ -164,6 +167,7 @@ async def _execute() -> AsyncOAuth2Client: token_endpoint=await executor.aresult(self._get_token_endpoint()), client_id=self._client_id, default_timeout=AUTH_DEFAULT_TIMEOUT, + verify=_default_ssl_context(), ) return _execute() @@ -172,6 +176,7 @@ async def _execute() -> AsyncOAuth2Client: token_endpoint=executor.result(self._get_token_endpoint()), client_id=self._client_id, default_timeout=AUTH_DEFAULT_TIMEOUT, + verify=_default_ssl_context(), ) def _get_session_user_pw(self, config: AuthClientPassword) -> Result: @@ -186,6 +191,7 @@ async def _execute() -> AsyncOAuth2Client: grant_type="password", scope=scope, default_timeout=AUTH_DEFAULT_TIMEOUT, + verify=_default_ssl_context(), ) token: dict = await session.fetch_token( username=config.username, password=config.password @@ -202,6 +208,7 @@ async def _execute() -> AsyncOAuth2Client: grant_type="password", scope=scope, default_timeout=AUTH_DEFAULT_TIMEOUT, + verify=_default_ssl_context(), ) token: dict = session.fetch_token(username=config.username, password=config.password) if "refresh_token" not in token: @@ -239,6 +246,7 @@ async def _execute() -> AsyncOAuth2Client: grant_type="client_credentials", token={"access_token": None, "expires_in": -100}, default_timeout=AUTH_DEFAULT_TIMEOUT, + verify=_default_ssl_context(), ) # explicitly fetch tokens. Otherwise, authlib will do it in the background and we might have race-conditions await session.fetch_token() @@ -254,6 +262,7 @@ async def _execute() -> AsyncOAuth2Client: grant_type="client_credentials", token={"access_token": None, "expires_in": -100}, default_timeout=AUTH_DEFAULT_TIMEOUT, + verify=_default_ssl_context(), ) # explicitly fetch tokens. Otherwise, authlib will do it in the background and we might have race-conditions session.fetch_token() diff --git a/weaviate/connect/base.py b/weaviate/connect/base.py index 8568985a7..d7b9062e0 100644 --- a/weaviate/connect/base.py +++ b/weaviate/connect/base.py @@ -1,4 +1,6 @@ import os +import ssl +import sys from typing import Any, Dict, Mapping, Optional, Sequence, Tuple, TypeVar, Union, cast from urllib.parse import urlparse @@ -31,8 +33,25 @@ def _grpc_web_shim_active() -> bool: return getattr(grpc, "__weaviate_client_web_shim__", False) is True +def _default_ssl_context(trust_env: bool = True) -> Union[ssl.SSLContext, bool]: + """The ``verify`` value for every httpx2 client: certifi's CA bundle, as gRPC uses. + + httpx2 defaults to the OS trust store (via truststore); pinning certifi keeps REST and + gRPC on the same roots and matches what httpx did. With ``trust_env``, SSL_CERT_FILE + and SSL_CERT_DIR still win, as they did with httpx. Under Emscripten the browser's + fetch verifies TLS, so httpx2's default is kept there. + """ + if sys.platform == "emscripten": + return True + if trust_env and (cafile := os.environ.get("SSL_CERT_FILE")): + return ssl.create_default_context(cafile=cafile) + if trust_env and (capath := os.environ.get("SSL_CERT_DIR")): + return ssl.create_default_context(capath=capath) + return ssl.create_default_context(cafile=certifi.where()) + + def _default_grpc_credentials() -> grpc.ChannelCredentials: - """SSL credentials trusting certifi's CA bundle, the same one httpx uses for REST. + """SSL credentials trusting certifi's CA bundle, the same one httpx2 uses for REST. grpcio's bundled roots only change with a grpcio release, so a CA distrust reaches certifi first. A roots file set via GRPC_DEFAULT_SSL_ROOTS_FILE_PATH still wins. diff --git a/weaviate/connect/v4.py b/weaviate/connect/v4.py index 8be4668a3..72f4c66a1 100644 --- a/weaviate/connect/v4.py +++ b/weaviate/connect/v4.py @@ -34,7 +34,7 @@ from grpc.aio import Channel as AsyncChannel # type: ignore # from grpclib.client import Channel -from httpx import ( +from httpx2 import ( AsyncClient, AsyncHTTPTransport, Client, @@ -61,6 +61,7 @@ from weaviate.connect.base import ( ConnectionParams, JSONPayload, + _default_ssl_context, _get_proxies, ) from weaviate.connect.integrations import _IntegrationConfig @@ -222,12 +223,14 @@ def _make_client(self, colour: executor.Colour) -> Union[AsyncClient, Client]: headers=self._headers, mounts=self._make_mounts(colour), trust_env=self.__trust_env, + verify=_default_ssl_context(self.__trust_env), ) if colour == "sync": return Client( headers=self._headers, mounts=self._make_mounts(colour), trust_env=self.__trust_env, + verify=_default_ssl_context(self.__trust_env), ) @overload @@ -249,6 +252,7 @@ def _make_mounts( proxy=Proxy(url=proxy), retries=self.__connection_config.session_pool_max_retries, trust_env=self.__trust_env, + verify=_default_ssl_context(self.__trust_env), ) for key, proxy in self._proxies.items() if key != "grpc" @@ -263,6 +267,7 @@ def _make_mounts( proxy=Proxy(url=proxy), retries=self.__connection_config.session_pool_max_retries, trust_env=self.__trust_env, + verify=_default_ssl_context(self.__trust_env), ) for key, proxy in self._proxies.items() if key != "grpc" @@ -667,7 +672,7 @@ def __get_timeout( ) -> Timeout: """Get the timeout for the request. - In this way, the client waits the `httpx` default of 5s when connecting to a socket (connect), writing chunks (write), and + In this way, the client waits the `httpx2` default of 5s when connecting to a socket (connect), writing chunks (write), and acquiring a connection from the pool (pool), but a custom amount as specified for reading the response (read). From the PoV of the user, a request is considered to be timed out if no response is received within the specified time. @@ -801,7 +806,7 @@ def resp(res: Response) -> None: async def _execute() -> None: try: - async with AsyncClient() as client: + async with AsyncClient(verify=_default_ssl_context()) as client: res = await client.get(PYPI_PACKAGE_URL, timeout=self.timeout_config.init) return resp(res) except (RequestError, OSError): @@ -813,7 +818,7 @@ async def _execute() -> None: return _execute() try: - with Client() as client: + with Client(verify=_default_ssl_context()) as client: res = client.get(PYPI_PACKAGE_URL, timeout=self.timeout_config.init) return resp(res) except (RequestError, OSError): diff --git a/weaviate/debug/executor.py b/weaviate/debug/executor.py index 818ead8a3..68b86ffdd 100644 --- a/weaviate/debug/executor.py +++ b/weaviate/debug/executor.py @@ -1,6 +1,6 @@ from typing import Dict, Generic, Optional -from httpx import Response +from httpx2 import Response from weaviate.classes.config import ConsistencyLevel from weaviate.connect import executor diff --git a/weaviate/embedded.py b/weaviate/embedded.py index 8e07c14e3..4c0def15f 100644 --- a/weaviate/embedded.py +++ b/weaviate/embedded.py @@ -17,10 +17,11 @@ from pathlib import Path from typing import Dict, Optional, Tuple -import httpx +import httpx2 import validators from weaviate import exceptions +from weaviate.connect.base import _default_ssl_context from weaviate.exceptions import WeaviateStartUpError from weaviate.logger import logger from weaviate.util import _decode_json_response_dict @@ -125,7 +126,10 @@ def __init__(self, options: EmbeddedOptions) -> None: self._parsed_weaviate_version = version_tag self._set_download_url_from_version_tag(version_tag) elif self.options.version == "latest": - response = httpx.get("https://api.github.com/repos/weaviate/weaviate/releases/latest") + response = httpx2.get( + "https://api.github.com/repos/weaviate/weaviate/releases/latest", + verify=_default_ssl_context(), + ) latest = _decode_json_response_dict(response, "get tag of latest weaviate release") assert latest is not None version_tag = latest["tag_name"] diff --git a/weaviate/exceptions.py b/weaviate/exceptions.py index f83e05224..8addbdf4d 100644 --- a/weaviate/exceptions.py +++ b/weaviate/exceptions.py @@ -3,7 +3,7 @@ from json.decoder import JSONDecodeError from typing import Optional, Tuple, Union, cast -import httpx +import httpx2 from grpc import Call, StatusCode # type: ignore from grpc.aio import AioRpcError # type: ignore from packaging import version @@ -31,7 +31,7 @@ def __init__(self, message: str = ""): class UnexpectedStatusCodeError(WeaviateBaseError): - def __init__(self, message: str, response: Union[httpx.Response, AioRpcError, Call]): + def __init__(self, message: str, response: Union[httpx2.Response, AioRpcError, Call]): """Is raised in case the status code returned from Weaviate is not handled in the client implementation and suggests an error. Custom code can act on the attributes: @@ -42,13 +42,13 @@ def __init__(self, message: str, response: Union[httpx.Response, AioRpcError, Ca message: An error message specific to the context, in which the error occurred. response: The request response of which the status code was unexpected. """ - if isinstance(response, httpx.Response): + if isinstance(response, httpx2.Response): self._status_code: int = response.status_code # Set error message try: body = response.json() - except (httpx.DecodingError, JSONDecodeError): + except (httpx2.DecodingError, JSONDecodeError): body = None msg = ( @@ -90,7 +90,7 @@ def error(self) -> Optional[str]: class ResponseCannotBeDecodedError(WeaviateBaseError): - def __init__(self, location: str, response: httpx.Response): + def __init__(self, location: str, response: httpx2.Response): """Raised when a weaviate response cannot be decoded to json. Args: @@ -457,7 +457,7 @@ def __init__(self, message: str, count: int) -> None: class InsufficientPermissionsError(UnexpectedStatusCodeError): """Is raised when a request to Weaviate fails due to insufficient permissions.""" - def __init__(self, res: Union[httpx.Response, AioRpcError, Call]) -> None: + def __init__(self, res: Union[httpx2.Response, AioRpcError, Call]) -> None: super().__init__("forbidden", res) diff --git a/weaviate/export/executor.py b/weaviate/export/executor.py index 516273c73..fb9eaac8c 100644 --- a/weaviate/export/executor.py +++ b/weaviate/export/executor.py @@ -4,7 +4,7 @@ import time from typing import Generic, List, Literal, Tuple, Union, overload -from httpx import Response +from httpx2 import Response from weaviate.backup.backup import STORAGE_NAMES from weaviate.connect import executor diff --git a/weaviate/groups/base.py b/weaviate/groups/base.py index 0f3cbc806..fd613dffa 100644 --- a/weaviate/groups/base.py +++ b/weaviate/groups/base.py @@ -1,6 +1,6 @@ from typing import Dict, Generic, List, Literal, Union, overload -from httpx import Response +from httpx2 import Response from weaviate.connect import executor from weaviate.connect.v4 import ConnectionType, _ExpectedStatusCodes diff --git a/weaviate/rbac/executor.py b/weaviate/rbac/executor.py index 064a9bd1a..2762f31b7 100644 --- a/weaviate/rbac/executor.py +++ b/weaviate/rbac/executor.py @@ -2,7 +2,7 @@ import json from typing import Dict, Generic, List, Optional, Sequence, Union, cast -from httpx import Response +from httpx2 import Response from typing_extensions import deprecated from weaviate.connect import executor diff --git a/weaviate/tokenization/executor.py b/weaviate/tokenization/executor.py index 33f1c05f9..1a43588b9 100644 --- a/weaviate/tokenization/executor.py +++ b/weaviate/tokenization/executor.py @@ -2,7 +2,7 @@ from typing import Any, Dict, Generic, List, Optional, Union, overload -from httpx import Response +from httpx2 import Response from weaviate.collections.classes.config import ( StopwordsConfig, diff --git a/weaviate/users/base.py b/weaviate/users/base.py index 21692e757..adeaf4173 100644 --- a/weaviate/users/base.py +++ b/weaviate/users/base.py @@ -1,7 +1,7 @@ from datetime import datetime, timezone from typing import Any, Dict, Generic, List, Literal, Optional, Union, cast, overload -from httpx import Response +from httpx2 import Response from typing_extensions import deprecated from weaviate.connect import executor diff --git a/weaviate/util.py b/weaviate/util.py index be61fe389..2034b0fa7 100644 --- a/weaviate/util.py +++ b/weaviate/util.py @@ -13,7 +13,7 @@ from typing import Any, Callable, Dict, Generator, List, Optional, Sequence, Tuple, Union, cast from urllib.parse import quote -import httpx +import httpx2 import validators from weaviate.exceptions import ( @@ -725,7 +725,9 @@ def _to_beacons(uuids: UUIDS, to_class: str = "") -> List[Dict[str, str]]: return [{"beacon": f"weaviate://localhost/{to_class}{uuid_to}"} for uuid_to in uuids] -def _decode_json_response_dict(response: httpx.Response, location: str) -> Optional[Dict[str, Any]]: +def _decode_json_response_dict( + response: httpx2.Response, location: str +) -> Optional[Dict[str, Any]]: if response is None: return None @@ -733,14 +735,14 @@ def _decode_json_response_dict(response: httpx.Response, location: str) -> Optio try: json_response = cast(Dict[str, Any], response.json()) return json_response - except (httpx.DecodingError, json.decoder.JSONDecodeError): + except (httpx2.DecodingError, json.decoder.JSONDecodeError): raise ResponseCannotBeDecodedError(location, response) raise UnexpectedStatusCodeError(location, response) def _decode_json_response_list( - response: httpx.Response, location: str + response: httpx2.Response, location: str ) -> Optional[List[Dict[str, Any]]]: if response is None: return None @@ -749,7 +751,7 @@ def _decode_json_response_list( try: json_response = response.json() return cast(list, json_response) - except (httpx.DecodingError, json.decoder.JSONDecodeError): + except (httpx2.DecodingError, json.decoder.JSONDecodeError): raise ResponseCannotBeDecodedError(location, response) raise UnexpectedStatusCodeError(location, response)