Skip to content

Commit f2bbf42

Browse files
SK-2954: Unify shared insert/validation/logging logic into common, add base insert response
Consolidates duplicated logic between v2 (PDB) and flowvault per architecture review: shared validation (vault config, credentials, log level), LogLevel/Logger, and insert field/table validation now live in common with per-variant message injection; adds BaseInsertResponse alongside BaseInsertRequest so each variant's InsertRequest/InsertResponse can extend a common base while keeping its own shape. Also fixes flowvault's insert() response shape (drop redundant 'data'/'table', flatten tokens, errors=None when empty) and a stale SDK_VERSION drift bug. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent b6757b0 commit f2bbf42

53 files changed

Lines changed: 1548 additions & 1341 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

common/client/__init__.py

Whitespace-only changes.

common/client/base_skyflow.py

Lines changed: 332 additions & 0 deletions
Large diffs are not rendered by default.

common/tests/client/__init__.py

Whitespace-only changes.
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import unittest
2+
3+
from common.errors import SkyflowError
4+
from common.utils import LogLevel, SkyflowMessages
5+
from common.utils.logger import Logger
6+
from common.client.base_skyflow import make_skyflow_class
7+
8+
9+
class FakeVaultClient:
10+
def __init__(self, config):
11+
self._config = dict(config)
12+
self.credentials = None
13+
self.logger = None
14+
15+
def get_config(self):
16+
return self._config
17+
18+
def update_config(self, config):
19+
self._config.update(config)
20+
21+
def set_logger(self, log_level, logger):
22+
self.logger = logger
23+
24+
def set_common_skyflow_credentials(self, credentials):
25+
self.credentials = credentials
26+
27+
28+
class FakeVaultController:
29+
def __init__(self, vault_client):
30+
self.vault_client = vault_client
31+
32+
33+
class FakeConnection:
34+
def __init__(self, vault_client):
35+
self.vault_client = vault_client
36+
37+
38+
class FakeDetect:
39+
def __init__(self, vault_client):
40+
self.vault_client = vault_client
41+
42+
43+
def _noop_validate(logger, config):
44+
return True
45+
46+
47+
def make_fake_skyflow(with_connections=False, with_detect=False):
48+
kwargs = dict(
49+
vault_client_cls=FakeVaultClient,
50+
vault_controller_cls=FakeVaultController,
51+
logger_cls=Logger,
52+
default_log_level=LogLevel.ERROR,
53+
skyflow_messages=SkyflowMessages,
54+
validate_vault_config=_noop_validate,
55+
validate_update_vault_config=_noop_validate,
56+
validate_log_level=_noop_validate,
57+
validate_credentials=_noop_validate,
58+
)
59+
if with_connections:
60+
kwargs.update(
61+
connection_cls=FakeConnection,
62+
validate_connection_config=_noop_validate,
63+
validate_update_connection_config=_noop_validate,
64+
)
65+
if with_detect:
66+
kwargs['detect_cls'] = FakeDetect
67+
return make_skyflow_class(**kwargs)
68+
69+
70+
VAULT_CONFIG = {"vault_id": "v1", "cluster_id": "c1", "credentials": {"token": "t"}}
71+
72+
73+
class TestMakeSkyflowClassBasics(unittest.TestCase):
74+
def test_build_returns_instance_of_the_produced_class_not_the_template(self):
75+
"""Regression pin: build()/builder() must resolve to the specific class produced by
76+
make_skyflow_class(), not the shared template -- two variants must never collide."""
77+
SkyflowA = make_fake_skyflow()
78+
SkyflowB = make_fake_skyflow()
79+
client_a = SkyflowA.builder().add_vault_config(VAULT_CONFIG).build()
80+
self.assertIsInstance(client_a, SkyflowA)
81+
self.assertNotIsInstance(client_a, SkyflowB)
82+
83+
def test_two_produced_classes_do_not_share_hooks(self):
84+
SkyflowWithDetect = make_fake_skyflow(with_detect=True)
85+
SkyflowWithoutDetect = make_fake_skyflow(with_detect=False)
86+
self.assertIsNotNone(SkyflowWithDetect.Builder._detect_cls)
87+
self.assertIsNone(SkyflowWithoutDetect.Builder._detect_cls)
88+
89+
def test_vault_config_crud(self):
90+
Skyflow = make_fake_skyflow()
91+
builder = Skyflow.builder()
92+
builder.add_vault_config(VAULT_CONFIG)
93+
client = builder.build()
94+
95+
vault_config = client.get_vault_config("v1")
96+
self.assertEqual(vault_config.get("vault_id"), "v1")
97+
98+
updated = dict(VAULT_CONFIG)
99+
updated["cluster_id"] = "c2"
100+
client.update_vault_config(updated)
101+
self.assertEqual(client.get_vault_config("v1").get("cluster_id"), "c2")
102+
103+
client.remove_vault_config("v1")
104+
with self.assertRaises(SkyflowError):
105+
client.get_vault_config("v1")
106+
107+
def test_vault_returns_the_controller(self):
108+
Skyflow = make_fake_skyflow()
109+
client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build()
110+
self.assertIsInstance(client.vault("v1"), FakeVaultController)
111+
112+
def test_add_skyflow_credentials_and_update(self):
113+
Skyflow = make_fake_skyflow()
114+
client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build()
115+
client.add_skyflow_credentials({"token": "a"})
116+
client.update_skyflow_credentials({"token": "b"})
117+
# no assertion error means both delegate correctly to the same underlying builder path
118+
119+
def test_set_get_and_deprecated_update_log_level(self):
120+
Skyflow = make_fake_skyflow()
121+
client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build()
122+
client.set_log_level(LogLevel.INFO)
123+
self.assertEqual(client.get_log_level(), LogLevel.INFO)
124+
125+
client.update_log_level(LogLevel.WARN)
126+
self.assertEqual(client.get_log_level(), LogLevel.WARN)
127+
128+
129+
class TestConnectionAndDetectGating(unittest.TestCase):
130+
def test_connection_methods_raise_when_connection_cls_not_supplied(self):
131+
Skyflow = make_fake_skyflow()
132+
client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build()
133+
with self.assertRaises(NotImplementedError):
134+
client.connection()
135+
with self.assertRaises(NotImplementedError):
136+
client.add_connection_config({})
137+
with self.assertRaises(NotImplementedError):
138+
client.remove_connection_config("x")
139+
with self.assertRaises(NotImplementedError):
140+
client.update_connection_config({})
141+
with self.assertRaises(NotImplementedError):
142+
client.get_connection_config("x")
143+
144+
def test_detect_raises_when_detect_cls_not_supplied(self):
145+
Skyflow = make_fake_skyflow()
146+
client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build()
147+
with self.assertRaises(NotImplementedError):
148+
client.detect()
149+
150+
def test_connection_config_crud_when_supplied(self):
151+
Skyflow = make_fake_skyflow(with_connections=True)
152+
connection_config = {"connection_id": "conn1", "connection_url": "https://x", "credentials": {"token": "t"}}
153+
client = Skyflow.builder().add_connection_config(connection_config).build()
154+
155+
self.assertIsInstance(client.connection("conn1"), FakeConnection)
156+
157+
updated = dict(connection_config)
158+
updated["connection_url"] = "https://y"
159+
client.update_connection_config(updated)
160+
self.assertEqual(client.get_connection_config("conn1").get("connection_url"), "https://y")
161+
162+
client.remove_connection_config("conn1")
163+
with self.assertRaises(SkyflowError):
164+
client.get_connection_config("conn1")
165+
166+
def test_detect_returns_detect_controller_when_supplied(self):
167+
Skyflow = make_fake_skyflow(with_detect=True)
168+
client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build()
169+
self.assertIsInstance(client.detect("v1"), FakeDetect)
170+
171+
def test_make_skyflow_class_requires_connection_validators_when_connection_cls_given(self):
172+
with self.assertRaises(ValueError):
173+
make_skyflow_class(
174+
vault_client_cls=FakeVaultClient,
175+
vault_controller_cls=FakeVaultController,
176+
logger_cls=Logger,
177+
default_log_level=LogLevel.ERROR,
178+
skyflow_messages=SkyflowMessages,
179+
validate_vault_config=_noop_validate,
180+
validate_update_vault_config=_noop_validate,
181+
validate_log_level=_noop_validate,
182+
validate_credentials=_noop_validate,
183+
connection_cls=FakeConnection,
184+
# validate_connection_config/validate_update_connection_config omitted on purpose
185+
)
186+
187+
188+
if __name__ == "__main__":
189+
unittest.main()

