From 2055d582b323d9ccaaff63350d30cec065f09608 Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:33:23 +0000 Subject: [PATCH 1/3] Refactor SubjectsEndpoint to DRY up filtering logic Collapsed duplicated site filtering logic in `SubjectsEndpoint` into a shared `_filter_by_site` method. This improves maintainability and ensures consistent behavior between synchronous and asynchronous operations. Verified with new unit tests covering both execution paths. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- imednet/endpoints/subjects.py | 12 ++-- .../unit/endpoints/test_subjects_filtering.py | 60 +++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 tests/unit/endpoints/test_subjects_filtering.py diff --git a/imednet/endpoints/subjects.py b/imednet/endpoints/subjects.py index 09975f91..0dd35cec 100644 --- a/imednet/endpoints/subjects.py +++ b/imednet/endpoints/subjects.py @@ -17,6 +17,11 @@ class SubjectsEndpoint(ListGetEndpoint[Subject]): MODEL = Subject _id_param = "subjectKey" + def _filter_by_site(self, subjects: List[Subject], site_id: str | int) -> List[Subject]: + # TUI Logic: Strict string comparison to handle int/str mismatch + target_site = str(site_id) + return [s for s in subjects if str(s.site_id) == target_site] + def list_by_site(self, study_key: str, site_id: str | int) -> List[Subject]: """ List subjects filtered by a specific site ID. @@ -24,12 +29,9 @@ def list_by_site(self, study_key: str, site_id: str | int) -> List[Subject]: Migrated from TUI logic to core SDK to support filtering. """ all_subjects = self.list(study_key) - # TUI Logic: Strict string comparison to handle int/str mismatch - target_site = str(site_id) - return [s for s in all_subjects if str(s.site_id) == target_site] + return self._filter_by_site(all_subjects, site_id) async def async_list_by_site(self, study_key: str, site_id: str | int) -> List[Subject]: """Asynchronously list subjects filtered by a specific site ID.""" all_subjects = await self.async_list(study_key) - target_site = str(site_id) - return [s for s in all_subjects if str(s.site_id) == target_site] + return self._filter_by_site(all_subjects, site_id) diff --git a/tests/unit/endpoints/test_subjects_filtering.py b/tests/unit/endpoints/test_subjects_filtering.py new file mode 100644 index 00000000..6a0ee1b4 --- /dev/null +++ b/tests/unit/endpoints/test_subjects_filtering.py @@ -0,0 +1,60 @@ +import pytest +from unittest.mock import Mock, AsyncMock +from typing import List +from imednet.endpoints.subjects import SubjectsEndpoint +from imednet.models.subjects import Subject + +@pytest.fixture +def subject_list(): + return [ + Subject(studyKey="sk", subjectId=1, siteId=101, subjectKey="s1"), + Subject(studyKey="sk", subjectId=2, siteId=102, subjectKey="s2"), + Subject(studyKey="sk", subjectId=3, siteId=101, subjectKey="s3"), + Subject(studyKey="sk", subjectId=4, siteId="101", subjectKey="s4"), # String siteId + Subject(studyKey="sk", subjectId=5, siteId="103", subjectKey="s5"), + ] + +def test_list_by_site_filtering(subject_list): + # Mock client and context as they are required by __init__ but not used if we mock list + mock_client = Mock() + mock_ctx = Mock() + + endpoint = SubjectsEndpoint(mock_client, mock_ctx) + endpoint.list = Mock(return_value=subject_list) + + # Act + filtered_int = endpoint.list_by_site("sk", 101) + filtered_str = endpoint.list_by_site("sk", "101") + filtered_mismatch = endpoint.list_by_site("sk", 999) + + # Assert + assert len(filtered_int) == 3 + assert {s.subject_id for s in filtered_int} == {1, 3, 4} + + assert len(filtered_str) == 3 + assert {s.subject_id for s in filtered_str} == {1, 3, 4} + + assert len(filtered_mismatch) == 0 + +@pytest.mark.asyncio +async def test_async_list_by_site_filtering(subject_list): + # Mock client and context + mock_client = Mock() + mock_ctx = Mock() + + endpoint = SubjectsEndpoint(mock_client, mock_ctx) + endpoint.async_list = AsyncMock(return_value=subject_list) + + # Act + filtered_int = await endpoint.async_list_by_site("sk", 101) + filtered_str = await endpoint.async_list_by_site("sk", "101") + filtered_mismatch = await endpoint.async_list_by_site("sk", 999) + + # Assert + assert len(filtered_int) == 3 + assert {s.subject_id for s in filtered_int} == {1, 3, 4} + + assert len(filtered_str) == 3 + assert {s.subject_id for s in filtered_str} == {1, 3, 4} + + assert len(filtered_mismatch) == 0 From b98503acfbf4d59f282f1e68a4d5d3277982f00e Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:38:28 +0000 Subject: [PATCH 2/3] Refactor SubjectsEndpoint to DRY up filtering logic Collapsed duplicated site filtering logic in `SubjectsEndpoint` into a shared `_filter_by_site` method. This improves maintainability and ensures consistent behavior between synchronous and asynchronous operations. Verified with new unit tests covering both execution paths. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- tests/unit/endpoints/test_subjects_filtering.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/unit/endpoints/test_subjects_filtering.py b/tests/unit/endpoints/test_subjects_filtering.py index 6a0ee1b4..4993d45f 100644 --- a/tests/unit/endpoints/test_subjects_filtering.py +++ b/tests/unit/endpoints/test_subjects_filtering.py @@ -1,19 +1,23 @@ -import pytest -from unittest.mock import Mock, AsyncMock from typing import List +from unittest.mock import AsyncMock, Mock + +import pytest + from imednet.endpoints.subjects import SubjectsEndpoint from imednet.models.subjects import Subject + @pytest.fixture def subject_list(): return [ Subject(studyKey="sk", subjectId=1, siteId=101, subjectKey="s1"), Subject(studyKey="sk", subjectId=2, siteId=102, subjectKey="s2"), Subject(studyKey="sk", subjectId=3, siteId=101, subjectKey="s3"), - Subject(studyKey="sk", subjectId=4, siteId="101", subjectKey="s4"), # String siteId + Subject(studyKey="sk", subjectId=4, siteId="101", subjectKey="s4"), # String siteId Subject(studyKey="sk", subjectId=5, siteId="103", subjectKey="s5"), ] + def test_list_by_site_filtering(subject_list): # Mock client and context as they are required by __init__ but not used if we mock list mock_client = Mock() @@ -36,6 +40,7 @@ def test_list_by_site_filtering(subject_list): assert len(filtered_mismatch) == 0 + @pytest.mark.asyncio async def test_async_list_by_site_filtering(subject_list): # Mock client and context From a8e3ea13bc4418991aa7773c33cb0876964c5dc1 Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:43:49 +0000 Subject: [PATCH 3/3] Refactor SubjectsEndpoint to DRY up filtering logic Collapsed duplicated site filtering logic in `SubjectsEndpoint` into a shared `_filter_by_site` method. This improves maintainability and ensures consistent behavior between synchronous and asynchronous operations. Verified with new unit tests covering both execution paths and adhered to code quality standards (formatting/linting). Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- tests/unit/endpoints/test_subjects_filtering.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/endpoints/test_subjects_filtering.py b/tests/unit/endpoints/test_subjects_filtering.py index 4993d45f..1c98b3f1 100644 --- a/tests/unit/endpoints/test_subjects_filtering.py +++ b/tests/unit/endpoints/test_subjects_filtering.py @@ -1,4 +1,3 @@ -from typing import List from unittest.mock import AsyncMock, Mock import pytest