Skip to content
Open
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
15 changes: 15 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
History
=======

1.1.8 (unreleased)
------------------

* Added typed Safe Data Preview support:

* ``safe_data_preview`` on ``InDataDiscoveryConfig`` (via ``SafeDataPreviewOptions``)
to configure or disable the preview.
* ``safe_data_preview`` on schema-discovery result columns and file-discovery locators,
typed by ``kind``.
Comment on lines +10 to +13

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line length is 120

Suggested change
* ``safe_data_preview`` on ``InDataDiscoveryConfig`` (via ``SafeDataPreviewOptions``)
to configure or disable the preview.
* ``safe_data_preview`` on schema-discovery result columns and file-discovery locators,
typed by ``kind``.
* ``safe_data_preview`` on ``InDataDiscoveryConfig`` (via ``SafeDataPreviewOptions``) to configure or disable the preview.
* ``safe_data_preview`` on schema-discovery result columns and file-discovery locators, typed by ``kind``.


* Added ``row_count`` to ``TableConstraints`` in schema-discovery ``table_metadata``.

* Added ``validate_discovery_config`` and ``validate_discovery_config_library``, plus a
``validation_error_details`` field on the discovery config and library models.

1.1.7 (2026-07-14)
------------------

Expand Down
66 changes: 66 additions & 0 deletions datamasque/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,40 @@
RunInfo,
UnfinishedRun,
)
from datamasque.client.models.safe_data_preview import (
BooleanPreview,
BooleanStatistics,
ColumnKind,
ColumnPreview,
CommonStatistics,
FirstCharsStatistics,
LengthEntry,
LengthsStatistics,
MaskedFormEntry,
NumericBin,
NumericHistograms,
NumericPreview,
NumericStatistics,
NumericSummaries,
NumericTemporalDisclosureLevel,
PatternComposition,
PatternEntry,
PatternsStatistics,
SafeDataPreview,
SafeDataPreviewOptions,
StringDisclosureLevel,
StringPreview,
StringStatistics,
TemporalBin,
TemporalHistograms,
TemporalPreview,
TemporalStatistics,
TemporalSummaries,
UnknownPreview,
UnsupportedPreview,
UnsupportedPreviewReason,
UnsupportedStatistics,
)
from datamasque.client.models.status import (
AsyncRulesetGenerationTaskStatus,
MaskingRunStatus,
Expand All @@ -131,6 +165,11 @@
"AsyncRulesetGenerationInProgressError",
"AsyncRulesetGenerationTaskStatus",
"AzureConnectionConfig",
"BooleanPreview",
"BooleanStatistics",
"ColumnKind",
"ColumnPreview",
"CommonStatistics",
"ConnectionConfig",
"ConnectionId",
"ConstraintColumns",
Expand Down Expand Up @@ -171,6 +210,7 @@
"FileId",
"FileOrContent",
"FileRulesetGenerationRequest",
"FirstCharsStatistics",
"ForeignKeyRef",
"GitSnapshot",
"HashColumnsTableConfig",
Expand All @@ -186,16 +226,28 @@
"InvalidLibraryError",
"InvalidRulesetError",
"JsonPath",
"LengthEntry",
"LengthsStatistics",
"LicenseInfo",
"Locator",
"MaskType",
"MaskedFormEntry",
"MaskingRunOptions",
"MaskingRunRequest",
"MaskingRunStatus",
"MongoConnectionConfig",
"MountedShareConnectionConfig",
"MssqlLinkedServerConnectionConfig",
"NumericBin",
"NumericHistograms",
"NumericPreview",
"NumericStatistics",
"NumericSummaries",
"NumericTemporalDisclosureLevel",
"OracleWalletFile",
"PatternComposition",
"PatternEntry",
"PatternsStatistics",
"ReferencingForeignKey",
"Ruleset",
"RulesetGenerationRequest",
Expand All @@ -213,6 +265,8 @@
"RunInfo",
"RunNotCancellableError",
"S3ConnectionConfig",
"SafeDataPreview",
"SafeDataPreviewOptions",
"SchemaDiscoveryColumn",
"SchemaDiscoveryFromConfigRequest",
"SchemaDiscoveryPage",
Expand All @@ -228,9 +282,21 @@
"SseConfig",
"SseSelection",
"SslZipFile",
"StringDisclosureLevel",
"StringPreview",
"StringStatistics",
"SwitchableLicenseMetadata",
"TableConstraints",
"TemporalBin",
"TemporalHistograms",
"TemporalPreview",
"TemporalStatistics",
"TemporalSummaries",
"UnfinishedRun",
"UnknownPreview",
"UnsupportedPreview",
"UnsupportedPreviewReason",
"UnsupportedStatistics",
"User",
"UserId",
"UserRole",
Expand Down
29 changes: 29 additions & 0 deletions datamasque/client/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import platform
import sys
import time
import warnings
from contextlib import contextmanager
from dataclasses import dataclass
Expand All @@ -17,6 +18,7 @@

from datamasque.client.exceptions import (
DataMasqueApiError,
DataMasqueException,
DataMasqueNotReadyError,
DataMasqueTransportError,
)
Expand Down Expand Up @@ -309,6 +311,33 @@ def _delete_if_exists(self, path: str, *, params: Optional[dict] = None) -> None

self._raise_for_status(response)

def _poll_until_done(
self,
fetch: Callable[[], _T],
is_done: Callable[[_T], bool],
initial: _T,
*,
timeout: float,
poll_interval: float,
) -> _T:
if is_done(initial):
return initial

deadline = time.monotonic() + timeout
latest = initial
while time.monotonic() < deadline:
time.sleep(poll_interval)
latest = fetch()
if is_done(latest):
return latest
return latest

def _delete_best_effort(self, delete: Callable[[], None], description: str) -> None:
try:
delete()
except DataMasqueException:
logger.warning('Failed to clean up %s; remove it manually.', description)

def _iter_paginated(
self,
path: str,
Expand Down
43 changes: 43 additions & 0 deletions datamasque/client/discovery_config_libraries.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import logging
import uuid
from typing import Optional

from datamasque.client.base import BaseClient
from datamasque.client.exceptions import DataMasqueApiError
from datamasque.client.models.discovery_config import DiscoveryConfigType
from datamasque.client.models.discovery_config_library import DiscoveryConfigLibrary, DiscoveryConfigLibraryId
from datamasque.client.models.status import ValidationStatus

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -95,6 +97,7 @@ def create_discovery_config_library(self, library: DiscoveryConfigLibrary) -> Di
library.id = created.id
library.is_valid = created.is_valid
library.validation_error = created.validation_error
library.validation_error_details = created.validation_error_details
library.created = created.created
library.modified = created.modified
logger.info('Creation of discovery config library "%s" successful', library.name)
Expand Down Expand Up @@ -123,6 +126,7 @@ def update_discovery_config_library(self, library: DiscoveryConfigLibrary) -> Di
updated = DiscoveryConfigLibrary.model_validate(response.json())
library.is_valid = updated.is_valid
library.validation_error = updated.validation_error
library.validation_error_details = updated.validation_error_details
library.modified = updated.modified
logger.debug('Update of discovery config library "%s" successful', library.name)
return library
Expand All @@ -141,6 +145,45 @@ def create_or_update_discovery_config_library(self, library: DiscoveryConfigLibr

return self.create_discovery_config_library(library)

def validate_discovery_config_library(
self,
library: DiscoveryConfigLibrary,
*,
timeout: float = 60.0,
poll_interval: float = 1.0,
) -> DiscoveryConfigLibrary:
"""Validate a discovery config library's YAML server-side and return it with the verdict populated."""

temp = DiscoveryConfigLibrary(
name=f"__dm_validate_{uuid.uuid4().hex}",
namespace=library.namespace,
yaml=library.yaml,
config_type=library.config_type,
)
created = self.create_discovery_config_library(temp)
settled = created
temp_id = created.id
if temp_id is not None:
library_id = temp_id
try:
settled = self._poll_until_done(
lambda: self.get_discovery_config_library(library_id),
lambda lib: lib.is_valid is not ValidationStatus.in_progress,
created,
timeout=timeout,
poll_interval=poll_interval,
)
finally:
self._delete_best_effort(
lambda: self.delete_discovery_config_library_by_id_if_exists(library_id),
f"discovery config library `{library_id}`",
)

library.is_valid = settled.is_valid
library.validation_error = settled.validation_error
library.validation_error_details = settled.validation_error_details
return library

def delete_discovery_config_library_by_id_if_exists(
self, library_id: DiscoveryConfigLibraryId, *, force: bool = False
) -> None:
Expand Down
42 changes: 42 additions & 0 deletions datamasque/client/discovery_configs.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import logging
import uuid
from typing import Iterator, Optional

from datamasque.client.base import BaseClient
from datamasque.client.exceptions import DataMasqueApiError, DataMasqueException
from datamasque.client.models.discovery_config import DiscoveryConfig, DiscoveryConfigId, DiscoveryConfigType
from datamasque.client.models.pagination import Page
from datamasque.client.models.status import ValidationStatus

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -84,6 +86,7 @@ def create_discovery_config(self, config: DiscoveryConfig) -> DiscoveryConfig:
config.id = created.id
config.is_valid = created.is_valid
config.validation_error = created.validation_error
config.validation_error_details = created.validation_error_details
config.created = created.created
config.modified = created.modified
logger.info('Creation of discovery config "%s" successful', config.name)
Expand All @@ -105,6 +108,7 @@ def update_discovery_config(self, config: DiscoveryConfig) -> DiscoveryConfig:
updated = DiscoveryConfig.model_validate(response.json())
config.is_valid = updated.is_valid
config.validation_error = updated.validation_error
config.validation_error_details = updated.validation_error_details
config.modified = updated.modified
logger.debug('Update of discovery config "%s" successful', config.name)
return config
Expand All @@ -123,6 +127,44 @@ def create_or_update_discovery_config(self, config: DiscoveryConfig) -> Discover

return self.create_discovery_config(config)

def validate_discovery_config(
self,
config: DiscoveryConfig,
*,
timeout: float = 60.0,
poll_interval: float = 1.0,
) -> DiscoveryConfig:
"""Validate a discovery config's YAML server-side and return it with the verdict populated."""

temp = DiscoveryConfig(
name=f"__dm_validate_{uuid.uuid4().hex}",
yaml=config.yaml,
config_type=config.config_type,
)
created = self.create_discovery_config(temp)
settled = created
temp_id = created.id
if temp_id is not None:
config_id = temp_id
try:
settled = self._poll_until_done(
lambda: self.get_discovery_config(config_id),
lambda c: c.is_valid is not ValidationStatus.in_progress,
created,
timeout=timeout,
poll_interval=poll_interval,
)
finally:
self._delete_best_effort(
lambda: self.delete_discovery_config_by_id_if_exists(config_id),
f"discovery config `{config_id}`",
)

config.is_valid = settled.is_valid
config.validation_error = settled.validation_error
config.validation_error_details = settled.validation_error_details
return config

def delete_discovery_config_by_id_if_exists(self, config_id: DiscoveryConfigId) -> None:
"""
Deletes the discovery config with the given ID.
Expand Down
Loading
Loading