From 2eb98c35bfe789cf892e88df6b4614e5ff4091d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 18:32:08 +0000 Subject: [PATCH 1/3] Initial plan From 86c19f7b828494c8cb22cd833cc7eaa802524fa6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 18:44:07 +0000 Subject: [PATCH 2/3] fix: remove stateful instance caching from endpoints for thread-safety Removes self._cache and all associated caching logic from GenericListGetEndpoint. Every list()/async_list() call now performs a fresh API request, eliminating race conditions and cross-request data leaks in concurrent environments. - Remove self._cache, _get_local_cache, _update_local_cache, _check_cache_hit - Remove refresh parameter from _list_sync, _list_async, _prepare_list_request - Simplify ListRequestState struct (remove cache, cached_result, has_filters fields) - Remove _enable_cache attribute from EndpointABC, EndpointProtocol - Remove refresh=True from FilterGetOperation and validation/cache.py list calls - Remove _enable_cache = True from studies, forms, variables, intervals endpoints - Update all tests to reflect stateless behavior - Update docs/caching.rst to document removal of native endpoint caching Agent-Logs-Url: https://github.com/fderuiter/imednet-python-sdk/sessions/372392a9-115d-44c0-ba22-f82f5556f061 Co-authored-by: fderuiter <127706008+fderuiter@users.noreply.github.com> --- docs/caching.rst | 34 +++---- .../core/src/imednet/core/endpoint/abc.py | 6 -- .../core/src/imednet/core/endpoint/base.py | 93 +------------------ .../core/endpoint/operations/filter_get.py | 2 - .../src/imednet/core/endpoint/protocols.py | 3 - .../core/src/imednet/core/endpoint/structs.py | 8 +- packages/core/src/imednet/endpoints/forms.py | 1 - .../core/src/imednet/endpoints/intervals.py | 1 - .../core/src/imednet/endpoints/studies.py | 1 - .../core/src/imednet/endpoints/variables.py | 1 - packages/core/src/imednet/validation/cache.py | 8 +- tests/unit/core/test_abc.py | 1 - tests/unit/endpoints/test_endpoints_async.py | 4 +- tests/unit/endpoints/test_forms_endpoint.py | 22 ++--- .../unit/endpoints/test_intervals_endpoint.py | 17 ++-- tests/unit/endpoints/test_records_endpoint.py | 4 +- tests/unit/endpoints/test_studies_endpoint.py | 15 +-- .../unit/endpoints/test_variables_endpoint.py | 17 ++-- tests/unit/test_core_endpoint_operations.py | 4 +- tests/unit/test_schema_validator.py | 20 ++-- tests/unit/test_utils_schema.py | 2 +- tests/unit/test_utils_schema_async.py | 6 +- tests/unit/test_workflows_record_update.py | 4 +- tests/workflows/test_record_update.py | 4 +- 24 files changed, 68 insertions(+), 210 deletions(-) diff --git a/docs/caching.rst b/docs/caching.rst index c52a260b..031dac7c 100644 --- a/docs/caching.rst +++ b/docs/caching.rst @@ -1,24 +1,22 @@ Caching Behavior ================ -This page explains how the SDK caches data to reduce unnecessary API calls. +This page explains how the SDK handles data caching. -Endpoint Caches ---------------- +Endpoint Caching +---------------- -The endpoints for studies, forms, variables and intervals maintain simple in-memory -caches. When `list()` or `async_list()` is called without any filters, the -results are stored on the endpoint instance. Subsequent calls return the cached -list instead of performing another request. Passing ``refresh=True`` bypasses the -cache and stores the fresh response. +The SDK no longer maintains any stateful in-memory cache on endpoint instances. +Every call to ``list()`` or ``async_list()`` performs a fresh API request, +ensuring that concurrent requests from different application contexts cannot +contaminate each other's data. -The caches live on the endpoint instances as dictionaries keyed by study. They -are not shared across SDK instances and are discarded when the SDK is garbage -collected. +If your application needs to reduce redundant API calls, implement caching at +the application layer using a library such as `cachetools`_, a request-level +cache middleware (e.g. ``httpx-cache``), or your framework's own cache +primitives (e.g. Django's cache framework or FastAPI dependency injection). -``get()`` methods use ``list(refresh=True)`` to ensure the most recent data is -returned. If your application needs to clear cached data manually you may call -``list(refresh=True)`` or create a new SDK instance. +.. _cachetools: https://cachetools.readthedocs.io/ Schema Cache ------------ @@ -36,11 +34,3 @@ on demand if it has not been cached yet. For offline tests, ``imednet.testing.fake_data`` includes helpers to generate forms, variables and records. These objects can be used with ``SchemaCache.refresh`` to validate payloads without hitting the API. - -Limitations ------------ - -The caching layer is purely in memory and is not thread-safe. It is intended to -minimize repeated requests in short-lived scripts or during interactive sessions. -Long running applications should periodically refresh or recreate the SDK to -avoid using stale data. diff --git a/packages/core/src/imednet/core/endpoint/abc.py b/packages/core/src/imednet/core/endpoint/abc.py index 111799f3..ceff65e4 100644 --- a/packages/core/src/imednet/core/endpoint/abc.py +++ b/packages/core/src/imednet/core/endpoint/abc.py @@ -43,12 +43,6 @@ def MODEL(self) -> Type[T]: # noqa: N802 Defaults to "id". Override in subclasses if needed. """ - _enable_cache: bool = False - """ - Whether this endpoint supports caching. - Defaults to False. Override in subclasses if needed. - """ - @abstractmethod def _build_path(self, *segments: Any) -> str: """ diff --git a/packages/core/src/imednet/core/endpoint/base.py b/packages/core/src/imednet/core/endpoint/base.py index ec488341..2e9461b6 100644 --- a/packages/core/src/imednet/core/endpoint/base.py +++ b/packages/core/src/imednet/core/endpoint/base.py @@ -7,7 +7,7 @@ import re import warnings from functools import lru_cache -from typing import Any, Callable, Dict, List, Optional, TypeVar, cast +from typing import Any, Callable, Dict, List, Optional, TypeVar from urllib.parse import quote from imednet.constants import DEFAULT_PAGE_SIZE @@ -107,15 +107,6 @@ class GenericListGetEndpoint( PARAM_PROCESSOR_CLS: type[ParamProcessor] = DefaultParamProcessor STUDY_KEY_STRATEGY: Optional[StudyKeyStrategy] = None - def __init__( - self, - client: RequestorProtocol, - ctx: object | None = None, - async_client: Optional[AsyncRequestorProtocol] = None, - ) -> None: - super().__init__(client, ctx, async_client) - self._cache: Optional[List[T] | Dict[str, List[T]]] = None - @property def study_key_strategy(self) -> StudyKeyStrategy: if self.STUDY_KEY_STRATEGY: @@ -169,83 +160,21 @@ def _resolve_params( return ParamState(study=study, params=params, other_filters=other_filters) - def _get_local_cache(self) -> Optional[List[T] | Dict[str, List[T]]]: - if not self._enable_cache: - return None - - if self.requires_study_key and self._cache is None: - self._cache = {} - return self._cache - - def _update_local_cache(self, result: List[T], study: str | None, has_filters: bool) -> None: - if has_filters or not self._enable_cache: - return - - if self.requires_study_key: - if self._cache is None: - self._cache = {} - if isinstance(self._cache, dict) and study is not None: - self._cache[study] = result - return - - self._cache = result - - def _check_cache_hit( - self, - study: Optional[str], - refresh: bool, - other_filters: Dict[str, Any], - cache: Optional[List[T] | Dict[str, List[T]]], - ) -> Optional[List[T]]: - if not self._enable_cache: - return None - - if self.requires_study_key: - if ( - isinstance(cache, dict) - and study is not None - and not other_filters - and not refresh - and study in cache - ): - return cache[study] - return None - - if isinstance(cache, list) and not other_filters and not refresh: - return cache - return None - def _prepare_list_request( self, study_key: Optional[str], extra_params: Optional[Dict[str, Any]], filters: Dict[str, Any], - refresh: bool, ) -> ListRequestState[T]: param_state = self._resolve_params(study_key, extra_params, filters) study = param_state.study params = param_state.params - other_filters = param_state.other_filters - - cache = self._get_local_cache() - cached_result = self._check_cache_hit(study, refresh, other_filters, cache) - if cached_result is not None: - return ListRequestState( - path="", - params={}, - study=study, - has_filters=False, - cache=None, - cached_result=cast(List[T], cached_result), - ) path = self._get_endpoint_path(study) return ListRequestState( path=path, params=params, study=study, - has_filters=bool(other_filters), - cache=cache, ) def _list_sync( @@ -254,22 +183,16 @@ def _list_sync( paginator_cls: type[Paginator], *, study_key: Optional[str] = None, - refresh: bool = False, extra_params: Optional[Dict[str, Any]] = None, **filters: Any, ) -> List[T]: - state = self._prepare_list_request(study_key, extra_params, filters, refresh) - if state.cached_result is not None: - return state.cached_result - - result = ListOperation[T]( + state = self._prepare_list_request(study_key, extra_params, filters) + return ListOperation[T]( path=state.path, params=state.params, page_size=self.PAGE_SIZE, parse_func=self._resolve_parse_func(), ).execute_sync(client, paginator_cls) - self._update_local_cache(result, state.study, state.has_filters) - return result async def _list_async( self, @@ -277,22 +200,16 @@ async def _list_async( paginator_cls: type[AsyncPaginator], *, study_key: Optional[str] = None, - refresh: bool = False, extra_params: Optional[Dict[str, Any]] = None, **filters: Any, ) -> List[T]: - state = self._prepare_list_request(study_key, extra_params, filters, refresh) - if state.cached_result is not None: - return state.cached_result - - result = await ListOperation[T]( + state = self._prepare_list_request(study_key, extra_params, filters) + return await ListOperation[T]( path=state.path, params=state.params, page_size=self.PAGE_SIZE, parse_func=self._resolve_parse_func(), ).execute_async(client, paginator_cls) - self._update_local_cache(result, state.study, state.has_filters) - return result def list(self, study_key: Optional[str] = None, **filters: Any) -> List[T]: return self._list_sync( diff --git a/packages/core/src/imednet/core/endpoint/operations/filter_get.py b/packages/core/src/imednet/core/endpoint/operations/filter_get.py index 294b6f68..0a06979d 100644 --- a/packages/core/src/imednet/core/endpoint/operations/filter_get.py +++ b/packages/core/src/imednet/core/endpoint/operations/filter_get.py @@ -72,7 +72,6 @@ def execute_sync( client, paginator_cls, study_key=self.study_key, - refresh=True, **self.filters, ) return self.validate_func(result, self.study_key, self.item_id) @@ -99,7 +98,6 @@ async def execute_async( client, paginator_cls, study_key=self.study_key, - refresh=True, **self.filters, ) return self.validate_func(result, self.study_key, self.item_id) diff --git a/packages/core/src/imednet/core/endpoint/protocols.py b/packages/core/src/imednet/core/endpoint/protocols.py index 53c6ef86..8496f5ec 100644 --- a/packages/core/src/imednet/core/endpoint/protocols.py +++ b/packages/core/src/imednet/core/endpoint/protocols.py @@ -26,7 +26,6 @@ class EndpointProtocol(Protocol): PATH: str MODEL: Type[JsonModel] _id_param: str - _enable_cache: bool requires_study_key: bool PAGE_SIZE: int @@ -61,7 +60,6 @@ def _list_sync( paginator_cls: type[Paginator], *, study_key: Optional[str] = None, - refresh: bool = False, extra_params: Optional[Dict[str, Any]] = None, **filters: Any, ) -> List[T]: @@ -74,7 +72,6 @@ async def _list_async( paginator_cls: type[AsyncPaginator], *, study_key: Optional[str] = None, - refresh: bool = False, extra_params: Optional[Dict[str, Any]] = None, **filters: Any, ) -> List[T]: diff --git a/packages/core/src/imednet/core/endpoint/structs.py b/packages/core/src/imednet/core/endpoint/structs.py index e93855db..c4d3d8fc 100644 --- a/packages/core/src/imednet/core/endpoint/structs.py +++ b/packages/core/src/imednet/core/endpoint/structs.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Dict, Generic, List, Optional, TypeVar +from typing import Any, Dict, Generic, Optional, TypeVar from imednet.models.json_base import JsonModel @@ -30,17 +30,11 @@ class ListRequestState(Generic[T]): Encapsulates the state required to execute a list request. Attributes: - cached_result: The result from cache, if any. path: The API path for the request. params: The query parameters. study: The study key. - has_filters: Whether filters other than study key are present. - cache: The cache object used for this request. """ path: str params: Dict[str, Any] study: Optional[str] - has_filters: bool - cache: Any - cached_result: Optional[List[T]] = None diff --git a/packages/core/src/imednet/endpoints/forms.py b/packages/core/src/imednet/endpoints/forms.py index eb0da02e..c5224356 100644 --- a/packages/core/src/imednet/endpoints/forms.py +++ b/packages/core/src/imednet/endpoints/forms.py @@ -16,5 +16,4 @@ class FormsEndpoint(EdcGenericListGetEndpoint[Form]): MODEL = Form _id_param = "formId" STUDY_KEY_STRATEGY = PopStudyKeyStrategy() - _enable_cache = True PAGE_SIZE = 500 diff --git a/packages/core/src/imednet/endpoints/intervals.py b/packages/core/src/imednet/endpoints/intervals.py index 1d2f980f..43bff683 100644 --- a/packages/core/src/imednet/endpoints/intervals.py +++ b/packages/core/src/imednet/endpoints/intervals.py @@ -16,5 +16,4 @@ class IntervalsEndpoint(EdcGenericListGetEndpoint[Interval]): MODEL = Interval _id_param = "intervalId" STUDY_KEY_STRATEGY = PopStudyKeyStrategy() - _enable_cache = True PAGE_SIZE = 500 diff --git a/packages/core/src/imednet/endpoints/studies.py b/packages/core/src/imednet/endpoints/studies.py index d7f5324f..2ce57ba8 100644 --- a/packages/core/src/imednet/endpoints/studies.py +++ b/packages/core/src/imednet/endpoints/studies.py @@ -14,5 +14,4 @@ class StudiesEndpoint(EdcGenericListGetEndpoint[Study]): PATH = "" MODEL = Study _id_param = "studyKey" - _enable_cache = True requires_study_key: bool = False diff --git a/packages/core/src/imednet/endpoints/variables.py b/packages/core/src/imednet/endpoints/variables.py index 8d182a5e..4dbb0ab8 100644 --- a/packages/core/src/imednet/endpoints/variables.py +++ b/packages/core/src/imednet/endpoints/variables.py @@ -16,5 +16,4 @@ class VariablesEndpoint(EdcGenericListGetEndpoint[Variable]): MODEL = Variable _id_param = "variableId" STUDY_KEY_STRATEGY = PopStudyKeyStrategy() - _enable_cache = True PAGE_SIZE = 500 diff --git a/packages/core/src/imednet/validation/cache.py b/packages/core/src/imednet/validation/cache.py index f5a082fb..52b7c824 100644 --- a/packages/core/src/imednet/validation/cache.py +++ b/packages/core/src/imednet/validation/cache.py @@ -37,7 +37,7 @@ async def _refresh_async( variables: VariablesEndpoint, study_key: Optional[str] = None, ) -> None: - vars_list = await variables.async_list(study_key=study_key, refresh=True) + vars_list = await variables.async_list(study_key=study_key) self.populate(vars_list) def _refresh_sync( @@ -46,7 +46,7 @@ def _refresh_sync( variables: VariablesEndpoint, study_key: Optional[str] = None, ) -> None: - vars_list = variables.list(study_key=study_key, refresh=True) + vars_list = variables.list(study_key=study_key) self.populate(vars_list) def refresh( @@ -234,7 +234,7 @@ def refresh(self, study_key: str) -> None: This method never raises :class:`~imednet.errors.ValidationError`; any API errors bubble up as :class:`~imednet.errors.ApiError`. """ - variables = self._sdk.variables.list(study_key=study_key, refresh=True) + variables = self._sdk.variables.list(study_key=study_key) self._refresh_common(variables) def validate_record(self, study_key: str, record: Dict[str, Any]) -> None: @@ -264,7 +264,7 @@ async def refresh(self, study_key: str) -> None: This method never raises :class:`~imednet.errors.ValidationError`; any API errors bubble up as :class:`~imednet.errors.ApiError`. """ - variables = await self._sdk.variables.async_list(study_key=study_key, refresh=True) + variables = await self._sdk.variables.async_list(study_key=study_key) self._refresh_common(variables) async def validate_record(self, study_key: str, record: Dict[str, Any]) -> None: diff --git a/tests/unit/core/test_abc.py b/tests/unit/core/test_abc.py index 6996635a..89849285 100644 --- a/tests/unit/core/test_abc.py +++ b/tests/unit/core/test_abc.py @@ -36,7 +36,6 @@ def test_endpoint_abc_properties(): assert endpoint.MODEL == MockModel assert endpoint.requires_study_key is True assert endpoint._id_param == "id" - assert endpoint._enable_cache is False def test_endpoint_abc_methods(): diff --git a/tests/unit/endpoints/test_endpoints_async.py b/tests/unit/endpoints/test_endpoints_async.py index b8604e14..097ff6eb 100644 --- a/tests/unit/endpoints/test_endpoints_async.py +++ b/tests/unit/endpoints/test_endpoints_async.py @@ -171,7 +171,7 @@ async def fake_impl(self, client, paginator, *, study_key=None, **filters): rec = await ep.async_get("S1", 1) - assert called == {"study_key": "S1", "filters": {"recordId": 1, "refresh": True}} + assert called == {"study_key": "S1", "filters": {"recordId": 1}} assert isinstance(rec, Record) @@ -179,7 +179,7 @@ async def fake_impl(self, client, paginator, *, study_key=None, **filters): async def test_async_get_record_not_found(monkeypatch, dummy_client, context, response_factory): ep = records.RecordsEndpoint(dummy_client, context, async_client=dummy_client) - async def fake_impl(self, client, paginator, *, study_key=None, refresh=False, **filters): + async def fake_impl(self, client, paginator, *, study_key=None, **filters): return [] monkeypatch.setattr(records.RecordsEndpoint, "_list_async", fake_impl) diff --git a/tests/unit/endpoints/test_forms_endpoint.py b/tests/unit/endpoints/test_forms_endpoint.py index 995abcf4..db175bc0 100644 --- a/tests/unit/endpoints/test_forms_endpoint.py +++ b/tests/unit/endpoints/test_forms_endpoint.py @@ -30,9 +30,8 @@ def test_get_success(monkeypatch, dummy_client, context): ep = forms.FormsEndpoint(dummy_client, context) called = {} - def fake_impl(self, client, paginator, *, study_key=None, refresh=False, **filters): + def fake_impl(self, client, paginator, *, study_key=None, **filters): called["study_key"] = study_key - called["refresh"] = refresh called["filters"] = filters return [Form(form_id=1)] @@ -40,14 +39,14 @@ def fake_impl(self, client, paginator, *, study_key=None, refresh=False, **filte res = ep.get("S1", 1) - assert called == {"study_key": "S1", "refresh": True, "filters": {"formId": 1}} + assert called == {"study_key": "S1", "filters": {"formId": 1}} assert isinstance(res, Form) def test_get_not_found(monkeypatch, dummy_client, context): ep = forms.FormsEndpoint(dummy_client, context) - def fake_impl(self, client, paginator, *, study_key=None, refresh=False, **filters): + def fake_impl(self, client, paginator, *, study_key=None, **filters): return [] monkeypatch.setattr(forms.FormsEndpoint, "_list_sync", fake_impl) @@ -56,26 +55,21 @@ def fake_impl(self, client, paginator, *, study_key=None, refresh=False, **filte ep.get("S1", 1) -def test_list_caches_by_study_key(dummy_client, context, paginator_factory): +def test_list_makes_request_per_call(dummy_client, context, paginator_factory): ep = forms.FormsEndpoint(dummy_client, context) capture = paginator_factory(forms, [{"formId": 1}]) - first = ep.list(study_key="S1") - second = ep.list(study_key="S1") - - assert capture["count"] == 1 - assert first == second - - ep.list(study_key="S2") + ep.list(study_key="S1") + ep.list(study_key="S1") assert capture["count"] == 2 -def test_list_refresh_bypasses_cache(dummy_client, context, paginator_factory): +def test_list_different_study_keys_make_separate_requests(dummy_client, context, paginator_factory): ep = forms.FormsEndpoint(dummy_client, context) capture = paginator_factory(forms, [{"formId": 1}]) ep.list(study_key="S1") - ep.list(study_key="S1", refresh=True) + ep.list(study_key="S2") assert capture["count"] == 2 diff --git a/tests/unit/endpoints/test_intervals_endpoint.py b/tests/unit/endpoints/test_intervals_endpoint.py index 2c747252..080a3166 100644 --- a/tests/unit/endpoints/test_intervals_endpoint.py +++ b/tests/unit/endpoints/test_intervals_endpoint.py @@ -25,7 +25,7 @@ def test_list_uses_default_study_and_page_size( def test_get_not_found(monkeypatch, dummy_client, context): ep = intervals.IntervalsEndpoint(dummy_client, context) - def fake_impl(self, client, paginator, *, study_key=None, refresh=False, **filters): + def fake_impl(self, client, paginator, *, study_key=None, **filters): return [] monkeypatch.setattr(intervals.IntervalsEndpoint, "_list_sync", fake_impl) @@ -34,26 +34,21 @@ def fake_impl(self, client, paginator, *, study_key=None, refresh=False, **filte ep.get("S1", 1) -def test_list_caches_by_study_key(dummy_client, context, paginator_factory): +def test_list_makes_request_per_call(dummy_client, context, paginator_factory): ep = intervals.IntervalsEndpoint(dummy_client, context) capture = paginator_factory(intervals, [{"intervalId": 1}]) - first = ep.list(study_key="S1") - second = ep.list(study_key="S1") - - assert capture["count"] == 1 - assert first == second - - ep.list(study_key="S2") + ep.list(study_key="S1") + ep.list(study_key="S1") assert capture["count"] == 2 -def test_list_refresh_bypasses_cache(dummy_client, context, paginator_factory): +def test_list_different_study_keys_make_separate_requests(dummy_client, context, paginator_factory): ep = intervals.IntervalsEndpoint(dummy_client, context) capture = paginator_factory(intervals, [{"intervalId": 1}]) ep.list(study_key="S1") - ep.list(study_key="S1", refresh=True) + ep.list(study_key="S2") assert capture["count"] == 2 diff --git a/tests/unit/endpoints/test_records_endpoint.py b/tests/unit/endpoints/test_records_endpoint.py index 084b9ce5..901c7631 100644 --- a/tests/unit/endpoints/test_records_endpoint.py +++ b/tests/unit/endpoints/test_records_endpoint.py @@ -39,7 +39,7 @@ def fake_impl(self, client, paginator, *, study_key=None, **filters): res = ep.get("S1", 1) - assert called == {"study_key": "S1", "filters": {"recordId": 1, "refresh": True}} + assert called == {"study_key": "S1", "filters": {"recordId": 1}} assert isinstance(res, Record) @@ -57,7 +57,7 @@ def fake_impl(self, client, paginator, *, study_key=None, **filters): res = ep.get(record_id=1) - assert called == {"study_key": None, "filters": {"recordId": 1, "refresh": True}} + assert called == {"study_key": None, "filters": {"recordId": 1}} assert isinstance(res, Record) diff --git a/tests/unit/endpoints/test_studies_endpoint.py b/tests/unit/endpoints/test_studies_endpoint.py index e6bacc5e..07ed61f5 100644 --- a/tests/unit/endpoints/test_studies_endpoint.py +++ b/tests/unit/endpoints/test_studies_endpoint.py @@ -40,22 +40,11 @@ def test_get_not_found(monkeypatch, dummy_client, context, paginator_factory): ep.get(None, "missing") -def test_list_caches_results(dummy_client, context, paginator_factory): - ep = studies.StudiesEndpoint(dummy_client, context) - captured = paginator_factory(studies, [{"studyKey": "S1"}]) - - first = ep.list() - second = ep.list() - - assert captured["count"] == 1 - assert first == second - - -def test_list_refresh_bypasses_cache(dummy_client, context, paginator_factory): +def test_list_each_call_makes_request(dummy_client, context, paginator_factory): ep = studies.StudiesEndpoint(dummy_client, context) captured = paginator_factory(studies, [{"studyKey": "S1"}]) ep.list() - ep.list(refresh=True) + ep.list() assert captured["count"] == 2 diff --git a/tests/unit/endpoints/test_variables_endpoint.py b/tests/unit/endpoints/test_variables_endpoint.py index 62d11434..2619d119 100644 --- a/tests/unit/endpoints/test_variables_endpoint.py +++ b/tests/unit/endpoints/test_variables_endpoint.py @@ -28,7 +28,7 @@ def test_list_requires_study_key_page_size( def test_get_not_found(monkeypatch, dummy_client, context): ep = variables.VariablesEndpoint(dummy_client, context) - def fake_impl(self, client, paginator, *, study_key=None, refresh=False, **filters): + def fake_impl(self, client, paginator, *, study_key=None, **filters): return [] monkeypatch.setattr(variables.VariablesEndpoint, "_list_sync", fake_impl) @@ -37,26 +37,21 @@ def fake_impl(self, client, paginator, *, study_key=None, refresh=False, **filte ep.get("S1", 1) -def test_list_caches_by_study_key(dummy_client, context, paginator_factory): +def test_list_makes_request_per_call(dummy_client, context, paginator_factory): ep = variables.VariablesEndpoint(dummy_client, context) capture = paginator_factory(variables, [{"variableId": 1}]) - first = ep.list(study_key="S1") - second = ep.list(study_key="S1") - - assert capture["count"] == 1 - assert first == second - - ep.list(study_key="S2") + ep.list(study_key="S1") + ep.list(study_key="S1") assert capture["count"] == 2 -def test_list_refresh_bypasses_cache(dummy_client, context, paginator_factory): +def test_list_different_study_keys_make_separate_requests(dummy_client, context, paginator_factory): ep = variables.VariablesEndpoint(dummy_client, context) capture = paginator_factory(variables, [{"variableId": 1}]) ep.list(study_key="S1") - ep.list(study_key="S1", refresh=True) + ep.list(study_key="S2") assert capture["count"] == 2 diff --git a/tests/unit/test_core_endpoint_operations.py b/tests/unit/test_core_endpoint_operations.py index e4b119f8..13192604 100644 --- a/tests/unit/test_core_endpoint_operations.py +++ b/tests/unit/test_core_endpoint_operations.py @@ -149,7 +149,7 @@ def test_filter_get_operation_sync(): assert result == {"id": 1} list_sync_func.assert_called_once_with( - client, paginator_cls, study_key="STUDY1", refresh=True, name="test" + client, paginator_cls, study_key="STUDY1", name="test" ) validate_func.assert_called_once_with([{"id": 1}], "STUDY1", 1) @@ -188,7 +188,7 @@ async def test_filter_get_operation_async(): assert result == {"id": 1} list_async_func.assert_called_once_with( - client, paginator_cls, study_key="STUDY1", refresh=True, name="test" + client, paginator_cls, study_key="STUDY1", name="test" ) validate_func.assert_called_once_with([{"id": 1}], "STUDY1", 1) diff --git a/tests/unit/test_schema_validator.py b/tests/unit/test_schema_validator.py index 3411ab29..d4a9c755 100644 --- a/tests/unit/test_schema_validator.py +++ b/tests/unit/test_schema_validator.py @@ -33,9 +33,9 @@ def test_validate_record_unknown_variable(async_mode: bool) -> None: validator.validate_record("STUDY", {"formKey": "F1", "data": {"bad": 1}}) if async_mode: - sdk.variables.async_list.assert_awaited_once_with(study_key="STUDY", refresh=True) + sdk.variables.async_list.assert_awaited_once_with(study_key="STUDY") else: - sdk.variables.list.assert_called_once_with(study_key="STUDY", refresh=True) + sdk.variables.list.assert_called_once_with(study_key="STUDY") @pytest.mark.parametrize("async_mode", [False, True]) @@ -54,9 +54,9 @@ def test_validate_record_wrong_type(async_mode: bool) -> None: validator.validate_record("STUDY", {"formKey": "F1", "data": {"age": "x"}}) if async_mode: - sdk.variables.async_list.assert_awaited_once_with(study_key="STUDY", refresh=True) + sdk.variables.async_list.assert_awaited_once_with(study_key="STUDY") else: - sdk.variables.list.assert_called_once_with(study_key="STUDY", refresh=True) + sdk.variables.list.assert_called_once_with(study_key="STUDY") @pytest.mark.parametrize("async_mode", [False, True]) @@ -75,9 +75,9 @@ def test_validate_record_unknown_form(async_mode: bool) -> None: validator.validate_record("STUDY", {"formKey": "BAD", "data": {}}) if async_mode: - sdk.variables.async_list.assert_awaited_once_with(study_key="STUDY", refresh=True) + sdk.variables.async_list.assert_awaited_once_with(study_key="STUDY") else: - sdk.variables.list.assert_called_once_with(study_key="STUDY", refresh=True) + sdk.variables.list.assert_called_once_with(study_key="STUDY") @pytest.mark.parametrize("async_mode", [False, True]) @@ -89,13 +89,13 @@ def test_refresh_called_when_form_not_cached(async_mode: bool) -> None: validator.refresh = AsyncMock(wraps=validator.refresh) # type: ignore[assignment] asyncio.run(validator.validate_record("STUDY", {"formKey": "F1", "data": {"age": 1}})) validator.refresh.assert_awaited_once_with("STUDY") - sdk.variables.async_list.assert_awaited_once_with(study_key="STUDY", refresh=True) + sdk.variables.async_list.assert_awaited_once_with(study_key="STUDY") else: validator = SchemaValidator(sdk) # type: ignore[assignment] validator.refresh = MagicMock(wraps=validator.refresh) # type: ignore[assignment] validator.validate_record("STUDY", {"formKey": "F1", "data": {"age": 1}}) validator.refresh.assert_called_once_with("STUDY") - sdk.variables.list.assert_called_once_with(study_key="STUDY", refresh=True) + sdk.variables.list.assert_called_once_with(study_key="STUDY") @pytest.mark.parametrize("async_mode", [False, True]) @@ -311,7 +311,7 @@ def test_base_schema_cache_refresh() -> None: variables.list.return_value = [var] cache.refresh(forms, variables, "STUDY") - variables.list.assert_called_once_with(study_key="STUDY", refresh=True) + variables.list.assert_called_once_with(study_key="STUDY") assert "F1" in cache.forms @@ -329,5 +329,5 @@ async def test_base_schema_cache_async_refresh() -> None: variables.async_list = AsyncMock(return_value=[var]) await cache.refresh(forms, variables, "STUDY") - variables.async_list.assert_awaited_once_with(study_key="STUDY", refresh=True) + variables.async_list.assert_awaited_once_with(study_key="STUDY") assert "F1" in cache.forms diff --git a/tests/unit/test_utils_schema.py b/tests/unit/test_utils_schema.py index f2c08faf..75458e5f 100644 --- a/tests/unit/test_utils_schema.py +++ b/tests/unit/test_utils_schema.py @@ -23,7 +23,7 @@ def test_schema_cache_refresh() -> None: assert cache.form_key_from_id(1) == "F1" assert cache.variables_for_form("F1")["age"] is var forms.list.assert_not_called() - variables.list.assert_called_once_with(study_key="S", refresh=True) + variables.list.assert_called_once_with(study_key="S") def test_check_type_int() -> None: diff --git a/tests/unit/test_utils_schema_async.py b/tests/unit/test_utils_schema_async.py index 04363258..522173b2 100644 --- a/tests/unit/test_utils_schema_async.py +++ b/tests/unit/test_utils_schema_async.py @@ -29,7 +29,7 @@ async def test_async_schema_cache_refresh() -> None: assert validator.schema.form_key_from_id(1) == "F1" assert validator.schema.variables_for_form("F1")["age"] is var - variables.async_list.assert_awaited_once_with(study_key="ST", refresh=True) + variables.async_list.assert_awaited_once_with(study_key="ST") @pytest.mark.asyncio @@ -41,7 +41,7 @@ async def test_validate_record_and_batch_async() -> None: record = {"formKey": "F1", "data": {"age": 1}} await validator.validate_record("ST", record) - sdk.variables.async_list.assert_awaited_once_with(study_key="ST", refresh=True) + sdk.variables.async_list.assert_awaited_once_with(study_key="ST") validator.validate_record = AsyncMock() # type: ignore[assignment] await validator.validate_batch("ST", [record, record]) @@ -57,4 +57,4 @@ async def test_unknown_form_refreshes_and_raises() -> None: with pytest.raises(ValidationError, match="Unknown form BAD"): await validator.validate_record("ST", {"formKey": "BAD", "data": {}}) - sdk.variables.async_list.assert_awaited_once_with(study_key="ST", refresh=True) + sdk.variables.async_list.assert_awaited_once_with(study_key="ST") diff --git a/tests/unit/test_workflows_record_update.py b/tests/unit/test_workflows_record_update.py index ab361c79..00ccfaf5 100644 --- a/tests/unit/test_workflows_record_update.py +++ b/tests/unit/test_workflows_record_update.py @@ -101,7 +101,7 @@ def test_create_or_update_records_validation() -> None: sdk.records.create.return_value = Job(jobId="1", batchId="1", state="PROCESSING") wf.create_or_update_records("STUDY", [{"formKey": "F1", "data": {"age": 5}}]) - sdk.variables.list.assert_called_once_with(study_key="STUDY", refresh=True) + sdk.variables.list.assert_called_once_with(study_key="STUDY") sdk.records.create.assert_called_once_with( "STUDY", [{"formKey": "F1", "data": {"age": 5}}], schema=wf._schema ) @@ -383,7 +383,7 @@ async def test_async_create_or_update_records_validation() -> None: sdk.records.async_create.return_value = Job(jobId="1", batchId="1", state="PROCESSING") await wf.async_create_or_update_records("STUDY", [{"formKey": "F1", "data": {"age": 5}}]) - sdk.variables.async_list.assert_called_once_with(study_key="STUDY", refresh=True) + sdk.variables.async_list.assert_called_once_with(study_key="STUDY") sdk.records.async_create.assert_called_once_with( "STUDY", [{"formKey": "F1", "data": {"age": 5}}], schema=wf._schema ) diff --git a/tests/workflows/test_record_update.py b/tests/workflows/test_record_update.py index 9e1d2ace..61568986 100644 --- a/tests/workflows/test_record_update.py +++ b/tests/workflows/test_record_update.py @@ -191,7 +191,7 @@ def test_create_or_update_records_refresh_and_validate(async_mode: bool) -> None asyncio.run( wf.async_create_or_update_records("STUDY", [{"formKey": "F1", "data": {"age": 5}}]) ) - sdk.variables.async_list.assert_awaited_once_with(study_key="STUDY", refresh=True) + sdk.variables.async_list.assert_awaited_once_with(study_key="STUDY") wf._validator.validate_batch.assert_awaited_once_with( "STUDY", [{"formKey": "F1", "data": {"age": 5}}], @@ -204,7 +204,7 @@ def test_create_or_update_records_refresh_and_validate(async_mode: bool) -> None else: wf._validator.validate_batch = MagicMock() # type: ignore[method-assign] wf.create_or_update_records("STUDY", [{"formKey": "F1", "data": {"age": 5}}]) - sdk.variables.list.assert_called_once_with(study_key="STUDY", refresh=True) + sdk.variables.list.assert_called_once_with(study_key="STUDY") wf._validator.validate_batch.assert_called_once_with( "STUDY", [{"formKey": "F1", "data": {"age": 5}}], From 69ac685a9ab5e73d774b7c5e978465ff24951f2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 20:22:07 +0000 Subject: [PATCH 3/3] style: fix Black formatting in test_core_endpoint_operations.py Agent-Logs-Url: https://github.com/fderuiter/imednet-python-sdk/sessions/6ad97d01-e771-4e35-a29e-6c640cf78df2 Co-authored-by: fderuiter <127706008+fderuiter@users.noreply.github.com> --- tests/unit/test_core_endpoint_operations.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_core_endpoint_operations.py b/tests/unit/test_core_endpoint_operations.py index 13192604..b510b2b7 100644 --- a/tests/unit/test_core_endpoint_operations.py +++ b/tests/unit/test_core_endpoint_operations.py @@ -148,9 +148,7 @@ def test_filter_get_operation_sync(): result = operation.execute_sync(client, paginator_cls) assert result == {"id": 1} - list_sync_func.assert_called_once_with( - client, paginator_cls, study_key="STUDY1", name="test" - ) + list_sync_func.assert_called_once_with(client, paginator_cls, study_key="STUDY1", name="test") validate_func.assert_called_once_with([{"id": 1}], "STUDY1", 1) @@ -187,9 +185,7 @@ async def test_filter_get_operation_async(): result = await operation.execute_async(client, paginator_cls) assert result == {"id": 1} - list_async_func.assert_called_once_with( - client, paginator_cls, study_key="STUDY1", name="test" - ) + list_async_func.assert_called_once_with(client, paginator_cls, study_key="STUDY1", name="test") validate_func.assert_called_once_with([{"id": 1}], "STUDY1", 1) @@ -270,5 +266,7 @@ async def test_record_create_operation_async(): assert result == {"status": "created"} client.post.assert_called_once_with( - "/create", json=[{"field1": "val1"}], headers={HEADER_EMAIL_NOTIFY: "user@example.com"} + "/create", + json=[{"field1": "val1"}], + headers={HEADER_EMAIL_NOTIFY: "user@example.com"}, )