entropy-data-cli 0.2.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.
- entropy_data_cli/__init__.py +3 -0
- entropy_data_cli/__main__.py +3 -0
- entropy_data_cli/cli.py +115 -0
- entropy_data_cli/client.py +146 -0
- entropy_data_cli/commands/__init__.py +0 -0
- entropy_data_cli/commands/access.py +128 -0
- entropy_data_cli/commands/certifications.py +80 -0
- entropy_data_cli/commands/connection.py +90 -0
- entropy_data_cli/commands/datacontracts.py +93 -0
- entropy_data_cli/commands/dataproducts.py +93 -0
- entropy_data_cli/commands/definitions.py +80 -0
- entropy_data_cli/commands/events.py +32 -0
- entropy_data_cli/commands/example_data.py +83 -0
- entropy_data_cli/commands/search.py +55 -0
- entropy_data_cli/commands/sourcesystems.py +80 -0
- entropy_data_cli/commands/teams.py +80 -0
- entropy_data_cli/commands/test_results.py +85 -0
- entropy_data_cli/config.py +141 -0
- entropy_data_cli/output.py +103 -0
- entropy_data_cli/util.py +22 -0
- entropy_data_cli-0.2.0.dist-info/METADATA +22 -0
- entropy_data_cli-0.2.0.dist-info/RECORD +25 -0
- entropy_data_cli-0.2.0.dist-info/WHEEL +4 -0
- entropy_data_cli-0.2.0.dist-info/entry_points.txt +2 -0
- entropy_data_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
entropy_data_cli/cli.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Entropy Data CLI — main application entry point."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Annotated, Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from entropy_data_cli import __version__
|
|
11
|
+
from entropy_data_cli.client import ApiError, EntropyDataClient
|
|
12
|
+
from entropy_data_cli.config import ConfigurationError, resolve_connection
|
|
13
|
+
from entropy_data_cli.output import OutputFormat
|
|
14
|
+
|
|
15
|
+
# Global state shared across commands
|
|
16
|
+
_connection_name: str | None = None
|
|
17
|
+
_cli_api_key: str | None = None
|
|
18
|
+
_cli_host: str | None = None
|
|
19
|
+
_output_format: OutputFormat = OutputFormat.table
|
|
20
|
+
_debug: bool = False
|
|
21
|
+
|
|
22
|
+
error_console = Console(stderr=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_client() -> EntropyDataClient:
|
|
26
|
+
"""Create an API client from the resolved connection config."""
|
|
27
|
+
config = resolve_connection(
|
|
28
|
+
connection_name=_connection_name,
|
|
29
|
+
cli_api_key=_cli_api_key,
|
|
30
|
+
cli_host=_cli_host,
|
|
31
|
+
)
|
|
32
|
+
return EntropyDataClient(config)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_output_format() -> OutputFormat:
|
|
36
|
+
return _output_format
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def handle_error(e: Exception) -> None:
|
|
40
|
+
"""Handle errors with appropriate output and exit codes."""
|
|
41
|
+
if _debug:
|
|
42
|
+
raise e
|
|
43
|
+
if isinstance(e, ConfigurationError):
|
|
44
|
+
error_console.print(f"[red]Configuration error: {e}[/red]")
|
|
45
|
+
raise SystemExit(2)
|
|
46
|
+
if isinstance(e, ApiError):
|
|
47
|
+
error_console.print(f"[red]API error: {e}[/red]")
|
|
48
|
+
raise SystemExit(1)
|
|
49
|
+
error_console.print(f"[red]Error: {e}[/red]")
|
|
50
|
+
raise SystemExit(1)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def version_callback(value: bool) -> None:
|
|
54
|
+
if value:
|
|
55
|
+
print(f"entropy-data {__version__}")
|
|
56
|
+
raise typer.Exit()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
app = typer.Typer(
|
|
60
|
+
name="entropy-data",
|
|
61
|
+
help="CLI for Entropy Data.",
|
|
62
|
+
no_args_is_help=True,
|
|
63
|
+
rich_markup_mode="rich",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.callback()
|
|
68
|
+
def main(
|
|
69
|
+
version: Annotated[
|
|
70
|
+
Optional[bool],
|
|
71
|
+
typer.Option("--version", "-v", help="Show version and exit.", callback=version_callback, is_eager=True),
|
|
72
|
+
] = None,
|
|
73
|
+
connection: Annotated[Optional[str], typer.Option("--connection", "-c", help="Named connection to use.")] = None,
|
|
74
|
+
api_key: Annotated[Optional[str], typer.Option("--api-key", help="API key (overrides config and env).")] = None,
|
|
75
|
+
host: Annotated[Optional[str], typer.Option("--host", help="API host URL (overrides config and env).")] = None,
|
|
76
|
+
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format.")] = OutputFormat.table,
|
|
77
|
+
debug: Annotated[bool, typer.Option("--debug", help="Enable debug output.")] = False,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Entropy Data CLI — manage your data platform from the command line."""
|
|
80
|
+
global _connection_name, _cli_api_key, _cli_host, _output_format, _debug
|
|
81
|
+
_connection_name = connection
|
|
82
|
+
_cli_api_key = api_key
|
|
83
|
+
_cli_host = host
|
|
84
|
+
_output_format = output
|
|
85
|
+
_debug = debug
|
|
86
|
+
if debug:
|
|
87
|
+
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# Register command groups
|
|
91
|
+
from entropy_data_cli.commands.access import access_app # noqa: E402
|
|
92
|
+
from entropy_data_cli.commands.certifications import certifications_app # noqa: E402
|
|
93
|
+
from entropy_data_cli.commands.connection import connection_app # noqa: E402
|
|
94
|
+
from entropy_data_cli.commands.datacontracts import datacontracts_app # noqa: E402
|
|
95
|
+
from entropy_data_cli.commands.dataproducts import dataproducts_app # noqa: E402
|
|
96
|
+
from entropy_data_cli.commands.definitions import definitions_app # noqa: E402
|
|
97
|
+
from entropy_data_cli.commands.events import events_app # noqa: E402
|
|
98
|
+
from entropy_data_cli.commands.example_data import example_data_app # noqa: E402
|
|
99
|
+
from entropy_data_cli.commands.search import search_app # noqa: E402
|
|
100
|
+
from entropy_data_cli.commands.sourcesystems import sourcesystems_app # noqa: E402
|
|
101
|
+
from entropy_data_cli.commands.teams import teams_app # noqa: E402
|
|
102
|
+
from entropy_data_cli.commands.test_results import test_results_app # noqa: E402
|
|
103
|
+
|
|
104
|
+
app.add_typer(connection_app, name="connection", help="Manage connections.")
|
|
105
|
+
app.add_typer(teams_app, name="teams", help="Manage teams.")
|
|
106
|
+
app.add_typer(dataproducts_app, name="dataproducts", help="Manage data products.")
|
|
107
|
+
app.add_typer(datacontracts_app, name="datacontracts", help="Manage data contracts.")
|
|
108
|
+
app.add_typer(access_app, name="access", help="Manage access (data usage agreements).")
|
|
109
|
+
app.add_typer(sourcesystems_app, name="sourcesystems", help="Manage source systems.")
|
|
110
|
+
app.add_typer(definitions_app, name="definitions", help="Manage definitions.")
|
|
111
|
+
app.add_typer(certifications_app, name="certifications", help="Manage certifications.")
|
|
112
|
+
app.add_typer(example_data_app, name="example-data", help="Manage example data.")
|
|
113
|
+
app.add_typer(test_results_app, name="test-results", help="Manage test results.")
|
|
114
|
+
app.add_typer(events_app, name="events", help="Poll events.")
|
|
115
|
+
app.add_typer(search_app, name="search", help="Search across resources.")
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""HTTP client for the Entropy Data API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from entropy_data_cli.config import ConnectionConfig
|
|
9
|
+
|
|
10
|
+
RESPONSE_HEADER_LOCATION_HTML = "location-html"
|
|
11
|
+
REQUEST_TIMEOUT = 30
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ApiError(Exception):
|
|
15
|
+
"""HTTP error from the Entropy Data API."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, status_code: int, message: str, url: str):
|
|
18
|
+
self.status_code = status_code
|
|
19
|
+
self.url = url
|
|
20
|
+
super().__init__(f"HTTP {status_code} from {url}: {message}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NotFoundError(ApiError):
|
|
24
|
+
"""404 from the API."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ValidationError(ApiError):
|
|
28
|
+
"""422 from the API."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _raise_for_status(response: requests.Response) -> None:
|
|
32
|
+
"""Raise appropriate ApiError subclass for non-2xx responses."""
|
|
33
|
+
if response.ok:
|
|
34
|
+
return
|
|
35
|
+
message = response.text
|
|
36
|
+
try:
|
|
37
|
+
body = response.json()
|
|
38
|
+
message = body.get("detail") or body.get("message") or body.get("title") or response.text
|
|
39
|
+
except (json.JSONDecodeError, AttributeError):
|
|
40
|
+
# If the response is HTML, extract a useful message instead of dumping raw markup
|
|
41
|
+
if "<html" in message.lower():
|
|
42
|
+
match = re.search(r"<title>([^<]+)</title>", message, re.IGNORECASE)
|
|
43
|
+
message = match.group(1).strip() if match else response.reason or "Server error"
|
|
44
|
+
if response.status_code == 404:
|
|
45
|
+
raise NotFoundError(response.status_code, message, response.url)
|
|
46
|
+
if response.status_code == 422:
|
|
47
|
+
raise ValidationError(response.status_code, message, response.url)
|
|
48
|
+
raise ApiError(response.status_code, message, response.url)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _has_next_page(response: requests.Response) -> bool:
|
|
52
|
+
"""Check if the Link header contains rel="next"."""
|
|
53
|
+
link = response.headers.get("Link", "")
|
|
54
|
+
return bool(re.search(r'rel="next"', link))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
MAX_RESOURCE_ID_LENGTH = 256
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _validate_resource_id(resource_id: str) -> None:
|
|
61
|
+
"""Reject empty, too-long, or path-traversal resource IDs."""
|
|
62
|
+
if not resource_id:
|
|
63
|
+
raise ValueError("Resource ID must not be empty.")
|
|
64
|
+
if len(resource_id) > MAX_RESOURCE_ID_LENGTH:
|
|
65
|
+
raise ValueError(f"Resource ID must not exceed {MAX_RESOURCE_ID_LENGTH} characters.")
|
|
66
|
+
if ".." in resource_id.split("/"):
|
|
67
|
+
raise ValueError(f"Resource ID must not contain path traversal: '{resource_id}'")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _validate_page(page: int) -> None:
|
|
71
|
+
"""Reject negative page numbers."""
|
|
72
|
+
if page < 0:
|
|
73
|
+
raise ValueError(f"Page number must not be negative: {page}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class EntropyDataClient:
|
|
77
|
+
def __init__(self, config: ConnectionConfig):
|
|
78
|
+
self.base_url = config.host.rstrip("/")
|
|
79
|
+
self.session = requests.Session()
|
|
80
|
+
self.session.headers.update(
|
|
81
|
+
{
|
|
82
|
+
"x-api-key": config.api_key,
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def list_resources(self, path: str, params: dict | None = None) -> tuple[list[dict], bool]:
|
|
88
|
+
"""GET /api/{path}. Returns (items, has_next_page)."""
|
|
89
|
+
if params and "p" in params:
|
|
90
|
+
_validate_page(int(params["p"]))
|
|
91
|
+
response = self.session.get(f"{self.base_url}/api/{path}", params=params, timeout=REQUEST_TIMEOUT)
|
|
92
|
+
_raise_for_status(response)
|
|
93
|
+
return response.json(), _has_next_page(response)
|
|
94
|
+
|
|
95
|
+
def get_resource(self, path: str, resource_id: str) -> dict:
|
|
96
|
+
"""GET /api/{path}/{id}."""
|
|
97
|
+
_validate_resource_id(resource_id)
|
|
98
|
+
response = self.session.get(f"{self.base_url}/api/{path}/{resource_id}", timeout=REQUEST_TIMEOUT)
|
|
99
|
+
_raise_for_status(response)
|
|
100
|
+
return response.json()
|
|
101
|
+
|
|
102
|
+
def put_resource(self, path: str, resource_id: str, body: dict) -> str | None:
|
|
103
|
+
"""PUT /api/{path}/{id}. Returns location-html URL if present."""
|
|
104
|
+
_validate_resource_id(resource_id)
|
|
105
|
+
if "id" in body and body["id"] != resource_id:
|
|
106
|
+
body = {**body, "id": resource_id}
|
|
107
|
+
response = self.session.put(f"{self.base_url}/api/{path}/{resource_id}", json=body, timeout=REQUEST_TIMEOUT)
|
|
108
|
+
_raise_for_status(response)
|
|
109
|
+
return response.headers.get(RESPONSE_HEADER_LOCATION_HTML)
|
|
110
|
+
|
|
111
|
+
def delete_resource(self, path: str, resource_id: str) -> None:
|
|
112
|
+
"""DELETE /api/{path}/{id}."""
|
|
113
|
+
_validate_resource_id(resource_id)
|
|
114
|
+
response = self.session.delete(f"{self.base_url}/api/{path}/{resource_id}", timeout=REQUEST_TIMEOUT)
|
|
115
|
+
_raise_for_status(response)
|
|
116
|
+
|
|
117
|
+
def post_action(self, path: str, resource_id: str, action: str) -> str | None:
|
|
118
|
+
"""POST /api/{path}/{id}/{action}. Returns location-html URL if present."""
|
|
119
|
+
_validate_resource_id(resource_id)
|
|
120
|
+
response = self.session.post(
|
|
121
|
+
f"{self.base_url}/api/{path}/{resource_id}/{action}", timeout=REQUEST_TIMEOUT
|
|
122
|
+
)
|
|
123
|
+
_raise_for_status(response)
|
|
124
|
+
return response.headers.get(RESPONSE_HEADER_LOCATION_HTML)
|
|
125
|
+
|
|
126
|
+
def post_resource(self, path: str, body: dict) -> str | None:
|
|
127
|
+
"""POST /api/{path}. Returns location-html URL if present."""
|
|
128
|
+
response = self.session.post(f"{self.base_url}/api/{path}", json=body, timeout=REQUEST_TIMEOUT)
|
|
129
|
+
_raise_for_status(response)
|
|
130
|
+
return response.headers.get(RESPONSE_HEADER_LOCATION_HTML)
|
|
131
|
+
|
|
132
|
+
def get_events(self, last_event_id: str | None = None) -> list[dict]:
|
|
133
|
+
"""GET /api/events. Returns list of events."""
|
|
134
|
+
params = {}
|
|
135
|
+
if last_event_id:
|
|
136
|
+
params["lastEventId"] = last_event_id
|
|
137
|
+
response = self.session.get(f"{self.base_url}/api/events", params=params, timeout=REQUEST_TIMEOUT)
|
|
138
|
+
_raise_for_status(response)
|
|
139
|
+
return response.json()
|
|
140
|
+
|
|
141
|
+
def search(self, query: str, **params) -> dict:
|
|
142
|
+
"""GET /api/search."""
|
|
143
|
+
params["query"] = query
|
|
144
|
+
response = self.session.get(f"{self.base_url}/api/search", params=params, timeout=REQUEST_TIMEOUT)
|
|
145
|
+
_raise_for_status(response)
|
|
146
|
+
return response.json()
|
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Access (data usage agreements) commands."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from entropy_data_cli.output import OutputFormat, print_link, print_resource, print_resource_list, print_success
|
|
9
|
+
from entropy_data_cli.util import read_body
|
|
10
|
+
|
|
11
|
+
access_app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
RESOURCE_PATH = "access"
|
|
13
|
+
RESOURCE_TYPE = "access"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@access_app.command("list")
|
|
17
|
+
def list_access(
|
|
18
|
+
page: Annotated[int, typer.Option("--page", "-p", help="Page number (0-indexed).")] = 0,
|
|
19
|
+
output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""List all access agreements."""
|
|
22
|
+
from entropy_data_cli.cli import get_client, get_output_format, handle_error
|
|
23
|
+
|
|
24
|
+
fmt = output or get_output_format()
|
|
25
|
+
try:
|
|
26
|
+
client = get_client()
|
|
27
|
+
data, has_next = client.list_resources(RESOURCE_PATH, params={"p": page})
|
|
28
|
+
print_resource_list(data, RESOURCE_TYPE, fmt, has_next_page=has_next, page=page)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
handle_error(e)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@access_app.command("get")
|
|
34
|
+
def get_access(
|
|
35
|
+
id: Annotated[str, typer.Argument(help="Access agreement ID.")],
|
|
36
|
+
output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Get an access agreement by ID."""
|
|
39
|
+
from entropy_data_cli.cli import get_client, get_output_format, handle_error
|
|
40
|
+
|
|
41
|
+
fmt = output or get_output_format()
|
|
42
|
+
try:
|
|
43
|
+
client = get_client()
|
|
44
|
+
data = client.get_resource(RESOURCE_PATH, id)
|
|
45
|
+
print_resource(data, RESOURCE_TYPE, fmt)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
handle_error(e)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@access_app.command("put")
|
|
51
|
+
def put_access(
|
|
52
|
+
id: Annotated[str, typer.Argument(help="Access agreement ID.")],
|
|
53
|
+
file: Annotated[Path, typer.Option("--file", "-f", help="JSON or YAML file (use - for stdin).")] = ...,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Create or update an access agreement."""
|
|
56
|
+
from entropy_data_cli.cli import get_client, handle_error
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
body = read_body(file)
|
|
60
|
+
client = get_client()
|
|
61
|
+
location = client.put_resource(RESOURCE_PATH, id, body)
|
|
62
|
+
print_success(f"Access agreement '{id}' saved.")
|
|
63
|
+
print_link(location)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
handle_error(e)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@access_app.command("delete")
|
|
69
|
+
def delete_access(
|
|
70
|
+
id: Annotated[str, typer.Argument(help="Access agreement ID.")],
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Delete an access agreement."""
|
|
73
|
+
from entropy_data_cli.cli import get_client, handle_error
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
client = get_client()
|
|
77
|
+
client.delete_resource(RESOURCE_PATH, id)
|
|
78
|
+
print_success(f"Access agreement '{id}' deleted.")
|
|
79
|
+
except Exception as e:
|
|
80
|
+
handle_error(e)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@access_app.command("approve")
|
|
84
|
+
def approve_access(
|
|
85
|
+
id: Annotated[str, typer.Argument(help="Access agreement ID.")],
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Approve an access agreement."""
|
|
88
|
+
from entropy_data_cli.cli import get_client, handle_error
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
client = get_client()
|
|
92
|
+
location = client.post_action(RESOURCE_PATH, id, "approve")
|
|
93
|
+
print_success(f"Access agreement '{id}' approved.")
|
|
94
|
+
print_link(location)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
handle_error(e)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@access_app.command("reject")
|
|
100
|
+
def reject_access(
|
|
101
|
+
id: Annotated[str, typer.Argument(help="Access agreement ID.")],
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Reject an access agreement."""
|
|
104
|
+
from entropy_data_cli.cli import get_client, handle_error
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
client = get_client()
|
|
108
|
+
location = client.post_action(RESOURCE_PATH, id, "reject")
|
|
109
|
+
print_success(f"Access agreement '{id}' rejected.")
|
|
110
|
+
print_link(location)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
handle_error(e)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@access_app.command("cancel")
|
|
116
|
+
def cancel_access(
|
|
117
|
+
id: Annotated[str, typer.Argument(help="Access agreement ID.")],
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Cancel an access agreement."""
|
|
120
|
+
from entropy_data_cli.cli import get_client, handle_error
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
client = get_client()
|
|
124
|
+
location = client.post_action(RESOURCE_PATH, id, "cancel")
|
|
125
|
+
print_success(f"Access agreement '{id}' cancelled.")
|
|
126
|
+
print_link(location)
|
|
127
|
+
except Exception as e:
|
|
128
|
+
handle_error(e)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Certifications commands."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from entropy_data_cli.output import OutputFormat, print_link, print_resource, print_resource_list, print_success
|
|
9
|
+
from entropy_data_cli.util import read_body
|
|
10
|
+
|
|
11
|
+
certifications_app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
RESOURCE_PATH = "certifications"
|
|
13
|
+
RESOURCE_TYPE = "certifications"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@certifications_app.command("list")
|
|
17
|
+
def list_certifications(
|
|
18
|
+
page: Annotated[int, typer.Option("--page", "-p", help="Page number (0-indexed).")] = 0,
|
|
19
|
+
output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""List all certifications."""
|
|
22
|
+
from entropy_data_cli.cli import get_client, get_output_format, handle_error
|
|
23
|
+
|
|
24
|
+
fmt = output or get_output_format()
|
|
25
|
+
try:
|
|
26
|
+
client = get_client()
|
|
27
|
+
data, has_next = client.list_resources(RESOURCE_PATH, params={"p": page})
|
|
28
|
+
print_resource_list(data, RESOURCE_TYPE, fmt, has_next_page=has_next, page=page)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
handle_error(e)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@certifications_app.command("get")
|
|
34
|
+
def get_certification(
|
|
35
|
+
id: Annotated[str, typer.Argument(help="Certification ID.")],
|
|
36
|
+
output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Get a certification by ID."""
|
|
39
|
+
from entropy_data_cli.cli import get_client, get_output_format, handle_error
|
|
40
|
+
|
|
41
|
+
fmt = output or get_output_format()
|
|
42
|
+
try:
|
|
43
|
+
client = get_client()
|
|
44
|
+
data = client.get_resource(RESOURCE_PATH, id)
|
|
45
|
+
print_resource(data, RESOURCE_TYPE, fmt)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
handle_error(e)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@certifications_app.command("put")
|
|
51
|
+
def put_certification(
|
|
52
|
+
id: Annotated[str, typer.Argument(help="Certification ID.")],
|
|
53
|
+
file: Annotated[Path, typer.Option("--file", "-f", help="JSON or YAML file (use - for stdin).")] = ...,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Create or update a certification."""
|
|
56
|
+
from entropy_data_cli.cli import get_client, handle_error
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
body = read_body(file)
|
|
60
|
+
client = get_client()
|
|
61
|
+
location = client.put_resource(RESOURCE_PATH, id, body)
|
|
62
|
+
print_success(f"Certification '{id}' saved.")
|
|
63
|
+
print_link(location)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
handle_error(e)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@certifications_app.command("delete")
|
|
69
|
+
def delete_certification(
|
|
70
|
+
id: Annotated[str, typer.Argument(help="Certification ID.")],
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Delete a certification."""
|
|
73
|
+
from entropy_data_cli.cli import get_client, handle_error
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
client = get_client()
|
|
77
|
+
client.delete_resource(RESOURCE_PATH, id)
|
|
78
|
+
print_success(f"Certification '{id}' deleted.")
|
|
79
|
+
except Exception as e:
|
|
80
|
+
handle_error(e)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Connection management commands."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
from entropy_data_cli import config as cfg
|
|
9
|
+
from entropy_data_cli.output import console, print_error, print_success
|
|
10
|
+
|
|
11
|
+
connection_app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@connection_app.command("list")
|
|
15
|
+
def list_connections() -> None:
|
|
16
|
+
"""List all configured connections."""
|
|
17
|
+
connections = cfg.list_connections()
|
|
18
|
+
if not connections:
|
|
19
|
+
console.print("No connections configured. Run: entropy-data connection add <name>")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
table = Table(show_header=True)
|
|
23
|
+
table.add_column("Name")
|
|
24
|
+
table.add_column("Host")
|
|
25
|
+
table.add_column("API Key")
|
|
26
|
+
table.add_column("Default")
|
|
27
|
+
for conn in connections:
|
|
28
|
+
table.add_row(
|
|
29
|
+
conn["name"],
|
|
30
|
+
conn["host"],
|
|
31
|
+
conn["api_key"],
|
|
32
|
+
"*" if conn["default"] else "",
|
|
33
|
+
)
|
|
34
|
+
console.print(table)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@connection_app.command("add")
|
|
38
|
+
def add_connection(
|
|
39
|
+
name: Annotated[str, typer.Argument(help="Connection name.")],
|
|
40
|
+
api_key: Annotated[str, typer.Option("--api-key", prompt="API key", help="The API key.")] = None,
|
|
41
|
+
host: Annotated[
|
|
42
|
+
str, typer.Option("--host", prompt="Host", prompt_required=False, help="API host URL.")
|
|
43
|
+
] = cfg.DEFAULT_HOST,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Add or update a named connection."""
|
|
46
|
+
try:
|
|
47
|
+
cfg.add_connection(name, api_key, host)
|
|
48
|
+
print_success(f"Connection '{name}' saved.")
|
|
49
|
+
except cfg.ConfigurationError as e:
|
|
50
|
+
print_error(str(e))
|
|
51
|
+
raise typer.Exit(1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@connection_app.command("remove")
|
|
55
|
+
def remove_connection(
|
|
56
|
+
name: Annotated[str, typer.Argument(help="Connection name to remove.")],
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Remove a named connection."""
|
|
59
|
+
try:
|
|
60
|
+
cfg.remove_connection(name)
|
|
61
|
+
print_success(f"Connection '{name}' removed.")
|
|
62
|
+
except cfg.ConfigurationError as e:
|
|
63
|
+
print_error(str(e))
|
|
64
|
+
raise typer.Exit(1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@connection_app.command("set-default")
|
|
68
|
+
def set_default(
|
|
69
|
+
name: Annotated[str, typer.Argument(help="Connection name to set as default.")],
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Set the default connection."""
|
|
72
|
+
try:
|
|
73
|
+
cfg.set_default_connection(name)
|
|
74
|
+
print_success(f"Default connection set to '{name}'.")
|
|
75
|
+
except cfg.ConfigurationError as e:
|
|
76
|
+
print_error(str(e))
|
|
77
|
+
raise typer.Exit(1)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@connection_app.command("test")
|
|
81
|
+
def test_connection() -> None:
|
|
82
|
+
"""Test the current connection by calling the API."""
|
|
83
|
+
from entropy_data_cli.cli import get_client, handle_error
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
client = get_client()
|
|
87
|
+
client.list_resources("teams", params={"p": "0"})
|
|
88
|
+
print_success("Connection successful.")
|
|
89
|
+
except Exception as e:
|
|
90
|
+
handle_error(e)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Data contracts commands."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from entropy_data_cli.output import OutputFormat, print_link, print_resource, print_resource_list, print_success
|
|
9
|
+
from entropy_data_cli.util import read_body
|
|
10
|
+
|
|
11
|
+
datacontracts_app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
RESOURCE_PATH = "datacontracts"
|
|
13
|
+
RESOURCE_TYPE = "datacontracts"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@datacontracts_app.command("list")
|
|
17
|
+
def list_datacontracts(
|
|
18
|
+
page: Annotated[int, typer.Option("--page", "-p", help="Page number (0-indexed).")] = 0,
|
|
19
|
+
query: Annotated[Optional[str], typer.Option("--query", "-q", help="Search term.")] = None,
|
|
20
|
+
owner: Annotated[Optional[str], typer.Option("--owner", help="Filter by owner.")] = None,
|
|
21
|
+
tag: Annotated[Optional[str], typer.Option("--tag", help="Filter by tag.")] = None,
|
|
22
|
+
sort: Annotated[Optional[str], typer.Option("--sort", help="Sort field.")] = None,
|
|
23
|
+
output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""List all data contracts."""
|
|
26
|
+
from entropy_data_cli.cli import get_client, get_output_format, handle_error
|
|
27
|
+
|
|
28
|
+
fmt = output or get_output_format()
|
|
29
|
+
try:
|
|
30
|
+
client = get_client()
|
|
31
|
+
params = {"p": page}
|
|
32
|
+
if query:
|
|
33
|
+
params["q"] = query
|
|
34
|
+
if owner:
|
|
35
|
+
params["owner"] = owner
|
|
36
|
+
if tag:
|
|
37
|
+
params["tag"] = tag
|
|
38
|
+
if sort:
|
|
39
|
+
params["sort"] = sort
|
|
40
|
+
data, has_next = client.list_resources(RESOURCE_PATH, params=params)
|
|
41
|
+
print_resource_list(data, RESOURCE_TYPE, fmt, has_next_page=has_next, page=page)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
handle_error(e)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@datacontracts_app.command("get")
|
|
47
|
+
def get_datacontract(
|
|
48
|
+
id: Annotated[str, typer.Argument(help="Data contract ID.")],
|
|
49
|
+
output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Get a data contract by ID."""
|
|
52
|
+
from entropy_data_cli.cli import get_client, get_output_format, handle_error
|
|
53
|
+
|
|
54
|
+
fmt = output or get_output_format()
|
|
55
|
+
try:
|
|
56
|
+
client = get_client()
|
|
57
|
+
data = client.get_resource(RESOURCE_PATH, id)
|
|
58
|
+
print_resource(data, RESOURCE_TYPE, fmt)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
handle_error(e)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@datacontracts_app.command("put")
|
|
64
|
+
def put_datacontract(
|
|
65
|
+
id: Annotated[str, typer.Argument(help="Data contract ID.")],
|
|
66
|
+
file: Annotated[Path, typer.Option("--file", "-f", help="JSON or YAML file (use - for stdin).")] = ...,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Create or update a data contract."""
|
|
69
|
+
from entropy_data_cli.cli import get_client, handle_error
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
body = read_body(file)
|
|
73
|
+
client = get_client()
|
|
74
|
+
location = client.put_resource(RESOURCE_PATH, id, body)
|
|
75
|
+
print_success(f"Data contract '{id}' saved.")
|
|
76
|
+
print_link(location)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
handle_error(e)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@datacontracts_app.command("delete")
|
|
82
|
+
def delete_datacontract(
|
|
83
|
+
id: Annotated[str, typer.Argument(help="Data contract ID.")],
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Delete a data contract."""
|
|
86
|
+
from entropy_data_cli.cli import get_client, handle_error
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
client = get_client()
|
|
90
|
+
client.delete_resource(RESOURCE_PATH, id)
|
|
91
|
+
print_success(f"Data contract '{id}' deleted.")
|
|
92
|
+
except Exception as e:
|
|
93
|
+
handle_error(e)
|