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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## v1.6.0

### Added
- `dm table-references` — manage table references (`list`, `get`, `create`,
`update`, `delete`), the named pointers to identity data used for
cross-system consistent masking. `create` accepts `--file` for a full
JSON definition or `--name`/`--connection`/`--source` flags plus the
format/CSV options (`--format`, `--delimiter`, `--encoding`,
`--quotechar`, `--null-string`) for the common case, and is a
create-or-update like `dm connections create`; `update` changes only the
fields passed, preserving the reference's id.

### Internal
- Extracted `resolve_connection` (name-or-ID lookup) into `client.py` and
routed `connections.py`'s `test`/`update` and `discover schema`/`discover
file` through it, replacing three separate inline copies.

## v1.5.1

### Added
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ dm libraries status <name> # Validation status; poll afte
dm libraries usage <name> # Show rulesets using it
```

### Table references

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.

This section explains a lot compared to the sections above and below. Could you list the commands directly for visual consistency?

Are the commands self-explanatory if the user types dm table-references --help? If not, additional usage information probably belong in SKILL.md

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Trimmed to match sibling sections — Connections/Rulesets/Libraries are all bare command blocks with inline comments, no prose. Checked dm table-references create --help directly: fully self-explanatory (examples + every flag's semantics), so nothing needs moving to SKILL.md


```console
dm table-references list # id, name, connection (ID), source
dm table-references get <name> # Show full details
dm table-references create --file reference.json # Create/update from JSON — connection must be an ID, not a name
dm table-references create --name <name> --connection <name-or-id> --source <path-or-schema.table>
dm table-references create --name <name> --connection <name-or-id> --source data.csv --format parquet # Format is explicit, never inferred from --source
dm table-references update <name> --source <new-path> # Change selected fields, preserving the id
dm table-references update <name> --delimiter ';' --null-string NULL # Any CSV/format flag replaces options wholesale, not a merge
dm table-references delete <name> # Delete a table reference
```

### In-flight masking

The IFM service runs alongside the admin server,
Expand Down
11 changes: 10 additions & 1 deletion claude-skills/datamasque-cli/skills/datamasque-cli/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: datamasque-cli
description: Use when the user wants to interact with a DataMasque instance — start masking runs, check run status, list connections or rulesets, manage seeds, manage ruleset libraries, check system health, configure the AI Engine, or any task involving the DataMasque API. Triggers on "mask the data", "start a run", "check the run", "list connections", "list rulesets", "upload a seed", "check DataMasque health", "dm status", "ruleset library", "configure the AI Engine", "set the AI Engine URL", or any request to operate DataMasque programmatically.
description: Use when the user wants to interact with a DataMasque instance — start masking runs, check run status, list connections or rulesets, manage seeds, manage ruleset libraries, manage table references, check system health, configure the AI Engine, or any task involving the DataMasque API. Triggers on "mask the data", "start a run", "check the run", "list connections", "list rulesets", "upload a seed", "check DataMasque health", "dm status", "ruleset library", "table reference", "cross-system consistent masking", "configure the AI Engine", "set the AI Engine URL", or any request to operate DataMasque programmatically.
argument-hint: e.g. "start a run with docx_masking on var_input_docx"
user-invocable: true
---
Expand Down Expand Up @@ -113,3 +113,12 @@ Pass repeated `--options key=value` for server-side knobs
`dm run start -c <x>`, `dm discover schema <x>`, etc. all try the name
first and fall back to a UUID match. Prefer names for readability;
reach for UUIDs only when names collide (rare).

- **Table references are the identity map behind cross-system consistent
masking.** `dm table-references create --name <n> --connection <c>
--source <path-or-schema.table>` registers one; a ruleset then addresses
it by name via the `table_reference` hash source in `hash_columns`
(`source_key`/`target_key`/`value`) — this CLI doesn't generate or
validate that ruleset syntax. `create` is create-or-update like
`dm connections create`; `--file`'s JSON needs a connection **ID**, not
a name, unlike the `--connection` flag.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ dependencies = [
"typer>=0.16.0,<1",
"rich>=13.8",
"tomli-w>=1.0.0,<2",
"datamasque-python>=1.2.3,<2",
"datamasque-python>=1.2.5,<2",
"pydantic>=2.5,<3",
]
classifiers = [
Expand Down
10 changes: 10 additions & 0 deletions src/datamasque_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from datamasque.client import DataMasqueClient, DataMasqueIfmClient
from datamasque.client.exceptions import DataMasqueApiError, DataMasqueTransportError, IfmAuthError
from datamasque.client.models.connection import ConnectionConfig
from datamasque.client.models.dm_instance import DataMasqueInstanceConfig
from datamasque.client.models.ifm import DataMasqueIfmInstanceConfig

Expand Down Expand Up @@ -212,3 +213,12 @@ def get_ifm_client(profile_name: str | None = None) -> DataMasqueIfmClient:
extra_auth_excs=(IfmAuthError,),
)
return client


def resolve_connection(client: DataMasqueClient, name_or_id: str) -> ConnectionConfig:
"""Resolve a connection by name or ID via the connection listing, aborting when not found."""
connections = client.list_connections()
match = next((c for c in connections if c.name == name_or_id or str(c.id) == name_or_id), None)
if match is None:
abort(f"Connection '{name_or_id}' not found.", code=ErrorCode.NOT_FOUND)
return match
18 changes: 8 additions & 10 deletions src/datamasque_cli/commands/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
S3ConnectionConfig,
SnowflakeConnectionConfig,
)
from pydantic import ValidationError

