entropy-data 0.3.2__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.
Files changed (63) hide show
  1. {entropy_data-0.3.2 → entropy_data-0.3.3}/CHANGELOG.md +9 -0
  2. {entropy_data-0.3.2 → entropy_data-0.3.3}/PKG-INFO +1 -1
  3. {entropy_data-0.3.2 → entropy_data-0.3.3}/pyproject.toml +1 -1
  4. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/cli.py +2 -0
  5. entropy_data-0.3.3/src/entropy_data/commands/connection.py +190 -0
  6. entropy_data-0.3.3/src/entropy_data/commands/organization.py +51 -0
  7. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/config.py +10 -3
  8. entropy_data-0.3.3/tests/commands/test_connection.py +176 -0
  9. entropy_data-0.3.3/tests/commands/test_organization.py +85 -0
  10. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/test_config.py +23 -0
  11. entropy_data-0.3.2/src/entropy_data/commands/connection.py +0 -90
  12. entropy_data-0.3.2/tests/commands/test_connection.py +0 -71
  13. {entropy_data-0.3.2 → entropy_data-0.3.3}/.editorconfig +0 -0
  14. {entropy_data-0.3.2 → entropy_data-0.3.3}/.github/dependabot.yml +0 -0
  15. {entropy_data-0.3.2 → entropy_data-0.3.3}/.github/pull_request_template.md +0 -0
  16. {entropy_data-0.3.2 → entropy_data-0.3.3}/.github/workflows/ci.yaml +0 -0
  17. {entropy_data-0.3.2 → entropy_data-0.3.3}/.github/workflows/release.yaml +0 -0
  18. {entropy_data-0.3.2 → entropy_data-0.3.3}/.gitignore +0 -0
  19. {entropy_data-0.3.2 → entropy_data-0.3.3}/.pre-commit-config.yaml +0 -0
  20. {entropy_data-0.3.2 → entropy_data-0.3.3}/CLAUDE.md +0 -0
  21. {entropy_data-0.3.2 → entropy_data-0.3.3}/Dockerfile +0 -0
  22. {entropy_data-0.3.2 → entropy_data-0.3.3}/LICENSE +0 -0
  23. {entropy_data-0.3.2 → entropy_data-0.3.3}/README.md +0 -0
  24. {entropy_data-0.3.2 → entropy_data-0.3.3}/release +0 -0
  25. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/__init__.py +0 -0
  26. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/__main__.py +0 -0
  27. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/client.py +0 -0
  28. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/__init__.py +0 -0
  29. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/access.py +0 -0
  30. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/api_keys.py +0 -0
  31. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/assets.py +0 -0
  32. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/certifications.py +0 -0
  33. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/costs.py +0 -0
  34. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/datacontracts.py +0 -0
  35. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/dataproducts.py +0 -0
  36. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/definitions.py +0 -0
  37. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/events.py +0 -0
  38. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/example_data.py +0 -0
  39. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/gitconnections.py +0 -0
  40. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/import_export.py +0 -0
  41. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/lineage.py +0 -0
  42. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/search.py +0 -0
  43. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/settings.py +0 -0
  44. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/sourcesystems.py +0 -0
  45. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/tags.py +0 -0
  46. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/teams.py +0 -0
  47. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/test_results.py +0 -0
  48. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/commands/usage.py +0 -0
  49. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/output.py +0 -0
  50. {entropy_data-0.3.2 → entropy_data-0.3.3}/src/entropy_data/util.py +0 -0
  51. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/__init__.py +0 -0
  52. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/commands/__init__.py +0 -0
  53. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/commands/test_api_keys.py +0 -0
  54. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/commands/test_assets.py +0 -0
  55. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/commands/test_costs.py +0 -0
  56. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/commands/test_gitconnections.py +0 -0
  57. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/commands/test_lineage.py +0 -0
  58. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/commands/test_settings.py +0 -0
  59. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/commands/test_tags.py +0 -0
  60. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/commands/test_teams.py +0 -0
  61. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/commands/test_usage.py +0 -0
  62. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/conftest.py +0 -0
  63. {entropy_data-0.3.2 → entropy_data-0.3.3}/tests/test_client.py +0 -0
