Skip to content

Commit 52a787c

Browse files
🛠️ Refactor: Extract PathGetOperation from PathGetEndpointMixin
- ♻️ 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>
1 parent 386dba4 commit 52a787c

3 files changed

Lines changed: 89 additions & 12 deletions

File tree

src/imednet/core/endpoint/mixins/get.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Any, Dict, Iterable, List, Optional
44

55
from imednet.core.endpoint.abc import EndpointABC
6+
from imednet.core.endpoint.operations.get import PathGetOperation
67
from imednet.core.paginator import AsyncPaginator, Paginator
78
from imednet.core.protocols import AsyncRequestorProtocol, RequestorProtocol
89

@@ -139,13 +140,6 @@ def _get_path_for_id(self, study_key: Optional[str], item_id: Any) -> str:
139140
def _raise_not_found(self, study_key: Optional[str], item_id: Any) -> None:
140141
raise ValueError(f"{self.MODEL.__name__} not found")
141142

142-
def _process_response(self, response: Any, study_key: Optional[str], item_id: Any) -> T:
143-
data = response.json()
144-
if not data:
145-
# Enforce strict validation for empty body
146-
self._raise_not_found(study_key, item_id)
147-
return self._parse_item(data)
148-
149143
def _get_path_sync(
150144
self,
151145
client: RequestorProtocol,
@@ -154,8 +148,12 @@ def _get_path_sync(
154148
item_id: Any,
155149
) -> T:
156150
path = self._get_path_for_id(study_key, item_id)
157-
response = client.get(path)
158-
return self._process_response(response, study_key, item_id)
151+
operation = PathGetOperation[T](
152+
path=path,
153+
parse_func=self._parse_item,
154+
not_found_func=lambda: self._raise_not_found(study_key, item_id),
155+
)
156+
return operation.execute_sync(client)
159157

160158
async def _get_path_async(
161159
self,
@@ -165,8 +163,12 @@ async def _get_path_async(
165163
item_id: Any,
166164
) -> T:
167165
path = self._get_path_for_id(study_key, item_id)
168-
response = await client.get(path)
169-
return self._process_response(response, study_key, item_id)
166+
operation = PathGetOperation[T](
167+
path=path,
168+
parse_func=self._parse_item,
169+
not_found_func=lambda: self._raise_not_found(study_key, item_id),
170+
)
171+
return await operation.execute_async(client)
170172

171173
def get(self, study_key: Optional[str], item_id: Any) -> T:
172174
"""Get an item by ID using direct path."""
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1+
from .get import PathGetOperation
12
from .list import ListOperation
23
from .record_create import RecordCreateOperation
34

4-
__all__ = ["ListOperation", "RecordCreateOperation"]
5+
__all__ = ["ListOperation", "PathGetOperation", "RecordCreateOperation"]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
Operation for executing get requests via direct path.
3+
4+
This module encapsulates the logic for fetching and parsing a single resource
5+
from the API using its ID.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from typing import Any, Callable, Generic, TypeVar
11+
12+
from imednet.core.protocols import AsyncRequestorProtocol, RequestorProtocol
13+
14+
T = TypeVar("T")
15+
16+
17+
class PathGetOperation(Generic[T]):
18+
"""
19+
Operation for executing get requests via direct path.
20+
21+
Encapsulates the logic for making the HTTP request, handling empty
22+
responses (not found), and parsing the result.
23+
"""
24+
25+
def __init__(
26+
self,
27+
path: str,
28+
parse_func: Callable[[Any], T],
29+
not_found_func: Callable[[], None],
30+
) -> None:
31+
"""
32+
Initialize the path get operation.
33+
34+
Args:
35+
path: The API endpoint path.
36+
parse_func: A function to parse a raw JSON item into the model T.
37+
not_found_func: A callback to raise the appropriate not found error.
38+
"""
39+
self.path = path
40+
self.parse_func = parse_func
41+
self.not_found_func = not_found_func
42+
43+
def _process_response(self, response: Any) -> T:
44+
"""Process the raw HTTP response."""
45+
data = response.json()
46+
if not data:
47+
self.not_found_func()
48+
return self.parse_func(data)
49+
50+
def execute_sync(self, client: RequestorProtocol) -> T:
51+
"""
52+
Execute synchronous get request.
53+
54+
Args:
55+
client: The synchronous HTTP client.
56+
57+
Returns:
58+
The parsed item.
59+
"""
60+
response = client.get(self.path)
61+
return self._process_response(response)
62+
63+
async def execute_async(self, client: AsyncRequestorProtocol) -> T:
64+
"""
65+
Execute asynchronous get request.
66+
67+
Args:
68+
client: The asynchronous HTTP client.
69+
70+
Returns:
71+
The parsed item.
72+
"""
73+
response = await client.get(self.path)
74+
return self._process_response(response)

0 commit comments

Comments
 (0)