entropy-data 0.3.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/__init__.py +5 -0
- entropy_data/__main__.py +3 -0
- entropy_data/cli.py +133 -0
- entropy_data/client.py +163 -0
- entropy_data/commands/__init__.py +0 -0
- entropy_data/commands/access.py +128 -0
- entropy_data/commands/api_keys.py +65 -0
- entropy_data/commands/assets.py +80 -0
- entropy_data/commands/certifications.py +80 -0
- entropy_data/commands/connection.py +90 -0
- entropy_data/commands/costs.py +61 -0
- entropy_data/commands/datacontracts.py +115 -0
- entropy_data/commands/dataproducts.py +93 -0
- entropy_data/commands/definitions.py +80 -0
- entropy_data/commands/events.py +32 -0
- entropy_data/commands/example_data.py +83 -0
- entropy_data/commands/import_export.py +134 -0
- entropy_data/commands/lineage.py +107 -0
- entropy_data/commands/search.py +55 -0
- entropy_data/commands/settings.py +68 -0
- entropy_data/commands/sourcesystems.py +80 -0
- entropy_data/commands/tags.py +84 -0
- entropy_data/commands/teams.py +80 -0
- entropy_data/commands/test_results.py +85 -0
- entropy_data/commands/usage.py +99 -0
- entropy_data/config.py +141 -0
- entropy_data/output.py +119 -0
- entropy_data/util.py +22 -0
- entropy_data-0.3.0.dist-info/METADATA +23 -0
- entropy_data-0.3.0.dist-info/RECORD +33 -0
- entropy_data-0.3.0.dist-info/WHEEL +4 -0
- entropy_data-0.3.0.dist-info/entry_points.txt +2 -0
- entropy_data-0.3.0.dist-info/licenses/LICENSE +21 -0
entropy_data/__init__.py
ADDED
entropy_data/__main__.py
ADDED
entropy_data/cli.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
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 dotenv import load_dotenv
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from entropy_data import __version__
|
|
12
|
+
from entropy_data.client import ApiError, EntropyDataClient
|
|
13
|
+
from entropy_data.config import ConfigurationError, resolve_connection
|
|
14
|
+
from entropy_data.output import OutputFormat
|
|
15
|
+
|
|
16
|
+
# Global state shared across commands
|
|
17
|
+
_connection_name: str | None = None
|
|
18
|
+
_cli_api_key: str | None = None
|
|
19
|
+
_cli_host: str | None = None
|
|
20
|
+
_output_format: OutputFormat = OutputFormat.table
|
|
21
|
+
_debug: bool = False
|
|
22
|
+
|
|
23
|
+
error_console = Console(stderr=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_client() -> EntropyDataClient:
|
|
27
|
+
"""Create an API client from the resolved connection config."""
|
|
28
|
+
config = resolve_connection(
|
|
29
|
+
connection_name=_connection_name,
|
|
30
|
+
cli_api_key=_cli_api_key,
|
|
31
|
+
cli_host=_cli_host,
|
|
32
|
+
)
|
|
33
|
+
return EntropyDataClient(config)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_output_format() -> OutputFormat:
|
|
37
|
+
return _output_format
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def handle_error(e: Exception) -> None:
|
|
41
|
+
"""Handle errors with appropriate output and exit codes."""
|
|
42
|
+
if _debug:
|
|
43
|
+
raise e
|
|
44
|
+
if isinstance(e, ConfigurationError):
|
|
45
|
+
error_console.print(f"[red]Configuration error: {e}[/red]")
|
|
46
|
+
raise SystemExit(2)
|
|
47
|
+
if isinstance(e, ApiError):
|
|
48
|
+
error_console.print(f"[red]API error: {e}[/red]")
|
|
49
|
+
raise SystemExit(1)
|
|
50
|
+
error_console.print(f"[red]Error: {e}[/red]")
|
|
51
|
+
raise SystemExit(1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def version_callback(value: bool) -> None:
|
|
55
|
+
if value:
|
|
56
|
+
print(f"entropy-data {__version__}")
|
|
57
|
+
raise typer.Exit()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
app = typer.Typer(
|
|
61
|
+
name="entropy-data",
|
|
62
|
+
help="CLI for Entropy Data.",
|
|
63
|
+
no_args_is_help=True,
|
|
64
|
+
rich_markup_mode="rich",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.callback()
|
|
69
|
+
def main(
|
|
70
|
+
version: Annotated[
|
|
71
|
+
Optional[bool],
|
|
72
|
+
typer.Option("--version", "-v", help="Show version and exit.", callback=version_callback, is_eager=True),
|
|
73
|
+
] = None,
|
|
74
|
+
connection: Annotated[Optional[str], typer.Option("--connection", "-c", help="Named connection to use.")] = None,
|
|
75
|
+
api_key: Annotated[Optional[str], typer.Option("--api-key", help="API key (overrides config and env).")] = None,
|
|
76
|
+
host: Annotated[Optional[str], typer.Option("--host", help="API host URL (overrides config and env).")] = None,
|
|
77
|
+
output: Annotated[OutputFormat, typer.Option("--output", "-o", help="Output format.")] = OutputFormat.table,
|
|
78
|
+
debug: Annotated[bool, typer.Option("--debug", help="Enable debug output.")] = False,
|
|
79
|
+
) -> None:
|
|
80
|
+
"""Entropy Data CLI — manage your data platform from the command line."""
|
|
81
|
+
global _connection_name, _cli_api_key, _cli_host, _output_format, _debug
|
|
82
|
+
load_dotenv()
|
|
83
|
+
_connection_name = connection
|
|
84
|
+
_cli_api_key = api_key
|
|
85
|
+
_cli_host = host
|
|
86
|
+
_output_format = output
|
|
87
|
+
_debug = debug
|
|
88
|
+
if debug:
|
|
89
|
+
logging.basicConfig(level=logging.DEBUG, stream=sys.stderr)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Register command groups
|
|
93
|
+
from entropy_data.commands.access import access_app # noqa: E402
|
|
94
|
+
from entropy_data.commands.api_keys import api_keys_app # noqa: E402
|
|
95
|
+
from entropy_data.commands.assets import assets_app # noqa: E402
|
|
96
|
+
from entropy_data.commands.certifications import certifications_app # noqa: E402
|
|
97
|
+
from entropy_data.commands.connection import connection_app # noqa: E402
|
|
98
|
+
from entropy_data.commands.costs import costs_app # noqa: E402
|
|
99
|
+
from entropy_data.commands.datacontracts import datacontracts_app # noqa: E402
|
|
100
|
+
from entropy_data.commands.dataproducts import dataproducts_app # noqa: E402
|
|
101
|
+
from entropy_data.commands.definitions import definitions_app # noqa: E402
|
|
102
|
+
from entropy_data.commands.events import events_app # noqa: E402
|
|
103
|
+
from entropy_data.commands.example_data import example_data_app # noqa: E402
|
|
104
|
+
from entropy_data.commands.import_export import import_app # noqa: E402
|
|
105
|
+
from entropy_data.commands.lineage import lineage_app # noqa: E402
|
|
106
|
+
from entropy_data.commands.search import search_app # noqa: E402
|
|
107
|
+
from entropy_data.commands.settings import settings_app # noqa: E402
|
|
108
|
+
from entropy_data.commands.sourcesystems import sourcesystems_app # noqa: E402
|
|
109
|
+
from entropy_data.commands.tags import tags_app # noqa: E402
|
|
110
|
+
from entropy_data.commands.teams import teams_app # noqa: E402
|
|
111
|
+
from entropy_data.commands.test_results import test_results_app # noqa: E402
|
|
112
|
+
from entropy_data.commands.usage import usage_app # noqa: E402
|
|
113
|
+
|
|
114
|
+
app.add_typer(connection_app, name="connection", help="Manage connections.")
|
|
115
|
+
app.add_typer(teams_app, name="teams", help="Manage teams.")
|
|
116
|
+
app.add_typer(dataproducts_app, name="dataproducts", help="Manage data products.")
|
|
117
|
+
app.add_typer(datacontracts_app, name="datacontracts", help="Manage data contracts.")
|
|
118
|
+
app.add_typer(access_app, name="access", help="Manage access (data usage agreements).")
|
|
119
|
+
app.add_typer(sourcesystems_app, name="sourcesystems", help="Manage source systems.")
|
|
120
|
+
app.add_typer(definitions_app, name="definitions", help="Manage definitions.")
|
|
121
|
+
app.add_typer(certifications_app, name="certifications", help="Manage certifications.")
|
|
122
|
+
app.add_typer(example_data_app, name="example-data", help="Manage example data.")
|
|
123
|
+
app.add_typer(test_results_app, name="test-results", help="Manage test results.")
|
|
124
|
+
app.add_typer(costs_app, name="costs", help="Manage costs.")
|
|
125
|
+
app.add_typer(assets_app, name="assets", help="Manage data assets.")
|
|
126
|
+
app.add_typer(tags_app, name="tags", help="Manage tags.")
|
|
127
|
+
app.add_typer(api_keys_app, name="api-keys", help="Manage API keys.")
|
|
128
|
+
app.add_typer(settings_app, name="settings", help="Manage organization settings.")
|
|
129
|
+
app.add_typer(events_app, name="events", help="Poll events.")
|
|
130
|
+
app.add_typer(lineage_app, name="lineage", help="Manage lineage (OpenLineage events).")
|
|
131
|
+
app.add_typer(search_app, name="search", help="Search across resources.")
|
|
132
|
+
app.add_typer(usage_app, name="usage", help="Manage usage (OpenTelemetry traces).")
|
|
133
|
+
app.add_typer(import_app, name="import", help="Import organization exports.")
|
entropy_data/client.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""HTTP client for the Entropy Data API."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from entropy_data.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(f"{self.base_url}/api/{path}/{resource_id}/{action}", timeout=REQUEST_TIMEOUT)
|
|
121
|
+
_raise_for_status(response)
|
|
122
|
+
return response.headers.get(RESPONSE_HEADER_LOCATION_HTML)
|
|
123
|
+
|
|
124
|
+
def post_action_json(self, path: str, resource_id: str, action: str, params: dict | None = None,
|
|
125
|
+
timeout: int = REQUEST_TIMEOUT) -> dict:
|
|
126
|
+
"""POST /api/{path}/{id}/{action} with query params. Returns response JSON."""
|
|
127
|
+
_validate_resource_id(resource_id)
|
|
128
|
+
response = self.session.post(
|
|
129
|
+
f"{self.base_url}/api/{path}/{resource_id}/{action}", params=params, timeout=timeout,
|
|
130
|
+
)
|
|
131
|
+
_raise_for_status(response)
|
|
132
|
+
return response.json()
|
|
133
|
+
|
|
134
|
+
def post_resource(self, path: str, body: dict, params: dict | None = None) -> str | None:
|
|
135
|
+
"""POST /api/{path}. Returns location-html URL if present."""
|
|
136
|
+
response = self.session.post(f"{self.base_url}/api/{path}", json=body, params=params, timeout=REQUEST_TIMEOUT)
|
|
137
|
+
_raise_for_status(response)
|
|
138
|
+
return response.headers.get(RESPONSE_HEADER_LOCATION_HTML)
|
|
139
|
+
|
|
140
|
+
def delete_resources(self, path: str, params: dict | None = None) -> dict:
|
|
141
|
+
"""DELETE /api/{path} with query params. Returns response JSON."""
|
|
142
|
+
response = self.session.delete(f"{self.base_url}/api/{path}", params=params, timeout=REQUEST_TIMEOUT)
|
|
143
|
+
_raise_for_status(response)
|
|
144
|
+
try:
|
|
145
|
+
return response.json()
|
|
146
|
+
except Exception:
|
|
147
|
+
return {}
|
|
148
|
+
|
|
149
|
+
def get_events(self, last_event_id: str | None = None) -> list[dict]:
|
|
150
|
+
"""GET /api/events. Returns list of events."""
|
|
151
|
+
params = {}
|
|
152
|
+
if last_event_id:
|
|
153
|
+
params["lastEventId"] = last_event_id
|
|
154
|
+
response = self.session.get(f"{self.base_url}/api/events", params=params, timeout=REQUEST_TIMEOUT)
|
|
155
|
+
_raise_for_status(response)
|
|
156
|
+
return response.json()
|
|
157
|
+
|
|
158
|
+
def search(self, query: str, **params) -> dict:
|
|
159
|
+
"""GET /api/search."""
|
|
160
|
+
params["query"] = query
|
|
161
|
+
response = self.session.get(f"{self.base_url}/api/search", params=params, timeout=REQUEST_TIMEOUT)
|
|
162
|
+
_raise_for_status(response)
|
|
163
|
+
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.output import OutputFormat, print_link, print_resource, print_resource_list, print_success
|
|
9
|
+
from entropy_data.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 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 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 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 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 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 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 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,65 @@
|
|
|
1
|
+
"""API Keys commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from entropy_data.output import OutputFormat, console, print_success
|
|
9
|
+
|
|
10
|
+
api_keys_app = typer.Typer(no_args_is_help=True)
|
|
11
|
+
RESOURCE_PATH = "api-keys"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@api_keys_app.command("create")
|
|
15
|
+
def create_api_key(
|
|
16
|
+
scope: Annotated[str, typer.Option("--scope", help="Scope: 'team' (read/write) or 'team_read' (read-only).")],
|
|
17
|
+
team_id: Annotated[str, typer.Option("--team-id", help="Team ID to scope the key to.")],
|
|
18
|
+
display_name: Annotated[
|
|
19
|
+
Optional[str], typer.Option("--display-name", help="Human-readable name for the key.")
|
|
20
|
+
] = None,
|
|
21
|
+
output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Create a team-scoped API key."""
|
|
24
|
+
from entropy_data.cli import get_client, get_output_format, handle_error
|
|
25
|
+
|
|
26
|
+
fmt = output or get_output_format()
|
|
27
|
+
try:
|
|
28
|
+
body = {"scope": scope, "teamId": team_id}
|
|
29
|
+
if display_name:
|
|
30
|
+
body["displayName"] = display_name
|
|
31
|
+
client = get_client()
|
|
32
|
+
response = client.session.post(
|
|
33
|
+
f"{client.base_url}/api/{RESOURCE_PATH}",
|
|
34
|
+
json=body,
|
|
35
|
+
timeout=30,
|
|
36
|
+
)
|
|
37
|
+
from entropy_data.client import _raise_for_status
|
|
38
|
+
|
|
39
|
+
_raise_for_status(response)
|
|
40
|
+
data = response.json()
|
|
41
|
+
if fmt == OutputFormat.json:
|
|
42
|
+
console.print_json(json.dumps(data))
|
|
43
|
+
else:
|
|
44
|
+
print_success(f"API key created: {data.get('organizationApiKeyId')}")
|
|
45
|
+
key = data.get("key")
|
|
46
|
+
if key:
|
|
47
|
+
console.print(f"[bold]Key:[/bold] {key}")
|
|
48
|
+
console.print("[dim]This key is only shown once. Store it securely.[/dim]")
|
|
49
|
+
except Exception as e:
|
|
50
|
+
handle_error(e)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@api_keys_app.command("delete")
|
|
54
|
+
def delete_api_key(
|
|
55
|
+
id: Annotated[str, typer.Argument(help="API key ID.")],
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Delete a team-scoped API key."""
|
|
58
|
+
from entropy_data.cli import get_client, handle_error
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
client = get_client()
|
|
62
|
+
client.delete_resource(RESOURCE_PATH, id)
|
|
63
|
+
print_success(f"API key '{id}' deleted.")
|
|
64
|
+
except Exception as e:
|
|
65
|
+
handle_error(e)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Assets commands."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from entropy_data.output import OutputFormat, print_link, print_resource, print_resource_list, print_success
|
|
9
|
+
from entropy_data.util import read_body
|
|
10
|
+
|
|
11
|
+
assets_app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
RESOURCE_PATH = "assets"
|
|
13
|
+
RESOURCE_TYPE = "assets"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@assets_app.command("list")
|
|
17
|
+
def list_assets(
|
|
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 data assets."""
|
|
22
|
+
from entropy_data.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
|
+
@assets_app.command("get")
|
|
34
|
+
def get_asset(
|
|
35
|
+
id: Annotated[str, typer.Argument(help="Asset ID.")],
|
|
36
|
+
output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Get a data asset by ID."""
|
|
39
|
+
from entropy_data.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
|
+
@assets_app.command("put")
|
|
51
|
+
def put_asset(
|
|
52
|
+
id: Annotated[str, typer.Argument(help="Asset ID.")],
|
|
53
|
+
file: Annotated[Path, typer.Option("--file", "-f", help="JSON or YAML file (use - for stdin).")] = ...,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Create or update a data asset."""
|
|
56
|
+
from entropy_data.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"Asset '{id}' saved.")
|
|
63
|
+
print_link(location)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
handle_error(e)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@assets_app.command("delete")
|
|
69
|
+
def delete_asset(
|
|
70
|
+
id: Annotated[str, typer.Argument(help="Asset ID.")],
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Delete a data asset."""
|
|
73
|
+
from entropy_data.cli import get_client, handle_error
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
client = get_client()
|
|
77
|
+
client.delete_resource(RESOURCE_PATH, id)
|
|
78
|
+
print_success(f"Asset '{id}' deleted.")
|
|
79
|
+
except Exception as e:
|
|
80
|
+
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.output import OutputFormat, print_link, print_resource, print_resource_list, print_success
|
|
9
|
+
from entropy_data.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 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 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 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 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)
|