Skip to content

Commit da03d94

Browse files
committed
feat: MCC-1496348 dataset versions
1 parent f9ea4d2 commit da03d94

12 files changed

Lines changed: 291 additions & 15 deletions

File tree

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ __pycache__/
66
# C extensions
77
*.so
88

9+
# Jupyter Notebook
10+
.ipynb_checkpoints
11+
*.ipynb
12+
913
# Distribution / packaging
1014
.Python
1115
build/

‎.pre-commit-config.yaml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
# pre-commit autoupdate
1313

1414
default_language_version:
15-
python: python3.11
15+
# Use the system 'python3' interpreter so pre-commit picks up the active
16+
# Python in your PATH (or the project's virtualenv). This avoids failures
17+
# when a specific patch-level interpreter like python3.11 is not installed.
18+
python: python3
1619

1720
repos:
1821
# ---------------------------------------------------------------------------

‎README.md‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from dataconnect import DataConnectClientfrom dataconnect import DataConnectClient
2-
31
# dataconnect-library-python
42

53
Python SDK for the [Medidata DataConnect](https://github.com/mdsol/dataconnect-library-r) service.
@@ -8,8 +6,7 @@ Python SDK for the [Medidata DataConnect](https://github.com/mdsol/dataconnect-l
86

97
## Transport note
108

11-
The DataConnect service uses **Apache Arrow Flight** (gRPC binary protocol),
12-
**not** a plain REST/HTTP API. `pyarrow.flight` is the primary transport
9+
The DataConnect service uses **Apache Arrow Flight** (gRPC binary protocol), and **not** a plain REST/HTTP API. `pyarrow.flight` is the primary transport
1310
dependency.
1411

1512
---

‎dataconnect/__init__.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
ServerError,
1414
ValidationError,
1515
)
16-
from dataconnect.models import Study, StudyEnvironment
16+
from dataconnect.models import DatasetVersion, Study, StudyEnvironment
1717

1818
__all__ = [
1919
# Client
2020
"DataConnectClient",
2121
# Domain models
2222
"Study",
2323
"StudyEnvironment",
24+
"DatasetVersion",
2425
# Exceptions — catch these in user application code
2526
"DataConnectError",
2627
"ConnectionError",

‎dataconnect/client.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
from __future__ import annotations
99

1010
from types import TracebackType
11+
from uuid import UUID
1112

12-
from dataconnect.models import Study
13+
from dataconnect.models import DatasetVersion, Study
1314
from dataconnect.service import DataConnectService, DefaultDataConnectService
1415

1516
_DEFAULT_HOST = "enodia-gateway.platform.imedidata.com"
@@ -46,6 +47,10 @@ def get_studies(self) -> list[Study]:
4647
"""List the studies the client is authorized to access."""
4748
return self._service.get_studies()
4849

50+
def get_dataset_versions(self, dataset_uuid: UUID) -> list[DatasetVersion]:
51+
"""List the dataset versions the client is authorized to access."""
52+
return self._service.get_dataset_versions(dataset_uuid)
53+
4954
# Lifecycle
5055

5156
def close(self) -> None:

‎dataconnect/models.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,12 @@ class Study:
1515
uuid: UUID
1616
name: str
1717
environments: list[StudyEnvironment] = field(default_factory=list)
18+
19+
20+
@dataclass(frozen=True)
21+
class DatasetVersion:
22+
study_uuid: UUID
23+
study_env_uuid: UUID
24+
dataset_uuid: UUID
25+
dataset_name: str
26+
dataset_version: str

‎dataconnect/service/base.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
from __future__ import annotations
44

55
from abc import ABC, abstractmethod
6+
from uuid import UUID
67

7-
from dataconnect.models import Study
8+
from dataconnect.models import DatasetVersion, Study
89

910

1011
class DataConnectService(ABC):
@@ -13,5 +14,8 @@ class DataConnectService(ABC):
1314
@abstractmethod
1415
def get_studies(self) -> list[Study]: ...
1516

17+
@abstractmethod
18+
def get_dataset_versions(self, dataset_uuid: UUID) -> list[DatasetVersion]: ...
19+
1620
@abstractmethod
1721
def close(self) -> None: ...

‎dataconnect/service/default.py‎

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from uuid import UUID
6+
57
from dataconnect.exceptions import (
68
AuthenticationError,
79
AuthorizationError,
@@ -12,9 +14,9 @@
1214
ServerError,
1315
ValidationError,
1416
)
15-
from dataconnect.models import Study
17+
from dataconnect.models import DatasetVersion, Study
1618
from dataconnect.service.base import DataConnectService
17-
from dataconnect.service.mappers import resource_to_study
19+
from dataconnect.service.mappers import resource_to_dataset_version, resource_to_study
1820
from dataconnect.transport.base import Transport
1921
from dataconnect.transport.errors import (
2022
TransportAuthenticationError,
@@ -29,6 +31,7 @@
2931

3032
# Server action identifiers
3133
_ACTION_LIST_STUDIES = "studies.list"
34+
_ACTION_LIST_DATASET_VERSIONS = "dataset_versions.list"
3235

3336

3437
def _translate_error(ex: TransportError) -> DataConnectError:
@@ -69,9 +72,22 @@ def get_studies(self) -> list[Study]:
6972

7073
try:
7174
return [resource_to_study(r) for r in resources]
72-
except (KeyError, TypeError, ValueError) as ex:
75+
except (IndexError, KeyError, TypeError, ValueError) as ex:
7376
raise ValidationError(f"Unexpected studies response format: {ex}") from ex
7477

78+
def get_dataset_versions(self, dataset_uuid: UUID) -> list[DatasetVersion]:
79+
request = ResourceQuery(action=_ACTION_LIST_DATASET_VERSIONS).append_body({"dataset_uuid": str(dataset_uuid)})
80+
81+
try:
82+
resources = self._transport.list_resources(request)
83+
except TransportError as ex:
84+
raise _translate_error(ex) from ex
85+
86+
try:
87+
return [resource_to_dataset_version(r) for r in resources]
88+
except (IndexError, KeyError, TypeError, ValueError) as ex:
89+
raise ValidationError(f"Unexpected dataset versions response format: {ex}") from ex
90+
7591
def close(self) -> None:
7692

7793
try:

‎dataconnect/service/mappers.py‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,38 @@
1010
import json
1111
from uuid import UUID
1212

13-
from dataconnect.models import Study, StudyEnvironment
13+
from dataconnect.exceptions import NotFoundError
14+
from dataconnect.models import DatasetVersion, Study, StudyEnvironment
1415
from dataconnect.transport.models import ResourceInfo
1516

1617

1718
def resource_to_study(resource: ResourceInfo) -> Study:
1819
"""Parse a transport-layer ``ResourceInfo`` into a ``Study`` domain object."""
1920

21+
if not resource or not resource.endpoints or not resource.endpoints[0].ticket:
22+
raise NotFoundError("Invalid resource: missing endpoints or ticket")
23+
2024
data = json.loads(resource.endpoints[0].ticket.decode("utf-8"))
2125

2226
return Study(
2327
uuid=UUID(data["uuid"]),
2428
name=data["name"],
2529
environments=[StudyEnvironment(uuid=UUID(e["uuid"]), name=e["name"]) for e in data.get("environments", [])],
2630
)
31+
32+
33+
def resource_to_dataset_version(resource: ResourceInfo) -> DatasetVersion:
34+
"""Parse a transport-layer ``ResourceInfo`` into a ``DatasetVersion`` domain object."""
35+
36+
if not resource or not resource.endpoints or not resource.endpoints[0].ticket:
37+
raise NotFoundError("Invalid resource: missing endpoints or ticket")
38+
39+
data = json.loads(resource.endpoints[0].ticket.decode("utf-8"))
40+
41+
return DatasetVersion(
42+
study_uuid=UUID(data["study_uuid"]),
43+
study_env_uuid=UUID(data["study_env_uuid"]),
44+
dataset_uuid=UUID(data["dataset_uuid"]),
45+
dataset_name=data["dataset_name"],
46+
dataset_version=data["dataset_version"],
47+
)

‎dataconnect/transport/arrow_flight/transport.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def _to_resource_info(info: flight.FlightInfo) -> ResourceInfo:
3939
# Maps service-layer action names to the flight_type value the Arrow Flight server expects.
4040
_ACTION_FLIGHT_TYPE: dict[str, str] = {
4141
"studies.list": "STUDIES",
42+
"dataset_versions.list": "VERSIONS",
4243
}
4344

4445

@@ -77,7 +78,9 @@ def list_resources(self, request: ResourceQuery) -> list[ResourceInfo]:
7778
flight_type = _ACTION_FLIGHT_TYPE.get(request.action)
7879

7980
if flight_type is None:
80-
raise TransportConnectionError(f"Unknown action: {request.action!r}")
81+
raise TransportStatusError(
82+
f"Unknown action: {request.action!r}", status_code=3, grpc_status="INVALID_ARGUMENT"
83+
)
8184

8285
body = json.loads(request.body) if request.body else {}
8386
criteria = json.dumps({**body, "flight_type": flight_type}, separators=(",", ":")).encode("utf-8")

0 commit comments

Comments
 (0)