From 96f3ecebefa35966f0897c012ad612889dc9244e Mon Sep 17 00:00:00 2001 From: Alex Fieraru Date: Wed, 20 May 2026 14:50:00 +0300 Subject: [PATCH 1/3] feat: Update get_studies function --- README.md | 4 ++- dataconnect/client.py | 4 +-- dataconnect/models.py | 6 +++++ dataconnect/service/base.py | 4 +-- dataconnect/service/default.py | 8 +++--- tests/test_service_default.py | 48 ++++++++++++++++------------------ 6 files changed, 40 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 8577f70..2d0dddb 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,9 @@ with DataConnectClient.connect( token="your-bearer-token", ) as client: - studies = client.get_studies(search_study_name="ACME") + result = client.get_studies(search_study_name="ACME") + print(result.total) # total number of studies accessible to the user + print(result.studies) # list of Study objects pagination = client.get_datasets(study_environment_uuid=UUID("cec9f2a7-07ba-4fa8-bfcf-34fbc5d56793")) datasets = pagination.items diff --git a/dataconnect/client.py b/dataconnect/client.py index eb9df21..7dd8834 100644 --- a/dataconnect/client.py +++ b/dataconnect/client.py @@ -12,7 +12,7 @@ import pandas as pd -from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, Study +from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, StudiesResult, Study from dataconnect.service import DataConnectService, DefaultDataConnectService _DEFAULT_HOST = "enodia-gateway.platform.imedidata.com" @@ -45,7 +45,7 @@ def connect( # Public API - def get_studies(self, search_study_name: str | None = None) -> list[Study]: + def get_studies(self, search_study_name: str | None = None) -> StudiesResult: """List the studies the client is authorized to access.""" return self._service.get_studies(search_study_name=search_study_name) diff --git a/dataconnect/models.py b/dataconnect/models.py index cc713b7..71d2abd 100644 --- a/dataconnect/models.py +++ b/dataconnect/models.py @@ -20,6 +20,12 @@ class Study: environments: list[StudyEnvironment] = field(default_factory=list) +@dataclass(frozen=True) +class StudiesResult: + total: int + studies: list[Study] + + @dataclass(frozen=True) class DatasetVersion: study_uuid: UUID diff --git a/dataconnect/service/base.py b/dataconnect/service/base.py index cf068fb..91f2f7e 100644 --- a/dataconnect/service/base.py +++ b/dataconnect/service/base.py @@ -7,14 +7,14 @@ import pandas as pd -from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, Study +from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, StudiesResult, Study class DataConnectService(ABC): """Abstract service interface — defines all operations available to the client.""" @abstractmethod - def get_studies(self, search_study_name: str | None = None) -> list[Study]: ... + def get_studies(self, search_study_name: str | None = None) -> StudiesResult: ... @abstractmethod def get_datasets( diff --git a/dataconnect/service/default.py b/dataconnect/service/default.py index 4f81317..ed25ae0 100644 --- a/dataconnect/service/default.py +++ b/dataconnect/service/default.py @@ -7,7 +7,7 @@ import pandas as pd from dataconnect.exceptions import ErrorDetail -from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, Pagination, Study +from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, Pagination, StudiesResult, Study from dataconnect.service.base import DataConnectService from dataconnect.service.error_handler import translate_error from dataconnect.service.mappers import ( @@ -35,7 +35,7 @@ def __init__(self, transport: Transport) -> None: # DataConnectService - def get_studies(self, search_study_name: str | None = None) -> list[Study]: + def get_studies(self, search_study_name: str | None = None) -> StudiesResult: """List studies the authenticated user can access. Args: @@ -52,7 +52,9 @@ def get_studies(self, search_study_name: str | None = None) -> list[Study]: try: resources = self._transport.list_resources(request) - return [resource_to_study(r) for r in resources] + total = resources[0].total_records if resources else 0 + studies = [resource_to_study(r) for r in resources] + return StudiesResult(total=total, studies=studies) except Exception as ex: raise translate_error(ex) from ex diff --git a/tests/test_service_default.py b/tests/test_service_default.py index ad91b99..b310ccb 100644 --- a/tests/test_service_default.py +++ b/tests/test_service_default.py @@ -3,6 +3,7 @@ # import pytest # from dataconnect.exceptions import ValidationError +# from dataconnect.models import StudiesResult # from dataconnect.service.default import DefaultDataConnectService # from dataconnect.transport.models import DataRef, ResourceInfo, ResourceQuery @@ -20,13 +21,13 @@ # return None -# def _study_resource(name: str = "Study A") -> ResourceInfo: +# def _study_resource(name: str = "Study A", total_records: int = 1) -> ResourceInfo: # payload = (f'{{"uuid":"12345678-1234-1234-1234-123456789abc","name":"{name}","environments":[]}}').encode() # return ResourceInfo( # descriptor=b"", # endpoints=[DataRef(ticket=payload)], -# total_records=1, +# total_records=total_records, # schema_bytes=b"", # ) @@ -35,54 +36,49 @@ # transport = StubTransport(resources=[_study_resource()]) # service = DefaultDataConnectService(transport) -# studies = service.get_studies() +# result = service.get_studies() -# assert len(studies) == 1 -# assert studies[0].name == "Study A" +# assert isinstance(result, StudiesResult) +# assert result.total == 1 +# assert len(result.studies) == 1 +# assert result.studies[0].name == "Study A" # assert transport.last_request is not None # assert transport.last_request.action == "studies.list" # assert transport.last_request.body == "" # def test_get_studies_with_search_name_sets_request_body() -> None: -# transport = StubTransport(resources=[_study_resource("Cardio Study")]) +# transport = StubTransport(resources=[_study_resource("Cardio Study", total_records=5)]) # service = DefaultDataConnectService(transport) -# studies = service.get_studies(search_study_name="Cardio") +# result = service.get_studies(search_study_name="Cardio") -# assert len(studies) == 1 -# assert studies[0].name == "Cardio Study" +# assert isinstance(result, StudiesResult) +# assert result.total == 5 +# assert len(result.studies) == 1 +# assert result.studies[0].name == "Cardio Study" # assert transport.last_request is not None # assert transport.last_request.body == '{"search_study_name":"Cardio"}' -# def test_get_studies_rejects_non_string_search_name() -> None: +# def test_get_studies_returns_zero_total_when_no_resources() -> None: # transport = StubTransport(resources=[]) # service = DefaultDataConnectService(transport) -# with pytest.raises(ValidationError, match="search_study_name must be a string"): -# service.get_studies(search_study_name=123) # type: ignore[arg-type] +# result = service.get_studies() -# assert transport.last_request is None +# assert isinstance(result, StudiesResult) +# assert result.total == 0 +# assert result.studies == [] # def test_get_studies_accepts_none_search_name() -> None: # transport = StubTransport(resources=[_study_resource()]) # service = DefaultDataConnectService(transport) -# studies = service.get_studies(search_study_name=None) +# result = service.get_studies(search_study_name=None) -# assert len(studies) == 1 +# assert isinstance(result, StudiesResult) +# assert len(result.studies) == 1 # assert transport.last_request is not None # assert transport.last_request.body == "" - -# def test_get_dataset_versions_sorted(monkeypatch): -# service = DefaultDataConnectService(transport=DummyTransport()) -# # Patch the mapping function to our dummy -# monkeypatch.setattr( -# "dataconnect.service.default.resource_to_dataset_version", -# dummy_resource_to_dataset_version, -# ) -# result = service.get_dataset_versions(UUID("073410b6-79be-3e7d-ae37-92f6e054013e")) -# versions = [dv.dataset_version for dv in result] -# assert versions == sorted(versions, reverse=True) From 9cd281d92ca1b0da798b38990c37d03ecacda5fc Mon Sep 17 00:00:00 2001 From: Alex Fieraru Date: Wed, 20 May 2026 15:18:07 +0300 Subject: [PATCH 2/3] feat: fix linting and copilot comments --- dataconnect/client.py | 2 +- dataconnect/service/base.py | 2 +- dataconnect/service/default.py | 9 +++++---- tests/test_client.py | 26 +++++++++++++++----------- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/dataconnect/client.py b/dataconnect/client.py index 7dd8834..76d172a 100644 --- a/dataconnect/client.py +++ b/dataconnect/client.py @@ -12,7 +12,7 @@ import pandas as pd -from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, StudiesResult, Study +from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, StudiesResult from dataconnect.service import DataConnectService, DefaultDataConnectService _DEFAULT_HOST = "enodia-gateway.platform.imedidata.com" diff --git a/dataconnect/service/base.py b/dataconnect/service/base.py index 91f2f7e..c85fae6 100644 --- a/dataconnect/service/base.py +++ b/dataconnect/service/base.py @@ -7,7 +7,7 @@ import pandas as pd -from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, StudiesResult, Study +from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, StudiesResult class DataConnectService(ABC): diff --git a/dataconnect/service/default.py b/dataconnect/service/default.py index ed25ae0..0a597df 100644 --- a/dataconnect/service/default.py +++ b/dataconnect/service/default.py @@ -7,7 +7,7 @@ import pandas as pd from dataconnect.exceptions import ErrorDetail -from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, Pagination, StudiesResult, Study +from dataconnect.models import Dataset, DatasetVersion, PaginatedResponse, Pagination, StudiesResult from dataconnect.service.base import DataConnectService from dataconnect.service.error_handler import translate_error from dataconnect.service.mappers import ( @@ -16,7 +16,7 @@ resource_to_fetched_data, resource_to_study, ) -from dataconnect.service.validators import validate_positive_int, validate_uuid +from dataconnect.service.validators import validate_positive_int, validate_search_study_name, validate_uuid from dataconnect.transport.base import Transport from dataconnect.transport.errors import TransportError from dataconnect.transport.models import DatasetTicket, ResourceQuery @@ -42,9 +42,10 @@ def get_studies(self, search_study_name: str | None = None) -> StudiesResult: search_study_name: Optional full or partial study name filter. Returns: - A list of :class:`Study` objects matching the criteria. + A :class:`StudiesResult` containing: + - ``total``: total number of studies accessible to the authenticated user. + - ``studies``: list of :class:`Study` objects matching the criteria. """ - # validate_search_study_name(search_study_name) request = ResourceQuery(action=_ACTION_LIST_STUDIES) if search_study_name and search_study_name.strip() != "": diff --git a/tests/test_client.py b/tests/test_client.py index ccb0a53..1c6e231 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,14 +1,14 @@ # from __future__ import annotations -# import sys -# from types import ModuleType +# # import sys +# # from types import ModuleType # from uuid import UUID -# import pandas as pd -# import pytest +# # import pandas as pd +# # import pytest # from dataconnect.client import DataConnectClient -# from dataconnect.models import Dataset, DatasetVersion, Study +# from dataconnect.models import Dataset, DatasetVersion, Study, StudiesResult # def _make_dataframe() -> pd.DataFrame: @@ -160,9 +160,9 @@ # self.search_study_name: str | None = None # self.was_closed = False -# def get_studies(self, search_study_name: str | None = None) -> list[Study]: +# def get_studies(self, search_study_name: str | None = None) -> StudiesResult: # self.search_study_name = search_study_name -# return [] +# return StudiesResult(total=0, studies=[]) # def close(self) -> None: # self.was_closed = True @@ -172,9 +172,11 @@ # service = StubService() # client = DataConnectClient(service) -# studies = client.get_studies() +# result = client.get_studies() -# assert studies == [] +# assert isinstance(result, StudiesResult) +# assert result.total == 0 +# assert result.studies == [] # assert service.search_study_name is None @@ -182,9 +184,11 @@ # service = StubService() # client = DataConnectClient(service) -# studies = client.get_studies(search_study_name="cardio") +# result = client.get_studies(search_study_name="cardio") -# assert studies == [] +# assert isinstance(result, StudiesResult) +# assert result.total == 0 +# assert result.studies == [] # assert service.search_study_name == "cardio" From a8388796a532d189cc8147a3a12704fa4eae9d53 Mon Sep 17 00:00:00 2001 From: Alex Fieraru Date: Wed, 20 May 2026 15:24:12 +0300 Subject: [PATCH 3/3] feat: fix linting --- dataconnect/service/default.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dataconnect/service/default.py b/dataconnect/service/default.py index 0a597df..2e35e34 100644 --- a/dataconnect/service/default.py +++ b/dataconnect/service/default.py @@ -16,7 +16,7 @@ resource_to_fetched_data, resource_to_study, ) -from dataconnect.service.validators import validate_positive_int, validate_search_study_name, validate_uuid +from dataconnect.service.validators import validate_positive_int, validate_uuid from dataconnect.transport.base import Transport from dataconnect.transport.errors import TransportError from dataconnect.transport.models import DatasetTicket, ResourceQuery