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/config.py ADDED
@@ -0,0 +1,141 @@
1
+ """Connection configuration management for ~/.entropy-data/config.toml."""
2
+
3
+ import os
4
+ import stat
5
+ import tomllib
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ import tomli_w
10
+
11
+ CONFIG_DIR = Path.home() / ".entropy-data"
12
+ CONFIG_FILE = CONFIG_DIR / "config.toml"
13
+ DEFAULT_HOST = "https://api.entropy-data.com"
14
+
15
+
16
+ class ConfigurationError(Exception):
17
+ """Missing or invalid configuration."""
18
+
19
+
20
+ @dataclass
21
+ class ConnectionConfig:
22
+ api_key: str
23
+ host: str = DEFAULT_HOST
24
+
25
+
26
+ def load_config() -> dict:
27
+ """Read ~/.entropy-data/config.toml, return empty dict if missing."""
28
+ if not CONFIG_FILE.exists():
29
+ return {}
30
+ with open(CONFIG_FILE, "rb") as f:
31
+ return tomllib.load(f)
32
+
33
+
34
+ def save_config(config: dict) -> None:
35
+ """Write config.toml with 0600 permissions."""
36
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
37
+ with open(CONFIG_FILE, "wb") as f:
38
+ tomli_w.dump(config, f)
39
+ CONFIG_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR)
40
+
41
+
42
+ def resolve_connection(
43
+ connection_name: str | None = None,
44
+ cli_api_key: str | None = None,
45
+ cli_host: str | None = None,
46
+ ) -> ConnectionConfig:
47
+ """Resolve connection with precedence: CLI options > env vars > config file."""
48
+ api_key = cli_api_key
49
+ host = cli_host
50
+
51
+ # Layer 2: environment variables
52
+ if api_key is None:
53
+ api_key = os.getenv("ENTROPY_DATA_API_KEY")
54
+ if host is None:
55
+ host = os.getenv("ENTROPY_DATA_HOST")
56
+
57
+ # Layer 3: config file
58
+ if api_key is None or host is None:
59
+ config = load_config()
60
+ connections = config.get("connections", {})
61
+
62
+ name = connection_name or config.get("default_connection_name")
63
+ if connection_name and connection_name not in connections:
64
+ raise ConfigurationError(f"Connection '{connection_name}' not found.")
65
+ if name and name in connections:
66
+ conn = connections[name]
67
+ if api_key is None:
68
+ api_key = conn.get("api_key")
69
+ if host is None:
70
+ host = conn.get("host")
71
+
72
+ # Default host
73
+ if host is None:
74
+ host = DEFAULT_HOST
75
+
76
+ if api_key is None:
77
+ raise ConfigurationError(
78
+ "No API key found. Set ENTROPY_DATA_API_KEY, use --api-key, or run: entropy-data connection add <name>"
79
+ )
80
+
81
+ return ConnectionConfig(api_key=api_key, host=host)
82
+
83
+
84
+ def add_connection(name: str, api_key: str, host: str = DEFAULT_HOST) -> None:
85
+ """Add or update a named connection."""
86
+ if not name or not name.strip():
87
+ raise ConfigurationError("Connection name must not be empty.")
88
+ config = load_config()
89
+ if "connections" not in config:
90
+ config["connections"] = {}
91
+ config["connections"][name] = {"api_key": api_key, "host": host}
92
+ # Set as default if it's the first connection
93
+ if "default_connection_name" not in config:
94
+ config["default_connection_name"] = name
95
+ save_config(config)
96
+
97
+
98
+ def remove_connection(name: str) -> None:
99
+ """Remove a named connection."""
100
+ config = load_config()
101
+ connections = config.get("connections", {})
102
+ if name not in connections:
103
+ raise ConfigurationError(f"Connection '{name}' not found.")
104
+ del connections[name]
105
+ # Clear default if we removed it
106
+ if config.get("default_connection_name") == name:
107
+ if connections:
108
+ config["default_connection_name"] = next(iter(connections))
109
+ else:
110
+ config.pop("default_connection_name", None)
111
+ save_config(config)
112
+
113
+
114
+ def set_default_connection(name: str) -> None:
115
+ """Set the default connection."""
116
+ config = load_config()
117
+ connections = config.get("connections", {})
118
+ if name not in connections:
119
+ raise ConfigurationError(f"Connection '{name}' not found.")
120
+ config["default_connection_name"] = name
121
+ save_config(config)
122
+
123
+
124
+ def list_connections() -> list[dict]:
125
+ """List all connections with masked API keys."""
126
+ config = load_config()
127
+ default_name = config.get("default_connection_name")
128
+ connections = config.get("connections", {})
129
+ result = []
130
+ for name, conn in connections.items():
131
+ api_key = conn.get("api_key", "")
132
+ masked = api_key[:4] + "..." + api_key[-4:] if len(api_key) > 8 else "****"
133
+ result.append(
134
+ {
135
+ "name": name,
136
+ "host": conn.get("host", DEFAULT_HOST),
137
+ "api_key": masked,
138
+ "default": name == default_name,
139
+ }
140
+ )
141
+ return result
entropy_data/output.py ADDED
@@ -0,0 +1,119 @@
1
+ """Output formatting for CLI results."""
2
+
3
+ import json
4
+ from enum import Enum
5
+
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ console = Console()
10
+ error_console = Console(stderr=True)
11
+
12
+
13
+ class OutputFormat(str, Enum):
14
+ table = "table"
15
+ json = "json"
16
+
17
+
18
+ # Column definitions per resource type: list of (header, dict_key)
19
+ RESOURCE_COLUMNS: dict[str, list[tuple[str, str]]] = {
20
+ "dataproducts": [("ID", "id"), ("Title", "name"), ("Status", "status"), ("Owner", "team.name")],
21
+ "datacontracts": [("ID", "id"), ("Title", "name"), ("Version", "version"), ("Owner", "team.name")],
22
+ "access": [
23
+ ("ID", "id"),
24
+ ("Purpose", "info.purpose"),
25
+ ("Status", "info.status"),
26
+ ("Active", "info.active"),
27
+ ("Provider", "provider.dataProductId"),
28
+ ("Consumer", "consumer.teamId"),
29
+ ],
30
+ "teams": [("ID", "id"), ("Name", "name"), ("Type", "type"), ("Parent", "parent")],
31
+ "sourcesystems": [("ID", "id"), ("Name", "name"), ("Owner", "owner")],
32
+ "definitions": [("ID", "id"), ("Name", "title"), ("Owner", "owner")],
33
+ "certifications": [("ID", "id"), ("Name", "name"), ("Rank", "rank"), ("Tag", "tag")],
34
+ "example-data": [("ID", "id"), ("Data Product", "dataProductId"), ("Schema", "schemaName")],
35
+ "test-results": [("ID", "id"), ("Data Contract", "dataContractId"), ("Result", "result")],
36
+ "events": [("ID", "id"), ("Type", "type"), ("Subject", "subject"), ("Time", "time")],
37
+ "costs": [("ID", "id"), ("Data Product", "dataProductId"), ("Amount", "amount"), ("Currency", "currency")],
38
+ "assets": [
39
+ ("ID", "id"),
40
+ ("Name", "info.name"),
41
+ ("Type", "info.type"),
42
+ ("Source", "info.source"),
43
+ ("Owner", "info.owner"),
44
+ ],
45
+ "tags": [("ID", "id"), ("Owner", "info.owner"), ("Description", "info.description")],
46
+ "lineage": [
47
+ ("Event Type", "eventType"),
48
+ ("Event Time", "eventTime"),
49
+ ("Job", "job.name"),
50
+ ("Namespace", "job.namespace"),
51
+ ],
52
+ "usage": [],
53
+ }
54
+
55
+
56
+ def _get_nested(data: dict, key: str) -> str:
57
+ """Get a nested value from a dict using dot notation."""
58
+ parts = key.split(".")
59
+ current = data
60
+ for part in parts:
61
+ if isinstance(current, dict):
62
+ current = current.get(part)
63
+ else:
64
+ return ""
65
+ return str(current) if current is not None else ""
66
+
67
+
68
+ def print_resource(data: dict, resource_type: str, fmt: OutputFormat) -> None:
69
+ """Print a single resource."""
70
+ if fmt == OutputFormat.json:
71
+ console.print_json(json.dumps(data))
72
+ return
73
+
74
+ columns = RESOURCE_COLUMNS.get(resource_type, [])
75
+ if columns:
76
+ table = Table(show_header=True)
77
+ for header, _ in columns:
78
+ table.add_column(header)
79
+ table.add_row(*[_get_nested(data, key) for _, key in columns])
80
+ console.print(table)
81
+ else:
82
+ console.print_json(json.dumps(data))
83
+
84
+
85
+ def print_resource_list(
86
+ data: list[dict], resource_type: str, fmt: OutputFormat, has_next_page: bool = False, page: int = 0
87
+ ) -> None:
88
+ """Print a list of resources."""
89
+ if fmt == OutputFormat.json:
90
+ console.print_json(json.dumps(data))
91
+ return
92
+
93
+ columns = RESOURCE_COLUMNS.get(resource_type, [])
94
+ if not columns:
95
+ console.print_json(json.dumps(data))
96
+ return
97
+
98
+ table = Table(show_header=True, title=f"{resource_type} (page {page})")
99
+ for header, _ in columns:
100
+ table.add_column(header)
101
+ for item in data:
102
+ table.add_row(*[_get_nested(item, key) for _, key in columns])
103
+ console.print(table)
104
+
105
+ if has_next_page:
106
+ console.print(f"\nMore results available. Use --page {page + 1} to see the next page.")
107
+
108
+
109
+ def print_success(message: str) -> None:
110
+ console.print(f"[green]{message}[/green]")
111
+
112
+
113
+ def print_link(url: str) -> None:
114
+ if url:
115
+ console.print(f"Open {url}")
116
+
117
+
118
+ def print_error(message: str) -> None:
119
+ error_console.print(f"[red]Error: {message}[/red]")
entropy_data/util.py ADDED
@@ -0,0 +1,22 @@
1
+ """Shared utilities."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import yaml
8
+
9
+
10
+ def read_body(file: Path) -> dict:
11
+ """Read JSON or YAML from file path or stdin (-)."""
12
+ if str(file) == "-":
13
+ content = sys.stdin.read()
14
+ else:
15
+ content = file.read_text()
16
+ try:
17
+ data = json.loads(content)
18
+ except json.JSONDecodeError:
19
+ data = yaml.safe_load(content)
20
+ if not isinstance(data, dict):
21
+ raise ValueError(f"Expected a JSON/YAML object, got {type(data).__name__}.")
22
+ return data
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: entropy-data
3
+ Version: 0.3.0
4
+ Summary: CLI for Entropy Data
5
+ Project-URL: Homepage, https://entropy-data.com
6
+ Project-URL: Documentation, https://docs.entropy-data.com
7
+ Project-URL: Repository, https://github.com/entropy-data/entropy-data-cli
8
+ Author-email: Entropy Data <support@entropy-data.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.12
12
+ Requires-Dist: pydantic<3.0,>=2.10
13
+ Requires-Dist: python-dotenv<2.0,>=1.0
14
+ Requires-Dist: pyyaml<7.0,>=6.0
15
+ Requires-Dist: requests<3.0,>=2.32
16
+ Requires-Dist: rich<16.0,>=14.0
17
+ Requires-Dist: tomli-w<2.0,>=1.0
18
+ Requires-Dist: typer<1.0,>=0.15.0
19
+ Provides-Extra: dev
20
+ Requires-Dist: pre-commit>=3.7; extra == 'dev'
21
+ Requires-Dist: pytest>=8.0; extra == 'dev'
22
+ Requires-Dist: responses>=0.25; extra == 'dev'
23
+ Requires-Dist: ruff>=0.8; extra == 'dev'
@@ -0,0 +1,33 @@
1
+ entropy_data/__init__.py,sha256=7gUBK-cksVtizDORiQOtbtPFmpRbZbMORdHsf0DwuuY,147
2
+ entropy_data/__main__.py,sha256=TPkjTet86UW21BUomzL6QcGH_jKFOFwM2iHApJ-sohk,40
3
+ entropy_data/cli.py,sha256=uZ7RUp1FNzVGm4u_dtWEYz2Ri5JgibAOZsA6gGZGEDc,5901
4
+ entropy_data/client.py,sha256=ADjeDNVH18R-DX-3aMxYdNdnq0Iz00mgC6T3WFkcCCg,6790
5
+ entropy_data/config.py,sha256=yYm5_y5dEs6uEwhey79FBeLwOOLqhm-hZ1kjtAcCuoc,4476
6
+ entropy_data/output.py,sha256=eesJqR_bxMqek9_bGqHaxFKQZcL5XLzzljb3zPrWqy0,3958
7
+ entropy_data/util.py,sha256=jtoyru6G0Ukroj_jQ7m1McAKEoroekxZnPAsoyJ95Yc,537
8
+ entropy_data/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ entropy_data/commands/access.py,sha256=-ymE9t6202zEK82S2wYJRug5mWTE9WJW9zvp_iQpDiI,4102
10
+ entropy_data/commands/api_keys.py,sha256=Jj1_lj3tUc1LhpK4H5rjfWYrqNZtZyO97aH4Q3VsWqs,2206
11
+ entropy_data/commands/assets.py,sha256=aPy652XANlOqG4eK0nXuhZgQ5Yiywn9EBQhKOOL_oSA,2566
12
+ entropy_data/commands/certifications.py,sha256=pIukkTX4k3-VHTF_VPU_e2-WcfkWw-UB9VKYAKK7N4s,2714
13
+ entropy_data/commands/connection.py,sha256=Os7QR0SGugMoHgxRYIBOFM14kGg5GkGsXhHSJ8Ycq98,2707
14
+ entropy_data/commands/costs.py,sha256=VABkHbv3NK_pfQFkl3EJktb-x5Oo40kT6r6KExg7q6U,1846
15
+ entropy_data/commands/datacontracts.py,sha256=Rg8b8zAlHfc2y4eL4ITfTih9vPiX5E_P5yb98vFy824,4032
16
+ entropy_data/commands/dataproducts.py,sha256=Esexadd3kQyDIztYt_LLyoLy0JSiq8yC9kH28QXrLf0,3280
17
+ entropy_data/commands/definitions.py,sha256=lWOegYFHRlFrl7rr8q0Amek67n3SOWEZ2x2QCQrPohU,2651
18
+ entropy_data/commands/events.py,sha256=dKfVuWCDJRTuyeJaMNuG1Ly_JbFIU_pPxhYCRzsO9D8,976
19
+ entropy_data/commands/example_data.py,sha256=D5ZZuUji-p6a6uJQCBrJwiUzb8VcW_nLHV0vhKybBGk,2807
20
+ entropy_data/commands/import_export.py,sha256=DuHNM11yfye1gymYH5n--sjqWpTE9wua0VVLnEfwsU8,4289
21
+ entropy_data/commands/lineage.py,sha256=NpUyaVMmDV7Jrkb66tNGHmJiDFnHP_AzMn34QrXB1Sc,3961
22
+ entropy_data/commands/search.py,sha256=YO0cGjDHCiQh9OEHPsNDTmYg4RHy0yP1bg8NIhHTiXc,1824
23
+ entropy_data/commands/settings.py,sha256=EBSNAV9AGpF5_4yXjxSLBD17-w361zG9CAU3Xipc6HM,2349
24
+ entropy_data/commands/sourcesystems.py,sha256=a5tzl69ntolVV_Nn94sdT1siPBMjtC8JHiARBcYJLgE,2703
25
+ entropy_data/commands/tags.py,sha256=IQ9HxljQ7r6bNweitGUxDLSslfmamztNHhV0TDB6bEA,2760
26
+ entropy_data/commands/teams.py,sha256=NeUAW59wHEQsrM1c-iw3bsJ3GQUVPV4g5ySls74p1m8,2525
27
+ entropy_data/commands/test_results.py,sha256=64uNhlXsIx5J-mfoVkO5j8Naox0-T_aF-00g7vOgWvk,2848
28
+ entropy_data/commands/usage.py,sha256=sE1JH5_3iNSRTwYFEZqHWBeRiAcjYMTv_M8nByiE6NA,3473
29
+ entropy_data-0.3.0.dist-info/METADATA,sha256=Hz9pp9brktLNN-BLY0d3vqIE_kV-7LCJ84_qD2c8sjQ,828
30
+ entropy_data-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
31
+ entropy_data-0.3.0.dist-info/entry_points.txt,sha256=2MI80plj9d4TOc2jMFpcyBGOidIQkH4-1jiV-oT1shk,54
32
+ entropy_data-0.3.0.dist-info/licenses/LICENSE,sha256=gn-OXYQ3yCus2bnJ2MHbmkOx7JUW5fBqzzN7U_Snbw4,1069
33
+ entropy_data-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ entropy-data = entropy_data.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Entropy Data
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.