From 1b7454feb6da179925e82bdc7ad4c5e762ce3f9b Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:22:43 +0000 Subject: [PATCH 1/4] fix(paginator): raise TypeError on non-list response in JsonListPaginator Previously, `JsonListPaginator` and `AsyncJsonListPaginator` would silently ignore non-list responses (like dictionaries or None) and return an empty iterator. This behavior could hide API errors or unexpected responses, leading to "silent failures". This commit enforces strict type checking: if the API response is not a list, a `TypeError` is raised. This aligns with the "Fail Fast" philosophy and Shield's reliability goals. Robustness tests have been added in `tests/unit/test_json_list_paginator_robustness.py`. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- imednet/core/paginator.py | 17 ++--- .../test_json_list_paginator_robustness.py | 71 +++++++++++++++++++ 2 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 tests/unit/test_json_list_paginator_robustness.py diff --git a/imednet/core/paginator.py b/imednet/core/paginator.py index 00956e67..cefbd9bd 100644 --- a/imednet/core/paginator.py +++ b/imednet/core/paginator.py @@ -105,10 +105,9 @@ def _iter_sync(self) -> Iterator[Any]: # Raw list endpoints do not support pagination params response: httpx.Response = client.get(self.path, params=self.params) payload = response.json() - if isinstance(payload, list): - yield from payload - else: - yield from [] + if not isinstance(payload, list): + raise TypeError(f"API response must be a list, got {type(payload).__name__}") + yield from payload class AsyncJsonListPaginator(AsyncPaginator): @@ -119,9 +118,7 @@ async def _iter_async(self) -> AsyncIterator[Any]: # Raw list endpoints do not support pagination params response: httpx.Response = await client.get(self.path, params=self.params) payload = response.json() - if isinstance(payload, list): - for item in payload: - yield item - else: - # Fallback for empty or malformed response - pass + if not isinstance(payload, list): + raise TypeError(f"API response must be a list, got {type(payload).__name__}") + for item in payload: + yield item diff --git a/tests/unit/test_json_list_paginator_robustness.py b/tests/unit/test_json_list_paginator_robustness.py new file mode 100644 index 00000000..259d79f3 --- /dev/null +++ b/tests/unit/test_json_list_paginator_robustness.py @@ -0,0 +1,71 @@ +from unittest.mock import Mock, AsyncMock + +import pytest + +from imednet.core.paginator import AsyncJsonListPaginator, JsonListPaginator + + +class MockClient: + def __init__(self, response_data): + self.response_data = response_data + + def get(self, path, params=None): + response = Mock() + response.json.return_value = self.response_data + return response + + +class MockAsyncClient: + def __init__(self, response_data): + self.response_data = response_data + + async def get(self, path, params=None): + response = Mock() + response.json.return_value = self.response_data + return response + + +def test_json_list_paginator_raises_on_dict(): + """ + Test that JsonListPaginator raises TypeError when the API returns a dictionary + instead of the expected list. + """ + # Simulate an error response or unexpected object structure + client = MockClient({"error": "Something went wrong", "details": "Unexpected format"}) + paginator = JsonListPaginator(client, "/path") + + # Currently this fails (returns empty list), we want it to raise TypeError + with pytest.raises(TypeError, match="API response must be a list"): + list(paginator) + + +def test_json_list_paginator_raises_on_none(): + """Test that JsonListPaginator raises TypeError when the API returns null.""" + client = MockClient(None) + paginator = JsonListPaginator(client, "/path") + + with pytest.raises(TypeError, match="API response must be a list"): + list(paginator) + + +@pytest.mark.asyncio +async def test_async_json_list_paginator_raises_on_dict(): + """ + Test that AsyncJsonListPaginator raises TypeError when the API returns a dictionary + instead of the expected list. + """ + client = MockAsyncClient({"error": "Async error"}) + paginator = AsyncJsonListPaginator(client, "/path") # type: ignore + + with pytest.raises(TypeError, match="API response must be a list"): + items = [item async for item in paginator] + + +@pytest.mark.asyncio +async def test_async_json_list_paginator_raises_on_none(): + """Test that AsyncJsonListPaginator raises TypeError when the API returns null.""" + client = MockAsyncClient(None) + paginator = AsyncJsonListPaginator(client, "/path") # type: ignore + + with pytest.raises(TypeError, match="API response must be a list"): + items = [item async for item in paginator] From 494029b63437429c75f004aeaf6c82c6e8e2669c Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:27:23 +0000 Subject: [PATCH 2/4] style: fix import sorting in test_json_list_paginator_robustness.py Reorders imports in `tests/unit/test_json_list_paginator_robustness.py` to comply with `isort` and project style guidelines. This fixes a CI failure in the Quality & Security job. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- tests/unit/test_json_list_paginator_robustness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_json_list_paginator_robustness.py b/tests/unit/test_json_list_paginator_robustness.py index 259d79f3..525cc939 100644 --- a/tests/unit/test_json_list_paginator_robustness.py +++ b/tests/unit/test_json_list_paginator_robustness.py @@ -1,4 +1,4 @@ -from unittest.mock import Mock, AsyncMock +from unittest.mock import AsyncMock, Mock import pytest From 373165241ee7f75e005b1876c94932dfe202cb17 Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:32:04 +0000 Subject: [PATCH 3/4] style: fix unused imports and variables in test_json_list_paginator_robustness.py Removes unused `AsyncMock` import and unused `items` variable assignment in `tests/unit/test_json_list_paginator_robustness.py` to comply with `ruff` linting rules. This fixes a CI failure in the Quality & Security job. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- tests/unit/test_json_list_paginator_robustness.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_json_list_paginator_robustness.py b/tests/unit/test_json_list_paginator_robustness.py index 525cc939..44a7653c 100644 --- a/tests/unit/test_json_list_paginator_robustness.py +++ b/tests/unit/test_json_list_paginator_robustness.py @@ -1,4 +1,4 @@ -from unittest.mock import AsyncMock, Mock +from unittest.mock import Mock import pytest @@ -58,7 +58,7 @@ async def test_async_json_list_paginator_raises_on_dict(): paginator = AsyncJsonListPaginator(client, "/path") # type: ignore with pytest.raises(TypeError, match="API response must be a list"): - items = [item async for item in paginator] + [item async for item in paginator] @pytest.mark.asyncio @@ -68,4 +68,4 @@ async def test_async_json_list_paginator_raises_on_none(): paginator = AsyncJsonListPaginator(client, "/path") # type: ignore with pytest.raises(TypeError, match="API response must be a list"): - items = [item async for item in paginator] + [item async for item in paginator] From b1589f1b5b2647df47b3d9ed0f8aa0ac7f05d80b Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:39:41 +0000 Subject: [PATCH 4/4] chore: update werkzeug to 3.1.6 to fix CVE-2026-27199 Updates `werkzeug` dependency to version `^3.1.6` to resolve a known vulnerability detected by `pip-audit` in CI. This also updates `poetry.lock` to reflect the change. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- poetry.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index fc1d01fe..c6447985 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2759,14 +2759,14 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "werkzeug" -version = "3.1.5" +version = "3.1.6" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc"}, - {file = "werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67"}, + {file = "werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131"}, + {file = "werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25"}, ] [package.dependencies] @@ -2800,4 +2800,4 @@ sqlalchemy = ["SQLAlchemy"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "f566c8f13cf3eb0c80d496ed8ecf21f9cdd87e0ad0277fe67db4440f8ab6bb09" +content-hash = "9eb342134be043a99cd62fb62e7c27ef7631d148f4038092163da233d629525e" diff --git a/pyproject.toml b/pyproject.toml index 4d308f2e..8981519a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ faker = "^24.9" pre-commit = "^4.2.0" virtualenv = "^20.36.1" isort = "^6.0.1" -werkzeug = "^3.1.5" +werkzeug = "^3.1.6" sphinx = "^6.2.0" sphinx-autodoc-typehints = "*" sphinx-rtd-theme = "^3.0.2"