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
27 changes: 27 additions & 0 deletions dataconnect/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from dataconnect.models import (
Dataset,
DatasetVersion,
DatetimeFormatsResult,
DryPublishResult,
PaginatedResponse,
PublishResult,
Expand Down Expand Up @@ -169,6 +170,32 @@ def publish(
datetime_formats=datetime_formats,
)

def get_datetime_formats(
self,
project_token: str,
format_type: str = "all",
) -> DatetimeFormatsResult:
"""Return the supported datetime formats filtered by ``format_type``.

Delegates directly to :meth:`DataConnectService.get_datetime_formats`.

Args:
project_token: Base64-encoded project token identifying the target
study, study environment, and project.
format_type: Server-side filter to apply. One of ``"all"``
(default), ``"date"``, or ``"datetime"``.

Returns:
A :class:`DatetimeFormatsResult` exposing the classified list via
:meth:`~DatetimeFormatsResult.all` and the type-filtered views via
:meth:`~DatetimeFormatsResult.dates` and
:meth:`~DatetimeFormatsResult.datetimes`.
"""
return self._service.get_datetime_formats(
project_token=project_token,
format_type=format_type,
)

# Lifecycle

def close(self) -> None:
Expand Down
42 changes: 42 additions & 0 deletions dataconnect/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,45 @@ class PublishResult:
duplicate_record_count: int | None = None
invalid_record_count: int | None = None
invalid_records: pd.DataFrame | None = None


@dataclass(frozen=True)
class DatetimeFormat:
"""A single supported datetime format string with its classification."""

format: str
"""The format string as returned by the server (e.g. ``"yyyy-MM-dd"``)."""

type: str
"""Either ``"date"`` (date-only) or ``"datetime"`` (date with time component)."""
Comment thread
butsyk-mdsol marked this conversation as resolved.


@dataclass
class DatetimeFormatsResult:
"""Result of a :meth:`DataConnectClient.get_datetime_formats` call.

Holds the full list of supported datetime formats returned by the server
and provides convenience accessors for the common views.

Examples:
>>> result = client.get_datetime_formats(project_token="...")
>>> for fmt in result.all():
... print(fmt.format, fmt.type)
>>> only_dates = result.dates() # list[str]
>>> only_datetimes = result.datetimes() # list[str]
"""

formats: list[DatetimeFormat] = field(default_factory=list)
"""The full list of supported formats, in the order returned by the server."""

def all(self) -> list[DatetimeFormat]:
"""Return every supported format with its type classification."""
return list(self.formats)

def dates(self) -> list[str]:
"""Return only the date-style format strings (no time component)."""
return [f.format for f in self.formats if f.type == "date"]

def datetimes(self) -> list[str]:
"""Return only the datetime-style format strings (with a time component)."""
return [f.format for f in self.formats if f.type == "datetime"]
10 changes: 10 additions & 0 deletions dataconnect/service/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from dataconnect.models import (
Dataset,
DatasetVersion,
DatetimeFormatsResult,
DryPublishResult,
PaginatedResponse,
PublishResult,
Expand Down Expand Up @@ -68,5 +69,14 @@ def publish(
"""Publish a dataset to the server and return the result."""
...

@abstractmethod
def get_datetime_formats(
self,
project_token: str,
format_type: str = "all",
) -> DatetimeFormatsResult:
"""Return the supported datetime formats filtered by ``format_type``."""
...

@abstractmethod
def close(self) -> None: ...
78 changes: 77 additions & 1 deletion dataconnect/service/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
from __future__ import annotations

import json
from datetime import UTC, datetime
from uuid import UUID

import pandas as pd

from dataconnect.exceptions import ValidationError
from dataconnect.models import (
Dataset,
DatasetVersion,
DatetimeFormat,
DatetimeFormatsResult,
DryPublishResult,
PaginatedResponse,
Pagination,
Expand All @@ -28,13 +32,22 @@
)
from dataconnect.transport.base import Transport
from dataconnect.transport.errors import TransportError
from dataconnect.transport.models import DatasetTicket, PublishRequest, ResourceQuery
from dataconnect.transport.models import (
DatasetTicket,
DatetimeFormatsRequest,
PublishRequest,
ResourceQuery,
)

# Server action identifiers
_ACTION_LIST_STUDIES = "studies.list"
_ACTION_LIST_DATASETS = "datasets.list"
_ACTION_LIST_DATASET_VERSIONS = "dataset_versions.list"

# Accepted values for the ``format_type`` filter passed to
# :meth:`DefaultDataConnectService.get_datetime_formats`.
_VALID_DATETIME_FORMAT_TYPES: frozenset[str] = frozenset({"all", "date", "datetime"})


class DefaultDataConnectService(DataConnectService):
"""Concrete service injected with an abstract ``Transport``."""
Expand Down Expand Up @@ -276,6 +289,69 @@ def publish(
except TransportError as ex:
raise translate_error(ex) from ex

def get_datetime_formats(
self,
project_token: str,
format_type: str = "all",
) -> DatetimeFormatsResult:
"""Return the supported datetime formats filtered by ``format_type``.

The ``format_type`` argument is normalised (stripped + lower-cased) and
validated client-side against the accepted set ``{"all", "date",
"datetime"}``. Invalid values raise :class:`ValidationError` *before*
any transport call is made.

The server filters the list according to ``format_type``; the service
then classifies each returned format as ``"date"`` or ``"datetime"``
(based on whether the format contains a time component ``"HH:mm"``) and
wraps everything in a :class:`DatetimeFormatsResult` that exposes the
:meth:`~DatetimeFormatsResult.all`,
:meth:`~DatetimeFormatsResult.dates` and
:meth:`~DatetimeFormatsResult.datetimes` helpers.

Args:
project_token: Base64-encoded project token identifying the target
study, study environment, and project. Required by the server
for authorization.
format_type: Filter to apply server-side. One of ``"all"``
(default), ``"date"``, or ``"datetime"``.

Returns:
A :class:`DatetimeFormatsResult` containing the classified formats.

Raises:
ValidationError: When ``format_type`` is not one of the accepted
values.
DataConnectError: Any :class:`TransportError` from the transport
layer is translated by :func:`translate_error` into the public
API's :class:`DataConnectError` hierarchy.
"""
normalized = (format_type or "all").strip().lower()

if normalized not in _VALID_DATETIME_FORMAT_TYPES:
raise ValidationError(
error_code="VAL_001",
message=(f"Invalid format_type: {format_type!r}. Accepted values: all, date, datetime."),
timestamp=datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
)

request = DatetimeFormatsRequest(project_token=project_token, format_type=normalized)

try:
raw_formats = self._transport.get_datetime_formats(request)
except TransportError as ex:
raise translate_error(ex) from ex

formats = [
DatetimeFormat(
format=fmt,
type="datetime" if "HH:mm" in fmt else "date",
)
for fmt in raw_formats
]

return DatetimeFormatsResult(formats=formats)

def close(self) -> None:
"""Close the underlying transport connection."""

Expand Down
35 changes: 35 additions & 0 deletions dataconnect/transport/arrow_flight/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
DataRef,
DatasetTicket,
DataTable,
DatetimeFormatsRequest,
DryPublishResponse,
PublishRequest,
PublishResponse,
Expand Down Expand Up @@ -382,5 +383,39 @@ def publish_dataset(self, publish_request: PublishRequest) -> PublishResponse:
except Exception as ex:
raise parse_dataconnect_error(ex) from ex

def get_datetime_formats(self, request: DatetimeFormatsRequest) -> list[str]:
"""Invoke the Arrow Flight ``get_datetime_formats`` action and return the format list.

The server expects a JSON body of the form
``{"project_token": "...", "type": "all|date|datetime"}`` and responds
with a single :class:`flight.Result` whose body is a JSON-encoded list
of format strings already filtered server-side.

Args:
request: A :class:`DatetimeFormatsRequest` carrying the project
token and the (already-validated) ``format_type`` filter.

Returns:
A list of supported format strings. Returns an empty list if the
server produced no results.

Raises:
TransportError: Any Arrow Flight or gRPC error is translated by
:func:`parse_dataconnect_error` before propagating.
"""
payload = {"project_token": request.project_token, "type": request.format_type}
action = flight.Action("get_datetime_formats", json.dumps(payload).encode("utf-8"))

try:
results = list(self._client.do_action(action, self._options()))

if not results:
return []

return json.loads(results[0].body.to_pybytes().decode("utf-8"))

except Exception as ex:
raise parse_dataconnect_error(ex) from ex

def close(self) -> None:
self._client.close()
10 changes: 10 additions & 0 deletions dataconnect/transport/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from dataconnect.transport.models import (
DatasetTicket,
DataTable,
DatetimeFormatsRequest,
DryPublishResponse,
PublishRequest,
PublishResponse,
Expand Down Expand Up @@ -55,6 +56,15 @@ def publish_dataset(self, publish_request: PublishRequest) -> PublishResponse:
``PublishResponse`` containing the complete result set.
"""

@abstractmethod
def get_datetime_formats(self, request: DatetimeFormatsRequest) -> list[str]:
"""Return the supported datetime format strings for the project.

The transport does not interpret ``format_type`` — that is the service
layer's responsibility. The server applies the filter and returns the
already-filtered list of format strings.
"""

@abstractmethod
def close(self) -> None:
"""Close the transport connection."""
15 changes: 15 additions & 0 deletions dataconnect/transport/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ class DataTable:
ipc_bytes: bytes


@dataclass(frozen=True)
class DatetimeFormatsRequest:
"""An outbound request to fetch the supported datetime formats.

Attributes:
project_token: Base64-encoded project token used by the server to
authorize the request.
format_type: Filter applied server-side. One of ``"all"``, ``"date"``,
or ``"datetime"``.
"""

project_token: str
format_type: str = "all"


@dataclass(frozen=True)
class PublishRequest:
"""A publish request containing the input configuration and the dataset to be published.
Expand Down
41 changes: 41 additions & 0 deletions guides/dataconnect_usage.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,47 @@
"You can retrieve this by navigating to `iMedidata` > `Data Connect` > `Transformations` and by clicking on the kebab menu on a project row and selecting `View/Modify`."
]
},
{
"cell_type": "markdown",
"id": "24dc262e",
"metadata": {},
"source": [
"### Discover supported datetime formats\n",
"\n",
"Use `get_datetime_formats` to list the format patterns the server accepts for the `datetime_formats` argument of `dry_publish` / `publish`. Pass `format_type=\"date\"` or `format_type=\"datetime\"` to restrict the result; the default `\"all\"` returns both kinds."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "addc2370",
"metadata": {},
"outputs": [],
"source": [
"from dataconnect import DataConnectClient\n",
"\n",
"user_token = \"usertoken\" # From iMedidata > Data Connect > Developer Center\n",
"project_token = \"project_token\" # From iMedidata > Data Connect > Transformations\n",
"\n",
"try:\n",
" with DataConnectClient.connect(token=user_token) as dataconnect_client:\n",
" # Default: return both date and datetime formats\n",
" formats_result = dataconnect_client.get_datetime_formats(\n",
" project_token=project_token,\n",
" format_type=\"all\",\n",
" )\n",
"\n",
" print(\"All supported formats:\")\n",
" for fmt in formats_result.all():\n",
" print(f\" {fmt.format} [{fmt.type}]\")\n",
"\n",
" print(\"\\nDate-only formats: \", formats_result.dates())\n",
" print(\"Datetime formats: \", formats_result.datetimes())\n",
"\n",
"except Exception as e:\n",
" print(e)"
]
},
{
"cell_type": "markdown",
"id": "69e733cb",
Expand Down
Loading
Loading