@@ -1,5 +1,14 @@
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
+
3
12
  ## [0.3.2]
4
13
 
5
14
  - Add `git-connection` subcommands to `dataproducts` and `datacontracts`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: entropy-data
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: CLI for Entropy Data
5
5
  Project-URL: Homepage, https://entropy-data.com
6
6
  Project-URL: Documentation, https://docs.entropy-data.com
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "entropy-data"
3
- version = "0.3.2"
3
+ version = "0.3.3"
4
4
  description = "CLI for Entropy Data"
5
5
  requires-python = ">=3.11"
6
6
  license = "MIT"
@@ -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).")
@@ -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)
@@ -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
- config["connections"][name] = {"api_key": api_key, "host": host}
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
  }
@@ -0,0 +1,176 @@
1
+ """Tests for connection commands."""
2
+
3
+ import json
4
+
5
+ import responses
6
+ from typer.testing import CliRunner
7
+
8
+ import entropy_data.config as cfg
9
+ from entropy_data.cli import app
10
+
11
+ runner = CliRunner()
12
+ BASE_URL = "https://api.entropy-data.com"
13
+ ORG_SETTINGS_URL = f"{BASE_URL}/api/organization/settings"
14
+
15
+
16
+ def test_connection_list_empty(tmp_path, monkeypatch):
17
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
18
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
19
+ result = runner.invoke(app, ["connection", "list"])
20
+ assert result.exit_code == 0
21
+ assert "No connections" in result.output
22
+
23
+
24
+ @responses.activate
25
+ def test_connection_add_and_list(tmp_path, monkeypatch):
26
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
27
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
28
+ # Auto-fetch fails (no responses registered) — connection still saves without vanity
29
+ result = runner.invoke(app, ["connection", "add", "prod", "--api-key", "mykey123456", "--host", BASE_URL])
30
+ assert result.exit_code == 0
31
+ assert "saved" in result.output
32
+
33
+ result = runner.invoke(app, ["connection", "list"])
34
+ assert result.exit_code == 0
35
+ assert "prod" in result.output
36
+ assert "*" in result.output # default marker
37
+
38
+
39
+ @responses.activate
40
+ def test_connection_add_fetches_vanity_url(tmp_path, monkeypatch):
41
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
42
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
43
+ responses.add(
44
+ responses.GET,
45
+ ORG_SETTINGS_URL,
46
+ json={"vanityUrl": "acme", "host": BASE_URL, "fullName": "Acme Corp", "logoUrl": ""},
47
+ status=200,
48
+ )
49
+ result = runner.invoke(app, ["connection", "add", "prod", "--api-key", "mykey", "--host", BASE_URL])
50
+ assert result.exit_code == 0
51
+ assert "saved" in result.output
52
+ assert "acme" in result.output # mentioned in the fetch confirmation line
53
+
54
+ result = runner.invoke(app, ["connection", "list"])
55
+ assert result.exit_code == 0
56
+ assert "acme" in result.output
57
+
58
+
59
+ @responses.activate
60
+ def test_connection_add_handles_fetch_failure(tmp_path, monkeypatch):
61
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
62
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
63
+ # Older server / endpoint missing → 404
64
+ responses.add(responses.GET, ORG_SETTINGS_URL, json={"detail": "Not found"}, status=404)
65
+ result = runner.invoke(app, ["connection", "add", "prod", "--api-key", "mykey", "--host", BASE_URL])
66
+ assert result.exit_code == 0
67
+ assert "saved" in result.output
68
+
69
+ result = runner.invoke(app, ["connection", "list"])
70
+ assert result.exit_code == 0
71
+ assert "prod" in result.output
72
+
73
+
74
+ @responses.activate
75
+ def test_connection_remove(tmp_path, monkeypatch):
76
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
77
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
78
+ runner.invoke(app, ["connection", "add", "prod", "--api-key", "key1", "--host", BASE_URL])
79
+ result = runner.invoke(app, ["connection", "remove", "prod"])
80
+ assert result.exit_code == 0
81
+ assert "removed" in result.output
82
+
83
+
84
+ @responses.activate
85
+ def test_connection_set_default(tmp_path, monkeypatch):
86
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
87
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
88
+ runner.invoke(app, ["connection", "add", "prod", "--api-key", "key1", "--host", BASE_URL])
89
+ runner.invoke(app, ["connection", "add", "dev", "--api-key", "key2", "--host", "http://localhost:8080"])
90
+ result = runner.invoke(app, ["connection", "set-default", "dev"])
91
+ assert result.exit_code == 0
92
+ assert "dev" in result.output
93
+
94
+
95
+ @responses.activate
96
+ def test_connection_test_success(tmp_path, monkeypatch):
97
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
98
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
99
+ runner.invoke(app, ["connection", "add", "prod", "--api-key", "key1", "--host", BASE_URL])
100
+ responses.add(responses.GET, f"{BASE_URL}/api/teams", json=[], status=200)
101
+ result = runner.invoke(app, ["connection", "test"])
102
+ assert result.exit_code == 0
103
+ assert "successful" in result.output
104
+
105
+
106
+ @responses.activate
107
+ def test_connection_get_masked_by_default(tmp_path, monkeypatch):
108
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
109
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
110
+ runner.invoke(app, ["connection", "add", "prod", "--api-key", "abcd1234efgh5678", "--host", BASE_URL])
111
+ result = runner.invoke(app, ["connection", "get", "prod"])
112
+ assert result.exit_code == 0
113
+ assert "abcd...5678" in result.output
114
+ assert "abcd1234efgh5678" not in result.output
115
+
116
+
117
+ @responses.activate
118
+ def test_connection_get_show_api_key(tmp_path, monkeypatch):
119
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
120
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
121
+ runner.invoke(app, ["connection", "add", "prod", "--api-key", "abcd1234efgh5678", "--host", BASE_URL])
122
+ result = runner.invoke(app, ["connection", "get", "prod", "--show-api-key"])
123
+ assert result.exit_code == 0
124
+ assert "abcd1234efgh5678" in result.output
125
+
126
+
127
+ @responses.activate
128
+ def test_connection_get_uses_default(tmp_path, monkeypatch):
129
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
130
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
131
+ runner.invoke(app, ["connection", "add", "prod", "--api-key", "abcd1234efgh5678", "--host", BASE_URL])
132
+ result = runner.invoke(app, ["connection", "get"])
133
+ assert result.exit_code == 0
134
+ assert "prod" in result.output
135
+ assert "default" in result.output
136
+
137
+
138
+ @responses.activate
139
+ def test_connection_get_json(tmp_path, monkeypatch):
140
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
141
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
142
+ runner.invoke(app, ["connection", "add", "prod", "--api-key", "abcd1234efgh5678", "--host", BASE_URL])
143
+ result = runner.invoke(app, ["connection", "get", "prod", "--show-api-key", "-o", "json"])
144
+ assert result.exit_code == 0
145
+ payload = json.loads(result.output)
146
+ assert payload["name"] == "prod"
147
+ assert payload["host"] == BASE_URL
148
+ assert payload["api_key"] == "abcd1234efgh5678"
149
+ assert payload["default"] is True
150
+
151
+
152
+ def test_connection_get_not_found(tmp_path, monkeypatch):
153
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
154
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
155
+ result = runner.invoke(app, ["connection", "get", "nope"])
156
+ assert result.exit_code == 1
157
+ assert "not found" in result.output.lower()
158
+
159
+
160
+ def test_connection_get_no_default(tmp_path, monkeypatch):
161
+ monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
162
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
163
+ result = runner.invoke(app, ["connection", "get"])
164
+ assert result.exit_code == 1
165
+ assert "no default" in result.output.lower() or "no connection specified" in result.output.lower()
166
+
167
+
168
+ def test_connection_help():
169
+ result = runner.invoke(app, ["connection", "--help"])
170
+ assert result.exit_code == 0
171
+ assert "list" in result.output
172
+ assert "get" in result.output
173
+ assert "add" in result.output
174
+ assert "remove" in result.output
175
+ assert "set-default" in result.output
176
+ assert "test" in result.output
@@ -0,0 +1,85 @@
1
+ """Tests for organization commands."""
2
+
3
+ import json
4
+
5
+ import responses
6
+ from typer.testing import CliRunner
7
+
8
+ import entropy_data.config as cfg
9
+ from entropy_data.cli import app
10
+
11
+ runner = CliRunner()
12
+ BASE_URL = "https://api.entropy-data.com"
13
+ ORG_SETTINGS_URL = f"{BASE_URL}/api/organization/settings"
14
+
15
+ ORG_PAYLOAD = {
16
+ "vanityUrl": "acme",
17
+ "host": "https://acme.entropy-data.com",
18
+ "fullName": "Acme Corp",
19
+ "logoUrl": "https://example.com/logo.png",
20
+ "supportEmailAddress": "support@acme.com",
21
+ "brand": "default",
22
+ "plan": "enterprise",
23
+ "sso": {
24
+ "issuer": "azuresso",
25
+ "tenant": "tenant-id",
26
+ "autoJoin": True,
27
+ },
28
+ }
29
+
30
+
31
+ @responses.activate
32
+ def test_organization_get_table(monkeypatch, tmp_path):
33
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
34
+ monkeypatch.setenv("ENTROPY_DATA_API_KEY", "test-key")
35
+ responses.add(responses.GET, ORG_SETTINGS_URL, json=ORG_PAYLOAD, status=200)
36
+ result = runner.invoke(app, ["organization", "get"])
37
+ assert result.exit_code == 0
38
+ assert "acme" in result.output
39
+ assert "Acme Corp" in result.output
40
+ assert "enterprise" in result.output
41
+ assert "azuresso" in result.output
42
+
43
+
44
+ @responses.activate
45
+ def test_organization_get_json(monkeypatch, tmp_path):
46
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
47
+ monkeypatch.setenv("ENTROPY_DATA_API_KEY", "test-key")
48
+ responses.add(responses.GET, ORG_SETTINGS_URL, json=ORG_PAYLOAD, status=200)
49
+ result = runner.invoke(app, ["organization", "get", "--output", "json"])
50
+ assert result.exit_code == 0
51
+ data = json.loads(result.output)
52
+ assert data["vanityUrl"] == "acme"
53
+ assert data["plan"] == "enterprise"
54
+ assert data["sso"]["issuer"] == "azuresso"
55
+
56
+
57
+ @responses.activate
58
+ def test_organization_get_minimal_payload(monkeypatch, tmp_path):
59
+ """Org without SSO / optional fields renders cleanly."""
60
+ monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
61
+ monkeypatch.setenv("ENTROPY_DATA_API_KEY", "test-key")
62
+ responses.add(
63
+ responses.GET,
64
+ ORG_SETTINGS_URL,
65
+ json={
66
+ "vanityUrl": "minimal",
67
+ "host": "https://minimal.entropy-data.com",
68
+ "fullName": "Minimal",
69
+ "logoUrl": "",
70
+ "supportEmailAddress": None,
71
+ "brand": None,
72
+ "plan": None,
73
+ "sso": None,
74
+ },
75
+ status=200,
76
+ )
77
+ result = runner.invoke(app, ["organization", "get"])
78
+ assert result.exit_code == 0
79
+ assert "minimal" in result.output
80
+
81
+
82
+ def test_organization_help():
83
+ result = runner.invoke(app, ["organization", "--help"])
84
+ assert result.exit_code == 0
85
+ assert "get" in result.output
@@ -104,6 +104,29 @@ def test_resolve_from_config(config_dir):
104
104
  conn = resolve_connection()
