diff --git a/pyproject.toml b/pyproject.toml index 58c986a..1dffbc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,6 @@ requires-python = ">=3.11" dependencies = [ "agct~=0.1.0", - "requests", "biopython", "tqdm", "cdot", @@ -41,6 +40,7 @@ dependencies = [ "cool-seq-tool==0.4.0.dev3", "ga4gh.vrs==2.0.0-a6", "gene_normalizer[etl,pg]==0.3.0-dev2", + "httpx~=0.28", "pydantic>=2", "python-dotenv", "setuptools>=68.0", # tmp -- ensure 3.12 compatibility @@ -61,7 +61,7 @@ tests = [ "pytest-mock", "pytest-cov", "pytest-asyncio", - "requests-mock" + "respx" ] dev = [ "ruff==0.2.0", diff --git a/src/api/routers/map.py b/src/api/routers/map.py index 3f0e9aa..9235061 100644 --- a/src/api/routers/map.py +++ b/src/api/routers/map.py @@ -5,7 +5,7 @@ from cool_seq_tool.schemas import AnnotationLayer from fastapi import APIRouter, HTTPException from fastapi.responses import JSONResponse -from requests import HTTPError +from httpx import HTTPStatusError from dcd_mapping.align import build_alignment_result from dcd_mapping.annotate import ( @@ -64,6 +64,7 @@ async def map_scoreset(urn: str, store_path: Path | None = None) -> JSONResponse records = get_scoreset_records(metadata, True, store_path) metadata = patch_target_sequence_type(metadata, records, force=False) except ScoresetNotSupportedError as e: + _logger.error("Scoreset not supported for %s: %s", urn, e) return JSONResponse( content=ScoresetMapping( metadata=None, @@ -72,6 +73,7 @@ async def map_scoreset(urn: str, store_path: Path | None = None) -> JSONResponse ) except ResourceAcquisitionError as e: msg = f"Unable to acquire resource from MaveDB: {e}" + _logger.error(msg) raise HTTPException(status_code=500, detail=msg) from e if not records: @@ -87,17 +89,21 @@ async def map_scoreset(urn: str, store_path: Path | None = None) -> JSONResponse alignment_results = build_alignment_result(metadata, True) except BlatNotFoundError as e: msg = "BLAT command appears missing. Ensure it is available on the $PATH or use the environment variable BLAT_BIN_PATH to point to it. See instructions in the README prerequisites section for more." + _logger.error("BLAT not found for %s: %s", urn, e) raise HTTPException(status_code=500, detail=msg) from e except ResourceAcquisitionError as e: msg = f"BLAT resource could not be acquired: {e}" + _logger.error(msg) raise HTTPException(status_code=500, detail=msg) from e except AlignmentError as e: + _logger.error("Alignment error for %s: %s", urn, e) return JSONResponse( content=ScoresetMapping( metadata=metadata, error_message=str(e).strip("'") ).model_dump(exclude_none=True) ) except ScoresetNotSupportedError as e: + _logger.error("Scoreset not supported during alignment for %s: %s", urn, e) return JSONResponse( content=ScoresetMapping( metadata=metadata, error_message=str(e).strip("'") @@ -111,11 +117,13 @@ async def map_scoreset(urn: str, store_path: Path | None = None) -> JSONResponse # on the target level and on the variant level for variants relative to that target # HTTPErrors and DataLookupErrors cause the mapping process to exit because these indicate # underlying issues with data providers. - except HTTPError as e: + except HTTPStatusError as e: msg = f"HTTP error occurred during transcript selection: {e}" + _logger.error(msg) raise HTTPException(status_code=500, detail=msg) from e except DataLookupError as e: msg = f"Data lookup error occurred during transcript selection: {e}" + _logger.error(msg) raise HTTPException(status_code=500, detail=msg) from e vrs_results = {} @@ -134,6 +142,7 @@ async def map_scoreset(urn: str, store_path: Path | None = None) -> JSONResponse UnsupportedReferenceSequencePrefixError, MissingSequenceIdError, ) as e: + _logger.error("VRS mapping error for %s: %s", urn, e) return JSONResponse( content=ScoresetMapping( metadata=metadata, error_message=str(e).strip("'") @@ -172,6 +181,7 @@ async def map_scoreset(urn: str, store_path: Path | None = None) -> JSONResponse VrsVersion.V_2, ) except Exception as e: + _logger.error("Unexpected error during annotation for %s: %s", urn, e) return JSONResponse( content=ScoresetMapping( metadata=metadata, error_message=str(e).strip("'") @@ -287,6 +297,7 @@ async def map_scoreset(urn: str, store_path: Path | None = None) -> JSONResponse del reference_sequences[target_gene].layers[layer] except Exception as e: + _logger.error("Unexpected error during result assembly for %s: %s", urn, e) return JSONResponse( content=ScoresetMapping( metadata=metadata, error_message=str(e).strip("'") diff --git a/src/dcd_mapping/align.py b/src/dcd_mapping/align.py index 1d25546..5f08128 100644 --- a/src/dcd_mapping/align.py +++ b/src/dcd_mapping/align.py @@ -7,7 +7,7 @@ from pathlib import Path from urllib.parse import urlparse -import requests +import httpx from Bio.SearchIO import HSP from Bio.SearchIO import parse as parse_blat from Bio.SearchIO._model import Hit, QueryResult @@ -84,7 +84,7 @@ def get_ref_genome_file( if not genome_file.exists(): try: http_download(url, genome_file, silent) - except requests.HTTPError as e: + except httpx.HTTPStatusError as e: msg = f"HTTPError when fetching reference genome file from {url}" _logger.error(msg) raise ResourceAcquisitionError(msg) from e @@ -378,11 +378,11 @@ def fetch_alignment( alignment_results[accession_id] = None else: url = f"{CDOT_URL}/transcript/{accession_id}" - r = requests.get(url, timeout=30) + r = httpx.get(url, timeout=30) try: r.raise_for_status() - except requests.HTTPError as e: + except httpx.HTTPStatusError as e: msg = f"Received HTTPError from {url} for scoreset {metadata.urn}" _logger.error(msg) raise ResourceAcquisitionError(msg) from e diff --git a/src/dcd_mapping/lookup.py b/src/dcd_mapping/lookup.py index 08cf75b..17b9cc3 100644 --- a/src/dcd_mapping/lookup.py +++ b/src/dcd_mapping/lookup.py @@ -14,8 +14,8 @@ from typing import Any import hgvs +import httpx import polars as pl -import requests from biocommons.seqrepo import SeqRepo from biocommons.seqrepo.seqaliasdb.seqaliasdb import sqlite3 from cdot.hgvs.dataproviders import ChainedSeqFetcher, FastaSeqFetcher, RESTDataProvider @@ -682,7 +682,7 @@ def get_overlapping_features_for_region( url, headers={"Content-Type": "application/json"} ) response.raise_for_status() - except requests.RequestException as e: + except httpx.HTTPError as e: _logger.error( "Failed to fetch overlapping features for region %s-%s on chromosome %s: %s", start, @@ -715,7 +715,7 @@ def get_uniprot_sequence(uniprot_id: str) -> str | None: :raise HTTPError: if response comes with an HTTP error code """ url = f"https://www.ebi.ac.uk/proteins/api/proteins?accession={uniprot_id.split(':')[1]}&format=json" - response = requests.get(url, timeout=30) + response = httpx.get(url, timeout=30) response.raise_for_status() json = response.json() return json[0]["sequence"]["sequence"] diff --git a/src/dcd_mapping/main.py b/src/dcd_mapping/main.py index 500904c..d146623 100644 --- a/src/dcd_mapping/main.py +++ b/src/dcd_mapping/main.py @@ -6,7 +6,7 @@ from pathlib import Path import click -from requests import HTTPError +from httpx import HTTPStatusError from dcd_mapping.align import build_alignment_result from dcd_mapping.annotate import ( @@ -205,7 +205,7 @@ async def map_scoreset( # on the target level and on the variant level for variants relative to that target # HTTPErrors and DataLookupErrors cause the mapping process to exit because these indicate # underlying issues with data providers. - except HTTPError as e: + except HTTPStatusError as e: _emit_info( f"HTTP error occurred during transcript selection: {e}", silent, diff --git a/src/dcd_mapping/mavedb_data.py b/src/dcd_mapping/mavedb_data.py index 1dad2b2..6bf0ee4 100644 --- a/src/dcd_mapping/mavedb_data.py +++ b/src/dcd_mapping/mavedb_data.py @@ -13,7 +13,7 @@ from pathlib import Path from typing import Any -import requests +import httpx from fastapi import HTTPException from pydantic import ValidationError @@ -27,6 +27,7 @@ MAVEDB_BASE_URL, authentication_header, http_download, + is_missing_value, ) from dcd_mapping.schemas import ( ScoreRow, @@ -56,7 +57,7 @@ def get_scoreset_urns() -> set[str]: :return: set of URN strings """ - r = requests.get( + r = httpx.get( f"{MAVEDB_BASE_URL}/api/v1/experiments/", timeout=30, headers=authentication_header(), @@ -100,14 +101,14 @@ def get_human_urns() -> list[str]: scoreset_urns = get_scoreset_urns() human_scoresets: list[str] = [] for urn in scoreset_urns: - r = requests.get( + r = httpx.get( f"{MAVEDB_BASE_URL}/api/v1/score-sets/{urn}", timeout=30, headers=authentication_header(), ) try: r.raise_for_status() - except requests.exceptions.HTTPError: + except httpx.HTTPStatusError: _logger.info("Unable to retrieve scoreset data for URN %s", urn) continue data = r.json() @@ -155,10 +156,10 @@ def get_raw_scoreset_metadata( metadata_file = dcd_mapping_dir / f"{scoreset_urn}_metadata.json" if not metadata_file.exists(): url = f"{MAVEDB_BASE_URL}/api/v1/score-sets/{scoreset_urn}" - r = requests.get(url, timeout=30, headers=authentication_header()) + r = httpx.get(url, timeout=30, headers=authentication_header()) try: r.raise_for_status() - except requests.HTTPError as e: + except httpx.HTTPStatusError as e: msg = f"Received HTTPError from {url} for scoreset {scoreset_urn}" _logger.error(msg) raise ResourceAcquisitionError(msg) from e @@ -246,13 +247,13 @@ def _load_scoreset_records( with path.open() as csvfile: reader = csv.DictReader(csvfile) for row in reader: - if row["score"] == "NA": + if is_missing_value(row["score"]): row["score"] = None else: row["score"] = row["score"] - if row["hgvs_nt"] != "NA": + if not is_missing_value(row["hgvs_nt"]): prefix = row["hgvs_nt"].split(":")[0] if ":" in row["hgvs_nt"] else None - elif row["hgvs_pro"] != "NA": + elif not is_missing_value(row["hgvs_pro"]): prefix = ( row["hgvs_pro"].split(":")[0] if ":" in row["hgvs_pro"] else None ) @@ -317,7 +318,7 @@ def get_scoreset_records( url = f"{MAVEDB_BASE_URL}/api/v1/score-sets/{metadata.urn}/scores" try: http_download(url, scores_csv, silent) - except requests.HTTPError as e: + except httpx.HTTPStatusError as e: msg = f"HTTPError when fetching scores CSV from {url}" _logger.error(msg) raise ResourceAcquisitionError(msg) from e diff --git a/src/dcd_mapping/resource_utils.py b/src/dcd_mapping/resource_utils.py index ef29402..0ea9aa3 100644 --- a/src/dcd_mapping/resource_utils.py +++ b/src/dcd_mapping/resource_utils.py @@ -5,11 +5,30 @@ from pathlib import Path import click -import requests +import httpx from tqdm import tqdm _logger = logging.getLogger(__name__) +# Common representations of missing/null data in CSV files +MISSING_VALUE_REPRESENTATIONS = frozenset( + { + "NA", + "N/A", + "na", + "n/a", + "NaN", + "nan", + "null", + "NULL", + "None", + "none", + "", + "-", + ".", + } +) + MAVEDB_API_KEY = os.environ.get("MAVEDB_API_KEY") MAVEDB_BASE_URL = os.environ.get("MAVEDB_BASE_URL") ENSEMBL_API_URL = os.environ.get("ENSEMBL_API_URL", "https://rest.ensembl.org") # TODO @@ -24,6 +43,22 @@ LOCAL_STORE_PATH.mkdir(exist_ok=True, parents=True) +def is_missing_value(value: str | None) -> bool: + """Check if a value represents missing/null data. + + This function recognizes multiple common representations of missing data + that may appear in CSV files from external sources, making the codebase + more resilient to upstream changes in NA representation. + + :param value: The value to check + :return: True if the value represents missing data, False otherwise + """ + if value is None: + return True + # Strip whitespace and check against known missing value representations + return value.strip() in MISSING_VALUE_REPRESENTATIONS + + def authentication_header() -> dict | None: """Fetch with api key envvar, if available.""" return {"X-API-key": MAVEDB_API_KEY} if MAVEDB_API_KEY is not None else None @@ -36,13 +71,11 @@ def http_download(url: str, out_path: Path, silent: bool = True) -> Path: :param out_path: location to save file to :param silent: show TQDM progress bar if true :return: Path if download successful - :raise requests.HTTPError: if request is unsuccessful + :raise httpx.HTTPStatusError: if request is unsuccessful """ if not silent: click.echo(f"Downloading {out_path.name} to {out_path.parents[0].absolute()}") - with requests.get( - url, stream=True, timeout=60, headers=authentication_header() - ) as r: + with httpx.stream("GET", url, timeout=60, headers=authentication_header()) as r: r.raise_for_status() total_size = int(r.headers.get("content-length", 0)) with out_path.open("wb") as h: @@ -54,12 +87,12 @@ def http_download(url: str, out_path: Path, silent: bool = True) -> Path: desc=out_path.name, ncols=80, ) as progress_bar: - for chunk in r.iter_content(chunk_size=8192): + for chunk in r.iter_bytes(chunk_size=8192): if chunk: h.write(chunk) progress_bar.update(len(chunk)) else: - for chunk in r.iter_content(chunk_size=8192): + for chunk in r.iter_bytes(chunk_size=8192): if chunk: h.write(chunk) return out_path @@ -67,7 +100,7 @@ def http_download(url: str, out_path: Path, silent: bool = True) -> Path: def request_with_backoff( url: str, max_retries: int = 5, backoff_factor: float = 0.3, **kwargs -) -> requests.Response: +) -> httpx.Response: """HTTP GET with exponential backoff only for retryable errors. Retries on: @@ -80,9 +113,9 @@ def request_with_backoff( attempt = 0 while attempt < max_retries: try: - kwargs.setdefault("timeout", 60) # Default timeout of 10 seconds - response = requests.get(url, **kwargs) # noqa: S113 - except (requests.Timeout, requests.ConnectionError): + kwargs.setdefault("timeout", 60) + response = httpx.get(url, **kwargs) + except (httpx.TimeoutException, httpx.ConnectError): # Retry on transient network failures if attempt == max_retries - 1: raise diff --git a/src/dcd_mapping/version.py b/src/dcd_mapping/version.py index e6693f5..61f5468 100644 --- a/src/dcd_mapping/version.py +++ b/src/dcd_mapping/version.py @@ -1,3 +1,3 @@ """Provide dcd mapping version""" -dcd_mapping_version = "2026.1.0" +dcd_mapping_version = "2026.1.1" diff --git a/src/dcd_mapping/vrs_map.py b/src/dcd_mapping/vrs_map.py index ea2bf25..b36e043 100644 --- a/src/dcd_mapping/vrs_map.py +++ b/src/dcd_mapping/vrs_map.py @@ -32,7 +32,7 @@ get_seqrepo, translate_hgvs_to_vrs, ) -from dcd_mapping.resource_utils import request_with_backoff +from dcd_mapping.resource_utils import is_missing_value, request_with_backoff from dcd_mapping.schemas import ( AlignmentResult, MappedScore, @@ -378,7 +378,11 @@ def _map_protein_coding_pro( :param transcript: The transcript selection information for a score set :return: VRS mapping object if mapping succeeds """ - if row.hgvs_pro in {"_wt", "_sy", "NA"} or len(row.hgvs_pro) == 3: + if ( + row.hgvs_pro in {"_wt", "_sy"} + or is_missing_value(row.hgvs_pro) + or len(row.hgvs_pro) == 3 + ): _logger.warning( "Can't process variant syntax %s for %s", row.hgvs_pro, row.accession ) @@ -700,7 +704,7 @@ def _hgvs_nt_is_valid(hgvs_nt: str) -> bool: :return: True if expression appears populated and valid """ return ( - (hgvs_nt != "NA") + (not is_missing_value(hgvs_nt)) and (hgvs_nt not in {"_wt", "_sy", "="}) and (len(hgvs_nt) != 3) ) @@ -713,7 +717,8 @@ def _hgvs_pro_is_valid(hgvs_pro: str) -> bool: :return: True if expression appears populated and valid """ return ( - (hgvs_pro not in {"_wt", "_sy", "NA"}) + (hgvs_pro not in {"_wt", "_sy"}) + and (not is_missing_value(hgvs_pro)) and (len(hgvs_pro) != 3) and ("fs" not in hgvs_pro) ) diff --git a/tests/test_lookup.py b/tests/test_lookup.py index 52ef287..db2f470 100644 --- a/tests/test_lookup.py +++ b/tests/test_lookup.py @@ -2,7 +2,7 @@ from unittest.mock import patch -import requests +import httpx from dcd_mapping.lookup import get_overlapping_features_for_region @@ -95,7 +95,7 @@ def __init__(self): def raise_for_status(self): msg = f"HTTP {self.status_code} Error" - raise requests.RequestException(msg) + raise httpx.HTTPError(msg) with ( patch("dcd_mapping.lookup.request_with_backoff", return_value=ErrorResponse()), diff --git a/tests/test_mavedb_data.py b/tests/test_mavedb_data.py index 751d0c7..260d724 100644 --- a/tests/test_mavedb_data.py +++ b/tests/test_mavedb_data.py @@ -3,8 +3,9 @@ import shutil from pathlib import Path +import httpx import pytest -import requests_mock +import respx from dcd_mapping.mavedb_data import get_scoreset_metadata, get_scoreset_records @@ -32,10 +33,9 @@ def test_get_scoreset_metadata( resources_data_dir: Path, scoreset_metadata_response: dict ): urn = "urn:mavedb:00000093-a-1" - with requests_mock.Mocker() as m: - m.get( - f"https://api.mavedb.org/api/v1/score-sets/{urn}", - json=scoreset_metadata_response[urn], + with respx.mock: + respx.get(f"https://api.mavedb.org/api/v1/score-sets/{urn}").mock( + return_value=httpx.Response(200, json=scoreset_metadata_response[urn]) ) scoreset_metadata = get_scoreset_metadata( urn, dcd_mapping_dir=resources_data_dir @@ -62,17 +62,15 @@ def test_get_scoreset_records( urn = "urn:mavedb:00000093-a-1" with (fixture_data_dir / f"{urn}_scores.csv").open() as f: scores_csv_text = f.read() - with requests_mock.Mocker() as m: - m.get( - f"https://api.mavedb.org/api/v1/score-sets/{urn}", - json=scoreset_metadata_response[urn], + with respx.mock: + respx.get(f"https://api.mavedb.org/api/v1/score-sets/{urn}").mock( + return_value=httpx.Response(200, json=scoreset_metadata_response[urn]) ) scoreset_metadata = get_scoreset_metadata( urn, dcd_mapping_dir=resources_data_dir ) - m.get( - f"https://api.mavedb.org/api/v1/score-sets/{urn}/scores", - text=scores_csv_text, + respx.get(f"https://api.mavedb.org/api/v1/score-sets/{urn}/scores").mock( + return_value=httpx.Response(200, text=scores_csv_text) ) scoreset_records = get_scoreset_records( scoreset_metadata, dcd_mapping_dir=resources_data_dir diff --git a/tests/test_resource_utils.py b/tests/test_resource_utils.py index 6c6055a..355c1eb 100644 --- a/tests/test_resource_utils.py +++ b/tests/test_resource_utils.py @@ -3,8 +3,8 @@ from contextlib import ExitStack from unittest import mock +import httpx import pytest -import requests from dcd_mapping.resource_utils import request_with_backoff @@ -16,8 +16,10 @@ def __init__(self, status_code=200, headers=None): def raise_for_status(self): if not (200 <= self.status_code < 300): - msg = f"HTTP {self.status_code}" - raise requests.HTTPError(msg) + request = httpx.Request("GET", "http://example.com") + response = httpx.Response(self.status_code, request=request) + msg = f"HTTP {self.status_code} Error" + raise httpx.HTTPStatusError(msg, request=request, response=response) def _sequence_side_effect(values): @@ -36,7 +38,7 @@ def _next(*args, **kwargs): # noqa: ANN002 def test_success_200_returns_response(): dummy = _DummyResponse(200) with mock.patch( - "dcd_mapping.resource_utils.requests.get", return_value=dummy + "dcd_mapping.resource_utils.httpx.get", return_value=dummy ) as get_mock: resp = request_with_backoff("http://example.com/resource") assert resp is dummy @@ -44,12 +46,12 @@ def test_success_200_returns_response(): def test_timeout_then_success_retries_with_backoff(): - first_exc = requests.Timeout("timeout") + first_exc = httpx.TimeoutException("timeout") second_resp = _DummyResponse(200) with ExitStack() as stack: get_mock = stack.enter_context( mock.patch( - "dcd_mapping.resource_utils.requests.get", + "dcd_mapping.resource_utils.httpx.get", side_effect=_sequence_side_effect([first_exc, second_resp]), ) ) @@ -64,18 +66,18 @@ def test_timeout_then_success_retries_with_backoff(): def test_connection_error_until_max_raises(): - seq = [requests.ConnectionError("conn err")] * 5 + seq = [httpx.ConnectError("conn err")] * 5 with ExitStack() as stack: stack.enter_context( mock.patch( - "dcd_mapping.resource_utils.requests.get", + "dcd_mapping.resource_utils.httpx.get", side_effect=_sequence_side_effect(seq), ) ) sleep_mock = stack.enter_context( mock.patch("dcd_mapping.resource_utils.time.sleep") ) - with pytest.raises(requests.ConnectionError): + with pytest.raises(httpx.ConnectError): request_with_backoff( "http://example.com/resource", max_retries=5, backoff_factor=0.1 ) @@ -90,7 +92,7 @@ def test_5xx_then_success_retries(): with ExitStack() as stack: get_mock = stack.enter_context( mock.patch( - "dcd_mapping.resource_utils.requests.get", + "dcd_mapping.resource_utils.httpx.get", side_effect=_sequence_side_effect(seq), ) ) @@ -108,14 +110,14 @@ def test_5xx_until_max_raises_http_error(): with ExitStack() as stack: stack.enter_context( mock.patch( - "dcd_mapping.resource_utils.requests.get", + "dcd_mapping.resource_utils.httpx.get", side_effect=_sequence_side_effect(seq), ) ) sleep_mock = stack.enter_context( mock.patch("dcd_mapping.resource_utils.time.sleep") ) - with pytest.raises(requests.HTTPError): + with pytest.raises(httpx.HTTPStatusError): request_with_backoff( "http://example.com/resource", max_retries=3, backoff_factor=0.1 ) @@ -129,7 +131,7 @@ def test_429_with_retry_after_header_respected(): with ExitStack() as stack: get_mock = stack.enter_context( mock.patch( - "dcd_mapping.resource_utils.requests.get", + "dcd_mapping.resource_utils.httpx.get", side_effect=_sequence_side_effect([resp1, resp2]), ) ) @@ -148,7 +150,7 @@ def test_429_with_bad_retry_after_falls_back_to_backoff(): with ExitStack() as stack: stack.enter_context( mock.patch( - "dcd_mapping.resource_utils.requests.get", + "dcd_mapping.resource_utils.httpx.get", side_effect=_sequence_side_effect([resp1, resp2]), ) ) @@ -164,12 +166,12 @@ def test_non_retryable_4xx_raises_immediately(): resp = _DummyResponse(404) with ExitStack() as stack: stack.enter_context( - mock.patch("dcd_mapping.resource_utils.requests.get", return_value=resp) + mock.patch("dcd_mapping.resource_utils.httpx.get", return_value=resp) ) sleep_mock = stack.enter_context( mock.patch("dcd_mapping.resource_utils.time.sleep") ) - with pytest.raises(requests.HTTPError): + with pytest.raises(httpx.HTTPStatusError): request_with_backoff("http://example.com/resource") # no sleeps for non-retryable errors sleep_mock.assert_not_called() @@ -179,7 +181,7 @@ def test_exhausted_retries_without_response_raises_request_exception(): # The only way to trigger the terminal state in the function is to not even # attempt a request (max_retries=0) with mock.patch( - "dcd_mapping.resource_utils.requests.get", return_value=_DummyResponse(500) + "dcd_mapping.resource_utils.httpx.get", return_value=_DummyResponse(500) ), mock.patch("dcd_mapping.resource_utils.time.sleep"), pytest.raises( Exception # noqa: PT011 ) as exc: @@ -190,7 +192,7 @@ def test_exhausted_retries_without_response_raises_request_exception(): def test_kwargs_are_passed_through_to_requests_get(): dummy = _DummyResponse(200) with mock.patch( - "dcd_mapping.resource_utils.requests.get", return_value=dummy + "dcd_mapping.resource_utils.httpx.get", return_value=dummy ) as get_mock: request_with_backoff( "http://example.com/resource", headers={"X-Test": "1"}, params={"q": "x"}