datamasque-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datamasque_cli/__init__.py +0 -0
- datamasque_cli/client.py +109 -0
- datamasque_cli/commands/__init__.py +0 -0
- datamasque_cli/commands/auth.py +154 -0
- datamasque_cli/commands/connections.py +325 -0
- datamasque_cli/commands/discovery.py +136 -0
- datamasque_cli/commands/files.py +77 -0
- datamasque_cli/commands/ruleset_libraries.py +156 -0
- datamasque_cli/commands/rulesets.py +303 -0
- datamasque_cli/commands/runs.py +526 -0
- datamasque_cli/commands/seeds.py +56 -0
- datamasque_cli/commands/system.py +118 -0
- datamasque_cli/commands/users.py +86 -0
- datamasque_cli/config.py +82 -0
- datamasque_cli/main.py +58 -0
- datamasque_cli/output.py +133 -0
- datamasque_cli/py.typed +0 -0
- datamasque_cli-1.0.0.dist-info/METADATA +269 -0
- datamasque_cli-1.0.0.dist-info/RECORD +22 -0
- datamasque_cli-1.0.0.dist-info/WHEEL +4 -0
- datamasque_cli-1.0.0.dist-info/entry_points.txt +2 -0
- datamasque_cli-1.0.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""System-level commands: health, licence, logs, admin-install."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from datamasque_cli.client import get_client
|
|
10
|
+
from datamasque_cli.commands.rulesets import export_bundle, import_bundle
|
|
11
|
+
from datamasque_cli.output import print_json, print_success, print_warning, render_output
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="System administration commands.")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command()
|
|
17
|
+
def health(
|
|
18
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
19
|
+
is_json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Check DataMasque instance health."""
|
|
22
|
+
client = get_client(profile)
|
|
23
|
+
client.healthcheck()
|
|
24
|
+
|
|
25
|
+
if is_json:
|
|
26
|
+
print_json({"status": "healthy"})
|
|
27
|
+
else:
|
|
28
|
+
print_success("Instance is healthy.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command("licence")
|
|
32
|
+
def licence(
|
|
33
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
34
|
+
is_json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Show licence information."""
|
|
37
|
+
client = get_client(profile)
|
|
38
|
+
info = client.get_current_license_info()
|
|
39
|
+
# Project to the fields a user actually checks. The full pydantic dump
|
|
40
|
+
# also includes nested SwitchableLicenseMetadata which is noisy.
|
|
41
|
+
expiry = info.expiry_date.isoformat() if info.expiry_date else None
|
|
42
|
+
data = {
|
|
43
|
+
"uuid": info.uuid,
|
|
44
|
+
"name": info.name,
|
|
45
|
+
"type": info.type,
|
|
46
|
+
"is_expired": info.is_expired,
|
|
47
|
+
"expiry_date": expiry,
|
|
48
|
+
"days_until_expiry": info.days_until_expiry,
|
|
49
|
+
"platform_name": info.platform_name,
|
|
50
|
+
}
|
|
51
|
+
render_output(data, is_json=is_json, title="Licence")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command()
|
|
55
|
+
def logs(
|
|
56
|
+
output_path: Path = typer.Option("datamasque-logs.tar.gz", "--output", "-o", help="Where to save the log file"),
|
|
57
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Download application logs."""
|
|
60
|
+
client = get_client(profile)
|
|
61
|
+
client.retrieve_application_logs(output_path)
|
|
62
|
+
print_success(f"Logs saved to {output_path}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command("export", hidden=True, deprecated=True)
|
|
66
|
+
def export_config(
|
|
67
|
+
output_path: Path = typer.Option("datamasque-export.zip", "--output", "-o", help="Where to save the export"),
|
|
68
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Deprecated alias for `dm rulesets export-bundle`."""
|
|
71
|
+
print_warning("`dm system export` is deprecated; use `dm rulesets export-bundle` instead.")
|
|
72
|
+
export_bundle(output_path=output_path, profile=profile)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command("import", hidden=True, deprecated=True)
|
|
76
|
+
def import_config(
|
|
77
|
+
file: Path = typer.Option(..., "--file", "-f", help="Path to import file", exists=True, readable=True),
|
|
78
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
79
|
+
is_confirmed: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
80
|
+
) -> None:
|
|
81
|
+
"""Deprecated alias for `dm rulesets import-bundle`."""
|
|
82
|
+
print_warning("`dm system import` is deprecated; use `dm rulesets import-bundle` instead.")
|
|
83
|
+
import_bundle(file=file, profile=profile, is_confirmed=is_confirmed)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("upload-licence")
|
|
87
|
+
def upload_licence(
|
|
88
|
+
file: Path = typer.Argument(help="Path to .lic licence file", exists=True, readable=True),
|
|
89
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Upload a DataMasque licence file."""
|
|
92
|
+
client = get_client(profile)
|
|
93
|
+
client.upload_license_file(str(file))
|
|
94
|
+
print_success(f"Licence file '{file.name}' uploaded.")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@app.command("admin-install")
|
|
98
|
+
def admin_install(
|
|
99
|
+
email: str = typer.Option(..., help="Admin email address"),
|
|
100
|
+
username: str = typer.Option("admin", help="Admin username"),
|
|
101
|
+
password: str = typer.Option(..., prompt=True, hide_input=True, help="Admin password"),
|
|
102
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Initial admin setup for a fresh DataMasque instance."""
|
|
105
|
+
client = get_client(profile)
|
|
106
|
+
client.admin_install(email=email, username=username, password=password)
|
|
107
|
+
print_success(f"Admin user '{username}' created.")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@app.command("set-locality")
|
|
111
|
+
def set_locality(
|
|
112
|
+
locality: str = typer.Argument(help="Locality string to set"),
|
|
113
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Set the system locality."""
|
|
116
|
+
client = get_client(profile)
|
|
117
|
+
client.set_locality(locality)
|
|
118
|
+
print_success(f"Locality set to '{locality}'.")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""User management commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from datamasque.client.models.user import User, UserRole
|
|
7
|
+
|
|
8
|
+
from datamasque_cli.client import get_client
|
|
9
|
+
from datamasque_cli.output import abort, print_success, render_output
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Manage users.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("list")
|
|
15
|
+
def list_users(
|
|
16
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
17
|
+
is_json: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
18
|
+
) -> None:
|
|
19
|
+
"""List all users."""
|
|
20
|
+
client = get_client(profile)
|
|
21
|
+
users = client.list_users()
|
|
22
|
+
|
|
23
|
+
data = [
|
|
24
|
+
{
|
|
25
|
+
"id": u.id,
|
|
26
|
+
"username": u.username,
|
|
27
|
+
"email": u.email,
|
|
28
|
+
"roles": ", ".join(r.value for r in u.roles),
|
|
29
|
+
}
|
|
30
|
+
for u in users
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
render_output(data, is_json=is_json, columns=["id", "username", "email", "roles"], title="Users")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command("create")
|
|
37
|
+
def create_user(
|
|
38
|
+
username: str = typer.Option(..., help="Username"),
|
|
39
|
+
email: str = typer.Option(..., help="Email address"),
|
|
40
|
+
role: list[str] = typer.Option(..., help="Role(s): superuser, mask_builder, mask_runner"),
|
|
41
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Create a new user."""
|
|
44
|
+
roles = [UserRole(r) for r in role]
|
|
45
|
+
user = User(username=username, email=email, password="", roles=roles)
|
|
46
|
+
|
|
47
|
+
client = get_client(profile)
|
|
48
|
+
created = client.create_or_update_user(user)
|
|
49
|
+
print_success(f"User '{username}' created. Temporary password: {created.password}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command("delete")
|
|
53
|
+
def delete_user(
|
|
54
|
+
username: str = typer.Argument(help="Username to delete"),
|
|
55
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
56
|
+
is_confirmed: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Delete a user by username."""
|
|
59
|
+
client = get_client(profile)
|
|
60
|
+
users = client.list_users()
|
|
61
|
+
|
|
62
|
+
if not any(u.username == username for u in users):
|
|
63
|
+
abort(f"User '{username}' not found.")
|
|
64
|
+
|
|
65
|
+
if not is_confirmed:
|
|
66
|
+
typer.confirm(f"Delete user '{username}'?", abort=True)
|
|
67
|
+
|
|
68
|
+
client.delete_user_by_username_if_exists(username)
|
|
69
|
+
print_success(f"User '{username}' deleted.")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command("reset-password")
|
|
73
|
+
def reset_password(
|
|
74
|
+
username: str = typer.Argument(help="Username to reset password for"),
|
|
75
|
+
profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"),
|
|
76
|
+
) -> None:
|
|
77
|
+
"""Reset a user's password."""
|
|
78
|
+
client = get_client(profile)
|
|
79
|
+
users = client.list_users()
|
|
80
|
+
|
|
81
|
+
match = next((u for u in users if u.username == username), None)
|
|
82
|
+
if match is None:
|
|
83
|
+
abort(f"User '{username}' not found.")
|
|
84
|
+
|
|
85
|
+
new_password = client.reset_password_for_user(match)
|
|
86
|
+
print_success(f"Password reset for '{username}'. New temporary password: {new_password}")
|
datamasque_cli/config.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Profile-based configuration management.
|
|
2
|
+
|
|
3
|
+
Stores credentials and connection details in ~/.config/datamasque-cli/config.toml.
|
|
4
|
+
Supports named profiles (default, staging, prod, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import tomllib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import tomli_w
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
CONFIG_DIR = Path.home() / ".config" / "datamasque-cli"
|
|
16
|
+
CONFIG_FILE = CONFIG_DIR / "config.toml"
|
|
17
|
+
DEFAULT_PROFILE = "default"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Profile(BaseModel):
|
|
21
|
+
url: str = ""
|
|
22
|
+
username: str = ""
|
|
23
|
+
password: str = ""
|
|
24
|
+
# Disable TLS verification for instances with self-signed or expired certs
|
|
25
|
+
# (typically local dev). Persisted to config so you don't re-pass it per call.
|
|
26
|
+
verify_ssl: bool = True
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def is_configured(self) -> bool:
|
|
30
|
+
return bool(self.url and self.username and self.password)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Config(BaseModel):
|
|
34
|
+
profiles: dict[str, Profile] = Field(default_factory=dict)
|
|
35
|
+
active_profile: str = DEFAULT_PROFILE
|
|
36
|
+
|
|
37
|
+
def get_profile(self, name: str | None = None) -> Profile:
|
|
38
|
+
name = name or self.active_profile
|
|
39
|
+
# Return a fresh default for unknown names so callers can check
|
|
40
|
+
# `is_configured` without mutating the stored profile dict.
|
|
41
|
+
return self.profiles.get(name, Profile())
|
|
42
|
+
|
|
43
|
+
def set_profile(self, name: str, profile: Profile) -> None:
|
|
44
|
+
self.profiles[name] = profile
|
|
45
|
+
|
|
46
|
+
def delete_profile(self, name: str) -> bool:
|
|
47
|
+
if name not in self.profiles:
|
|
48
|
+
return False
|
|
49
|
+
del self.profiles[name]
|
|
50
|
+
return True
|
|
51
|
+
|
|
52
|
+
def list_profile_names(self) -> list[str]:
|
|
53
|
+
return list(self.profiles.keys())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def load_config() -> Config:
|
|
57
|
+
if not CONFIG_FILE.exists():
|
|
58
|
+
return Config()
|
|
59
|
+
|
|
60
|
+
with open(CONFIG_FILE, "rb") as f:
|
|
61
|
+
data = tomllib.load(f)
|
|
62
|
+
|
|
63
|
+
# Ignore unknown top-level or per-profile keys (e.g. the dead `verify_ssl`
|
|
64
|
+
# that older configs may still carry) so on-disk files written by previous
|
|
65
|
+
# releases keep loading cleanly.
|
|
66
|
+
profiles_raw = data.get("profiles", {}) or {}
|
|
67
|
+
profiles = {
|
|
68
|
+
name: Profile.model_validate({k: v for k, v in profile.items() if k in Profile.model_fields})
|
|
69
|
+
for name, profile in profiles_raw.items()
|
|
70
|
+
}
|
|
71
|
+
return Config(profiles=profiles, active_profile=data.get("active_profile", DEFAULT_PROFILE))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def save_config(config: Config) -> None:
|
|
75
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
|
|
77
|
+
data = config.model_dump()
|
|
78
|
+
|
|
79
|
+
with open(CONFIG_FILE, "wb") as f:
|
|
80
|
+
tomli_w.dump(data, f)
|
|
81
|
+
|
|
82
|
+
CONFIG_FILE.chmod(0o600)
|
datamasque_cli/main.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""DataMasque CLI entry point.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
dm auth login
|
|
5
|
+
dm run start --connection mydb --ruleset myrules
|
|
6
|
+
dm run list --status running
|
|
7
|
+
dm rulesets list --json
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from importlib.metadata import version as pkg_version
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
|
|
17
|
+
from datamasque_cli.commands import (
|
|
18
|
+
auth,
|
|
19
|
+
connections,
|
|
20
|
+
discovery,
|
|
21
|
+
files,
|
|
22
|
+
ruleset_libraries,
|
|
23
|
+
rulesets,
|
|
24
|
+
runs,
|
|
25
|
+
seeds,
|
|
26
|
+
system,
|
|
27
|
+
users,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
app = typer.Typer(
|
|
31
|
+
name="dm",
|
|
32
|
+
help="DataMasque CLI — manage data masking from the command line.",
|
|
33
|
+
no_args_is_help=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
app.add_typer(auth.app, name="auth")
|
|
37
|
+
app.add_typer(connections.app, name="connections")
|
|
38
|
+
app.add_typer(rulesets.app, name="rulesets")
|
|
39
|
+
app.add_typer(runs.app, name="run")
|
|
40
|
+
app.add_typer(users.app, name="users")
|
|
41
|
+
app.add_typer(discovery.app, name="discover")
|
|
42
|
+
app.add_typer(seeds.app, name="seeds")
|
|
43
|
+
app.add_typer(files.app, name="files")
|
|
44
|
+
app.add_typer(system.app, name="system")
|
|
45
|
+
app.add_typer(ruleset_libraries.app, name="libraries")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command()
|
|
49
|
+
def version() -> None:
|
|
50
|
+
"""Show the CLI version."""
|
|
51
|
+
console = Console(stderr=True)
|
|
52
|
+
console.print(" [#7B36F5]▷◁[/#7B36F5] ", end="")
|
|
53
|
+
console.print("[bold #7B36F5]DataMasque[/bold #7B36F5] CLI", end=" ")
|
|
54
|
+
typer.echo(f"v{pkg_version('datamasque-cli')}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
app()
|
datamasque_cli/output.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Output formatting for the CLI.
|
|
2
|
+
|
|
3
|
+
Supports JSON output (--json) for machine consumption,
|
|
4
|
+
and rich tables for human-readable display.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, NoReturn
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich.theme import Theme
|
|
16
|
+
|
|
17
|
+
_DM_THEME = Theme(
|
|
18
|
+
{
|
|
19
|
+
"status.finished": "bold green",
|
|
20
|
+
"status.finished_with_warnings": "bold yellow",
|
|
21
|
+
"status.running": "bold cyan",
|
|
22
|
+
"status.queued": "dim",
|
|
23
|
+
"status.failed": "bold red",
|
|
24
|
+
"status.cancelled": "dim strike",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Diagnostic messages go to stderr so piped JSON output stays clean.
|
|
29
|
+
console = Console(stderr=True, theme=_DM_THEME)
|
|
30
|
+
stdout_console = Console(theme=_DM_THEME)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Any top-level field whose lowercased name contains one of these substrings
|
|
34
|
+
# is replaced by `<redacted>` when a value dict passes through
|
|
35
|
+
# `redact_sensitive_fields`. Matches datamasque-python's SENSITIVE_REQUEST_DATA_KEYS.
|
|
36
|
+
_SENSITIVE_FIELD_SUBSTRINGS = ("password", "secret", "token", "key", "credential")
|
|
37
|
+
_REDACTED = "<redacted>"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def redact_sensitive_fields(data: dict[str, Any]) -> dict[str, Any]:
|
|
41
|
+
"""Return a copy of `data` with values of sensitive-named keys replaced by `<redacted>`.
|
|
42
|
+
|
|
43
|
+
Matches any key whose lowercased name contains `password`, `secret`, `token`,
|
|
44
|
+
`key`, or `credential`. Does not recurse into nested dicts/lists.
|
|
45
|
+
"""
|
|
46
|
+
return {
|
|
47
|
+
key: _REDACTED if any(word in key.lower() for word in _SENSITIVE_FIELD_SUBSTRINGS) else value
|
|
48
|
+
for key, value in data.items()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def print_json(data: object) -> None:
|
|
53
|
+
typer.echo(json.dumps(data, indent=2, default=str))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def print_table(
|
|
57
|
+
columns: list[str],
|
|
58
|
+
rows: list[list[Any]],
|
|
59
|
+
title: str | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
table = Table(title=title, show_header=True, header_style="bold cyan")
|
|
62
|
+
for col in columns:
|
|
63
|
+
table.add_column(col)
|
|
64
|
+
for row in rows:
|
|
65
|
+
table.add_row(*[str(v) if v is not None else "" for v in row])
|
|
66
|
+
stdout_console.print(table)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def print_kv(data: dict[str, Any], title: str | None = None) -> None:
|
|
70
|
+
"""Print key-value pairs as a two-column table."""
|
|
71
|
+
table = Table(title=title, show_header=False, show_edge=False, padding=(0, 2))
|
|
72
|
+
table.add_column("Key", style="bold")
|
|
73
|
+
table.add_column("Value")
|
|
74
|
+
for key, value in data.items():
|
|
75
|
+
table.add_row(key, str(value) if value is not None else "")
|
|
76
|
+
stdout_console.print(table)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def print_success(message: str) -> None:
|
|
80
|
+
console.print(f"[green]{message}[/green]")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def print_error(message: str) -> None:
|
|
84
|
+
console.print(f"[red]Error:[/red] {message}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def print_warning(message: str) -> None:
|
|
88
|
+
console.print(f"[yellow]Warning:[/yellow] {message}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def print_info(message: str) -> None:
|
|
92
|
+
console.print(f"[dim]{message}[/dim]")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def style_status(status: str) -> str:
|
|
96
|
+
"""Wrap a run status string in the appropriate colour tag."""
|
|
97
|
+
style_name = f"status.{status}"
|
|
98
|
+
return f"[{style_name}]{status}[/{style_name}]"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def render_output(
|
|
102
|
+
data: object,
|
|
103
|
+
*,
|
|
104
|
+
is_json: bool,
|
|
105
|
+
columns: list[str] | None = None,
|
|
106
|
+
title: str | None = None,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Unified output dispatcher.
|
|
109
|
+
|
|
110
|
+
When `is_json` is True, dumps `data` as JSON to stdout.
|
|
111
|
+
Otherwise renders a rich table from a list-of-dicts or a key-value dict.
|
|
112
|
+
"""
|
|
113
|
+
if is_json:
|
|
114
|
+
print_json(data)
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if not data:
|
|
118
|
+
print_info("No results.")
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
if isinstance(data, list) and len(data) > 0 and isinstance(data[0], dict):
|
|
122
|
+
cols = columns or list(data[0].keys())
|
|
123
|
+
rows = [[item.get(c) for c in cols] for item in data]
|
|
124
|
+
print_table(cols, rows, title=title)
|
|
125
|
+
elif isinstance(data, dict):
|
|
126
|
+
print_kv(data, title=title)
|
|
127
|
+
else:
|
|
128
|
+
typer.echo(data)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def abort(message: str) -> NoReturn:
|
|
132
|
+
print_error(message)
|
|
133
|
+
raise SystemExit(1)
|
datamasque_cli/py.typed
ADDED
|
File without changes
|