From e2a533cb1c6d3b597fcb977887eb2d3c0256a08f Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Wed, 15 Jul 2026 09:46:07 +1200 Subject: [PATCH 1/2] feat: Add --config, --idd and --preview options to dm discover schema --- src/datamasque_cli/commands/discovery.py | 50 ++++++++++++++++++++- tests/commands/test_discovery.py | 57 ++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/src/datamasque_cli/commands/discovery.py b/src/datamasque_cli/commands/discovery.py index 9db83af..5983f5f 100644 --- a/src/datamasque_cli/commands/discovery.py +++ b/src/datamasque_cli/commands/discovery.py @@ -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 @@ -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. @@ -44,10 +65,35 @@ def schema_discovery( `dm discover schema-results ` once this run reaches a terminal state (poll with `dm run status `). """ + 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}'. " diff --git a/tests/commands/test_discovery.py b/tests/commands/test_discovery.py index c35218b..81e70e1 100644 --- a/tests/commands/test_discovery.py +++ b/tests/commands/test_discovery.py @@ -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() From db6eb26ed72a82659cc876cc873e0027d8f69a9c Mon Sep 17 00:00:00 2001 From: Kane Williams Date: Wed, 15 Jul 2026 09:46:07 +1200 Subject: [PATCH 2/2] build: Require datamasque-python 1.1.7 for Safe Data Preview options --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b4dfa47..e8848ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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",