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.
@@ -0,0 +1,3 @@
1
+ """Entropy Data CLI — command-line interface for Entropy Data."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,3 @@
1
+ from entropy_data_cli.cli import app
2
+
3
+ app()
@@ -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)