diff --git a/haystack/components/connectors/openapi_service.py b/haystack/components/connectors/openapi_service.py index 4ba1a777c8..4970f84c41 100644 --- a/haystack/components/connectors/openapi_service.py +++ b/haystack/components/connectors/openapi_service.py @@ -174,7 +174,7 @@ class OpenAPIServiceConnector: ```python import json - import requests + import httpx from haystack.components.connectors import OpenAPIServiceConnector from haystack.dataclasses import ChatMessage, ToolCall @@ -187,7 +187,7 @@ class OpenAPIServiceConnector: message = ChatMessage.from_assistant(tool_calls=[tool_call]) serper_token = Secret.from_env_var("SERPERDEV_API_KEY").resolve_value() - serperdev_openapi_spec = json.loads(requests.get("https://bit.ly/serper_dev_spec").text) + serperdev_openapi_spec = json.loads(httpx.get("https://bit.ly/serper_dev_spec", follow_redirects=True).text) service_connector = OpenAPIServiceConnector() result = service_connector.run( messages=[message], diff --git a/haystack/components/rankers/hugging_face_tei.py b/haystack/components/rankers/hugging_face_tei.py index e1ac6f84c7..5e5957854f 100644 --- a/haystack/components/rankers/hugging_face_tei.py +++ b/haystack/components/rankers/hugging_face_tei.py @@ -7,6 +7,8 @@ from typing import Any from urllib.parse import urljoin +import httpx + from haystack import Document, component, default_from_dict, default_to_dict from haystack.utils import Secret from haystack.utils.misc import _deduplicate_documents @@ -133,7 +135,7 @@ def _compose_response( :returns: A dictionary with the following keys: - `documents`: A list of reranked documents. - :raises requests.exceptions.RequestException: + :raises RuntimeError: - If the API request fails. :raises RuntimeError: @@ -186,7 +188,7 @@ def run( :returns: A dictionary with the following keys: - `documents`: A list of reranked documents. - :raises requests.exceptions.RequestException: + :raises RuntimeError: - If the API request fails. :raises RuntimeError: @@ -211,15 +213,18 @@ def run( headers["Authorization"] = f"Bearer {self.token.resolve_value()}" # Call the external service with retry - response = request_with_retry( - method="POST", - url=urljoin(self.url, "/rerank"), - json=payload, - timeout=self.timeout, - headers=headers, - attempts=self.max_retries, - status_codes_to_retry=self.retry_status_codes, - ) + try: + response = request_with_retry( + method="POST", + url=urljoin(self.url, "/rerank"), + json=payload, + timeout=self.timeout, + headers=headers, + attempts=self.max_retries, + status_codes_to_retry=self.retry_status_codes, + ) + except httpx.HTTPStatusError as e: + raise RuntimeError(f"HuggingFaceTEIRanker API call failed. Error: {e}, Response: {e.response.text}") from e result: dict[str, str] | list[dict[str, Any]] = response.json() @@ -270,15 +275,18 @@ async def run_async( headers["Authorization"] = f"Bearer {self.token.resolve_value()}" # Call the external service with retry - response = await async_request_with_retry( - method="POST", - url=urljoin(self.url, "/rerank"), - json=payload, - timeout=self.timeout, - headers=headers, - attempts=self.max_retries, - status_codes_to_retry=self.retry_status_codes, - ) + try: + response = await async_request_with_retry( + method="POST", + url=urljoin(self.url, "/rerank"), + json=payload, + timeout=self.timeout, + headers=headers, + attempts=self.max_retries, + status_codes_to_retry=self.retry_status_codes, + ) + except httpx.HTTPStatusError as e: + raise RuntimeError(f"HuggingFaceTEIRanker API call failed. Error: {e}, Response: {e.response.text}") from e result: dict[str, str] | list[dict[str, Any]] = response.json() diff --git a/haystack/components/websearch/searchapi.py b/haystack/components/websearch/searchapi.py index 71796900d4..88694d7501 100644 --- a/haystack/components/websearch/searchapi.py +++ b/haystack/components/websearch/searchapi.py @@ -114,6 +114,11 @@ def run(self, query: str) -> dict[str, list[Document] | list[str]]: except httpx.ConnectTimeout as error: raise TimeoutError(f"Request to {self.__class__.__name__} timed out.") from error + except httpx.HTTPStatusError as e: + raise SearchApiError( + f"An error occurred while querying {self.__class__.__name__}. Error: {e}, Response: {e.response.text}" + ) from e + except httpx.HTTPError as e: raise SearchApiError(f"An error occurred while querying {self.__class__.__name__}. Error: {e}") from e @@ -149,6 +154,11 @@ async def run_async(self, query: str) -> dict[str, list[Document] | list[str]]: except httpx.ConnectTimeout as error: raise TimeoutError(f"Request to {self.__class__.__name__} timed out.") from error + except httpx.HTTPStatusError as e: + raise SearchApiError( + f"An error occurred while querying {self.__class__.__name__}. Error: {e}, Response: {e.response.text}" + ) from e + except httpx.HTTPError as e: raise SearchApiError(f"An error occurred while querying {self.__class__.__name__}. Error: {e}") from e diff --git a/haystack/components/websearch/serper_dev.py b/haystack/components/websearch/serper_dev.py index fda18f3dd2..a3eacf181f 100644 --- a/haystack/components/websearch/serper_dev.py +++ b/haystack/components/websearch/serper_dev.py @@ -159,6 +159,11 @@ def run(self, query: str) -> dict[str, list[Document] | list[str]]: except httpx.ConnectTimeout as error: raise TimeoutError(f"Request to {self.__class__.__name__} timed out.") from error + except httpx.HTTPStatusError as e: + raise SerperDevError( + f"An error occurred while querying {self.__class__.__name__}. Error: {e}, Response: {e.response.text}" + ) from e + except httpx.HTTPError as e: raise SerperDevError(f"An error occurred while querying {self.__class__.__name__}. Error: {e}") from e @@ -194,6 +199,11 @@ async def run_async(self, query: str) -> dict[str, list[Document] | list[str]]: except httpx.ConnectTimeout as error: raise TimeoutError(f"Request to {self.__class__.__name__} timed out.") from error + except httpx.HTTPStatusError as e: + raise SerperDevError( + f"An error occurred while querying {self.__class__.__name__}. Error: {e}, Response: {e.response.text}" + ) from e + except httpx.HTTPError as e: raise SerperDevError(f"An error occurred while querying {self.__class__.__name__}. Error: {e}") from e diff --git a/haystack/core/pipeline/draw.py b/haystack/core/pipeline/draw.py index b61f3ab209..9aec94fc70 100644 --- a/haystack/core/pipeline/draw.py +++ b/haystack/core/pipeline/draw.py @@ -9,8 +9,8 @@ import zlib from typing import Any +import httpx import networkx -import requests from haystack import logging from haystack.core.errors import PipelineDrawingError @@ -234,7 +234,7 @@ def _to_mermaid_image( logger.debug("Rendering graph at {url}", url=url) try: - resp = requests.get(url, timeout=timeout) + resp = httpx.get(url, timeout=timeout) if resp.status_code >= 400: logger.warning( "Failed to draw the pipeline: {server_url} returned status {status_code}", diff --git a/haystack/utils/requests_utils.py b/haystack/utils/requests_utils.py index 8d870ee676..c5270cc055 100644 --- a/haystack/utils/requests_utils.py +++ b/haystack/utils/requests_utils.py @@ -6,7 +6,6 @@ from typing import Any import httpx -import requests from tenacity import after_log, before_log, retry, retry_if_exception_type, stop_after_attempt, wait_exponential logger = logging.getLogger(__file__) @@ -14,7 +13,7 @@ def request_with_retry( attempts: int = 3, status_codes_to_retry: list[int] | None = None, **kwargs: Any -) -> requests.Response: +) -> httpx.Response: """ Executes an HTTP request with a configurable exponential backoff retry on failures. @@ -35,26 +34,11 @@ def request_with_retry( # Sending an HTTP request with custom timeout in seconds res = request_with_retry(method="GET", url="https://example.com", timeout=5) - # Sending an HTTP request with custom authorization handling - class CustomAuth(requests.auth.AuthBase): - def __call__(self, r): - r.headers["authorization"] = "Basic " - return r - - res = request_with_retry(method="GET", url="https://example.com", auth=CustomAuth()) - - # All of the above combined - res = request_with_retry( - method="GET", - url="https://example.com", - auth=CustomAuth(), - attempts=10, - status_codes_to_retry=[408, 503], - timeout=5 - ) + # Sending an HTTP request with custom headers + res = request_with_retry(method="GET", url="https://example.com", headers={"Authorization": "Bearer "}) # Sending a POST request - res = request_with_retry(method="POST", url="https://example.com", data={"key": "value"}, attempts=10) + res = request_with_retry(method="POST", url="https://example.com", json={"key": "value"}, attempts=10) # Retry all 5xx status codes res = request_with_retry(method="GET", url="https://example.com", status_codes_to_retry=list(range(500, 600))) @@ -66,9 +50,9 @@ def __call__(self, r): List of HTTP status codes that will trigger a retry. When param is `None`, HTTP 408, 418, 429 and 503 will be retried. :param kwargs: - Optional arguments that `request` accepts. + Optional arguments that `httpx.Client.request` accepts. :returns: - The `Response` object. + The `httpx.Response` object. """ if status_codes_to_retry is None: @@ -77,20 +61,21 @@ def __call__(self, r): @retry( reraise=True, wait=wait_exponential(), - retry=retry_if_exception_type((requests.HTTPError, TimeoutError)), + retry=retry_if_exception_type((httpx.HTTPError, TimeoutError)), stop=stop_after_attempt(attempts), before=before_log(logger, logging.DEBUG), after=after_log(logger, logging.DEBUG), ) - def run() -> requests.Response: + def run() -> httpx.Response: timeout = kwargs.pop("timeout", 10) - res = requests.request(**kwargs, timeout=timeout) + with httpx.Client() as client: + res = client.request(**kwargs, timeout=timeout) - if res.status_code in status_codes_to_retry: - # We raise only for the status codes that must trigger a retry - res.raise_for_status() + if res.status_code in status_codes_to_retry: + # We raise only for the status codes that must trigger a retry + res.raise_for_status() - return res + return res res = run() # We raise here too in case the request failed with a status code that diff --git a/pyproject.toml b/pyproject.toml index 2f03c5ad6f..40a549bc96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ dependencies = [ "more-itertools", # TextDocumentSplitter "networkx", # Pipeline graphs "typing_extensions>=4.7", # Extended typing features (NotRequired, etc.) - "requests", + "httpx", "numpy", "python-dateutil", "jsonschema", # JsonSchemaValidator, Tool diff --git a/releasenotes/notes/httpx-3f5560ae23ab91b1.yaml b/releasenotes/notes/httpx-3f5560ae23ab91b1.yaml new file mode 100644 index 0000000000..19ec77dcd6 --- /dev/null +++ b/releasenotes/notes/httpx-3f5560ae23ab91b1.yaml @@ -0,0 +1,13 @@ +--- +enhancements: + - | + Standardize HTTP request handling in Haystack by adopting ``httpx`` for both synchronous and asynchronous requests, + replacing ``requests``. Error reporting for failed requests has also been improved: exceptions now include + additional details alongside the reason field. +upgrade: + - | + As part of the migration from ``requests`` to ``httpx``, ``request_with_retry`` and ``async_request_with_retry`` + (in ``haystack.utils.requests_utils``) no longer raise ``requests.exceptions.RequestException`` on failure; + they now raise ``httpx.HTTPError`` instead. This also affects ``HuggingFaceTEIRanker``, which relies on these + utilities. Users catching ``requests.exceptions.RequestException`` should update their code to catch + ``httpx.HTTPError``. diff --git a/test/components/connectors/test_openapi_service.py b/test/components/connectors/test_openapi_service.py index 9ea33a283d..71abf4313c 100644 --- a/test/components/connectors/test_openapi_service.py +++ b/test/components/connectors/test_openapi_service.py @@ -7,8 +7,8 @@ from typing import Any from unittest.mock import MagicMock, Mock, patch +import httpx import pytest -import requests from openapi3 import OpenAPI from haystack import Pipeline @@ -266,10 +266,11 @@ def prepare_fc_params(openai_functions_schema: dict[str, Any]) -> dict[str, Any] pipe.connect("openapi_container.service_response", "final_prompt_adapter.service_response") pipe.connect("final_prompt_adapter", "llm.messages") - serperdev_spec = requests.get( - "https://gist.githubusercontent.com/vblagoje/241a000f2a77c76be6efba71d49e2856/raw/722ccc7fe6170a744afce3e3fb3a30fdd095c184/serper.json" + serperdev_spec = httpx.get( + "https://gist.githubusercontent.com/vblagoje/241a000f2a77c76be6efba71d49e2856/raw/722ccc7fe6170a744afce3e3fb3a30fdd095c184/serper.json", + follow_redirects=True, ).json() - system_prompt = requests.get("https://bit.ly/serper_dev_system").text + system_prompt = httpx.get("https://bit.ly/serper_dev_system", follow_redirects=True).text query = "Why did Elon Musk sue OpenAI?" diff --git a/test/components/rankers/test_hugging_face_tei.py b/test/components/rankers/test_hugging_face_tei.py index 2fd2472879..bcfbb06020 100644 --- a/test/components/rankers/test_hugging_face_tei.py +++ b/test/components/rankers/test_hugging_face_tei.py @@ -6,7 +6,6 @@ import httpx import pytest -import requests from haystack import Document from haystack.components.rankers.hugging_face_tei import HuggingFaceTEIRanker, TruncationDirection @@ -93,7 +92,7 @@ def test_empty_documents(self, del_hf_env_vars): def test_run_with_mock(self, mock_request, del_hf_env_vars): """Test run method with mocked API response""" # Setup mock response - mock_response = MagicMock(spec=requests.Response) + mock_response = MagicMock(spec=httpx.Response) mock_response.json.return_value = [ {"index": 2, "score": 0.95}, {"index": 1, "score": 0.85}, @@ -141,7 +140,7 @@ def test_run_with_mock(self, mock_request, del_hf_env_vars): def test_run_with_truncation_direction(self, mock_request, del_hf_env_vars): """Test run method with truncation direction parameter""" # Setup mock response - mock_response = MagicMock(spec=requests.Response) + mock_response = MagicMock(spec=httpx.Response) mock_response.json.return_value = [{"index": 0, "score": 0.95}] mock_request.return_value = mock_response @@ -174,7 +173,7 @@ def test_run_with_truncation_direction(self, mock_request, del_hf_env_vars): def test_run_with_custom_top_k(self, mock_request, del_hf_env_vars): """Test run method with custom top_k parameter""" # Setup mock response with 5 documents - mock_response = MagicMock(spec=requests.Response) + mock_response = MagicMock(spec=httpx.Response) mock_response.json.return_value = [ {"index": 4, "score": 0.95}, {"index": 3, "score": 0.90}, @@ -210,7 +209,7 @@ def test_run_with_custom_top_k(self, mock_request, del_hf_env_vars): @patch("haystack.components.rankers.hugging_face_tei.request_with_retry") def test_run_deduplicates_documents(self, mock_request, del_hf_env_vars): """Test that duplicate documents are removed before sending to the API.""" - mock_response = MagicMock(spec=requests.Response) + mock_response = MagicMock(spec=httpx.Response) mock_response.json.return_value = [{"index": 1, "score": 0.9}, {"index": 0, "score": 0.2}] mock_request.return_value = mock_response @@ -241,7 +240,7 @@ def test_run_deduplicates_documents(self, mock_request, del_hf_env_vars): def test_error_handling(self, mock_request, del_hf_env_vars): """Test error handling in the ranker""" # Setup mock response with error - mock_response = MagicMock(spec=requests.Response) + mock_response = MagicMock(spec=httpx.Response) mock_response.json.return_value = {"error": "Some error occurred", "error_type": "TestError"} mock_request.return_value = mock_response diff --git a/test/core/pipeline/test_draw.py b/test/core/pipeline/test_draw.py index 5234e6d553..d9faee9c96 100644 --- a/test/core/pipeline/test_draw.py +++ b/test/core/pipeline/test_draw.py @@ -4,8 +4,8 @@ from unittest.mock import MagicMock, patch +import httpx import pytest -import requests from haystack.core.errors import PipelineDrawingError from haystack.core.pipeline import Pipeline @@ -26,31 +26,31 @@ def test_to_mermaid_image(): assert image_data -@patch("haystack.core.pipeline.draw.requests") -def test_to_mermaid_image_does_not_edit_graph(mock_requests): +@patch("haystack.core.pipeline.draw.httpx") +def test_to_mermaid_image_does_not_edit_graph(mock_httpx): pipe = Pipeline() pipe.add_component("comp1", AddFixedValue(add=3)) pipe.add_component("comp2", Double()) pipe.connect("comp1.result", "comp2.value") pipe.connect("comp2.value", "comp1.value") - mock_requests.get.return_value = MagicMock(status_code=200) + mock_httpx.get.return_value = MagicMock(status_code=200) expected_pipe = pipe.to_dict() _to_mermaid_image(pipe.graph) assert expected_pipe == pipe.to_dict() -@patch("haystack.core.pipeline.draw.requests") -def test_to_mermaid_image_applies_timeout(mock_requests): +@patch("haystack.core.pipeline.draw.httpx") +def test_to_mermaid_image_applies_timeout(mock_httpx): pipe = Pipeline() pipe.add_component("comp1", Double()) pipe.add_component("comp2", Double()) pipe.connect("comp1", "comp2") - mock_requests.get.return_value = MagicMock(status_code=200) + mock_httpx.get.return_value = MagicMock(status_code=200) _to_mermaid_image(pipe.graph, timeout=1) - assert mock_requests.get.call_args[1]["timeout"] == 1 + assert mock_httpx.get.call_args[1]["timeout"] == 1 def test_to_mermaid_image_failing_request(tmp_path): @@ -60,10 +60,10 @@ def test_to_mermaid_image_failing_request(tmp_path): pipe.connect("comp1", "comp2") pipe.connect("comp2", "comp1") - with patch("haystack.core.pipeline.draw.requests.get") as mock_get: + with patch("haystack.core.pipeline.draw.httpx.get") as mock_get: def raise_for_status(self): - raise requests.HTTPError() + raise httpx.HTTPError("error") mock_response = MagicMock() mock_response.status_code = 429 @@ -167,7 +167,7 @@ def test_to_mermaid_image_scale_without_dimensions(): _to_mermaid_image(pipe.graph, params={"format": "img", "scale": 2}) -@patch("haystack.core.pipeline.draw.requests.get") +@patch("haystack.core.pipeline.draw.httpx.get") def test_to_mermaid_image_server_error(mock_get): # Test server failure pipe = Pipeline() @@ -176,7 +176,7 @@ def test_to_mermaid_image_server_error(mock_get): pipe.connect("comp1", "comp2") def raise_for_status(self): - raise requests.HTTPError() + raise httpx.HTTPError("error") mock_response = MagicMock() mock_response.status_code = 500 diff --git a/test/utils/test_callable_serialization.py b/test/utils/test_callable_serialization.py index ceac6358bb..c39acafe65 100644 --- a/test/utils/test_callable_serialization.py +++ b/test/utils/test_callable_serialization.py @@ -2,8 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 +import httpx import pytest -import requests from haystack.components.generators.utils import print_streaming_chunk from haystack.core.errors import DeserializationError, SerializationError @@ -39,8 +39,8 @@ def test_callable_serialization_non_local(): assert result == "haystack.components.generators.utils.print_streaming_chunk" # check serialization of another library's callable - result = serialize_callable(requests.api.get) - assert result == "requests.api.get" + result = serialize_callable(httpx.get) + assert result == "httpx.get" def test_fully_qualified_import_deserialization(): @@ -79,9 +79,9 @@ def test_callable_deserialization(): def test_callable_deserialization_non_local(): - result = serialize_callable(requests.api.get) + result = serialize_callable(httpx.get) fn = deserialize_callable(result) - assert fn is requests.api.get + assert fn is httpx.get def test_classmethod_serialization_deserialization(): diff --git a/test/utils/test_requests_utils.py b/test/utils/test_requests_utils.py index 32b23cb706..fd6de9b302 100644 --- a/test/utils/test_requests_utils.py +++ b/test/utils/test_requests_utils.py @@ -6,19 +6,10 @@ import httpx import pytest -import requests from haystack.utils.requests_utils import async_request_with_retry, request_with_retry -@pytest.fixture -def mock_requests_response(): - response = MagicMock(spec=requests.Response) - response.status_code = 200 - response.raise_for_status.return_value = None - return response - - @pytest.fixture def mock_httpx_response(): response = MagicMock(spec=httpx.Response) @@ -28,54 +19,54 @@ def mock_httpx_response(): class TestRequestWithRetry: - def test_request_with_retry_success(self, mock_requests_response): + def test_request_with_retry_success(self, mock_httpx_response): """Test that request_with_retry works with default parameters""" - with patch("requests.request", return_value=mock_requests_response) as mock_request: + with patch("httpx.Client.request", return_value=mock_httpx_response) as mock_request: response = request_with_retry(method="GET", url="https://example.com") - assert response == mock_requests_response + assert response == mock_httpx_response mock_request.assert_called_once_with(method="GET", url="https://example.com", timeout=10) - def test_request_with_retry_custom_attempts(self, mock_requests_response): + def test_request_with_retry_custom_attempts(self, mock_httpx_response): """Test that request_with_retry respects custom attempts parameter""" - with patch("requests.request", return_value=mock_requests_response) as mock_request: + with patch("httpx.Client.request", return_value=mock_httpx_response) as mock_request: response = request_with_retry(method="GET", url="https://example.com", attempts=5) - assert response == mock_requests_response + assert response == mock_httpx_response mock_request.assert_called_once_with(method="GET", url="https://example.com", timeout=10) - def test_request_with_retry_custom_status_codes(self, mock_requests_response): + def test_request_with_retry_custom_status_codes(self, mock_httpx_response): """Test that request_with_retry respects custom status_codes_to_retry parameter""" - with patch("requests.request", return_value=mock_requests_response) as mock_request: + with patch("httpx.Client.request", return_value=mock_httpx_response) as mock_request: response = request_with_retry(method="GET", url="https://example.com", status_codes_to_retry=[500, 502]) - assert response == mock_requests_response + assert response == mock_httpx_response mock_request.assert_called_once_with(method="GET", url="https://example.com", timeout=10) - def test_request_with_retry_custom_timeout(self, mock_requests_response): + def test_request_with_retry_custom_timeout(self, mock_httpx_response): """Test that request_with_retry respects custom timeout parameter""" - with patch("requests.request", return_value=mock_requests_response) as mock_request: + with patch("httpx.Client.request", return_value=mock_httpx_response) as mock_request: response = request_with_retry(method="GET", url="https://example.com", timeout=30) - assert response == mock_requests_response + assert response == mock_httpx_response mock_request.assert_called_once_with(method="GET", url="https://example.com", timeout=30) - def test_request_with_retry_with_headers(self, mock_requests_response): + def test_request_with_retry_with_headers(self, mock_httpx_response): """Test that request_with_retry passes headers correctly""" headers = {"Authorization": "Bearer token123"} - with patch("requests.request", return_value=mock_requests_response) as mock_request: + with patch("httpx.Client.request", return_value=mock_httpx_response) as mock_request: response = request_with_retry(method="GET", url="https://example.com", headers=headers) - assert response == mock_requests_response + assert response == mock_httpx_response mock_request.assert_called_once_with(method="GET", url="https://example.com", headers=headers, timeout=10) - def test_request_with_retry_with_json(self, mock_requests_response): + def test_request_with_retry_with_json(self, mock_httpx_response): """Test that request_with_retry passes JSON data correctly""" json_data = {"key": "value"} - with patch("requests.request", return_value=mock_requests_response) as mock_request: + with patch("httpx.Client.request", return_value=mock_httpx_response) as mock_request: response = request_with_retry(method="POST", url="https://example.com", json=json_data) - assert response == mock_requests_response + assert response == mock_httpx_response mock_request.assert_called_once_with(method="POST", url="https://example.com", json=json_data, timeout=10) def test_request_with_retry_retries_on_error(self): @@ -84,15 +75,14 @@ def test_request_with_retry_retries_on_error(self): # Mock time.sleep used by tenacity to keep this test fast mock_sleep.return_value = None - error_response = requests.Response() - error_response.status_code = 503 - - success_response = requests.Response() - success_response.status_code = 200 + success_response = httpx.Response(status_code=200, request=httpx.Request("GET", "https://example.com")) - with patch("requests.request") as mock_request: + with patch("httpx.Client.request") as mock_request: # First call raises an error, second call succeeds - mock_request.side_effect = [requests.exceptions.HTTPError("Server error"), success_response] + mock_request.side_effect = [ + httpx.RequestError("Server error", request=httpx.Request("GET", "https://example.com")), + success_response, + ] response = request_with_retry(method="GET", url="https://example.com", attempts=2) @@ -106,20 +96,20 @@ def test_request_with_retry_retries_on_status_code(self): # Mock time.sleep used by tenacity to keep this test fast mock_sleep.return_value = None - error_response = requests.Response() - error_response.status_code = 503 + error_response = httpx.Response(status_code=503, request=httpx.Request("GET", "https://example.com")) def raise_for_status(): if error_response.status_code in [503]: - raise requests.exceptions.HTTPError("Service Unavailable") + raise httpx.HTTPStatusError( + "Service Unavailable", request=error_response.request, response=error_response + ) error_response.raise_for_status = raise_for_status - success_response = requests.Response() - success_response.status_code = 200 + success_response = httpx.Response(status_code=200, request=httpx.Request("GET", "https://example.com")) success_response.raise_for_status = lambda: None - with patch("requests.request") as mock_request: + with patch("httpx.Client.request") as mock_request: # First call returns error status code, second call succeeds mock_request.side_effect = [error_response, success_response]