105
105
  assert conn.api_key == "mykey"
106
106
  assert conn.host == "https://custom.host"
107
+ assert conn.vanity_url is None
108
+
109
+
110
+ def test_add_and_resolve_with_vanity_url(config_dir):
111
+ add_connection("prod", "mykey", "https://custom.host", vanity_url="acme")
112
+ conn = resolve_connection()
113
+ assert conn.vanity_url == "acme"
114
+ config = load_config()
115
+ assert config["connections"]["prod"]["vanity_url"] == "acme"
116
+
117
+
118
+ def test_add_without_vanity_url_omits_key(config_dir):
119
+ add_connection("prod", "mykey")
120
+ config = load_config()
121
+ assert "vanity_url" not in config["connections"]["prod"]
122
+
123
+
124
+ def test_list_connections_includes_vanity_url(config_dir):
125
+ add_connection("prod", "abcd1234efgh5678", vanity_url="acme")
126
+ add_connection("dev", "key2")
127
+ result = {row["name"]: row for row in list_connections()}
128
+ assert result["prod"]["vanity_url"] == "acme"
129
+ assert result["dev"]["vanity_url"] is None
107
130
 
108
131
 
109
132
  def test_resolve_env_overrides_config(config_dir, monkeypatch):
@@ -1,90 +0,0 @@
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 import config as cfg
9
- from entropy_data.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 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)
@@ -1,71 +0,0 @@
1
- """Tests for connection commands."""
2
-
3
- import responses
4
- from typer.testing import CliRunner
5
-
6
- import entropy_data.config as cfg
7
- from entropy_data.cli import app
8
-
9
- runner = CliRunner()
10
- BASE_URL = "https://api.entropy-data.com"
11
-
12
-
13
- def test_connection_list_empty(tmp_path, monkeypatch):
14
- monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
15
- monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
16
- result = runner.invoke(app, ["connection", "list"])
17
- assert result.exit_code == 0
18
- assert "No connections" in result.output
19
-
20
-
21
- def test_connection_add_and_list(tmp_path, monkeypatch):
22
- monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
23
- monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
24
- result = runner.invoke(app, ["connection", "add", "prod", "--api-key", "mykey123456", "--host", BASE_URL])
25
- assert result.exit_code == 0
26
- assert "saved" in result.output
27
-
28
- result = runner.invoke(app, ["connection", "list"])
29
- assert result.exit_code == 0
30
- assert "prod" in result.output
31
- assert "*" in result.output # default marker
32
-
33
-
34
- def test_connection_remove(tmp_path, monkeypatch):
35
- monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
36
- monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
37
- runner.invoke(app, ["connection", "add", "prod", "--api-key", "key1", "--host", BASE_URL])
38
- result = runner.invoke(app, ["connection", "remove", "prod"])
39
- assert result.exit_code == 0
40
- assert "removed" in result.output
41
-
42
-
43
- def test_connection_set_default(tmp_path, monkeypatch):
44
- monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
45
- monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
46
- runner.invoke(app, ["connection", "add", "prod", "--api-key", "key1", "--host", BASE_URL])
47
- runner.invoke(app, ["connection", "add", "dev", "--api-key", "key2", "--host", "http://localhost:8080"])
48
- result = runner.invoke(app, ["connection", "set-default", "dev"])
49
- assert result.exit_code == 0
50
- assert "dev" in result.output
51
-
52
-
53
- @responses.activate
54
- def test_connection_test_success(tmp_path, monkeypatch):
55
- monkeypatch.setattr(cfg, "CONFIG_DIR", tmp_path)
56
- monkeypatch.setattr(cfg, "CONFIG_FILE", tmp_path / "config.toml")
57
- runner.invoke(app, ["connection", "add", "prod", "--api-key", "key1", "--host", BASE_URL])
58
- responses.add(responses.GET, f"{BASE_URL}/api/teams", json=[], status=200)
59
- result = runner.invoke(app, ["connection", "test"])
60
- assert result.exit_code == 0
61
- assert "successful" in result.output
62
-
63
-
64
- def test_connection_help():
65
- result = runner.invoke(app, ["connection", "--help"])
66
- assert result.exit_code == 0
67
- assert "list" in result.output
68
- assert "add" in result.output
69
- assert "remove" in result.output
70
- assert "set-default" in result.output
71
- assert "test" in result.output
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes