entropy-data 0.3.1__tar.gz → 0.3.3__tar.gz
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-0.3.1 → entropy_data-0.3.3}/CHANGELOG.md +13 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/PKG-INFO +1 -1
- {entropy_data-0.3.1 → entropy_data-0.3.3}/pyproject.toml +1 -1
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/cli.py +2 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/client.py +53 -3
- entropy_data-0.3.3/src/entropy_data/commands/connection.py +190 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/datacontracts.py +9 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/dataproducts.py +9 -0
- entropy_data-0.3.3/src/entropy_data/commands/gitconnections.py +183 -0
- entropy_data-0.3.3/src/entropy_data/commands/organization.py +51 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/config.py +10 -3
- entropy_data-0.3.3/tests/commands/test_connection.py +176 -0
- entropy_data-0.3.3/tests/commands/test_gitconnections.py +255 -0
- entropy_data-0.3.3/tests/commands/test_organization.py +85 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/test_config.py +23 -0
- entropy_data-0.3.1/src/entropy_data/commands/connection.py +0 -90
- entropy_data-0.3.1/tests/commands/test_connection.py +0 -71
- {entropy_data-0.3.1 → entropy_data-0.3.3}/.editorconfig +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/.github/dependabot.yml +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/.github/pull_request_template.md +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/.github/workflows/ci.yaml +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/.github/workflows/release.yaml +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/.gitignore +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/.pre-commit-config.yaml +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/CLAUDE.md +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/Dockerfile +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/LICENSE +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/README.md +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/release +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/__init__.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/__main__.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/__init__.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/access.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/api_keys.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/assets.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/certifications.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/costs.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/definitions.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/events.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/example_data.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/import_export.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/lineage.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/search.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/settings.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/sourcesystems.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/tags.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/teams.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/test_results.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/commands/usage.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/output.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/src/entropy_data/util.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/__init__.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/commands/__init__.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/commands/test_api_keys.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/commands/test_assets.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/commands/test_costs.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/commands/test_lineage.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/commands/test_settings.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/commands/test_tags.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/commands/test_teams.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/commands/test_usage.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/conftest.py +0 -0
- {entropy_data-0.3.1 → entropy_data-0.3.3}/tests/test_client.py +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.3.3]
|
|
6
|
+
|
|
7
|
+
- Add `entropy-data organization get` to fetch organization settings (vanity URL, host, full name, plan, SSO) for the API key in use. Backed by the new `GET /api/organization/settings` endpoint.
|
|
8
|
+
- Add `entropy-data connection get [name]` to inspect a stored connection. API key is masked by default; pass `--show-api-key` to print it in clear text. Use `-o json` for scripting.
|
|
9
|
+
- `entropy-data connection add` auto-fetches the organization vanity URL via `/api/organization/settings` and stores it on the connection. Best-effort: older servers or network errors fall back to no vanity URL.
|
|
10
|
+
- `connection list` surfaces the stored vanity URL.
|
|
11
|
+
|
|
12
|
+
## [0.3.2]
|
|
13
|
+
|
|
14
|
+
- Add `git-connection` subcommands to `dataproducts` and `datacontracts`
|
|
15
|
+
|
|
3
16
|
## [0.3.1]
|
|
4
17
|
|
|
5
18
|
- Support Python 3.11 (lowered minimum from 3.12)
|
|
@@ -103,6 +103,7 @@ from entropy_data.commands.events import events_app # noqa: E402
|
|
|
103
103
|
from entropy_data.commands.example_data import example_data_app # noqa: E402
|
|
104
104
|
from entropy_data.commands.import_export import import_app # noqa: E402
|
|
105
105
|
from entropy_data.commands.lineage import lineage_app # noqa: E402
|
|
106
|
+
from entropy_data.commands.organization import organization_app # noqa: E402
|
|
106
107
|
from entropy_data.commands.search import search_app # noqa: E402
|
|
107
108
|
from entropy_data.commands.settings import settings_app # noqa: E402
|
|
108
109
|
from entropy_data.commands.sourcesystems import sourcesystems_app # noqa: E402
|
|
@@ -125,6 +126,7 @@ app.add_typer(costs_app, name="costs", help="Manage costs.")
|
|
|
125
126
|
app.add_typer(assets_app, name="assets", help="Manage data assets.")
|
|
126
127
|
app.add_typer(tags_app, name="tags", help="Manage tags.")
|
|
127
128
|
app.add_typer(api_keys_app, name="api-keys", help="Manage API keys.")
|
|
129
|
+
app.add_typer(organization_app, name="organization", help="Get organization details.")
|
|
128
130
|
app.add_typer(settings_app, name="settings", help="Manage organization settings.")
|
|
129
131
|
app.add_typer(events_app, name="events", help="Poll events.")
|
|
130
132
|
app.add_typer(lineage_app, name="lineage", help="Manage lineage (OpenLineage events).")
|
|
@@ -121,12 +121,15 @@ class EntropyDataClient:
|
|
|
121
121
|
_raise_for_status(response)
|
|
122
122
|
return response.headers.get(RESPONSE_HEADER_LOCATION_HTML)
|
|
123
123
|
|
|
124
|
-
def post_action_json(
|
|
125
|
-
|
|
124
|
+
def post_action_json(
|
|
125
|
+
self, path: str, resource_id: str, action: str, params: dict | None = None, timeout: int = REQUEST_TIMEOUT
|
|
126
|
+
) -> dict:
|
|
126
127
|
"""POST /api/{path}/{id}/{action} with query params. Returns response JSON."""
|
|
127
128
|
_validate_resource_id(resource_id)
|
|
128
129
|
response = self.session.post(
|
|
129
|
-
f"{self.base_url}/api/{path}/{resource_id}/{action}",
|
|
130
|
+
f"{self.base_url}/api/{path}/{resource_id}/{action}",
|
|
131
|
+
params=params,
|
|
132
|
+
timeout=timeout,
|
|
130
133
|
)
|
|
131
134
|
_raise_for_status(response)
|
|
132
135
|
return response.json()
|
|
@@ -155,6 +158,53 @@ class EntropyDataClient:
|
|
|
155
158
|
_raise_for_status(response)
|
|
156
159
|
return response.json()
|
|
157
160
|
|
|
161
|
+
def get_gitconnection(self, path: str, resource_id: str) -> dict:
|
|
162
|
+
"""GET /api/{path}/{id}/gitconnection."""
|
|
163
|
+
_validate_resource_id(resource_id)
|
|
164
|
+
response = self.session.get(
|
|
165
|
+
f"{self.base_url}/api/{path}/{resource_id}/gitconnection",
|
|
166
|
+
timeout=REQUEST_TIMEOUT,
|
|
167
|
+
)
|
|
168
|
+
_raise_for_status(response)
|
|
169
|
+
return response.json()
|
|
170
|
+
|
|
171
|
+
def put_gitconnection(self, path: str, resource_id: str, body: dict) -> dict:
|
|
172
|
+
"""PUT /api/{path}/{id}/gitconnection."""
|
|
173
|
+
_validate_resource_id(resource_id)
|
|
174
|
+
response = self.session.put(
|
|
175
|
+
f"{self.base_url}/api/{path}/{resource_id}/gitconnection",
|
|
176
|
+
json=body,
|
|
177
|
+
timeout=REQUEST_TIMEOUT,
|
|
178
|
+
)
|
|
179
|
+
_raise_for_status(response)
|
|
180
|
+
return response.json()
|
|
181
|
+
|
|
182
|
+
def delete_gitconnection(self, path: str, resource_id: str) -> None:
|
|
183
|
+
"""DELETE /api/{path}/{id}/gitconnection."""
|
|
184
|
+
_validate_resource_id(resource_id)
|
|
185
|
+
response = self.session.delete(
|
|
186
|
+
f"{self.base_url}/api/{path}/{resource_id}/gitconnection",
|
|
187
|
+
timeout=REQUEST_TIMEOUT,
|
|
188
|
+
)
|
|
189
|
+
_raise_for_status(response)
|
|
190
|
+
|
|
191
|
+
def gitconnection_action(
|
|
192
|
+
self,
|
|
193
|
+
path: str,
|
|
194
|
+
resource_id: str,
|
|
195
|
+
action: str,
|
|
196
|
+
body: dict | None = None,
|
|
197
|
+
) -> dict:
|
|
198
|
+
"""POST /api/{path}/{id}/gitconnection/{action}. action ∈ {pull, push, push-pr}."""
|
|
199
|
+
_validate_resource_id(resource_id)
|
|
200
|
+
response = self.session.post(
|
|
201
|
+
f"{self.base_url}/api/{path}/{resource_id}/gitconnection/{action}",
|
|
202
|
+
json=body if body is not None else None,
|
|
203
|
+
timeout=REQUEST_TIMEOUT,
|
|
204
|
+
)
|
|
205
|
+
_raise_for_status(response)
|
|
206
|
+
return response.json()
|
|
207
|
+
|
|
158
208
|
def search(self, query: str, **params) -> dict:
|
|
159
209
|
"""GET /api/search."""
|
|
160
210
|
params["query"] = query
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Connection management commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from entropy_data import config as cfg
|
|
10
|
+
from entropy_data.output import OutputFormat, console, print_error, print_success
|
|
11
|
+
|
|
12
|
+
connection_app = typer.Typer(no_args_is_help=True)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _mask_api_key(api_key: str) -> str:
|
|
16
|
+
"""Mask an API key for display (first/last 4 visible)."""
|
|
17
|
+
if len(api_key) > 8:
|
|
18
|
+
return api_key[:4] + "..." + api_key[-4:]
|
|
19
|
+
return "****"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _fetch_vanity_url(api_key: str, host: str) -> str | None:
|
|
23
|
+
"""Best-effort fetch of the org vanity URL via /api/organization/settings.
|
|
24
|
+
|
|
25
|
+
Returns None on any failure (older server, network error, etc.) so callers
|
|
26
|
+
can fall back to None instead of failing the whole `connection add`.
|
|
27
|
+
"""
|
|
28
|
+
from entropy_data.client import REQUEST_TIMEOUT, EntropyDataClient
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
client = EntropyDataClient(cfg.ConnectionConfig(api_key=api_key, host=host))
|
|
32
|
+
response = client.session.get(
|
|
33
|
+
f"{client.base_url}/api/organization/settings",
|
|
34
|
+
timeout=REQUEST_TIMEOUT,
|
|
35
|
+
)
|
|
36
|
+
if response.ok:
|
|
37
|
+
return response.json().get("vanityUrl")
|
|
38
|
+
except Exception:
|
|
39
|
+
return None
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@connection_app.command("list")
|
|
44
|
+
def list_connections() -> None:
|
|
45
|
+
"""List all configured connections."""
|
|
46
|
+
connections = cfg.list_connections()
|
|
47
|
+
if not connections:
|
|
48
|
+
console.print("No connections configured. Run: entropy-data connection add <name>")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
table = Table(show_header=True)
|
|
52
|
+
table.add_column("Name")
|
|
53
|
+
table.add_column("Host")
|
|
54
|
+
table.add_column("Vanity URL")
|
|
55
|
+
table.add_column("API Key")
|
|
56
|
+
table.add_column("Default")
|
|
57
|
+
for conn in connections:
|
|
58
|
+
table.add_row(
|
|
59
|
+
conn["name"],
|
|
60
|
+
conn["host"],
|
|
61
|
+
conn.get("vanity_url") or "",
|
|
62
|
+
conn["api_key"],
|
|
63
|
+
"*" if conn["default"] else "",
|
|
64
|
+
)
|
|
65
|
+
console.print(table)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@connection_app.command("get")
|
|
69
|
+
def get_connection(
|
|
70
|
+
name: Annotated[
|
|
71
|
+
Optional[str],
|
|
72
|
+
typer.Argument(help="Connection name. Defaults to the default connection."),
|
|
73
|
+
] = None,
|
|
74
|
+
show_api_key: Annotated[
|
|
75
|
+
bool,
|
|
76
|
+
typer.Option("--show-api-key", help="Print the API key in clear text (default: masked)."),
|
|
77
|
+
] = False,
|
|
78
|
+
output: Annotated[
|
|
79
|
+
Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")
|
|
80
|
+
] = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Get details of a named connection (use --show-api-key to reveal the key)."""
|
|
83
|
+
from entropy_data.cli import get_output_format
|
|
84
|
+
|
|
85
|
+
config = cfg.load_config()
|
|
86
|
+
connections = config.get("connections", {})
|
|
87
|
+
|
|
88
|
+
resolved_name = name or config.get("default_connection_name")
|
|
89
|
+
if resolved_name is None:
|
|
90
|
+
print_error(
|
|
91
|
+
"No connection specified and no default set. "
|
|
92
|
+
"Run: entropy-data connection set-default <name>"
|
|
93
|
+
)
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
if resolved_name not in connections:
|
|
96
|
+
print_error(f"Connection '{resolved_name}' not found.")
|
|
97
|
+
raise typer.Exit(1)
|
|
98
|
+
|
|
99
|
+
conn = connections[resolved_name]
|
|
100
|
+
api_key_value = conn.get("api_key", "")
|
|
101
|
+
displayed_key = api_key_value if show_api_key else _mask_api_key(api_key_value)
|
|
102
|
+
is_default = config.get("default_connection_name") == resolved_name
|
|
103
|
+
|
|
104
|
+
fmt = output or get_output_format()
|
|
105
|
+
if fmt == OutputFormat.json:
|
|
106
|
+
payload = {
|
|
107
|
+
"name": resolved_name,
|
|
108
|
+
"host": conn.get("host", cfg.DEFAULT_HOST),
|
|
109
|
+
"vanity_url": conn.get("vanity_url"),
|
|
110
|
+
"api_key": displayed_key,
|
|
111
|
+
"default": is_default,
|
|
112
|
+
}
|
|
113
|
+
console.print_json(json.dumps(payload))
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
table = Table(show_header=False)
|
|
117
|
+
table.add_column("Field", style="cyan")
|
|
118
|
+
table.add_column("Value")
|
|
119
|
+
table.add_row("name", resolved_name)
|
|
120
|
+
table.add_row("host", conn.get("host", cfg.DEFAULT_HOST))
|
|
121
|
+
if conn.get("vanity_url"):
|
|
122
|
+
table.add_row("vanity_url", conn["vanity_url"])
|
|
123
|
+
table.add_row("api_key", displayed_key)
|
|
124
|
+
if is_default:
|
|
125
|
+
table.add_row("default", "yes")
|
|
126
|
+
console.print(table)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@connection_app.command("add")
|
|
130
|
+
def add_connection(
|
|
131
|
+
name: Annotated[str, typer.Argument(help="Connection name.")],
|
|
132
|
+
api_key: Annotated[str, typer.Option("--api-key", prompt="API key", help="The API key.")] = None,
|
|
133
|
+
host: Annotated[
|
|
134
|
+
str, typer.Option("--host", prompt="Host", prompt_required=False, help="API host URL.")
|
|
135
|
+
] = cfg.DEFAULT_HOST,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""Add or update a named connection.
|
|
138
|
+
|
|
139
|
+
The organization vanity URL is read from /api/organization/settings using the
|
|
140
|
+
provided API key. Fetching is best-effort: older servers or network errors
|
|
141
|
+
fall back to no vanity URL on the stored connection.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
vanity_url = _fetch_vanity_url(api_key, host)
|
|
145
|
+
if vanity_url:
|
|
146
|
+
console.print(f"Fetched organization vanity URL '[cyan]{vanity_url}[/cyan]' from {host}.")
|
|
147
|
+
cfg.add_connection(name, api_key, host, vanity_url=vanity_url)
|
|
148
|
+
print_success(f"Connection '{name}' saved.")
|
|
149
|
+
except cfg.ConfigurationError as e:
|
|
150
|
+
print_error(str(e))
|
|
151
|
+
raise typer.Exit(1)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@connection_app.command("remove")
|
|
155
|
+
def remove_connection(
|
|
156
|
+
name: Annotated[str, typer.Argument(help="Connection name to remove.")],
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Remove a named connection."""
|
|
159
|
+
try:
|
|
160
|
+
cfg.remove_connection(name)
|
|
161
|
+
print_success(f"Connection '{name}' removed.")
|
|
162
|
+
except cfg.ConfigurationError as e:
|
|
163
|
+
print_error(str(e))
|
|
164
|
+
raise typer.Exit(1)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@connection_app.command("set-default")
|
|
168
|
+
def set_default(
|
|
169
|
+
name: Annotated[str, typer.Argument(help="Connection name to set as default.")],
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Set the default connection."""
|
|
172
|
+
try:
|
|
173
|
+
cfg.set_default_connection(name)
|
|
174
|
+
print_success(f"Default connection set to '{name}'.")
|
|
175
|
+
except cfg.ConfigurationError as e:
|
|
176
|
+
print_error(str(e))
|
|
177
|
+
raise typer.Exit(1)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@connection_app.command("test")
|
|
181
|
+
def test_connection() -> None:
|
|
182
|
+
"""Test the current connection by calling the API."""
|
|
183
|
+
from entropy_data.cli import get_client, handle_error
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
client = get_client()
|
|
187
|
+
client.list_resources("teams", params={"p": "0"})
|
|
188
|
+
print_success("Connection successful.")
|
|
189
|
+
except Exception as e:
|
|
190
|
+
handle_error(e)
|
|
@@ -113,3 +113,12 @@ def delete_datacontract(
|
|
|
113
113
|
print_success(f"Data contract '{id}' deleted.")
|
|
114
114
|
except Exception as e:
|
|
115
115
|
handle_error(e)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
from entropy_data.commands.gitconnections import make_gitconnection_app # noqa: E402
|
|
119
|
+
|
|
120
|
+
datacontracts_app.add_typer(
|
|
121
|
+
make_gitconnection_app(RESOURCE_PATH, "Data contract"),
|
|
122
|
+
name="gitconnection",
|
|
123
|
+
help="Manage the git connection.",
|
|
124
|
+
)
|
|
@@ -91,3 +91,12 @@ def delete_dataproduct(
|
|
|
91
91
|
print_success(f"Data product '{id}' deleted.")
|
|
92
92
|
except Exception as e:
|
|
93
93
|
handle_error(e)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
from entropy_data.commands.gitconnections import make_gitconnection_app # noqa: E402
|
|
97
|
+
|
|
98
|
+
dataproducts_app.add_typer(
|
|
99
|
+
make_gitconnection_app(RESOURCE_PATH, "Data product"),
|
|
100
|
+
name="gitconnection",
|
|
101
|
+
help="Manage the git connection.",
|
|
102
|
+
)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""Git connection subcommands. Used as a sub-typer of dataproducts and datacontracts."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from entropy_data.output import OutputFormat, print_link, print_success
|
|
9
|
+
|
|
10
|
+
GIT_CONNECTION_TYPES = ("github", "gitlab", "bitbucket", "azuredevops")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def make_gitconnection_app(resource_path: str, resource_label: str) -> typer.Typer:
|
|
14
|
+
"""Build a Typer app exposing /api/{resource_path}/{id}/gitconnection operations.
|
|
15
|
+
|
|
16
|
+
`resource_path` is the URL segment ("dataproducts" or "datacontracts").
|
|
17
|
+
`resource_label` is the human-readable name shown in success messages.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(no_args_is_help=True)
|
|
21
|
+
|
|
22
|
+
@app.command("get")
|
|
23
|
+
def get_(
|
|
24
|
+
id: Annotated[str, typer.Argument(help=f"{resource_label} ID.")],
|
|
25
|
+
output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Get the git connection."""
|
|
28
|
+
from entropy_data.cli import get_client, get_output_format, handle_error
|
|
29
|
+
|
|
30
|
+
fmt = output or get_output_format()
|
|
31
|
+
try:
|
|
32
|
+
client = get_client()
|
|
33
|
+
data = client.get_gitconnection(resource_path, id)
|
|
34
|
+
if fmt == OutputFormat.json:
|
|
35
|
+
print(json.dumps(data, indent=2))
|
|
36
|
+
else:
|
|
37
|
+
print(json.dumps(data, indent=2))
|
|
38
|
+
except Exception as e:
|
|
39
|
+
handle_error(e)
|
|
40
|
+
|
|
41
|
+
@app.command("put")
|
|
42
|
+
def put_(
|
|
43
|
+
id: Annotated[str, typer.Argument(help=f"{resource_label} ID.")],
|
|
44
|
+
repository_url: Annotated[str, typer.Option("--repository-url", help="URL of the Git repository.")] = ...,
|
|
45
|
+
repository_path: Annotated[
|
|
46
|
+
str, typer.Option("--repository-path", help="Path to the YAML file in the repository.")
|
|
47
|
+
] = ...,
|
|
48
|
+
repository_branch: Annotated[
|
|
49
|
+
Optional[str],
|
|
50
|
+
typer.Option("--repository-branch", help="Branch to use. Defaults to 'main'."),
|
|
51
|
+
] = None,
|
|
52
|
+
git_connection_type: Annotated[
|
|
53
|
+
Optional[str],
|
|
54
|
+
typer.Option(
|
|
55
|
+
"--git-connection-type",
|
|
56
|
+
help=f"Git provider type. One of: {', '.join(GIT_CONNECTION_TYPES)}.",
|
|
57
|
+
),
|
|
58
|
+
] = None,
|
|
59
|
+
host: Annotated[
|
|
60
|
+
Optional[str],
|
|
61
|
+
typer.Option("--host", help="Host of a self-hosted git provider. Omit for SaaS."),
|
|
62
|
+
] = None,
|
|
63
|
+
git_credential_external_id: Annotated[
|
|
64
|
+
Optional[str],
|
|
65
|
+
typer.Option(
|
|
66
|
+
"--git-credential-external-id",
|
|
67
|
+
help="External ID of a stored git credential to use.",
|
|
68
|
+
),
|
|
69
|
+
] = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Create or update the git connection."""
|
|
72
|
+
from entropy_data.cli import get_client, handle_error
|
|
73
|
+
|
|
74
|
+
if git_connection_type and git_connection_type not in GIT_CONNECTION_TYPES:
|
|
75
|
+
raise typer.BadParameter(
|
|
76
|
+
f"Must be one of: {', '.join(GIT_CONNECTION_TYPES)}",
|
|
77
|
+
param_hint="--git-connection-type",
|
|
78
|
+
)
|
|
79
|
+
if not git_connection_type and not git_credential_external_id:
|
|
80
|
+
raise typer.BadParameter(
|
|
81
|
+
"At least one of --git-connection-type or --git-credential-external-id must be provided.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
body: dict = {"repositoryUrl": repository_url, "repositoryPath": repository_path}
|
|
85
|
+
if repository_branch:
|
|
86
|
+
body["repositoryBranch"] = repository_branch
|
|
87
|
+
if git_connection_type:
|
|
88
|
+
body["gitConnectionType"] = git_connection_type
|
|
89
|
+
if host:
|
|
90
|
+
body["host"] = host
|
|
91
|
+
if git_credential_external_id:
|
|
92
|
+
body["gitCredentialExternalId"] = git_credential_external_id
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
client = get_client()
|
|
96
|
+
data = client.put_gitconnection(resource_path, id, body)
|
|
97
|
+
print_success(f"Git connection saved for {resource_label.lower()} '{id}'.")
|
|
98
|
+
print_link(data.get("webLink"))
|
|
99
|
+
except Exception as e:
|
|
100
|
+
handle_error(e)
|
|
101
|
+
|
|
102
|
+
@app.command("delete")
|
|
103
|
+
def delete_(
|
|
104
|
+
id: Annotated[str, typer.Argument(help=f"{resource_label} ID.")],
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Delete the git connection."""
|
|
107
|
+
from entropy_data.cli import get_client, handle_error
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
client = get_client()
|
|
111
|
+
client.delete_gitconnection(resource_path, id)
|
|
112
|
+
print_success(f"Git connection deleted for {resource_label.lower()} '{id}'.")
|
|
113
|
+
except Exception as e:
|
|
114
|
+
handle_error(e)
|
|
115
|
+
|
|
116
|
+
@app.command("pull")
|
|
117
|
+
def pull_(
|
|
118
|
+
id: Annotated[str, typer.Argument(help=f"{resource_label} ID.")],
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Pull the file from Git into Entropy Data."""
|
|
121
|
+
from entropy_data.cli import get_client, handle_error
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
client = get_client()
|
|
125
|
+
data = client.gitconnection_action(resource_path, id, "pull")
|
|
126
|
+
print_success(f"{resource_label} '{id}' pulled from Git.")
|
|
127
|
+
print_link(data.get("webLink"))
|
|
128
|
+
except Exception as e:
|
|
129
|
+
handle_error(e)
|
|
130
|
+
|
|
131
|
+
@app.command("push")
|
|
132
|
+
def push_(
|
|
133
|
+
id: Annotated[str, typer.Argument(help=f"{resource_label} ID.")],
|
|
134
|
+
commit_message: Annotated[
|
|
135
|
+
Optional[str], typer.Option("--commit-message", help="Custom commit message.")
|
|
136
|
+
] = None,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Push the current Entropy Data file to Git."""
|
|
139
|
+
from entropy_data.cli import get_client, handle_error
|
|
140
|
+
|
|
141
|
+
body: dict | None = {"commitMessage": commit_message} if commit_message else None
|
|
142
|
+
try:
|
|
143
|
+
client = get_client()
|
|
144
|
+
data = client.gitconnection_action(resource_path, id, "push", body)
|
|
145
|
+
print_success(f"{resource_label} '{id}' pushed to Git.")
|
|
146
|
+
print_link(data.get("webLink"))
|
|
147
|
+
except Exception as e:
|
|
148
|
+
handle_error(e)
|
|
149
|
+
|
|
150
|
+
@app.command("push-pr")
|
|
151
|
+
def push_pr(
|
|
152
|
+
id: Annotated[str, typer.Argument(help=f"{resource_label} ID.")],
|
|
153
|
+
commit_message: Annotated[
|
|
154
|
+
Optional[str], typer.Option("--commit-message", help="Custom commit message.")
|
|
155
|
+
] = None,
|
|
156
|
+
branch_name: Annotated[Optional[str], typer.Option("--branch-name", help="Name of the new branch.")] = None,
|
|
157
|
+
title: Annotated[Optional[str], typer.Option("--title", help="Pull request title.")] = None,
|
|
158
|
+
comment: Annotated[
|
|
159
|
+
Optional[str],
|
|
160
|
+
typer.Option("--comment", help="Pull request description / comment."),
|
|
161
|
+
] = None,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Push the current Entropy Data file to Git as a pull/merge request."""
|
|
164
|
+
from entropy_data.cli import get_client, handle_error
|
|
165
|
+
|
|
166
|
+
body: dict = {}
|
|
167
|
+
if commit_message:
|
|
168
|
+
body["commitMessage"] = commit_message
|
|
169
|
+
if branch_name:
|
|
170
|
+
body["branchName"] = branch_name
|
|
171
|
+
if title:
|
|
172
|
+
body["title"] = title
|
|
173
|
+
if comment:
|
|
174
|
+
body["comment"] = comment
|
|
175
|
+
try:
|
|
176
|
+
client = get_client()
|
|
177
|
+
data = client.gitconnection_action(resource_path, id, "push-pr", body or None)
|
|
178
|
+
print_success(f"{resource_label} '{id}' pushed to Git as pull request.")
|
|
179
|
+
print_link(data.get("webLink"))
|
|
180
|
+
except Exception as e:
|
|
181
|
+
handle_error(e)
|
|
182
|
+
|
|
183
|
+
return app
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Organization commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Annotated, Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from entropy_data.output import OutputFormat, console
|
|
10
|
+
|
|
11
|
+
organization_app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@organization_app.command("get")
|
|
15
|
+
def get_organization(
|
|
16
|
+
output: Annotated[Optional[OutputFormat], typer.Option("--output", "-o", help="Output format.")] = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Get settings of the organization the current API key is bound to."""
|
|
19
|
+
from entropy_data.cli import get_client, get_output_format, handle_error
|
|
20
|
+
from entropy_data.client import REQUEST_TIMEOUT, _raise_for_status
|
|
21
|
+
|
|
22
|
+
fmt = output or get_output_format()
|
|
23
|
+
try:
|
|
24
|
+
client = get_client()
|
|
25
|
+
response = client.session.get(
|
|
26
|
+
f"{client.base_url}/api/organization/settings",
|
|
27
|
+
timeout=REQUEST_TIMEOUT,
|
|
28
|
+
)
|
|
29
|
+
_raise_for_status(response)
|
|
30
|
+
data = response.json()
|
|
31
|
+
if fmt == OutputFormat.json:
|
|
32
|
+
console.print_json(json.dumps(data))
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
table = Table(show_header=False)
|
|
36
|
+
table.add_column("Field", style="cyan")
|
|
37
|
+
table.add_column("Value")
|
|
38
|
+
for key in ("vanityUrl", "host", "fullName", "logoUrl", "supportEmailAddress", "brand", "plan"):
|
|
39
|
+
value = data.get(key)
|
|
40
|
+
if value:
|
|
41
|
+
table.add_row(key, str(value))
|
|
42
|
+
sso = data.get("sso")
|
|
43
|
+
if sso:
|
|
44
|
+
table.add_row("sso.issuer", sso.get("issuer", ""))
|
|
45
|
+
if sso.get("tenant"):
|
|
46
|
+
table.add_row("sso.tenant", sso["tenant"])
|
|
47
|
+
if sso.get("autoJoin") is not None:
|
|
48
|
+
table.add_row("sso.autoJoin", str(sso["autoJoin"]))
|
|
49
|
+
console.print(table)
|
|
50
|
+
except Exception as e:
|
|
51
|
+
handle_error(e)
|
|
@@ -21,6 +21,7 @@ class ConfigurationError(Exception):
|
|
|
21
21
|
class ConnectionConfig:
|
|
22
22
|
api_key: str
|
|
23
23
|
host: str = DEFAULT_HOST
|
|
24
|
+
vanity_url: str | None = None
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def load_config() -> dict:
|
|
@@ -47,6 +48,7 @@ def resolve_connection(
|
|
|
47
48
|
"""Resolve connection with precedence: CLI options > env vars > config file."""
|
|
48
49
|
api_key = cli_api_key
|
|
49
50
|
host = cli_host
|
|
51
|
+
vanity_url: str | None = None
|
|
50
52
|
|
|
51
53
|
# Layer 2: environment variables
|
|
52
54
|
if api_key is None:
|
|
@@ -68,6 +70,7 @@ def resolve_connection(
|
|
|
68
70
|
api_key = conn.get("api_key")
|
|
69
71
|
if host is None:
|
|
70
72
|
host = conn.get("host")
|
|
73
|
+
vanity_url = conn.get("vanity_url")
|
|
71
74
|
|
|
72
75
|
# Default host
|
|
73
76
|
if host is None:
|
|
@@ -78,17 +81,20 @@ def resolve_connection(
|
|
|
78
81
|
"No API key found. Set ENTROPY_DATA_API_KEY, use --api-key, or run: entropy-data connection add <name>"
|
|
79
82
|
)
|
|
80
83
|
|
|
81
|
-
return ConnectionConfig(api_key=api_key, host=host)
|
|
84
|
+
return ConnectionConfig(api_key=api_key, host=host, vanity_url=vanity_url)
|
|
82
85
|
|
|
83
86
|
|
|
84
|
-
def add_connection(name: str, api_key: str, host: str = DEFAULT_HOST) -> None:
|
|
87
|
+
def add_connection(name: str, api_key: str, host: str = DEFAULT_HOST, vanity_url: str | None = None) -> None:
|
|
85
88
|
"""Add or update a named connection."""
|
|
86
89
|
if not name or not name.strip():
|
|
87
90
|
raise ConfigurationError("Connection name must not be empty.")
|
|
88
91
|
config = load_config()
|
|
89
92
|
if "connections" not in config:
|
|
90
93
|
config["connections"] = {}
|
|
91
|
-
|
|
94
|
+
entry: dict = {"api_key": api_key, "host": host}
|
|
95
|
+
if vanity_url:
|
|
96
|
+
entry["vanity_url"] = vanity_url
|
|
97
|
+
config["connections"][name] = entry
|
|
92
98
|
# Set as default if it's the first connection
|
|
93
99
|
if "default_connection_name" not in config:
|
|
94
100
|
config["default_connection_name"] = name
|
|
@@ -134,6 +140,7 @@ def list_connections() -> list[dict]:
|
|
|
134
140
|
{
|
|
135
141
|
"name": name,
|
|
136
142
|
"host": conn.get("host", DEFAULT_HOST),
|
|
143
|
+
"vanity_url": conn.get("vanity_url"),
|
|
137
144
|
"api_key": masked,
|
|
138
145
|
"default": name == default_name,
|
|
139
146
|
}
|