Skip to content

Commit e6b1049

Browse files
committed
feat(cli): Add session management capabilities
1 parent 8ee03b4 commit e6b1049

File tree

3 files changed

+309
-0
lines changed

3 files changed

+309
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import json
2+
import os
3+
from typing import Dict
4+
5+
import click
6+
from tabulate import tabulate
7+
8+
from datahub.cli.cli_utils import fixup_gms_url, generate_access_token
9+
from datahub.cli.config_utils import load_client_config, persist_raw_datahub_config
10+
from datahub.ingestion.graph.config import DatahubClientConfig
11+
12+
DATAHUB_SESSIONS_PATH = os.path.expanduser("~/.datahub/sessions.json")
13+
14+
15+
def load_sessions() -> Dict[str, DatahubClientConfig]:
16+
if not os.path.exists(DATAHUB_SESSIONS_PATH):
17+
return {}
18+
with open(DATAHUB_SESSIONS_PATH, "r") as f:
19+
raw_sessions = json.load(f)
20+
return {
21+
name: DatahubClientConfig.parse_obj(config)
22+
for name, config in raw_sessions.items()
23+
}
24+
25+
26+
def save_sessions(sessions: Dict[str, DatahubClientConfig]) -> None:
27+
os.makedirs(os.path.dirname(DATAHUB_SESSIONS_PATH), exist_ok=True)
28+
with open(DATAHUB_SESSIONS_PATH, "w") as f:
29+
json.dump(
30+
{name: config.dict() for name, config in sessions.items()}, f, indent=2
31+
)
32+
33+
34+
@click.group()
35+
def session() -> None:
36+
"""Manage DataHub session profiles"""
37+
pass
38+
39+
40+
@session.command()
41+
@click.option(
42+
"--use-password",
43+
type=bool,
44+
is_flag=True,
45+
default=False,
46+
help="If passed then uses password to initialise token.",
47+
)
48+
def create(use_password: bool) -> None:
49+
"""Create profile with which to connect to a DataHub instance"""
50+
51+
sessions = load_sessions()
52+
53+
click.echo(
54+
"Configure which datahub instance to connect to (https://your-instance.acryl.io/gms for Acryl hosted users)"
55+
)
56+
host = click.prompt(
57+
"Enter your DataHub host", type=str, default="http://localhost:8080"
58+
)
59+
host = fixup_gms_url(host)
60+
if use_password:
61+
username = click.prompt("Enter your DataHub username", type=str)
62+
password = click.prompt(
63+
"Enter your DataHub password",
64+
type=str,
65+
)
66+
_, token = generate_access_token(
67+
username=username, password=password, gms_url=host
68+
)
69+
else:
70+
token = click.prompt(
71+
"Enter your DataHub access token",
72+
type=str,
73+
default="",
74+
)
75+
76+
profile_name = click.prompt("Enter name for profile", type=str)
77+
78+
config = DatahubClientConfig(server=host, token=token)
79+
sessions[profile_name] = config
80+
save_sessions(sessions)
81+
click.echo(f"Created profile: {profile_name}")
82+
83+
84+
@session.command()
85+
def list() -> None:
86+
"""List all session profiles"""
87+
sessions = load_sessions()
88+
if not sessions:
89+
click.echo("No profiles found")
90+
return
91+
92+
headers = ["Profile", "URL"]
93+
table_data = [[name, config.server] for name, config in sessions.items()]
94+
click.echo(tabulate(table_data, headers=headers))
95+
96+
97+
@session.command()
98+
@click.argument("profile", type=str)
99+
def delete(profile: str) -> None:
100+
"""Delete a session profile"""
101+
sessions = load_sessions()
102+
if profile not in sessions:
103+
click.echo(f"Profile {profile} not found")
104+
return
105+
106+
del sessions[profile]
107+
save_sessions(sessions)
108+
click.echo(f"Deleted profile: {profile}")
109+
110+
111+
@session.command()
112+
@click.argument("profile", type=str)
113+
def use(profile: str) -> None:
114+
"""Set the active session"""
115+
sessions = load_sessions()
116+
session = sessions.get(profile)
117+
if session:
118+
persist_raw_datahub_config(session.dict())
119+
click.echo(f"Using profile {profile}")
120+
else:
121+
click.echo(f"Profile {profile} not found")
122+
return
123+
124+
125+
@session.command()
126+
@click.option(
127+
"--profile",
128+
type=str,
129+
required=True,
130+
help="Name of profile under which to save the current datahubenv config",
131+
)
132+
def save(profile: str) -> None:
133+
"""Save the current active datahubenv config as a session"""
134+
sessions = load_sessions()
135+
if profile in sessions:
136+
click.echo(
137+
f"Profile {profile} already exists, please make sure to use a unique profile name"
138+
)
139+
return
140+
141+
config = load_client_config()
142+
sessions[profile] = config
143+
save_sessions(sessions)
144+
click.echo(f"Saved current datahubenv as profile: {profile}")

metadata-ingestion/src/datahub/entrypoints.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from datahub.cli.ingest_cli import ingest
2727
from datahub.cli.migrate import migrate
2828
from datahub.cli.put_cli import put
29+
from datahub.cli.session_cli import session
2930
from datahub.cli.specific.assertions_cli import assertions
3031
from datahub.cli.specific.datacontract_cli import datacontract
3132
from datahub.cli.specific.dataproduct_cli import dataproduct
@@ -185,6 +186,7 @@ def init(use_password: bool = False) -> None:
185186
datahub.add_command(datacontract)
186187
datahub.add_command(assertions)
187188
datahub.add_command(container)
189+
datahub.add_command(session)
188190

189191
try:
190192
from datahub.cli.iceberg_cli import iceberg
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import json
2+
import os
3+
from unittest.mock import patch
4+
5+
import pytest
6+
from click.testing import CliRunner
7+
8+
from datahub.cli.session_cli import session
9+
from datahub.ingestion.graph.config import DatahubClientConfig
10+
11+
12+
@pytest.fixture
13+
def mock_sessions_file(tmp_path):
14+
# Create a temporary sessions file for testing
15+
test_sessions_file = tmp_path / "sessions.json"
16+
with patch(
17+
"datahub.cli.session_cli.DATAHUB_SESSIONS_PATH", str(test_sessions_file)
18+
):
19+
yield test_sessions_file
20+
# Cleanup
21+
if test_sessions_file.exists():
22+
test_sessions_file.unlink()
23+
24+
25+
@pytest.fixture
26+
def sample_config():
27+
return DatahubClientConfig(server="http://localhost:8080", token="test-token")
28+
29+
30+
def test_create_session(mock_sessions_file):
31+
runner = CliRunner()
32+
with runner.isolated_filesystem():
33+
# Test creating a new profile
34+
result = runner.invoke(
35+
session,
36+
["create"],
37+
input="http://localhost:8080\ntest-token\ntest-profile\n",
38+
)
39+
assert result.exit_code == 0
40+
assert "Created profile: test-profile" in result.output
41+
42+
# Verify the session was saved
43+
with open(mock_sessions_file) as f:
44+
saved_sessions = json.load(f)
45+
assert "test-profile" in saved_sessions
46+
assert saved_sessions["test-profile"]["server"] == "http://localhost:8080"
47+
assert saved_sessions["test-profile"]["token"] == "test-token"
48+
49+
50+
def test_list_sessions_empty_data(mock_sessions_file):
51+
runner = CliRunner()
52+
# Create an empty sessions file
53+
os.makedirs(os.path.dirname(mock_sessions_file), exist_ok=True)
54+
with open(mock_sessions_file, "w") as f:
55+
json.dump({}, f)
56+
57+
result = runner.invoke(session, ["list"])
58+
assert result.exit_code == 0
59+
assert "No profiles found" in result.output
60+
61+
62+
def test_list_sessions(mock_sessions_file, sample_config):
63+
runner = CliRunner()
64+
# Setup test data
65+
sessions_data = {"test-profile": sample_config.dict()}
66+
os.makedirs(os.path.dirname(mock_sessions_file), exist_ok=True)
67+
with open(mock_sessions_file, "w") as f:
68+
json.dump(sessions_data, f)
69+
70+
result = runner.invoke(session, ["list"])
71+
assert result.exit_code == 0
72+
assert "test-profile" in result.output
73+
assert "http://localhost:8080" in result.output
74+
75+
76+
def test_delete_session(mock_sessions_file, sample_config):
77+
runner = CliRunner()
78+
# Setup test data
79+
sessions_data = {"test-profile": sample_config.dict()}
80+
os.makedirs(os.path.dirname(mock_sessions_file), exist_ok=True)
81+
with open(mock_sessions_file, "w") as f:
82+
json.dump(sessions_data, f)
83+
84+
# Test deleting existing profile
85+
result = runner.invoke(session, ["delete", "test-profile"])
86+
assert result.exit_code == 0
87+
assert "Deleted profile: test-profile" in result.output
88+
89+
# Verify profile was deleted
90+
with open(mock_sessions_file) as f:
91+
saved_sessions = json.load(f)
92+
assert "test-profile" not in saved_sessions
93+
94+
95+
def test_delete_unknown_session(mock_sessions_file, sample_config):
96+
runner = CliRunner()
97+
# Setup test data with a known profile
98+
sessions_data = {"test-profile": sample_config.dict()}
99+
os.makedirs(os.path.dirname(mock_sessions_file), exist_ok=True)
100+
with open(mock_sessions_file, "w") as f:
101+
json.dump(sessions_data, f)
102+
103+
# Test deleting non-existent profile
104+
result = runner.invoke(session, ["delete", "unknown-profile"])
105+
assert result.exit_code == 0
106+
assert "Profile unknown-profile not found" in result.output
107+
108+
# Verify original profile still exists
109+
with open(mock_sessions_file) as f:
110+
saved_sessions = json.load(f)
111+
assert "test-profile" in saved_sessions
112+
113+
114+
def test_use_session(mock_sessions_file, sample_config):
115+
runner = CliRunner()
116+
# Setup test data
117+
sessions_data = {"test-profile": sample_config.dict()}
118+
os.makedirs(os.path.dirname(mock_sessions_file), exist_ok=True)
119+
with open(mock_sessions_file, "w") as f:
120+
json.dump(sessions_data, f)
121+
122+
with patch("datahub.cli.session_cli.persist_raw_datahub_config") as mock_persist:
123+
result = runner.invoke(session, ["use", "test-profile"])
124+
assert result.exit_code == 0
125+
assert "Using profile test-profile" in result.output
126+
mock_persist.assert_called_once_with(sample_config.dict())
127+
128+
129+
def test_save_session(mock_sessions_file, sample_config):
130+
runner = CliRunner()
131+
with patch(
132+
"datahub.cli.session_cli.load_client_config", return_value=sample_config
133+
):
134+
result = runner.invoke(session, ["save", "--profile", "new-profile"])
135+
assert result.exit_code == 0
136+
assert "Saved current datahubenv as profile: new-profile" in result.output
137+
138+
# Verify the session was saved
139+
with open(mock_sessions_file) as f:
140+
saved_sessions = json.load(f)
141+
assert "new-profile" in saved_sessions
142+
assert saved_sessions["new-profile"]["server"] == "http://localhost:8080"
143+
assert saved_sessions["new-profile"]["token"] == "test-token"
144+
145+
146+
def test_create_session_with_password(mock_sessions_file):
147+
runner = CliRunner()
148+
with runner.isolated_filesystem(), patch(
149+
"datahub.cli.session_cli.generate_access_token",
150+
return_value=("username", "generated-token"),
151+
):
152+
result = runner.invoke(
153+
session,
154+
["create", "--use-password"],
155+
input="http://localhost:8080\nusername\npassword\ntest-profile\n",
156+
)
157+
assert result.exit_code == 0
158+
assert "Created profile: test-profile" in result.output
159+
160+
# Verify the session was saved with generated token
161+
with open(mock_sessions_file) as f:
162+
saved_sessions = json.load(f)
163+
assert saved_sessions["test-profile"]["token"] == "generated-token"

0 commit comments

Comments
 (0)