Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions src/imednet/core/endpoint/mixins/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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."""
Expand Down
3 changes: 2 additions & 1 deletion src/imednet/core/endpoint/operations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .get import PathGetOperation
from .list import ListOperation
from .record_create import RecordCreateOperation

__all__ = ["ListOperation", "RecordCreateOperation"]
__all__ = ["ListOperation", "PathGetOperation", "RecordCreateOperation"]
74 changes: 74 additions & 0 deletions src/imednet/core/endpoint/operations/get.py
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +28 to +48
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not_found_func is typed as Callable[[], None] and _process_response() continues to call parse_func(data) after invoking it. This makes the operation’s contract ambiguous/unsafe if a caller provides a callback that returns normally (it would then attempt to parse an empty payload). Consider typing not_found_func as Callable[[], NoReturn] (and/or explicitly stopping execution after calling it) so both type-checkers and runtime behavior enforce the intended “must raise” contract.

Copilot uses AI. Check for mistakes.

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)