Skip to content

Commit f1d5879

Browse files
committed
SNOW-2306184: config refactor - separate parsing from reading and move history handling logic to own class
1 parent e050a89 commit f1d5879

18 files changed

+2497
-813
lines changed

src/snowflake/cli/api/config_ng/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,25 @@
2424
- Read-only, immutable configuration sources
2525
"""
2626

27+
from snowflake.cli.api.config_ng.constants import (
28+
FILE_SOURCE_NAMES,
29+
INTERNAL_CLI_PARAMETERS,
30+
ConfigSection,
31+
)
2732
from snowflake.cli.api.config_ng.core import (
2833
ConfigValue,
2934
ResolutionEntry,
3035
ResolutionHistory,
3136
SourceType,
3237
ValueSource,
3338
)
39+
from snowflake.cli.api.config_ng.dict_utils import deep_merge
40+
from snowflake.cli.api.config_ng.merge_operations import (
41+
create_default_connection_from_params,
42+
extract_root_level_connection_params,
43+
merge_params_into_connections,
44+
)
45+
from snowflake.cli.api.config_ng.parsers import SnowSQLParser, TOMLParser
3446
from snowflake.cli.api.config_ng.presentation import ResolutionPresenter
3547
from snowflake.cli.api.config_ng.resolution_logger import (
3648
check_value_source,
@@ -47,6 +59,8 @@
4759
ConfigurationResolver,
4860
ResolutionHistoryTracker,
4961
)
62+
from snowflake.cli.api.config_ng.source_factory import create_default_sources
63+
from snowflake.cli.api.config_ng.source_manager import SourceManager
5064
from snowflake.cli.api.config_ng.sources import (
5165
CliConfigFile,
5266
CliEnvironment,
@@ -64,17 +78,25 @@
6478
"CliConfigFile",
6579
"CliEnvironment",
6680
"CliParameters",
81+
"ConfigSection",
6782
"ConfigurationResolver",
6883
"ConfigValue",
6984
"ConnectionsConfigFile",
7085
"ConnectionSpecificEnvironment",
86+
"create_default_connection_from_params",
87+
"create_default_sources",
88+
"deep_merge",
7189
"explain_configuration",
7290
"export_resolution_history",
91+
"extract_root_level_connection_params",
92+
"FILE_SOURCE_NAMES",
7393
"format_summary_for_display",
7494
"get_merged_variables",
7595
"get_resolution_summary",
7696
"get_resolver",
97+
"INTERNAL_CLI_PARAMETERS",
7798
"is_resolution_logging_available",
99+
"merge_params_into_connections",
78100
"ResolutionEntry",
79101
"ResolutionHistory",
80102
"ResolutionHistoryTracker",
@@ -83,7 +105,10 @@
83105
"show_resolution_chain",
84106
"SnowSQLConfigFile",
85107
"SnowSQLEnvironment",
108+
"SnowSQLParser",
86109
"SnowSQLSection",
110+
"SourceManager",
87111
"SourceType",
112+
"TOMLParser",
88113
"ValueSource",
89114
]
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright (c) 2024 Snowflake Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Constants for configuration system."""
16+
17+
from enum import Enum
18+
from typing import Final, Literal
19+
20+
21+
class ConfigSection(str, Enum):
22+
"""Configuration section names."""
23+
24+
CONNECTIONS = "connections"
25+
VARIABLES = "variables"
26+
CLI = "cli"
27+
CLI_LOGS = "cli.logs"
28+
CLI_FEATURES = "cli.features"
29+
30+
def __str__(self) -> str:
31+
"""Return the string value for backward compatibility."""
32+
return self.value
33+
34+
35+
# Environment variable names
36+
SNOWFLAKE_HOME_ENV: Final[str] = "SNOWFLAKE_HOME"
37+
38+
# Internal CLI parameters that should not be treated as connection parameters
39+
INTERNAL_CLI_PARAMETERS: Final[set[str]] = {
40+
"enable_diag",
41+
"temporary_connection",
42+
"default_connection_name",
43+
"connection_name",
44+
"diag_log_path",
45+
"diag_allowlist_path",
46+
"mfa_passcode",
47+
}
48+
49+
# Define Literal type for file source names
50+
FileSourceName = Literal[
51+
"snowsql_config",
52+
"cli_config_toml",
53+
"connections_toml",
54+
]
55+
56+
# Source names that represent file-based configuration sources
57+
FILE_SOURCE_NAMES: Final[set[str]] = {
58+
"snowsql_config",
59+
"cli_config_toml",
60+
"connections_toml",
61+
}

src/snowflake/cli/api/config_ng/core.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,26 @@ def source_type(self) -> SourceType:
127127
...
128128

