From ecbcf17f736fb4ee54c34a95341ed8690fd5d07b Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:29:40 +0000 Subject: [PATCH 1/3] Refactor JobsEndpoint and mixins to use ListEndpoint - Extracted `ListEndpoint` from `ListGetEndpoint` in `imednet/core/endpoint/mixins.py` to allow endpoints to reuse list logic without get logic. - Updated `ListGetEndpoint` to inherit from `ListEndpoint`. - Updated `JobsEndpoint` to inherit from `ListEndpoint` and `PathGetEndpointMixin`, removing redundant `list` and `async_list` implementations. - Updated `tests/conftest.py` to correctly patch `ListEndpoint.PAGINATOR_CLS` and `ASYNC_PAGINATOR_CLS` to fix tests relying on `paginator_factory`. - Verified changes with `reproduce_jobs.py` and existing test suite. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- imednet/core/endpoint/mixins.py | 31 +++++++++++++--------- imednet/endpoints/jobs.py | 46 +++------------------------------ tests/conftest.py | 8 ++++++ 3 files changed, 31 insertions(+), 54 deletions(-) diff --git a/imednet/core/endpoint/mixins.py b/imednet/core/endpoint/mixins.py index f51dc7b4..494ceaef 100644 --- a/imednet/core/endpoint/mixins.py +++ b/imednet/core/endpoint/mixins.py @@ -404,20 +404,35 @@ class ListGetEndpointMixin(ListEndpointMixin[T], FilterGetEndpointMixin[T]): pass -class ListGetEndpoint(BaseEndpoint, ListGetEndpointMixin[T]): - """Endpoint base class implementing ``list`` and ``get`` helpers.""" +class ListEndpoint(BaseEndpoint, ListEndpointMixin[T]): + """Endpoint base class implementing ``list`` helpers.""" + + PAGINATOR_CLS: type[Paginator] = Paginator + ASYNC_PAGINATOR_CLS: type[AsyncPaginator] = AsyncPaginator def _get_context( self, is_async: bool ) -> tuple[RequestorProtocol | AsyncRequestorProtocol, type[Paginator] | type[AsyncPaginator]]: if is_async: - return self._require_async_client(), AsyncPaginator - return self._client, Paginator + return self._require_async_client(), self.ASYNC_PAGINATOR_CLS + return self._client, self.PAGINATOR_CLS def _list_common(self, is_async: bool, **kwargs: Any) -> List[T] | Awaitable[List[T]]: client, paginator = self._get_context(is_async) return self._list_impl(client, paginator, **kwargs) + def list(self, study_key: Optional[str] = None, **filters: Any) -> List[T]: + return cast(List[T], self._list_common(False, study_key=study_key, **filters)) + + async def async_list(self, study_key: Optional[str] = None, **filters: Any) -> List[T]: + return await cast( + Awaitable[List[T]], self._list_common(True, study_key=study_key, **filters) + ) + + +class ListGetEndpoint(ListEndpoint[T], FilterGetEndpointMixin[T]): + """Endpoint base class implementing ``list`` and ``get`` helpers.""" + def _get_common( self, is_async: bool, @@ -428,14 +443,6 @@ def _get_common( client, paginator = self._get_context(is_async) return self._get_impl(client, paginator, study_key=study_key, item_id=item_id) - def list(self, study_key: Optional[str] = None, **filters: Any) -> List[T]: - return cast(List[T], self._list_common(False, study_key=study_key, **filters)) - - async def async_list(self, study_key: Optional[str] = None, **filters: Any) -> List[T]: - return await cast( - Awaitable[List[T]], self._list_common(True, study_key=study_key, **filters) - ) - def get(self, study_key: Optional[str], item_id: Any) -> T: return cast(T, self._get_common(False, study_key=study_key, item_id=item_id)) diff --git a/imednet/endpoints/jobs.py b/imednet/endpoints/jobs.py index 0078d9ac..08732827 100644 --- a/imednet/endpoints/jobs.py +++ b/imednet/endpoints/jobs.py @@ -2,13 +2,12 @@ from typing import Any, Awaitable, List, Optional, cast -from imednet.core.endpoint.base import BaseEndpoint -from imednet.core.endpoint.mixins import ListEndpointMixin, PathGetEndpointMixin +from imednet.core.endpoint.mixins import ListEndpoint, PathGetEndpointMixin from imednet.core.paginator import AsyncJsonListPaginator, JsonListPaginator from imednet.models.jobs import JobStatus -class JobsEndpoint(BaseEndpoint, ListEndpointMixin[JobStatus], PathGetEndpointMixin[JobStatus]): +class JobsEndpoint(ListEndpoint[JobStatus], PathGetEndpointMixin[JobStatus]): """ API endpoint for retrieving status and details of jobs in an iMedNet study. @@ -18,6 +17,8 @@ class JobsEndpoint(BaseEndpoint, ListEndpointMixin[JobStatus], PathGetEndpointMi PATH = "jobs" MODEL = JobStatus + PAGINATOR_CLS = JsonListPaginator + ASYNC_PAGINATOR_CLS = AsyncJsonListPaginator def _raise_not_found(self, study_key: Optional[str], item_id: Any) -> None: raise ValueError(f"Job {item_id} not found in study {study_key}") @@ -66,42 +67,3 @@ async def async_get(self, study_key: str, batch_id: str) -> JobStatus: Awaitable[JobStatus], self._get_impl_path(client, study_key=study_key, item_id=batch_id, is_async=True), ) - - def list(self, study_key: str) -> List[JobStatus]: - """ - List all jobs for a specific study. - - Args: - study_key: Study identifier - - Returns: - List of JobStatus objects - """ - return cast( - List[JobStatus], - self._list_impl( - self._client, - JsonListPaginator, - study_key=study_key, - ), - ) - - async def async_list(self, study_key: str) -> List[JobStatus]: - """ - Asynchronously list all jobs for a specific study. - - Args: - study_key: Study identifier - - Returns: - List of JobStatus objects - """ - client = self._require_async_client() - return await cast( - Awaitable[List[JobStatus]], - self._list_impl( - client, - AsyncJsonListPaginator, - study_key=study_key, - ), - ) diff --git a/tests/conftest.py b/tests/conftest.py index 9d653744..0e3e2036 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,6 +56,9 @@ def __iter__(self): import imednet.core.endpoint.mixins as mixins_module monkeypatch.setattr(mixins_module, "Paginator", DummyPaginator) + # Also patch the class attribute since it's now used + if hasattr(mixins_module, "ListEndpoint"): + monkeypatch.setattr(mixins_module.ListEndpoint, "PAGINATOR_CLS", DummyPaginator) return captured return factory @@ -82,6 +85,11 @@ async def __aiter__(self): import imednet.core.endpoint.mixins as mixins_module monkeypatch.setattr(mixins_module, "AsyncPaginator", DummyPaginator) + # Also patch the class attribute since it's now used + if hasattr(mixins_module, "ListEndpoint"): + monkeypatch.setattr( + mixins_module.ListEndpoint, "ASYNC_PAGINATOR_CLS", DummyPaginator + ) return captured return factory From 8351db0dd966b4f4254f0f140e556233c728d965 Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:35:18 +0000 Subject: [PATCH 2/3] Fix formatting in tests/conftest.py - Applied black and isort formatting to tests/conftest.py to fix CI failure. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- tests/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0e3e2036..6270577d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -87,9 +87,7 @@ async def __aiter__(self): monkeypatch.setattr(mixins_module, "AsyncPaginator", DummyPaginator) # Also patch the class attribute since it's now used if hasattr(mixins_module, "ListEndpoint"): - monkeypatch.setattr( - mixins_module.ListEndpoint, "ASYNC_PAGINATOR_CLS", DummyPaginator - ) + monkeypatch.setattr(mixins_module.ListEndpoint, "ASYNC_PAGINATOR_CLS", DummyPaginator) return captured return factory From 75cfed9268f3cb62753aa7c7d9a2a318c5e3d703 Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Tue, 10 Feb 2026 20:40:00 +0000 Subject: [PATCH 3/3] Remove unused List import from jobs.py - Removed unused `List` import from `imednet/endpoints/jobs.py` to fix `ruff` linting failure. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- imednet/endpoints/jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imednet/endpoints/jobs.py b/imednet/endpoints/jobs.py index 08732827..b76b18f9 100644 --- a/imednet/endpoints/jobs.py +++ b/imednet/endpoints/jobs.py @@ -1,6 +1,6 @@ """Endpoint for checking job status in a study.""" -from typing import Any, Awaitable, List, Optional, cast +from typing import Any, Awaitable, Optional, cast from imednet.core.endpoint.mixins import ListEndpoint, PathGetEndpointMixin from imednet.core.paginator import AsyncJsonListPaginator, JsonListPaginator