From ccbb2fe92aed69ffdfcda8d97e2115f6c11e8b6d Mon Sep 17 00:00:00 2001 From: fderuiter <127706008+fderuiter@users.noreply.github.com> Date: Sat, 28 Feb 2026 15:13:42 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20Refactor:=20Extract=20P?= =?UTF-8?q?athGetOperation=20from=20PathGetEndpointMixin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ♻️ DRY: Moved the repeated HTTP fetch and parsing logic out of `PathGetEndpointMixin` into a dedicated `PathGetOperation` class. - 🧱 SOLID: Improved single responsibility by separating the definition of the path GET mechanism in the mixin from the execution of the request and response parsing. - 📉 Type-Safe: Ensured the new abstraction preserves `Generic[T]` typing and handles context isolation correctly through constructor injection of `parse_func` and `not_found_func`. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- src/imednet/core/endpoint/mixins/get.py | 24 +++--- .../core/endpoint/operations/__init__.py | 3 +- src/imednet/core/endpoint/operations/get.py | 74 +++++++++++++++++++ 3 files changed, 89 insertions(+), 12 deletions(-) create mode 100644 src/imednet/core/endpoint/operations/get.py diff --git a/src/imednet/core/endpoint/mixins/get.py b/src/imednet/core/endpoint/mixins/get.py index 616f9c20..cd295ec6 100644 --- a/src/imednet/core/endpoint/mixins/get.py +++ b/src/imednet/core/endpoint/mixins/get.py @@ -3,6 +3,7 @@ from typing import Any, Dict, Iterable, List, Optional from imednet.core.endpoint.abc import EndpointABC +from imednet.core.endpoint.operations.get import PathGetOperation from imednet.core.paginator import AsyncPaginator, Paginator from imednet.core.protocols import AsyncRequestorProtocol, RequestorProtocol @@ -139,13 +140,6 @@ def _get_path_for_id(self, study_key: Optional[str], item_id: Any) -> str: def _raise_not_found(self, study_key: Optional[str], item_id: Any) -> None: raise ValueError(f"{self.MODEL.__name__} not found") - def _process_response(self, response: Any, study_key: Optional[str], item_id: Any) -> T: - data = response.json() - if not data: - # Enforce strict validation for empty body - self._raise_not_found(study_key, item_id) - return self._parse_item(data) - def _get_path_sync( self, client: RequestorProtocol, @@ -154,8 +148,12 @@ def _get_path_sync( item_id: Any, ) -> T: path = self._get_path_for_id(study_key, item_id) - response = client.get(path) - return self._process_response(response, study_key, item_id) + operation = PathGetOperation[T]( + path=path, + parse_func=self._parse_item, + not_found_func=lambda: self._raise_not_found(study_key, item_id), + ) + return operation.execute_sync(client) async def _get_path_async( self, @@ -165,8 +163,12 @@ async def _get_path_async( item_id: Any, ) -> T: path = self._get_path_for_id(study_key, item_id) - response = await client.get(path) - return self._process_response(response, study_key, item_id) + operation = PathGetOperation[T]( + path=path, + parse_func=self._parse_item, + not_found_func=lambda: self._raise_not_found(study_key, item_id), + ) + return await operation.execute_async(client) def get(self, study_key: Optional[str], item_id: Any) -> T: """Get an item by ID using direct path.""" diff --git a/src/imednet/core/endpoint/operations/__init__.py b/src/imednet/core/endpoint/operations/__init__.py index 6dd15cc7..b98dd128 100644 --- a/src/imednet/core/endpoint/operations/__init__.py +++ b/src/imednet/core/endpoint/operations/__init__.py @@ -1,4 +1,5 @@ +from .get import PathGetOperation from .list import ListOperation from .record_create import RecordCreateOperation -__all__ = ["ListOperation", "RecordCreateOperation"] +__all__ = ["ListOperation", "PathGetOperation", "RecordCreateOperation"] diff --git a/src/imednet/core/endpoint/operations/get.py b/src/imednet/core/endpoint/operations/get.py new file mode 100644 index 00000000..595f7d69 --- /dev/null +++ b/src/imednet/core/endpoint/operations/get.py @@ -0,0 +1,74 @@ +""" +Operation for executing get requests via direct path. + +This module encapsulates the logic for fetching and parsing a single resource +from the API using its ID. +""" + +from __future__ import annotations + +from typing import Any, Callable, Generic, TypeVar + +from imednet.core.protocols import AsyncRequestorProtocol, RequestorProtocol + +T = TypeVar("T") + + +class PathGetOperation(Generic[T]): + """ + Operation for executing get requests via direct path. + + Encapsulates the logic for making the HTTP request, handling empty + responses (not found), and parsing the result. + """ + + def __init__( + self, + path: str, + parse_func: Callable[[Any], T], + not_found_func: Callable[[], None], + ) -> None: + """ + Initialize the path get operation. + + Args: + path: The API endpoint path. + parse_func: A function to parse a raw JSON item into the model T. + not_found_func: A callback to raise the appropriate not found error. + """ + self.path = path + self.parse_func = parse_func + self.not_found_func = not_found_func + + def _process_response(self, response: Any) -> T: + """Process the raw HTTP response.""" + data = response.json() + if not data: + self.not_found_func() + return self.parse_func(data) + + def execute_sync(self, client: RequestorProtocol) -> T: + """ + Execute synchronous get request. + + Args: + client: The synchronous HTTP client. + + Returns: + The parsed item. + """ + response = client.get(self.path) + return self._process_response(response) + + async def execute_async(self, client: AsyncRequestorProtocol) -> T: + """ + Execute asynchronous get request. + + Args: + client: The asynchronous HTTP client. + + Returns: + The parsed item. + """ + response = await client.get(self.path) + return self._process_response(response)