from datamasque_cli.client import get_client
from datamasque_cli.client import get_client, resolve_connection
from datamasque_cli.errors import ErrorCode, abort, abort_api_error, confirm_or_abort
from datamasque_cli.fileio import read_json_object_or_abort
from datamasque_cli.output import print_success, redact_sensitive_fields, render_output
Expand Down Expand Up @@ -218,7 +219,10 @@ def _create_from_file(client: DataMasqueClient, file: Path) -> None:
data["database_type"] = DatabaseType(data["database_type"])

klass = _CONNECTION_CLASSES[conn_type]
config = klass(**data)
try:
config = klass(**data)
except ValidationError as exc:
abort(f"{file} does not match the expected format: {exc}", code=ErrorCode.INVALID_INPUT)
client.create_or_update_connection(config)
print_success(f"Connection '{config.name}' created/updated.")

Expand Down Expand Up @@ -298,10 +302,7 @@ def test_connection(
success, a warning, or a hard failure.
"""
client = get_client(profile)

match = next((c for c in client.list_connections() if c.name == name or str(c.id) == name), None)
if match is None:
abort(f"Connection '{name}' not found.", code=ErrorCode.NOT_FOUND)
match = resolve_connection(client, name)

try:
response = client.make_request("POST", f"/api/connections/{match.id}/test/", data={})
Expand Down Expand Up @@ -334,10 +335,7 @@ def update_connection(
references it stays intact. Pass only the fields that should change.
"""
client = get_client(profile)

match = next((c for c in client.list_connections() if c.name == name or str(c.id) == name), None)
if match is None:
abort(f"Connection '{name}' not found.", code=ErrorCode.NOT_FOUND)
match = resolve_connection(client, name)

updates: dict[str, object] = {
key: value
Expand Down
14 changes: 3 additions & 11 deletions src/datamasque_cli/commands/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from datamasque.client.models.discovery_config import DiscoveryConfigId, DiscoveryConfigType
from datamasque.client.models.status import MaskingRunStatus

from datamasque_cli.client import get_client
from datamasque_cli.client import get_client, resolve_connection
from datamasque_cli.commands import discovery_config_libraries, discovery_configs
from datamasque_cli.errors import (
ErrorCode,
Expand Down Expand Up @@ -79,14 +79,6 @@ def _write_or_echo(content: str, output: Path | None, success_label: str) -> Non
print_success(f"{success_label} written to {output}")


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


def _resolve_discovery_config_id(
client: DataMasqueClient, name: str, expected_type: DiscoveryConfigType
) -> DiscoveryConfigId:
Expand Down Expand Up @@ -126,7 +118,7 @@ def schema_discovery(
(poll with `dm run status <run-id>`).
"""
client = get_client(profile)
conn_id = _resolve_connection_id(client, connection)
conn_id = str(resolve_connection(client, connection).id)

try:
if config is not None:
Expand Down Expand Up @@ -168,7 +160,7 @@ def start_file_discovery(
(poll with `dm run status <run-id>`).
"""
client = get_client(profile)
conn_id = _resolve_connection_id(client, connection)
conn_id = str(resolve_connection(client, connection).id)

try:
if config is not None:
Expand Down
Loading
Loading