Skip to content
Draft
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ requires-python = ">=3.11"
dependencies = [
"typer>=0.15.0",
"tomli-w>=1.0.0",
"datamasque-python>=1.0.0,<2",
"datamasque-python>=1.1.7,<2",
]
classifiers = [
"Development Status :: 4 - Beta",
Expand Down
50 changes: 48 additions & 2 deletions src/datamasque_cli/commands/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
import typer
from datamasque.client import DataMasqueClient, RunId
from datamasque.client.models.connection import ConnectionId
from datamasque.client.models.discovery import SchemaDiscoveryRequest
from datamasque.client.models.discovery import (
InDataDiscoveryConfig,
SafeDataPreviewOptions,
SchemaDiscoveryFromConfigRequest,
SchemaDiscoveryRequest,
)
from datamasque.client.models.discovery_config import DiscoveryConfigId

from datamasque_cli.client import get_client
from datamasque_cli.output import ErrorCode, abort, print_json, print_success, render_output, should_emit_json
Expand All @@ -33,9 +39,24 @@ def _resolve_connection_id(client: DataMasqueClient, name_or_id: str) -> str:
return str(match.id)


def _resolve_discovery_config_id(client: DataMasqueClient, name_or_id: str) -> str:
"""Resolve a discovery config name or ID to its UUID string."""
match = next((c for c in client.list_discovery_configs() if c.name == name_or_id or str(c.id) == name_or_id), None)
if match is None:
abort(f"Discovery config '{name_or_id}' not found.", code=ErrorCode.NOT_FOUND)
return str(match.id)


@app.command("schema")
def schema_discovery(
connection: str = typer.Argument(help="Connection name or ID"),
config: str | None = typer.Option(
None, "--config", help="Discovery config name or ID to run with (holds IDD rules and preview settings)"
),
idd: bool | None = typer.Option(None, "--idd/--no-idd", help="Enable or disable in-data discovery"),
preview: bool | None = typer.Option(
None, "--preview/--no-preview", help="Enable or disable Safe Data Preview (requires --idd)"
),
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
) -> None:
"""Start a schema-discovery run on a connection.
Expand All @@ -44,10 +65,35 @@ def schema_discovery(
`dm discover schema-results <run-id>` once this run reaches a terminal state
(poll with `dm run status <run-id>`).
"""
if config is not None and (idd is not None or preview is not None):
abort("--config carries its own IDD and preview settings; drop --idd/--preview.", code=ErrorCode.INVALID_INPUT)
if preview is not None and idd is None:
abort(
"--preview/--no-preview requires --idd (the preview rides on in-data discovery).",
code=ErrorCode.INVALID_INPUT,
)

client = get_client(profile)
conn_id = _resolve_connection_id(client, connection)

request = SchemaDiscoveryRequest(connection=ConnectionId(conn_id))
if config is not None:
config_id = _resolve_discovery_config_id(client, config)
from_config_request = SchemaDiscoveryFromConfigRequest(
connection=ConnectionId(conn_id), discovery_config=DiscoveryConfigId(config_id)
)
run_id = client.start_schema_discovery_run_from_config(from_config_request)
print_success(
f"Schema discovery run {run_id} started for connection '{connection}' with config '{config}'. "
f"Once finished, list results with: dm discover schema-results {run_id}"
)
return

in_data_discovery = None
if idd is not None:
safe_data_preview = SafeDataPreviewOptions(enabled=preview) if preview is not None else None
in_data_discovery = InDataDiscoveryConfig(enabled=idd, safe_data_preview=safe_data_preview)

request = SchemaDiscoveryRequest(connection=ConnectionId(conn_id), in_data_discovery=in_data_discovery)
run_id = client.start_schema_discovery_run(request)
print_success(
f"Schema discovery run {run_id} started for connection '{connection}'. "
Expand Down
57 changes: 57 additions & 0 deletions tests/commands/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,66 @@ def test_schema_starts_discovery_run_and_points_at_results(mock_get_client: Magi
(call,) = client.start_schema_discovery_run.call_args_list
(request,) = call.args
assert request.connection == "abc-123"
assert request.in_data_discovery is None
assert "dm discover schema-results 99" in result.stderr


@patch(f"{MODULE}.get_client")
def test_schema_sends_idd_and_preview_options(mock_get_client: MagicMock, runner: CliRunner) -> None:
client = MagicMock()
mock_get_client.return_value = client
client.list_connections.return_value = [
SimpleNamespace(id="abc-123", name="my_db", mask_type="database"),
]
client.start_schema_discovery_run.return_value = 99

result = runner.invoke(app, ["discover", "schema", "my_db", "--idd", "--no-preview"])

assert result.exit_code == 0
(call,) = client.start_schema_discovery_run.call_args_list
(request,) = call.args
assert request.in_data_discovery.enabled is True
assert request.in_data_discovery.safe_data_preview.enabled is False


@patch(f"{MODULE}.get_client")
def test_schema_starts_discovery_run_from_config(mock_get_client: MagicMock, runner: CliRunner) -> None:
client = MagicMock()
mock_get_client.return_value = client
client.list_connections.return_value = [
SimpleNamespace(id="abc-123", name="my_db", mask_type="database"),
]
client.list_discovery_configs.return_value = [
SimpleNamespace(id="cfg-9", name="pii_config"),
]
client.start_schema_discovery_run_from_config.return_value = 7

result = runner.invoke(app, ["discover", "schema", "my_db", "--config", "pii_config"])

assert result.exit_code == 0
(call,) = client.start_schema_discovery_run_from_config.call_args_list
(request,) = call.args
assert request.connection == "abc-123"
assert request.discovery_config == "cfg-9"
assert "dm discover schema-results 7" in result.stderr


@patch(f"{MODULE}.get_client")
def test_schema_rejects_config_combined_with_idd_flags(mock_get_client: MagicMock, runner: CliRunner) -> None:
result = runner.invoke(app, ["discover", "schema", "my_db", "--config", "pii_config", "--idd"])

assert result.exit_code != 0
mock_get_client.assert_not_called()


@patch(f"{MODULE}.get_client")
def test_schema_rejects_preview_without_idd(mock_get_client: MagicMock, runner: CliRunner) -> None:
result = runner.invoke(app, ["discover", "schema", "my_db", "--no-preview"])

assert result.exit_code != 0
mock_get_client.assert_not_called()


@patch(f"{MODULE}.get_client")
def test_schema_results_lists_with_flattened_rows(mock_get_client: MagicMock, runner: CliRunner) -> None:
client = MagicMock()
Expand Down
Loading