129129
@abstractmethod
130-
def discover(self, key: Optional[str] = None) -> Dict[str, ConfigValue]:
130+
def discover(self, key: Optional[str] = None) -> Dict[str, Any]:
131131
"""
132-
Discover configuration values from this source.
132+
Discover configuration values as nested dict structure.
133+
134+
Sources return configuration as nested dictionaries that reflect
135+
the natural structure of the configuration. For example:
136+
{"connections": {"prod": {"account": "val"}}}
137+
138+
Empty connections are represented as empty dicts:
139+
{"connections": {"prod": {}}}
140+
141+
General parameters (not connection-specific) are at the root level:
142+
{"database": "mydb", "role": "myrole"}
133143
134144
Args:
135-
key: Specific key to discover, or None to discover all values
145+
key: Specific key path to discover (dot-separated), or None for all
136146
137147
Returns:
138-
Dictionary mapping configuration keys to ConfigValue objects.
139-
Returns empty dict if no values found.
148+
Nested dictionary of configuration values. Returns empty dict
149+
if no values found.
140150
"""
141151
...
142152

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Copyright (c) 2024 Snowflake Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Utility functions for nested dictionary operations."""
16+
17+
from typing import Any, Dict
18+
19+
20+
def deep_merge(base: Dict[str, Any], overlay: Dict[str, Any]) -> Dict[str, Any]:
21+
"""
22+
Deep merge two dictionaries. Overlay values win on conflict.
23+
24+
Recursively merges nested dictionaries. Non-dict values from overlay
25+
replace values in base.
26+
27+
Example:
28+
base = {"a": {"b": 1, "c": 2}}
29+
overlay = {"a": {"c": 3, "d": 4}}
30+
result = {"a": {"b": 1, "c": 3, "d": 4}}
31+
32+
Args:
33+
base: Base dictionary
34+
overlay: Overlay dictionary (wins on conflicts)
35+
36+
Returns:
37+
Merged dictionary
38+
"""
39+
result = base.copy()
40+
41+
for key, value in overlay.items():
42+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
43+
result[key] = deep_merge(result[key], value)
44+
else:
45+
result[key] = value
46+
47+
return result
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Copyright (c) 2024 Snowflake Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Pure functions for configuration merging operations."""
16+
17+
from typing import Any, Dict
18+
19+
from snowflake.cli.api.config_ng.constants import (
20+
INTERNAL_CLI_PARAMETERS,
21+
ConfigSection,
22+
)
23+
24+
25+
def extract_root_level_connection_params(
26+
config: Dict[str, Any],
27+
) -> tuple[Dict[str, Any], Dict[str, Any]]:
28+
"""
29+
Extract root-level connection parameters from config.
30+
31+
Connection parameters at root level (not under any section) should
32+
be treated as general connection parameters that apply to all connections.
33+
34+
Args:
35+
config: Configuration dictionary with mixed sections and parameters
36+
37+
Returns:
38+
Tuple of (connection_params, remaining_config)
39+
40+
Example:
41+
Input: {"account": "acc", "cli": {...}, "connections": {...}}
42+
Output: ({"account": "acc"}, {"cli": {...}, "connections": {...}})
43+
"""
44+
known_sections = {s.value for s in ConfigSection}
45+
46+
connection_params = {}
47+
remaining = {}
48+
49+
for key, value in config.items():
50+
# Check if this key is a known section or internal parameter
51+
is_section = key in known_sections or any(
52+
key.startswith(s + ".") for s in known_sections
53+
)
54+
is_internal = key in INTERNAL_CLI_PARAMETERS
55+
56+
if not is_section and not is_internal:
57+
# Root-level parameter that's not a section = connection parameter
58+
connection_params[key] = value
59+
else:
60+
remaining[key] = value
61+
62+
return connection_params, remaining
63+
64+
65+
def merge_params_into_connections(
66+
connections: Dict[str, Dict[str, Any]], params: Dict[str, Any]
67+
) -> Dict[str, Dict[str, Any]]:
68+
"""
69+
Merge parameters into all existing connections.
70+
71+
Used for overlay sources where root-level connection params apply to all connections.
72+
The params overlay (override) values in each connection.
73+
74+
Args:
75+
connections: Dictionary of connection configurations
76+
params: Parameters to merge into each connection
77+
78+
Returns:
79+
Dictionary of connections with params merged in
80+
81+
Example:
82+
Input:
83+
connections = {"dev": {"account": "dev_acc", "user": "dev_user"}}
84+
params = {"user": "override_user", "password": "new_pass"}
85+
Output:
86+
{"dev": {"account": "dev_acc", "user": "override_user", "password": "new_pass"}}
87+
"""
88+
from snowflake.cli.api.config_ng.dict_utils import deep_merge
89+
90+
result = {}
91+
for conn_name, conn_config in connections.items():
92+
if isinstance(conn_config, dict):
93+
result[conn_name] = deep_merge(conn_config, params)
94+
else:
95+
result[conn_name] = conn_config
96+
97+
return result
98+
99+
100+
def create_default_connection_from_params(
101+
params: Dict[str, Any],
102+
) -> Dict[str, Dict[str, Any]]:
103+
"""
104+
Create a default connection from connection parameters.
105+
106+
Args:
107+
params: Connection parameters
108+
109+
Returns:
110+
Dictionary with "default" connection containing the params
111+
112+
Example:
113+
Input: {"account": "acc", "user": "usr"}
114+
Output: {"default": {"account": "acc", "user": "usr"}}
115+
"""
116+
if not params:
117+
return {}
118+
return {"default": params.copy()}

0 commit comments

Comments
 (0)