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
11 changes: 10 additions & 1 deletion dataconnect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@
ServerError,
ValidationError,
)
from dataconnect.models import DatasetVersion, DryPublishResult, PaginatedResponse, Pagination, Study, StudyEnvironment
from dataconnect.models import (
DatasetVersion,
DryPublishResult,
PaginatedResponse,
Pagination,
PublishResult,
Study,
StudyEnvironment,
)

__all__ = [
# Client
Expand All @@ -23,6 +31,7 @@
"DryPublishResult",
"PaginatedResponse",
"Pagination",
"PublishResult",
# Exceptions — catch these in user application code
"DataConnectError",
"AuthenticationError",
Expand Down
65 changes: 64 additions & 1 deletion dataconnect/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@

import pandas as pd

from dataconnect.models import Dataset, DatasetVersion, DryPublishResult, PaginatedResponse, StudiesResult
from dataconnect.models import (
Dataset,
DatasetVersion,
DryPublishResult,
PaginatedResponse,
PublishResult,
StudiesResult,
)
from dataconnect.service import DataConnectService, DefaultDataConnectService

_DEFAULT_HOST = "enodia-gateway.platform.imedidata.com"
Expand Down Expand Up @@ -96,7 +103,26 @@ def dry_publish(
data: pd.DataFrame,
datetime_formats: dict[str, str] | None = None,
) -> DryPublishResult:
"""Validate a dataset against the server without persisting any changes.

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

Args:
project_token: Base64-encoded project token identifying the target
study, study environment, and project.
dataset_name: Name of the dataset to validate.
key_columns: Column names that form the unique key for the dataset.
source_datasets: UUIDs of the source datasets the published dataset
is derived from.
data: The dataset to validate as a ``pd.DataFrame``.
datetime_formats: Optional mapping of column name → datetime format
string (e.g. ``{"visit_date": "yyyy-MM-dd"}``).

Returns:
A :class:`DryPublishResult` containing the server's validation
outcome, including per-field validity flags, error messages, and
an optional ``invalid_records`` DataFrame.
"""
return self._service.dry_publish(
project_token=project_token,
dataset_name=dataset_name,
Expand All @@ -106,6 +132,43 @@ def dry_publish(
datetime_formats=datetime_formats,
)

def publish(
self,
project_token: str,
dataset_name: str,
key_columns: list[str],
source_datasets: list[UUID],
data: pd.DataFrame,
datetime_formats: dict[str, str] | None = None,
) -> PublishResult:
"""Publish a dataset to the server.

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

Args:
project_token: Base64-encoded project token identifying the target
study, study environment, and project.
dataset_name: Name of the dataset to publish.
key_columns: Column names that form the unique key for the dataset.
source_datasets: UUIDs of the source datasets the published dataset
is derived from.
data: The dataset to publish as a ``pd.DataFrame``.
datetime_formats: Optional mapping of column name → datetime format
string (e.g. ``{"visit_date": "yyyy-MM-dd"}``).

Comment thread
slingampalli-mdsol marked this conversation as resolved.
Returns:
A :class:`PublishResult` containing the server's publish outcome,
including dataset UUID, version, and record counts.
"""
return self._service.publish(
project_token=project_token,
dataset_name=dataset_name,
key_columns=key_columns,
source_datasets=source_datasets,
data=data,
datetime_formats=datetime_formats,
)

# Lifecycle

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


@dataclass
class PublishResult:
"""Result of a publish operation, including status and details."""

status: bool
dataset_name: str | None = None
dataset_uuid: str | None = None
dataset_version: int | None = None
dataset_batch_number: int | None = None
valid_record_count: int | None = None
duplicate_record_count: int | None = None
invalid_record_count: int | None = None
invalid_records: pd.DataFrame | None = None
26 changes: 24 additions & 2 deletions dataconnect/service/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@

import pandas as pd

from dataconnect.models import Dataset, DatasetVersion, DryPublishResult, PaginatedResponse, StudiesResult
from dataconnect.models import (
Dataset,
DatasetVersion,
DryPublishResult,
PaginatedResponse,
PublishResult,
StudiesResult,
)


class DataConnectService(ABC):
Expand Down Expand Up @@ -44,7 +51,22 @@ def dry_publish(
source_datasets: list[UUID],
data: pd.DataFrame,
datetime_formats: dict[str, str] | None = None,
) -> DryPublishResult: ...
) -> DryPublishResult:
"""Validate a dataset against the server without persisting any changes."""
...

@abstractmethod
def publish(
self,
project_token: str,
dataset_name: str,
key_columns: list[str],
source_datasets: list[UUID],
data: pd.DataFrame,
datetime_formats: dict[str, str] | None = None,
) -> PublishResult:
"""Publish a dataset to the server and return the result."""
...

@abstractmethod
def close(self) -> None: ...
73 changes: 72 additions & 1 deletion dataconnect/service/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,20 @@

import pandas as pd

from dataconnect.models import Dataset, DatasetVersion, DryPublishResult, PaginatedResponse, Pagination, StudiesResult
from dataconnect.models import (
Dataset,
DatasetVersion,
DryPublishResult,
PaginatedResponse,
Pagination,
PublishResult,
StudiesResult,
)
from dataconnect.service.base import DataConnectService
from dataconnect.service.error_handler import translate_error
from dataconnect.service.mappers import (
dry_publish_response_to_domain,
publish_response_to_domain,
resource_to_dataset,
resource_to_dataset_version,
resource_to_fetched_data,
Expand Down Expand Up @@ -205,6 +214,68 @@ def dry_publish(
except TransportError as ex:
raise translate_error(ex) from ex

def publish(
self,
project_token: str,
dataset_name: str,
key_columns: list[str],
source_datasets: list[UUID],
data: pd.DataFrame,
datetime_formats: dict[str, str] | None = None,
) -> PublishResult:
"""Publish a dataset to the server and return the result.

Encodes the publish configuration as a JSON ``input_config`` string,
sets ``is_dry_publish: False`` so the server treats the call as a
live publish run, then delegates to the transport layer.

The server response is mapped to a :class:`PublishResult` via
:func:`publish_response_to_domain`.

Args:
project_token: Base64-encoded project token identifying the target
study, study environment, and project.
dataset_name: Name of the dataset to publish.
key_columns: Column names that form the unique key for the dataset.
source_datasets: UUIDs of the source datasets the published dataset
is derived from.
data: The dataset to publish as a ``pd.DataFrame``.
datetime_formats: Optional mapping of column name → datetime format
string (e.g. ``{"visit_date": "yyyy-MM-dd"}``). Defaults to
an empty dict when omitted.

Returns:
A :class:`PublishResult` containing the server's publish outcome,
including dataset UUID, version, and record counts.

Raises:
DataConnectError: Any :class:`TransportError` from the transport
layer is translated by :func:`translate_error` into the public
API's :class:`DataConnectError` hierarchy before propagating.
"""
request = PublishRequest(
input_config=json.dumps(
{
"is_dry_publish": False,
"project_token": project_token,
"dataset_name": dataset_name,
"dataset_description": dataset_name,
"key_columns": key_columns,
"source_datasets": [str(uuid) for uuid in source_datasets],
"datetime_formats": datetime_formats or {},
}
),
data=data,
)

try:
publish_result = self._transport.publish_dataset(request)

return publish_response_to_domain(publish_result)

except TransportError as ex:
raise translate_error(ex) from ex

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

Expand Down
35 changes: 33 additions & 2 deletions dataconnect/service/mappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
import pyarrow as pa

from dataconnect.exceptions import NotFoundError
from dataconnect.models import Dataset, DatasetVersion, DryPublishResult, Study, StudyEnvironment
from dataconnect.transport.models import DataTable, DryPublishResponse, ResourceInfo
from dataconnect.models import Dataset, DatasetVersion, DryPublishResult, PublishResult, Study, StudyEnvironment
from dataconnect.transport.models import DataTable, DryPublishResponse, PublishResponse, ResourceInfo


def resource_to_study(resource: ResourceInfo) -> Study:
Expand Down Expand Up @@ -124,3 +124,34 @@ def dry_publish_response_to_domain(result: DryPublishResponse | None) -> DryPubl
invalid_record_count=result.invalid_record_count,
invalid_records=result.invalid_records,
)


def publish_response_to_domain(result: PublishResponse | None) -> PublishResult:
"""Map a transport-layer ``PublishResponse`` to a ``PublishResult`` domain object.

``PublishResponse`` carries flat, typed fields returned by the server after a
publish call. The mapping is direct for all shared fields.

Args:
result: The transport-layer result returned by
:meth:`Transport.publish_dataset`. Pass ``None`` to obtain a
default :class:`PublishResult` with ``status=False`` and all
other fields left at their default values.

Returns:
A :class:`PublishResult` suitable for returning to the caller.
"""
if result is None:
return PublishResult(status=False)

return PublishResult(
status=result.status,
dataset_name=result.dataset_name,
dataset_uuid=result.dataset_uuid,
dataset_version=result.dataset_version,
dataset_batch_number=result.dataset_batch_number,
valid_record_count=result.valid_record_count,
duplicate_record_count=result.duplicate_record_count,
invalid_record_count=result.invalid_record_count,
invalid_records=result.invalid_records,
)
2 changes: 2 additions & 0 deletions dataconnect/transport/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
DataTable,
DryPublishResponse,
PublishRequest,
PublishResponse,
ResourceInfo,
ResourceQuery,
)
Expand All @@ -24,5 +25,6 @@
"DatasetTicket",
"DataTable",
"PublishRequest",
"PublishResponse",
"DryPublishResponse",
]
Loading
Loading