Skip to content

Commit aaa9c8b

Browse files
committed
rework StackletConfig more
1 parent 5d59083 commit aaa9c8b

3 files changed

Lines changed: 134 additions & 43 deletions

File tree

‎stacklet/client/platform/cli.py‎

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# Copyright Stacklet, Inc.
22
# SPDX-License-Identifier: Apache-2.0
33

4-
import json
5-
import os
64
import pathlib
75
from urllib.parse import urlsplit, urlunsplit
86

@@ -128,24 +126,18 @@ def configure(
128126
"""
129127
Interactively save a Stacklet Config file
130128
"""
131-
config = {
132-
"api": api,
133-
"region": region,
134-
"cognito_client_id": cognito_client_id,
135-
"cognito_user_pool_id": cognito_user_pool_id,
136-
"idp_id": idp_id,
137-
"auth_url": auth_url,
138-
"cubejs": cubejs,
139-
}
140-
141-
StackletConfig.validate(config)
142-
143-
if not os.path.exists(location):
144-
dirs = location.rsplit("/", 1)[0]
145-
os.makedirs(os.path.expanduser(dirs), exist_ok=True)
146-
147-
with open(os.path.expanduser(location), "w+") as f:
148-
f.write(json.dumps(config))
129+
config = StackletConfig.from_dict(
130+
{
131+
"api": api,
132+
"region": region,
133+
"cognito_client_id": cognito_client_id,
134+
"cognito_user_pool_id": cognito_user_pool_id,
135+
"idp_id": idp_id,
136+
"auth_url": auth_url,
137+
"cubejs": cubejs,
138+
}
139+
)
140+
config.to_file(location)
149141
click.echo(f"Saved config to {location}")
150142

151143

@@ -277,13 +269,9 @@ def auto_configure(url, prefix, idp, location):
277269
except KeyError as err:
278270
raise click.ClickException(f"The configuration details are missing a required key: {err}")
279271

280-
config_file = location.expanduser()
281-
if not config_file.exists():
282-
config_file.parent.mkdir(parents=True, exist_ok=True)
283-
284-
config_file.write_text(json.dumps(formatted_config, indent=4, sort_keys=True))
285-
286-
click.echo(f"Saved config to {config_file}")
272+
config = StackletConfig.from_dict(formatted_config)
273+
config.to_file(location)
274+
click.echo(f"Saved config to {location}")
287275

288276

289277
@cli.command()
@@ -297,7 +285,7 @@ def show(obj):
297285
id_details = jwt.decode(id_token, options={"verify_signature": False})
298286
click.echo(fmt(id_details))
299287
click.echo()
300-
click.echo(fmt(obj.config.to_json()))
288+
click.echo(fmt(obj.config.to_dict()))
301289

302290

303291
@cli.command(short_help="Login to Stacklet")

‎stacklet/client/platform/config.py‎

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
DEFAULT_OUTPUT_FORMAT = "yaml"
2424

25+
JSONDict = dict[str, t.Any]
26+
2527

2628
class StackletConfig:
2729
schema = {
@@ -35,6 +37,15 @@ class StackletConfig:
3537
"auth_url": {"type": "string"},
3638
"cubejs": {"type": "string"},
3739
},
40+
"required": [
41+
"api",
42+
"cognito_user_pool_id",
43+
"cognito_client_id",
44+
"region",
45+
"idp_id",
46+
"auth_url",
47+
"cubejs",
48+
],
3849
}
3950

4051
def __init__(
@@ -55,7 +66,12 @@ def __init__(
5566
self.auth_url = auth_url
5667
self.cubejs = cubejs
5768

58-
def to_json(self):
69+
@classmethod
70+
def from_dict(cls, config: JSONDict):
71+
cls.validate(config)
72+
return cls(**config)
73+
74+
def to_dict(self) -> JSONDict:
5975
return dict(
6076
api=self.api,
6177
cognito_user_pool_id=self.cognito_user_pool_id,
@@ -66,27 +82,28 @@ def to_json(self):
6682
cubejs=self.cubejs,
6783
)
6884

69-
@classmethod
70-
def validate(cls, config):
71-
try:
72-
validate(instance=config, schema=cls.schema)
73-
except ValidationError as err:
74-
raise ConfigValidationException(str(err))
85+
def to_file(self, filename):
86+
path = Path(filename).expanduser()
87+
path.parent.mkdir(parents=True, exist_ok=True)
88+
with path.open("w") as fd:
89+
json.dump(self.to_dict(), fd, indent=4, sort_keys=True)
7590

7691
@classmethod
7792
def from_file(cls, filename):
7893
path = Path(filename).expanduser()
7994
if path.exists():
8095
with path.open() as f:
81-
content = json.load(f)
96+
config = json.load(f)
8297
else:
83-
content = {}
84-
85-
cls.validate(content)
86-
return cls(**content)
87-
98+
config = {}
99+
return cls.from_dict(config)
88100

89-
JSONDict = dict[str, t.Any]
101+
@classmethod
102+
def validate(cls, config: JSONDict):
103+
try:
104+
validate(instance=config, schema=cls.schema)
105+
except ValidationError as err:
106+
raise ConfigValidationException(str(err))
90107

91108

92109
class StackletCredentials:

‎tests/test_config.py‎

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import json
12
import os
23
from contextlib import contextmanager
34
from unittest.mock import patch
45

56
import pytest
67

7-
from stacklet.client.platform.config import StackletCredentials
8+
from stacklet.client.platform.config import StackletConfig, StackletCredentials
9+
from stacklet.client.platform.exceptions import ConfigValidationException
810

911

1012
@pytest.fixture
@@ -45,3 +47,87 @@ def test_api_token_prefer_env(self, creds):
4547
creds.write("id token", "access token")
4648
with self.env_api_key("a-b-c"):
4749
assert creds.api_token() == "a-b-c"
50+
51+
52+
class TestStackletConfig:
53+
@pytest.fixture
54+
def config(self):
55+
return {
56+
"api": "https://api.example.com",
57+
"cognito_user_pool_id": "pool-123",
58+
"cognito_client_id": "client-456",
59+
"region": "us-east-1",
60+
"idp_id": "idp-789",
61+
"auth_url": "https://auth.example.com",
62+
"cubejs": "https://cubejs.example.com",
63+
}
64+
65+
def test_validate_validpresent(self, config):
66+
StackletConfig.validate(config)
67+
68+
@pytest.mark.parametrize(
69+
"missing_field",
70+
[
71+
"api",
72+
"cognito_user_pool_id",
73+
"cognito_client_id",
74+
"region",
75+
"idp_id",
76+
"auth_url",
77+
"cubejs",
78+
],
79+
)
80+
def test_validate_missing_required_field(self, config, missing_field):
81+
config_copy = config.copy()
82+
del config_copy[missing_field]
83+
with pytest.raises(ConfigValidationException) as err:
84+
StackletConfig.validate(config_copy)
85+
assert f"'{missing_field}' is a required property" in str(err.value)
86+
87+
def test_from_file_valid_config(self, config, tmp_path):
88+
config_file = tmp_path / "config.json"
89+
config_file.write_text(json.dumps(config))
90+
stacklet_config = StackletConfig.from_file(config_file)
91+
assert stacklet_config.api == config["api"]
92+
assert stacklet_config.cognito_user_pool_id == config["cognito_user_pool_id"]
93+
assert stacklet_config.cognito_client_id == config["cognito_client_id"]
94+
assert stacklet_config.region == config["region"]
95+
assert stacklet_config.idp_id == config["idp_id"]
96+
assert stacklet_config.auth_url == config["auth_url"]
97+
assert stacklet_config.cubejs == config["cubejs"]
98+
99+
def test_from_file_missing_file(self, tmp_path):
100+
config_file = tmp_path / "nonexistent.json"
101+
with pytest.raises(ConfigValidationException):
102+
StackletConfig.from_file(config_file)
103+
104+
def test_from_file_invalid_config(self, tmp_path):
105+
config_file = tmp_path / "config.json"
106+
incomplete_config = {"api": "https://api.example.com"}
107+
config_file.write_text(json.dumps(incomplete_config))
108+
with pytest.raises(ConfigValidationException):
109+
StackletConfig.from_file(config_file)
110+
111+
def test_to_file(self, config, tmp_path):
112+
config_file = tmp_path / "output.json"
113+
stacklet_config = StackletConfig(**config)
114+
stacklet_config.to_file(config_file)
115+
assert config_file.exists()
116+
117+
loaded_config = StackletConfig.from_file(config_file)
118+
assert loaded_config.api == config["api"]
119+
assert loaded_config.cognito_user_pool_id == config["cognito_user_pool_id"]
120+
assert loaded_config.cognito_client_id == config["cognito_client_id"]
121+
assert loaded_config.region == config["region"]
122+
assert loaded_config.idp_id == config["idp_id"]
123+
assert loaded_config.auth_url == config["auth_url"]
124+
assert loaded_config.cubejs == config["cubejs"]
125+
126+
def test_to_file_creates_parent_directories(self, config, tmp_path):
127+
config_file = tmp_path / "nested" / "dir" / "config.json"
128+
stacklet_config = StackletConfig(**config)
129+
stacklet_config.to_file(config_file)
130+
assert config_file.exists()
131+
132+
loaded_config = StackletConfig.from_file(config_file)
133+
assert loaded_config.api == config["api"]

0 commit comments

Comments
 (0)