From b3452e544b3439a3ab432a325b65832b00c336d2 Mon Sep 17 00:00:00 2001 From: Nastassia Makarava Date: Thu, 7 May 2026 15:19:30 -0400 Subject: [PATCH 1/9] feat: added search_study_name parameter and unit tests --- dataconnect/client.py | 4 +- dataconnect/service/base.py | 2 +- dataconnect/service/default.py | 7 ++- dataconnect/service/validators.py | 18 ++++++ dataconnect/test.ipynb | 88 ++++++++++++++++++++++++++++++ tests/test_client.py | 49 ++++++++++++++--- tests/test_service_default.py | 91 +++++++++++++++++++++++++++++++ 7 files changed, 248 insertions(+), 11 deletions(-) create mode 100644 dataconnect/service/validators.py create mode 100644 dataconnect/test.ipynb create mode 100644 tests/test_service_default.py diff --git a/dataconnect/client.py b/dataconnect/client.py index 84ce20d..346f5f6 100644 --- a/dataconnect/client.py +++ b/dataconnect/client.py @@ -42,9 +42,9 @@ def connect( # Public API - def get_studies(self) -> list[Study]: + def get_studies(self, search_study_name: str | None = None) -> list[Study]: """List the studies the client is authorized to access.""" - return self._service.get_studies() + return self._service.get_studies(search_study_name=search_study_name) # Lifecycle diff --git a/dataconnect/service/base.py b/dataconnect/service/base.py index 78a26b0..bf56dc6 100644 --- a/dataconnect/service/base.py +++ b/dataconnect/service/base.py @@ -11,7 +11,7 @@ class DataConnectService(ABC): """Abstract service interface — defines all operations available to the client.""" @abstractmethod - def get_studies(self) -> list[Study]: ... + def get_studies(self, search_study_name: str | None = None) -> list[Study]: ... @abstractmethod def close(self) -> None: ... diff --git a/dataconnect/service/default.py b/dataconnect/service/default.py index dd287cf..b91c8f5 100644 --- a/dataconnect/service/default.py +++ b/dataconnect/service/default.py @@ -15,6 +15,7 @@ from dataconnect.models import Study from dataconnect.service.base import DataConnectService from dataconnect.service.mappers import resource_to_study +from dataconnect.service.validators import validate_search_study_name from dataconnect.transport.base import Transport from dataconnect.transport.errors import ( TransportAuthenticationError, @@ -58,9 +59,13 @@ def __init__(self, transport: Transport) -> None: # DataConnectService - def get_studies(self) -> list[Study]: + def get_studies(self, search_study_name: str | None = None) -> list[Study]: + + validate_search_study_name(search_study_name) request = ResourceQuery(action=_ACTION_LIST_STUDIES) + if search_study_name is not None: + request = request.append_body({"searchStudyName": search_study_name}) try: resources = self._transport.list_resources(request) diff --git a/dataconnect/service/validators.py b/dataconnect/service/validators.py new file mode 100644 index 0000000..4f4881b --- /dev/null +++ b/dataconnect/service/validators.py @@ -0,0 +1,18 @@ +"""Input validation helpers for service-layer operations.""" + +from __future__ import annotations + +from dataconnect.exceptions import ValidationError + + +def validate_search_study_name(search_study_name: str | None) -> None: + """Validate optional study-name filter used by ``get_studies``.""" + + if search_study_name is None: + return + + if not isinstance(search_study_name, str): + raise ValidationError("search_study_name must be a string") + + if not search_study_name.strip(): + raise ValidationError("search_study_name must not be empty") diff --git a/dataconnect/test.ipynb b/dataconnect/test.ipynb new file mode 100644 index 0000000..8e1f93b --- /dev/null +++ b/dataconnect/test.ipynb @@ -0,0 +1,88 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "a1aed35a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "from dataconnect import DataConnectClient\n", + "\n", + "with DataConnectClient.connect(\n", + " host=\"enodia-gateway-sandbox.platform.imedidata.net\",\n", + " port=443,\n", + " token=\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiNDQ1ZmFjNi1hOTE2LTRlM2EtOGFlMS04ZmY1ZDJmMjE0ZmMiLCJzdWIiOiJhMGIxZWQxZS05YTNmLTQ4Y2YtODE5Yy1iMWExZGIwYTgyMmQiLCJhdWQiOlsiYjQ0NWZhYzYtYTkxNi00ZTNhLThhZTEtOGZmNWQyZjIxNGZjIl0sImV4cCI6MTc4NTk1NjEzMCwiaWF0IjoxNzc4MTgwMTMwLCJqdGkiOiIxM2VhMjgwZC1kODZiLTQ3OTktOTJkYS01ZTRmZGYwOWU4ZWYifQ.o0H_xM-_COmqa33HBbDgJ-A7E1u6PeNv5giIPy44QIs9IthbJJyecNwVanTogWiAZMqnq6pXVu41GnyjYEt-d8Oh_HOUibH0xu-8KEnw-8ZcStuCyzmj4qLJjJmm4YHE05W0KQNG2BlZ9f2073y_8oxfrkytSxAtJZlJEVOUZBWI9VA9S1eUMGSGTdAv7Io4lnM66kNseCaJzzuFjIb3fgbrIS5tDq7kgR34-jDGQXULHUF5oziS9w5JbSURAppKoIeTvrht4biSXJA79J0K8qq7f271zdFeaCf7BYx9-MlrOuR0YYTBO2puqPuNLWYtQqofu1jd3LkiDwJIRkS2DQ\",\n", + ") as client:\n", + " print(client)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "902399ab", + "metadata": {}, + "outputs": [ + { + "ename": "ConnectionError", + "evalue": "Unexpected error during list_resources: FlightClient is closed", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mArrowInvalid\u001b[39m Traceback (most recent call last)", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/dataconnect-library-python/dataconnect/transport/arrow_flight/transport.py:89\u001b[39m, in \u001b[36mArrowFlightTransport.list_resources\u001b[39m\u001b[34m(self, request)\u001b[39m\n\u001b[32m 88\u001b[39m raw_flights = \u001b[38;5;28mself\u001b[39m._client.list_flights(criteria, \u001b[38;5;28mself\u001b[39m._options())\n\u001b[32m---> \u001b[39m\u001b[32m89\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43m[\u001b[39;49m\u001b[30;43m_to_resource_info\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mf\u001b[39;49m\u001b[30;43m)\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43;01mfor\u001b[39;49;00m\u001b[30;43m \u001b[39;49m\u001b[30;43mf\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43;01min\u001b[39;49;00m\u001b[30;43m \u001b[39;49m\u001b[30;43mraw_flights\u001b[39;49m\u001b[30;43m]\u001b[39;49m\n\u001b[32m 91\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m flight.FlightUnauthenticatedError \u001b[38;5;28;01mas\u001b[39;00m ex:\n", + "\u001b[36mFile \u001b[39m\u001b[32mpyarrow/_flight.pyx:1649\u001b[39m, in \u001b[36mlist_flights\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m-> \u001b[39m\u001b[32m1649\u001b[39m \u001b[33m'Could not get source, probably due dynamically evaluated source code.'\u001b[39m\n", + "\u001b[36mFile \u001b[39m\u001b[32mpyarrow/_flight.pyx:1653\u001b[39m, in \u001b[36mpyarrow._flight.FlightClient.list_flights\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m-> \u001b[39m\u001b[32m1653\u001b[39m \u001b[33m'Could not get source, probably due dynamically evaluated source code.'\u001b[39m\n", + "\u001b[36mFile \u001b[39m\u001b[32mpyarrow/_flight.pyx:78\u001b[39m, in \u001b[36mpyarrow._flight.check_flight_status\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m---> \u001b[39m\u001b[32m78\u001b[39m \u001b[33m'Could not get source, probably due dynamically evaluated source code.'\u001b[39m\n", + "\u001b[36mFile \u001b[39m\u001b[32mpyarrow/error.pxi:92\u001b[39m, in \u001b[36mpyarrow.lib.check_status\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m---> \u001b[39m\u001b[32m92\u001b[39m \u001b[33m'Could not get source, probably due dynamically evaluated source code.'\u001b[39m\n", + "\u001b[31mArrowInvalid\u001b[39m: FlightClient is closed", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[31mTransportConnectionError\u001b[39m Traceback (most recent call last)", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/dataconnect-library-python/dataconnect/service/default.py:66\u001b[39m, in \u001b[36mDefaultDataConnectService.get_studies\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 65\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m66\u001b[39m resources = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_transport\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mlist_resources\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 67\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m TransportError \u001b[38;5;28;01mas\u001b[39;00m ex:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/dataconnect-library-python/dataconnect/transport/arrow_flight/transport.py:102\u001b[39m, in \u001b[36mArrowFlightTransport.list_resources\u001b[39m\u001b[34m(self, request)\u001b[39m\n\u001b[32m 101\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m ex:\n\u001b[32m--> \u001b[39m\u001b[32m102\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m TransportConnectionError(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mUnexpected error during list_resources: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mex\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mex\u001b[39;00m\n", + "\u001b[31mTransportConnectionError\u001b[39m: Unexpected error during list_resources: FlightClient is closed", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[31mConnectionError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m studies = client.get_studies()\n\u001b[32m 2\u001b[39m print(studies)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/dataconnect-library-python/dataconnect/client.py:47\u001b[39m, in \u001b[36mDataConnectClient.get_studies\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 45\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mget_studies\u001b[39m(\u001b[38;5;28mself\u001b[39m) -> \u001b[38;5;28mlist\u001b[39m[Study]:\n\u001b[32m 46\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"List the studies the client is authorized to access.\"\"\"\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m47\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_service\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mget_studies\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/dataconnect-library-python/dataconnect/service/default.py:68\u001b[39m, in \u001b[36mDefaultDataConnectService.get_studies\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 66\u001b[39m resources = \u001b[38;5;28mself\u001b[39m._transport.list_resources(request)\n\u001b[32m 67\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m TransportError \u001b[38;5;28;01mas\u001b[39;00m ex:\n\u001b[32m---> \u001b[39m\u001b[32m68\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m _translate_error(ex) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mex\u001b[39;00m\n\u001b[32m 70\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 71\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m [resource_to_study(r) \u001b[38;5;28;01mfor\u001b[39;00m r \u001b[38;5;129;01min\u001b[39;00m resources]\n", + "\u001b[31mConnectionError\u001b[39m: Unexpected error during list_resources: FlightClient is closed" + ] + } + ], + "source": [ + "studies = client.get_studies()\n", + "print(studies)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tests/test_client.py b/tests/test_client.py index cb45e43..d71a399 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,11 +1,46 @@ -import pytest +from __future__ import annotations +from dataconnect.client import DataConnectClient +from dataconnect.models import Study -def test_client() -> None: - assert True +class StubService: + def __init__(self) -> None: + self.search_study_name: str | None = None + self.was_closed = False -@pytest.mark.benchmark -def test_dummy_benchmark() -> None: - # Dummy benchmark test to satisfy CI - assert True + def get_studies(self, search_study_name: str | None = None) -> list[Study]: + self.search_study_name = search_study_name + return [] + + def close(self) -> None: + self.was_closed = True + + +def test_get_studies_without_filter_delegates_to_service() -> None: + service = StubService() + client = DataConnectClient(service) + + studies = client.get_studies() + + assert studies == [] + assert service.search_study_name is None + + +def test_get_studies_with_filter_delegates_to_service() -> None: + service = StubService() + client = DataConnectClient(service) + + studies = client.get_studies(search_study_name="cardio") + + assert studies == [] + assert service.search_study_name == "cardio" + + +def test_close_delegates_to_service() -> None: + service = StubService() + client = DataConnectClient(service) + + client.close() + + assert service.was_closed diff --git a/tests/test_service_default.py b/tests/test_service_default.py new file mode 100644 index 0000000..fec7416 --- /dev/null +++ b/tests/test_service_default.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import pytest + +from dataconnect.exceptions import ValidationError +from dataconnect.service.default import DefaultDataConnectService +from dataconnect.transport.models import DataRef, ResourceInfo, ResourceQuery + + +class StubTransport: + def __init__(self, resources: list[ResourceInfo]) -> None: + self.resources = resources + self.last_request: ResourceQuery | None = None + + def list_resources(self, request: ResourceQuery) -> list[ResourceInfo]: + self.last_request = request + return self.resources + + def close(self) -> None: + return None + + +def _study_resource(name: str = "Study A") -> ResourceInfo: + payload = ( + '{"uuid":"12345678-1234-1234-1234-123456789abc",' + f'"name":"{name}",' + '"environments":[]}' + ).encode("utf-8") + + return ResourceInfo( + descriptor=b"", + endpoints=[DataRef(ticket=payload)], + total_records=1, + schema_bytes=b"", + ) + + +def test_get_studies_without_search_name_uses_empty_request_body() -> None: + transport = StubTransport(resources=[_study_resource()]) + service = DefaultDataConnectService(transport) + + studies = service.get_studies() + + assert len(studies) == 1 + assert 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")]) + service = DefaultDataConnectService(transport) + + studies = service.get_studies(search_study_name="Cardio") + + assert len(studies) == 1 + assert studies[0].name == "Cardio Study" + assert transport.last_request is not None + assert transport.last_request.body == '{"searchStudyName":"Cardio"}' + + +def test_get_studies_rejects_empty_search_name() -> None: + transport = StubTransport(resources=[]) + service = DefaultDataConnectService(transport) + + with pytest.raises(ValidationError, match="search_study_name must not be empty"): + service.get_studies(search_study_name=" ") + + assert transport.last_request is None + + +def test_get_studies_rejects_non_string_search_name() -> 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] + + assert transport.last_request is None + + +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) + + assert len(studies) == 1 + assert transport.last_request is not None + assert transport.last_request.body == "" From a1bf019439d0f803b4bc71b06c794d374669bbf0 Mon Sep 17 00:00:00 2001 From: Nastassia Makarava Date: Thu, 7 May 2026 15:24:12 -0400 Subject: [PATCH 2/9] Removed test file with test token --- dataconnect/test.ipynb | 88 ------------------------------------------ 1 file changed, 88 deletions(-) delete mode 100644 dataconnect/test.ipynb diff --git a/dataconnect/test.ipynb b/dataconnect/test.ipynb deleted file mode 100644 index 8e1f93b..0000000 --- a/dataconnect/test.ipynb +++ /dev/null @@ -1,88 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "a1aed35a", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n" - ] - } - ], - "source": [ - "from dataconnect import DataConnectClient\n", - "\n", - "with DataConnectClient.connect(\n", - " host=\"enodia-gateway-sandbox.platform.imedidata.net\",\n", - " port=443,\n", - " token=\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiNDQ1ZmFjNi1hOTE2LTRlM2EtOGFlMS04ZmY1ZDJmMjE0ZmMiLCJzdWIiOiJhMGIxZWQxZS05YTNmLTQ4Y2YtODE5Yy1iMWExZGIwYTgyMmQiLCJhdWQiOlsiYjQ0NWZhYzYtYTkxNi00ZTNhLThhZTEtOGZmNWQyZjIxNGZjIl0sImV4cCI6MTc4NTk1NjEzMCwiaWF0IjoxNzc4MTgwMTMwLCJqdGkiOiIxM2VhMjgwZC1kODZiLTQ3OTktOTJkYS01ZTRmZGYwOWU4ZWYifQ.o0H_xM-_COmqa33HBbDgJ-A7E1u6PeNv5giIPy44QIs9IthbJJyecNwVanTogWiAZMqnq6pXVu41GnyjYEt-d8Oh_HOUibH0xu-8KEnw-8ZcStuCyzmj4qLJjJmm4YHE05W0KQNG2BlZ9f2073y_8oxfrkytSxAtJZlJEVOUZBWI9VA9S1eUMGSGTdAv7Io4lnM66kNseCaJzzuFjIb3fgbrIS5tDq7kgR34-jDGQXULHUF5oziS9w5JbSURAppKoIeTvrht4biSXJA79J0K8qq7f271zdFeaCf7BYx9-MlrOuR0YYTBO2puqPuNLWYtQqofu1jd3LkiDwJIRkS2DQ\",\n", - ") as client:\n", - " print(client)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "902399ab", - "metadata": {}, - "outputs": [ - { - "ename": "ConnectionError", - "evalue": "Unexpected error during list_resources: FlightClient is closed", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mArrowInvalid\u001b[39m Traceback (most recent call last)", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/dataconnect-library-python/dataconnect/transport/arrow_flight/transport.py:89\u001b[39m, in \u001b[36mArrowFlightTransport.list_resources\u001b[39m\u001b[34m(self, request)\u001b[39m\n\u001b[32m 88\u001b[39m raw_flights = \u001b[38;5;28mself\u001b[39m._client.list_flights(criteria, \u001b[38;5;28mself\u001b[39m._options())\n\u001b[32m---> \u001b[39m\u001b[32m89\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43m[\u001b[39;49m\u001b[30;43m_to_resource_info\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mf\u001b[39;49m\u001b[30;43m)\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43;01mfor\u001b[39;49;00m\u001b[30;43m \u001b[39;49m\u001b[30;43mf\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43;01min\u001b[39;49;00m\u001b[30;43m \u001b[39;49m\u001b[30;43mraw_flights\u001b[39;49m\u001b[30;43m]\u001b[39;49m\n\u001b[32m 91\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m flight.FlightUnauthenticatedError \u001b[38;5;28;01mas\u001b[39;00m ex:\n", - "\u001b[36mFile \u001b[39m\u001b[32mpyarrow/_flight.pyx:1649\u001b[39m, in \u001b[36mlist_flights\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m-> \u001b[39m\u001b[32m1649\u001b[39m \u001b[33m'Could not get source, probably due dynamically evaluated source code.'\u001b[39m\n", - "\u001b[36mFile \u001b[39m\u001b[32mpyarrow/_flight.pyx:1653\u001b[39m, in \u001b[36mpyarrow._flight.FlightClient.list_flights\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m-> \u001b[39m\u001b[32m1653\u001b[39m \u001b[33m'Could not get source, probably due dynamically evaluated source code.'\u001b[39m\n", - "\u001b[36mFile \u001b[39m\u001b[32mpyarrow/_flight.pyx:78\u001b[39m, in \u001b[36mpyarrow._flight.check_flight_status\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m---> \u001b[39m\u001b[32m78\u001b[39m \u001b[33m'Could not get source, probably due dynamically evaluated source code.'\u001b[39m\n", - "\u001b[36mFile \u001b[39m\u001b[32mpyarrow/error.pxi:92\u001b[39m, in \u001b[36mpyarrow.lib.check_status\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m---> \u001b[39m\u001b[32m92\u001b[39m \u001b[33m'Could not get source, probably due dynamically evaluated source code.'\u001b[39m\n", - "\u001b[31mArrowInvalid\u001b[39m: FlightClient is closed", - "\nThe above exception was the direct cause of the following exception:\n", - "\u001b[31mTransportConnectionError\u001b[39m Traceback (most recent call last)", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/dataconnect-library-python/dataconnect/service/default.py:66\u001b[39m, in \u001b[36mDefaultDataConnectService.get_studies\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 65\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m66\u001b[39m resources = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_transport\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mlist_resources\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 67\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m TransportError \u001b[38;5;28;01mas\u001b[39;00m ex:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/dataconnect-library-python/dataconnect/transport/arrow_flight/transport.py:102\u001b[39m, in \u001b[36mArrowFlightTransport.list_resources\u001b[39m\u001b[34m(self, request)\u001b[39m\n\u001b[32m 101\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m ex:\n\u001b[32m--> \u001b[39m\u001b[32m102\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m TransportConnectionError(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mUnexpected error during list_resources: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mex\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mex\u001b[39;00m\n", - "\u001b[31mTransportConnectionError\u001b[39m: Unexpected error during list_resources: FlightClient is closed", - "\nThe above exception was the direct cause of the following exception:\n", - "\u001b[31mConnectionError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m studies = client.get_studies()\n\u001b[32m 2\u001b[39m print(studies)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/dataconnect-library-python/dataconnect/client.py:47\u001b[39m, in \u001b[36mDataConnectClient.get_studies\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 45\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mget_studies\u001b[39m(\u001b[38;5;28mself\u001b[39m) -> \u001b[38;5;28mlist\u001b[39m[Study]:\n\u001b[32m 46\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"List the studies the client is authorized to access.\"\"\"\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m47\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_service\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mget_studies\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/dataconnect-library-python/dataconnect/service/default.py:68\u001b[39m, in \u001b[36mDefaultDataConnectService.get_studies\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 66\u001b[39m resources = \u001b[38;5;28mself\u001b[39m._transport.list_resources(request)\n\u001b[32m 67\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m TransportError \u001b[38;5;28;01mas\u001b[39;00m ex:\n\u001b[32m---> \u001b[39m\u001b[32m68\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m _translate_error(ex) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mex\u001b[39;00m\n\u001b[32m 70\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 71\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m [resource_to_study(r) \u001b[38;5;28;01mfor\u001b[39;00m r \u001b[38;5;129;01min\u001b[39;00m resources]\n", - "\u001b[31mConnectionError\u001b[39m: Unexpected error during list_resources: FlightClient is closed" - ] - } - ], - "source": [ - "studies = client.get_studies()\n", - "print(studies)\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.13.11" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From a7d9c6145e9a7d04f859b7813956dba0ff40cc43 Mon Sep 17 00:00:00 2001 From: Nastassia Makarava Date: Thu, 7 May 2026 16:19:34 -0400 Subject: [PATCH 3/9] fix: Apply ruff lint and format fixes --- tests/test_service_default.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/test_service_default.py b/tests/test_service_default.py index fec7416..1670474 100644 --- a/tests/test_service_default.py +++ b/tests/test_service_default.py @@ -21,11 +21,7 @@ def close(self) -> None: def _study_resource(name: str = "Study A") -> ResourceInfo: - payload = ( - '{"uuid":"12345678-1234-1234-1234-123456789abc",' - f'"name":"{name}",' - '"environments":[]}' - ).encode("utf-8") + payload = (f'{{"uuid":"12345678-1234-1234-1234-123456789abc","name":"{name}","environments":[]}}').encode() return ResourceInfo( descriptor=b"", From 7a5a8c0eafb1c965d318a24207afabc5a409ddff Mon Sep 17 00:00:00 2001 From: Nastassia Makarava Date: Thu, 7 May 2026 16:28:38 -0400 Subject: [PATCH 4/9] fix: tests --- tests/test_client.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index d71a399..648289c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,9 +1,21 @@ from __future__ import annotations +import pytest + from dataconnect.client import DataConnectClient from dataconnect.models import Study +def test_client() -> None: + assert True + + +@pytest.mark.benchmark +def test_dummy_benchmark() -> None: + # Dummy benchmark test to satisfy CI + assert True + + class StubService: def __init__(self) -> None: self.search_study_name: str | None = None From 9d2556ee89b7d77e6f455d5f9f014e1e23c8384e Mon Sep 17 00:00:00 2001 From: Nastassia Makarava Date: Fri, 8 May 2026 08:56:32 -0400 Subject: [PATCH 5/9] fix: query param name --- dataconnect/service/default.py | 2 +- tests/test_service_default.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dataconnect/service/default.py b/dataconnect/service/default.py index b91c8f5..679649d 100644 --- a/dataconnect/service/default.py +++ b/dataconnect/service/default.py @@ -65,7 +65,7 @@ def get_studies(self, search_study_name: str | None = None) -> list[Study]: request = ResourceQuery(action=_ACTION_LIST_STUDIES) if search_study_name is not None: - request = request.append_body({"searchStudyName": search_study_name}) + request = request.append_body({"search_study_name": search_study_name}) try: resources = self._transport.list_resources(request) diff --git a/tests/test_service_default.py b/tests/test_service_default.py index 1670474..0930327 100644 --- a/tests/test_service_default.py +++ b/tests/test_service_default.py @@ -53,7 +53,7 @@ def test_get_studies_with_search_name_sets_request_body() -> None: assert len(studies) == 1 assert studies[0].name == "Cardio Study" assert transport.last_request is not None - assert transport.last_request.body == '{"searchStudyName":"Cardio"}' + assert transport.last_request.body == '{"search_study_name":"Cardio"}' def test_get_studies_rejects_empty_search_name() -> None: From 98a590060d058a045722025046d65ec3e872029d Mon Sep 17 00:00:00 2001 From: Nastassia Makarava Date: Fri, 8 May 2026 10:23:40 -0400 Subject: [PATCH 6/9] fix: updated validation to allow empty string to be aligned with R --- dataconnect/service/validators.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dataconnect/service/validators.py b/dataconnect/service/validators.py index 4f4881b..c4b003c 100644 --- a/dataconnect/service/validators.py +++ b/dataconnect/service/validators.py @@ -13,6 +13,3 @@ def validate_search_study_name(search_study_name: str | None) -> None: if not isinstance(search_study_name, str): raise ValidationError("search_study_name must be a string") - - if not search_study_name.strip(): - raise ValidationError("search_study_name must not be empty") From a1495d293895bd3fa481b176b1e36db98efbb0a1 Mon Sep 17 00:00:00 2001 From: Nastassia Makarava Date: Fri, 8 May 2026 10:48:34 -0400 Subject: [PATCH 7/9] fix: fixed tests --- tests/test_service_default.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/test_service_default.py b/tests/test_service_default.py index 0930327..d1ac135 100644 --- a/tests/test_service_default.py +++ b/tests/test_service_default.py @@ -56,16 +56,6 @@ def test_get_studies_with_search_name_sets_request_body() -> None: assert transport.last_request.body == '{"search_study_name":"Cardio"}' -def test_get_studies_rejects_empty_search_name() -> None: - transport = StubTransport(resources=[]) - service = DefaultDataConnectService(transport) - - with pytest.raises(ValidationError, match="search_study_name must not be empty"): - service.get_studies(search_study_name=" ") - - assert transport.last_request is None - - def test_get_studies_rejects_non_string_search_name() -> None: transport = StubTransport(resources=[]) service = DefaultDataConnectService(transport) From f6c07c4ca61bb6606e38afa3cdbd7cf76f849b40 Mon Sep 17 00:00:00 2001 From: Nastassia Makarava Date: Fri, 8 May 2026 13:49:08 -0400 Subject: [PATCH 8/9] fix: fixes per feedbacks --- dataconnect/service/default.py | 2 +- dataconnect/service/validators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dataconnect/service/default.py b/dataconnect/service/default.py index 679649d..9a83299 100644 --- a/dataconnect/service/default.py +++ b/dataconnect/service/default.py @@ -64,7 +64,7 @@ def get_studies(self, search_study_name: str | None = None) -> list[Study]: validate_search_study_name(search_study_name) request = ResourceQuery(action=_ACTION_LIST_STUDIES) - if search_study_name is not None: + if search_study_name and search_study_name.strip() != "": request = request.append_body({"search_study_name": search_study_name}) try: diff --git a/dataconnect/service/validators.py b/dataconnect/service/validators.py index c4b003c..5909d36 100644 --- a/dataconnect/service/validators.py +++ b/dataconnect/service/validators.py @@ -8,7 +8,7 @@ def validate_search_study_name(search_study_name: str | None) -> None: """Validate optional study-name filter used by ``get_studies``.""" - if search_study_name is None: + if not search_study_name: return if not isinstance(search_study_name, str): From 414c0378d6fd01e07cc0480a2cb5fa94f64025e2 Mon Sep 17 00:00:00 2001 From: Nastassia Makarava Date: Fri, 8 May 2026 14:07:59 -0400 Subject: [PATCH 9/9] fix: fixed tests --- tests/test_client.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index 471a134..92ad150 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -28,13 +28,6 @@ def close(self) -> None: self.closed += 1 -def test_get_studies_returns_service_result() -> None: - studies = [Study(uuid=UUID("64a98a9b-1512-44c8-92af-e4cab0183670"), name="Study A")] - client = DataConnectClient(_FakeService(studies=studies)) - - assert client.get_studies() == studies - - def test_get_dataset_versions_forwards_uuid_to_service() -> None: dataset_uuid = UUID("073410b6-79be-3e7d-ae37-92f6e054013e") versions = [