common/tests/utils/__init__.py

Whitespace-only changes.

common/tests/utils/validations/__init__.py

Whitespace-only changes.
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import unittest
2+
3+
from common.errors import SkyflowError
4+
from common.utils import SkyflowMessages, LogLevel, Env
5+
from common.utils.validations import (
6+
validate_vault_config,
7+
validate_update_vault_config,
8+
validate_credentials,
9+
validate_log_level,
10+
)
11+
12+
VALID_VAULT_CONFIG = {
13+
"vault_id": "vault123",
14+
"cluster_id": "cluster1",
15+
"env": Env.PROD,
16+
"credentials": {"api_key": "sky-abcde-" + "f" * 32},
17+
}
18+
19+
20+
class FakeMessages:
21+
"""Stand-in message catalog to confirm validate_vault_config/etc. actually use the
22+
`messages` param passed in, rather than silently falling back to common's own."""
23+
24+
class Error:
25+
class _M:
26+
def __init__(self, text):
27+
self._text = text
28+
29+
@property
30+
def value(self):
31+
return self._text
32+
33+
def format(self, *args, **kwargs):
34+
return self._text
35+
36+
EMPTY_VAULT_ID = _M("FAKE: empty vault id")
37+
INVALID_VAULT_ID = _M("FAKE: invalid vault id")
38+
EMPTY_CLUSTER_ID = _M("FAKE: empty cluster id")
39+
INVALID_CLUSTER_ID = _M("FAKE: invalid cluster id")
40+
EMPTY_CREDENTIALS = _M("FAKE: empty credentials")
41+
INVALID_ENV = _M("FAKE: invalid env")
42+
INVALID_KEY = _M("FAKE: invalid key")
43+
INVALID_LOG_LEVEL = _M("FAKE: invalid log level")
44+
INVALID_CREDENTIALS = _M("FAKE: invalid credentials")
45+
INVALID_CREDENTIALS_IN_CONFIG = _M("FAKE: invalid credentials in config")
46+
47+
class ErrorLogs:
48+
class _M:
49+
def __init__(self, text):
50+
self._text = text
51+
52+
@property
53+
def value(self):
54+
return self._text
55+
56+
VAULTID_IS_REQUIRED = _M("fake log")
57+
CLUSTER_ID_IS_REQUIRED = _M("fake log")
58+
CONNECTION_ID_IS_REQUIRED = _M("fake log")
59+
INVALID_CONNECTION_URL = _M("fake log")
60+
EMPTY_VAULTID = _M("fake log")
61+
EMPTY_CLUSTER_ID = _M("fake log")
62+
EMPTY_CONNECTION_ID = _M("fake log")
63+
EMPTY_CONNECTION_URL = _M("fake log")
64+
EMPTY_CREDENTIALS_PATH = _M("fake log")
65+
EMPTY_CREDENTIALS_STRING = _M("fake log")
66+
EMPTY_TOKEN_VALUE = _M("fake log")
67+
EMPTY_API_KEY_VALUE = _M("fake log")
68+
INVALID_KEY = _M("fake log")
69+
INVALID_LOG_LEVEL = _M("fake log")
70+
ENV_IS_REQUIRED = _M("fake log")
71+
72+
class Info:
73+
class _M:
74+
def __init__(self, text):
75+
self._text = text
76+
77+
@property
78+
def value(self):
79+
return self._text
80+
81+
VALIDATING_VAULT_CONFIG = _M("fake info")
82+
83+
84+
class TestValidateVaultConfig(unittest.TestCase):
85+
def test_valid_config_passes(self):
86+
self.assertTrue(validate_vault_config(None, dict(VALID_VAULT_CONFIG)))
87+
88+
def test_missing_vault_id_raises(self):
89+
config = dict(VALID_VAULT_CONFIG)
90+
del config["vault_id"]
91+
with self.assertRaises(SkyflowError):
92+
validate_vault_config(None, config)
93+
94+
def test_unknown_key_raises(self):
95+
config = dict(VALID_VAULT_CONFIG)
96+
config["unexpected"] = True
97+
with self.assertRaises(SkyflowError):
98+
validate_vault_config(None, config)
99+
100+
def test_empty_credentials_raises(self):
101+
config = dict(VALID_VAULT_CONFIG)
102+
config["credentials"] = {}
103+
with self.assertRaises(SkyflowError):
104+
validate_vault_config(None, config)
105+
106+
def test_credentials_are_validated(self):
107+
config = dict(VALID_VAULT_CONFIG)
108+
config["credentials"] = {"api_key": "not-a-valid-key"}
109+
with self.assertRaises(SkyflowError):
110+
validate_vault_config(None, config)
111+
112+
def test_uses_injected_messages_for_raised_error(self):
113+
config = dict(VALID_VAULT_CONFIG)
114+
del config["vault_id"]
115+
with self.assertRaises(SkyflowError) as ctx:
116+
validate_vault_config(None, config, messages=FakeMessages)
117+
self.assertIn("FAKE", ctx.exception.message)
118+
119+
def test_defaults_to_common_messages_when_not_injected(self):
120+
config = dict(VALID_VAULT_CONFIG)
121+
del config["vault_id"]
122+
with self.assertRaises(SkyflowError) as ctx:
123+
validate_vault_config(None, config)
124+
self.assertEqual(ctx.exception.message, SkyflowMessages.Error.INVALID_VAULT_ID.value)
125+
126+
127+
class TestValidateUpdateVaultConfig(unittest.TestCase):
128+
def test_valid_update_passes(self):
129+
self.assertTrue(validate_update_vault_config(None, dict(VALID_VAULT_CONFIG)))
130+
131+
def test_credentials_required_on_update(self):
132+
"""Unlike validate_vault_config, credentials are mandatory here."""
133+
config = dict(VALID_VAULT_CONFIG)
134+
del config["credentials"]
135+
with self.assertRaises(SkyflowError):
136+
validate_update_vault_config(None, config)
137+
138+
def test_uses_injected_messages(self):
139+
config = dict(VALID_VAULT_CONFIG)
140+
del config["credentials"]
141+
with self.assertRaises(SkyflowError) as ctx:
142+
validate_update_vault_config(None, config, messages=FakeMessages)
143+
self.assertIn("FAKE", ctx.exception.message)
144+
145+
146+
class TestValidateCredentialsMessageInjection(unittest.TestCase):
147+
def test_uses_injected_messages(self):
148+
with self.assertRaises(SkyflowError) as ctx:
149+
validate_credentials(None, {}, messages=FakeMessages)
150+
self.assertIn("FAKE", ctx.exception.message)
151+
152+
def test_defaults_to_common_messages(self):
153+
with self.assertRaises(SkyflowError) as ctx:
154+
validate_credentials(None, {})
155+
self.assertEqual(ctx.exception.message, SkyflowMessages.Error.INVALID_CREDENTIALS.value)
156+
157+
158+
class TestValidateLogLevelMessageInjection(unittest.TestCase):
159+
def test_valid_log_level_passes(self):
160+
validate_log_level(None, LogLevel.INFO) # should not raise
161+
162+
def test_uses_injected_messages(self):
163+
with self.assertRaises(SkyflowError) as ctx:
164+
validate_log_level(None, "not-a-log-level", messages=FakeMessages)
165+
self.assertIn("FAKE", ctx.exception.message)
166+
167+
168+
if __name__ == "__main__":
169+
unittest.main()

common/tests/vault/data/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)