snowflake-cli 3.2.1__py3-none-any.whl → 3.3.0__py3-none-any.whl
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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/constants.py +4 -0
- snowflake/cli/_app/snow_connector.py +12 -0
- snowflake/cli/_app/telemetry.py +10 -3
- snowflake/cli/_plugins/connection/util.py +12 -19
- snowflake/cli/_plugins/helpers/commands.py +207 -1
- snowflake/cli/_plugins/nativeapp/artifacts.py +10 -4
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +41 -17
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +7 -0
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -1
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +42 -32
- snowflake/cli/_plugins/nativeapp/commands.py +92 -2
- snowflake/cli/_plugins/nativeapp/constants.py +5 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +221 -288
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +772 -89
- snowflake/cli/_plugins/nativeapp/entities/application_package_child_interface.py +43 -0
- snowflake/cli/_plugins/nativeapp/feature_flags.py +5 -1
- snowflake/cli/_plugins/nativeapp/release_channel/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_channel/commands.py +212 -0
- snowflake/cli/_plugins/nativeapp/release_directive/__init__.py +13 -0
- snowflake/cli/_plugins/nativeapp/release_directive/commands.py +165 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +9 -17
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +80 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +999 -75
- snowflake/cli/_plugins/nativeapp/utils.py +11 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +5 -1
- snowflake/cli/_plugins/nativeapp/version/commands.py +31 -4
- snowflake/cli/_plugins/notebook/manager.py +4 -2
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +234 -4
- snowflake/cli/_plugins/spcs/common.py +129 -0
- snowflake/cli/_plugins/spcs/services/commands.py +134 -14
- snowflake/cli/_plugins/spcs/services/manager.py +169 -1
- snowflake/cli/_plugins/stage/manager.py +12 -4
- snowflake/cli/_plugins/streamlit/manager.py +8 -1
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +153 -2
- snowflake/cli/_plugins/workspace/commands.py +3 -2
- snowflake/cli/_plugins/workspace/manager.py +8 -4
- snowflake/cli/api/cli_global_context.py +22 -1
- snowflake/cli/api/config.py +6 -2
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/constants.py +9 -1
- snowflake/cli/api/entities/common.py +85 -0
- snowflake/cli/api/entities/utils.py +9 -8
- snowflake/cli/api/errno.py +60 -3
- snowflake/cli/api/feature_flags.py +20 -4
- snowflake/cli/api/metrics.py +21 -27
- snowflake/cli/api/project/definition_conversion.py +1 -2
- snowflake/cli/api/project/schemas/project_definition.py +27 -6
- snowflake/cli/api/project/schemas/v1/streamlit/streamlit.py +1 -1
- snowflake/cli/api/project/util.py +45 -0
- snowflake/cli/api/rest_api.py +3 -2
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/METADATA +13 -13
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/RECORD +56 -51
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/WHEEL +1 -1
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.2.1.dist-info → snowflake_cli-3.3.0.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/__about__.py
CHANGED
snowflake/cli/_app/constants.py
CHANGED
|
@@ -17,3 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
from typing import Literal
|
|
18
18
|
|
|
19
19
|
PARAM_APPLICATION_NAME: Literal["snowcli"] = "snowcli"
|
|
20
|
+
|
|
21
|
+
# This is also defined on server side. Changing this parameter would require
|
|
22
|
+
# a change in https://github.com/snowflakedb/snowflake
|
|
23
|
+
INTERNAL_APPLICATION_NAME: Literal["SNOWFLAKE_CLI"] = "SNOWFLAKE_CLI"
|
|
@@ -21,7 +21,9 @@ from typing import Dict, Optional
|
|
|
21
21
|
|
|
22
22
|
import snowflake.connector
|
|
23
23
|
from click.exceptions import ClickException
|
|
24
|
+
from snowflake.cli.__about__ import VERSION
|
|
24
25
|
from snowflake.cli._app.constants import (
|
|
26
|
+
INTERNAL_APPLICATION_NAME,
|
|
25
27
|
PARAM_APPLICATION_NAME,
|
|
26
28
|
)
|
|
27
29
|
from snowflake.cli._app.secret import SecretType
|
|
@@ -35,6 +37,7 @@ from snowflake.cli.api.exceptions import (
|
|
|
35
37
|
InvalidConnectionConfiguration,
|
|
36
38
|
SnowflakeConnectionError,
|
|
37
39
|
)
|
|
40
|
+
from snowflake.cli.api.feature_flags import FeatureFlag
|
|
38
41
|
from snowflake.cli.api.secure_path import SecurePath
|
|
39
42
|
from snowflake.connector import SnowflakeConnection
|
|
40
43
|
from snowflake.connector.errors import DatabaseError, ForbiddenError
|
|
@@ -150,6 +153,8 @@ def connect_to_snowflake(
|
|
|
150
153
|
|
|
151
154
|
_update_connection_application_name(connection_parameters)
|
|
152
155
|
|
|
156
|
+
_update_internal_application_info(connection_parameters)
|
|
157
|
+
|
|
153
158
|
try:
|
|
154
159
|
# Whatever output is generated when creating connection,
|
|
155
160
|
# we don't want it in our output. This is particularly important
|
|
@@ -238,6 +243,13 @@ def _update_connection_application_name(connection_parameters: Dict):
|
|
|
238
243
|
connection_parameters.update(connection_application_params)
|
|
239
244
|
|
|
240
245
|
|
|
246
|
+
def _update_internal_application_info(connection_parameters: Dict):
|
|
247
|
+
"""Update internal application data if ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID is enabled."""
|
|
248
|
+
if FeatureFlag.ENABLE_SEPARATE_AUTHENTICATION_POLICY_ID.is_enabled():
|
|
249
|
+
connection_parameters["internal_application_name"] = INTERNAL_APPLICATION_NAME
|
|
250
|
+
connection_parameters["internal_application_version"] = VERSION
|
|
251
|
+
|
|
252
|
+
|
|
241
253
|
def _load_pem_from_file(private_key_file: str) -> SecretType:
|
|
242
254
|
with SecurePath(private_key_file).open(
|
|
243
255
|
"rb", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB
|
snowflake/cli/_app/telemetry.py
CHANGED
|
@@ -67,6 +67,10 @@ class CLITelemetryField(Enum):
|
|
|
67
67
|
CONFIG_FEATURE_FLAGS = "config_feature_flags"
|
|
68
68
|
# Metrics
|
|
69
69
|
COUNTERS = "counters"
|
|
70
|
+
SPANS = "spans"
|
|
71
|
+
COMPLETED_SPANS = "completed_spans"
|
|
72
|
+
NUM_SPANS_PAST_DEPTH_LIMIT = "num_spans_past_depth_limit"
|
|
73
|
+
NUM_SPANS_PAST_TOTAL_LIMIT = "num_spans_past_total_limit"
|
|
70
74
|
# Information
|
|
71
75
|
EVENT = "event"
|
|
72
76
|
ERROR_MSG = "error_msg"
|
|
@@ -129,9 +133,12 @@ def _get_command_metrics() -> TelemetryDict:
|
|
|
129
133
|
cli_context = get_cli_context()
|
|
130
134
|
|
|
131
135
|
return {
|
|
132
|
-
CLITelemetryField.COUNTERS:
|
|
133
|
-
|
|
134
|
-
|
|
136
|
+
CLITelemetryField.COUNTERS: cli_context.metrics.counters,
|
|
137
|
+
CLITelemetryField.SPANS: {
|
|
138
|
+
CLITelemetryField.COMPLETED_SPANS.value: cli_context.metrics.completed_spans,
|
|
139
|
+
CLITelemetryField.NUM_SPANS_PAST_DEPTH_LIMIT.value: cli_context.metrics.num_spans_past_depth_limit,
|
|
140
|
+
CLITelemetryField.NUM_SPANS_PAST_TOTAL_LIMIT.value: cli_context.metrics.num_spans_past_total_limit,
|
|
141
|
+
},
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
|
|
@@ -19,7 +19,6 @@ import logging
|
|
|
19
19
|
import os
|
|
20
20
|
from enum import Enum
|
|
21
21
|
from functools import lru_cache
|
|
22
|
-
from textwrap import dedent
|
|
23
22
|
from typing import Any, Dict, Optional
|
|
24
23
|
|
|
25
24
|
from click.exceptions import ClickException
|
|
@@ -57,11 +56,12 @@ class UIParameter(Enum):
|
|
|
57
56
|
NA_ENFORCE_MANDATORY_FILTERS = (
|
|
58
57
|
"ENFORCE_MANDATORY_FILTERS_FOR_SAME_ACCOUNT_INSTALLATION"
|
|
59
58
|
)
|
|
59
|
+
NA_FEATURE_RELEASE_CHANNELS = "FEATURE_RELEASE_CHANNELS"
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
def get_ui_parameter(
|
|
63
63
|
conn: SnowflakeConnection, parameter: UIParameter, default: Any
|
|
64
|
-
) ->
|
|
64
|
+
) -> Any:
|
|
65
65
|
"""
|
|
66
66
|
Returns the value of a single UI parameter.
|
|
67
67
|
If the parameter is not found, the default value is returned.
|
|
@@ -77,21 +77,19 @@ def get_ui_parameters(conn: SnowflakeConnection) -> Dict[UIParameter, Any]:
|
|
|
77
77
|
Returns the UI parameters from the SYSTEM$BOOTSTRAP_DATA_REQUEST function
|
|
78
78
|
"""
|
|
79
79
|
|
|
80
|
-
parameters_to_fetch =
|
|
80
|
+
parameters_to_fetch = [param.value for param in UIParameter]
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
path => 'clientParamsInfo'
|
|
87
|
-
)) where value['name'] in ('{"', '".join(parameters_to_fetch)}');
|
|
88
|
-
"""
|
|
89
|
-
)
|
|
82
|
+
# Parsing of the Json and the filtering is happening here in Snowflake CLI
|
|
83
|
+
# in order to avoid requiring a warehouse in Snowflake
|
|
84
|
+
query = "call system$bootstrap_data_request('CLIENT_PARAMS_INFO')"
|
|
85
|
+
*_, cursor = conn.execute_string(query)
|
|
90
86
|
|
|
91
|
-
|
|
87
|
+
json_map = json.loads(cursor.fetchone()[0])
|
|
92
88
|
|
|
93
89
|
return {
|
|
94
|
-
UIParameter(row["
|
|
90
|
+
UIParameter(row["name"]): row["value"]
|
|
91
|
+
for row in json_map["clientParamsInfo"]
|
|
92
|
+
if row["name"] in parameters_to_fetch
|
|
95
93
|
}
|
|
96
94
|
|
|
97
95
|
|
|
@@ -103,12 +101,7 @@ def is_regionless_redirect(conn: SnowflakeConnection) -> bool:
|
|
|
103
101
|
assume it's regionless, as this is true for most production deployments.
|
|
104
102
|
"""
|
|
105
103
|
try:
|
|
106
|
-
return (
|
|
107
|
-
get_ui_parameter(
|
|
108
|
-
conn, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, "true"
|
|
109
|
-
).lower()
|
|
110
|
-
== "true"
|
|
111
|
-
)
|
|
104
|
+
return get_ui_parameter(conn, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, True)
|
|
112
105
|
except:
|
|
113
106
|
log.warning(
|
|
114
107
|
"Cannot determine regionless redirect; assuming True.", exc_info=True
|
|
@@ -14,17 +14,30 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import logging
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, List, Optional
|
|
20
|
+
|
|
17
21
|
import typer
|
|
18
22
|
import yaml
|
|
19
23
|
from click import ClickException
|
|
20
24
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
21
|
-
from snowflake.cli.api.
|
|
25
|
+
from snowflake.cli.api.config import (
|
|
26
|
+
ConnectionConfig,
|
|
27
|
+
add_connection_to_proper_file,
|
|
28
|
+
get_all_connections,
|
|
29
|
+
set_config_value,
|
|
30
|
+
)
|
|
31
|
+
from snowflake.cli.api.console import cli_console
|
|
32
|
+
from snowflake.cli.api.output.types import CommandResult, MessageResult
|
|
22
33
|
from snowflake.cli.api.project.definition_conversion import (
|
|
23
34
|
convert_project_definition_to_v2,
|
|
24
35
|
)
|
|
25
36
|
from snowflake.cli.api.project.definition_manager import DefinitionManager
|
|
26
37
|
from snowflake.cli.api.secure_path import SecurePath
|
|
27
38
|
|
|
39
|
+
log = logging.getLogger(__name__)
|
|
40
|
+
|
|
28
41
|
app = SnowTyperFactory(
|
|
29
42
|
name="helpers",
|
|
30
43
|
help="Helper commands.",
|
|
@@ -88,3 +101,196 @@ def v1_to_v2(
|
|
|
88
101
|
width=float("inf"), # Don't break lines
|
|
89
102
|
)
|
|
90
103
|
return MessageResult("Project definition migrated to version 2.")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@app.command(name="import-snowsql-connections", requires_connection=False)
|
|
107
|
+
def import_snowsql_connections(
|
|
108
|
+
custom_snowsql_config_files: Optional[List[Path]] = typer.Option(
|
|
109
|
+
None,
|
|
110
|
+
"--snowsql-config-file",
|
|
111
|
+
help="Specifies file paths to custom SnowSQL configuration. The option can be used multiple times to specify more than 1 file.",
|
|
112
|
+
dir_okay=False,
|
|
113
|
+
exists=True,
|
|
114
|
+
),
|
|
115
|
+
default_cli_connection_name: str = typer.Option(
|
|
116
|
+
"default",
|
|
117
|
+
"--default-connection-name",
|
|
118
|
+
help="Specifies the name which will be given in Snowflake CLI to the default connection imported from SnowSQL.",
|
|
119
|
+
),
|
|
120
|
+
**options,
|
|
121
|
+
) -> CommandResult:
|
|
122
|
+
"""Import your existing connections from your SnowSQL configuration."""
|
|
123
|
+
|
|
124
|
+
snowsql_config_files: list[Path] = custom_snowsql_config_files or [
|
|
125
|
+
Path("/etc/snowsql.cnf"),
|
|
126
|
+
Path("/etc/snowflake/snowsql.cnf"),
|
|
127
|
+
Path("/usr/local/etc/snowsql.cnf"),
|
|
128
|
+
Path.home() / Path(".snowsql.cnf"),
|
|
129
|
+
Path.home() / Path(".snowsql/config"),
|
|
130
|
+
]
|
|
131
|
+
snowsql_config_secure_paths: list[SecurePath] = [
|
|
132
|
+
SecurePath(p) for p in snowsql_config_files
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
all_imported_connections = _read_all_connections_from_snowsql(
|
|
136
|
+
default_cli_connection_name, snowsql_config_secure_paths
|
|
137
|
+
)
|
|
138
|
+
_validate_and_save_connections_imported_from_snowsql(
|
|
139
|
+
default_cli_connection_name, all_imported_connections
|
|
140
|
+
)
|
|
141
|
+
return MessageResult(
|
|
142
|
+
"Connections successfully imported from SnowSQL to Snowflake CLI."
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _read_all_connections_from_snowsql(
|
|
147
|
+
default_cli_connection_name: str, snowsql_config_files: List[SecurePath]
|
|
148
|
+
) -> dict[str, dict]:
|
|
149
|
+
import configparser
|
|
150
|
+
|
|
151
|
+
imported_default_connection: dict[str, Any] = {}
|
|
152
|
+
imported_named_connections: dict[str, dict] = {}
|
|
153
|
+
|
|
154
|
+
for file in snowsql_config_files:
|
|
155
|
+
if not file.exists():
|
|
156
|
+
cli_console.step(
|
|
157
|
+
f"SnowSQL config file [{str(file.path)}] does not exist. Skipping."
|
|
158
|
+
)
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
cli_console.step(f"Trying to read connections from [{str(file.path)}].")
|
|
162
|
+
snowsql_config = configparser.ConfigParser()
|
|
163
|
+
snowsql_config.read(file.path)
|
|
164
|
+
|
|
165
|
+
if "connections" in snowsql_config and snowsql_config.items("connections"):
|
|
166
|
+
cli_console.step(
|
|
167
|
+
f"Reading SnowSQL's default connection configuration from [{str(file.path)}]"
|
|
168
|
+
)
|
|
169
|
+
snowsql_default_connection = snowsql_config.items("connections")
|
|
170
|
+
imported_default_connection.update(
|
|
171
|
+
_convert_connection_from_snowsql_config_section(
|
|
172
|
+
snowsql_default_connection
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
other_snowsql_connection_section_names = [
|
|
177
|
+
section_name
|
|
178
|
+
for section_name in snowsql_config.sections()
|
|
179
|
+
if section_name.startswith("connections.")
|
|
180
|
+
]
|
|
181
|
+
for snowsql_connection_section_name in other_snowsql_connection_section_names:
|
|
182
|
+
cli_console.step(
|
|
183
|
+
f"Reading SnowSQL's connection configuration [{snowsql_connection_section_name}] from [{str(file.path)}]"
|
|
184
|
+
)
|
|
185
|
+
snowsql_named_connection = snowsql_config.items(
|
|
186
|
+
snowsql_connection_section_name
|
|
187
|
+
)
|
|
188
|
+
if not snowsql_named_connection:
|
|
189
|
+
cli_console.step(
|
|
190
|
+
f"Empty connection configuration [{snowsql_connection_section_name}] in [{str(file.path)}]. Skipping."
|
|
191
|
+
)
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
connection_name = snowsql_connection_section_name.removeprefix(
|
|
195
|
+
"connections."
|
|
196
|
+
)
|
|
197
|
+
imported_named_conenction = _convert_connection_from_snowsql_config_section(
|
|
198
|
+
snowsql_named_connection
|
|
199
|
+
)
|
|
200
|
+
if connection_name in imported_named_connections:
|
|
201
|
+
imported_named_connections[connection_name].update(
|
|
202
|
+
imported_named_conenction
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
imported_named_connections[connection_name] = imported_named_conenction
|
|
206
|
+
|
|
207
|
+
def imported_default_connection_as_named_connection():
|
|
208
|
+
name = _validate_imported_default_connection_name(
|
|
209
|
+
default_cli_connection_name, imported_named_connections
|
|
210
|
+
)
|
|
211
|
+
return {name: imported_default_connection}
|
|
212
|
+
|
|
213
|
+
named_default_connection = (
|
|
214
|
+
imported_default_connection_as_named_connection()
|
|
215
|
+
if imported_default_connection
|
|
216
|
+
else {}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return imported_named_connections | named_default_connection
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _validate_imported_default_connection_name(
|
|
223
|
+
name_candidate: str, other_snowsql_connections: dict[str, dict]
|
|
224
|
+
) -> str:
|
|
225
|
+
if name_candidate in other_snowsql_connections:
|
|
226
|
+
new_name_candidate = typer.prompt(
|
|
227
|
+
f"Chosen default connection name '{name_candidate}' is already taken by other connection being imported from SnowSQL. Please choose a different name for your default connection"
|
|
228
|
+
)
|
|
229
|
+
return _validate_imported_default_connection_name(
|
|
230
|
+
new_name_candidate, other_snowsql_connections
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
return name_candidate
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _convert_connection_from_snowsql_config_section(
|
|
237
|
+
snowsql_connection: list[tuple[str, Any]]
|
|
238
|
+
) -> dict[str, Any]:
|
|
239
|
+
from ast import literal_eval
|
|
240
|
+
|
|
241
|
+
key_names_replacements = {
|
|
242
|
+
"accountname": "account",
|
|
243
|
+
"username": "user",
|
|
244
|
+
"databasename": "database",
|
|
245
|
+
"dbname": "database",
|
|
246
|
+
"schemaname": "schema",
|
|
247
|
+
"warehousename": "warehouse",
|
|
248
|
+
"rolename": "role",
|
|
249
|
+
"private_key_path": "private_key_file",
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
def parse_value(value: Any):
|
|
253
|
+
try:
|
|
254
|
+
parsed_value = literal_eval(value)
|
|
255
|
+
except Exception:
|
|
256
|
+
parsed_value = value
|
|
257
|
+
return parsed_value
|
|
258
|
+
|
|
259
|
+
cli_connection: dict[str, Any] = {}
|
|
260
|
+
for key, value in snowsql_connection:
|
|
261
|
+
cli_key = key_names_replacements.get(key, key)
|
|
262
|
+
cli_value = parse_value(value)
|
|
263
|
+
cli_connection[cli_key] = cli_value
|
|
264
|
+
return cli_connection
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _validate_and_save_connections_imported_from_snowsql(
|
|
268
|
+
default_cli_connection_name: str, all_imported_connections: dict[str, Any]
|
|
269
|
+
):
|
|
270
|
+
existing_cli_connection_names: set[str] = set(get_all_connections().keys())
|
|
271
|
+
imported_connections_to_save: dict[str, Any] = {}
|
|
272
|
+
for (
|
|
273
|
+
imported_connection_name,
|
|
274
|
+
imported_connection,
|
|
275
|
+
) in all_imported_connections.items():
|
|
276
|
+
if imported_connection_name in existing_cli_connection_names:
|
|
277
|
+
override_cli_connection = typer.confirm(
|
|
278
|
+
f"Connection '{imported_connection_name}' already exists in Snowflake CLI, do you want to use SnowSQL definition and override existing connection in Snowflake CLI?"
|
|
279
|
+
)
|
|
280
|
+
if not override_cli_connection:
|
|
281
|
+
continue
|
|
282
|
+
imported_connections_to_save[imported_connection_name] = imported_connection
|
|
283
|
+
|
|
284
|
+
for name, connection in imported_connections_to_save.items():
|
|
285
|
+
cli_console.step(f"Saving [{name}] connection in Snowflake CLI's config.")
|
|
286
|
+
add_connection_to_proper_file(name, ConnectionConfig.from_dict(connection))
|
|
287
|
+
|
|
288
|
+
if default_cli_connection_name in imported_connections_to_save:
|
|
289
|
+
cli_console.step(
|
|
290
|
+
f"Setting [{default_cli_connection_name}] connection as Snowflake CLI's default connection."
|
|
291
|
+
)
|
|
292
|
+
set_config_value(
|
|
293
|
+
section=None,
|
|
294
|
+
key="default_connection_name",
|
|
295
|
+
value=default_cli_connection_name,
|
|
296
|
+
)
|
|
@@ -22,6 +22,7 @@ from textwrap import dedent
|
|
|
22
22
|
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
|
23
23
|
|
|
24
24
|
from click.exceptions import ClickException
|
|
25
|
+
from snowflake.cli.api.cli_global_context import span
|
|
25
26
|
from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB
|
|
26
27
|
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import PathMapping
|
|
27
28
|
from snowflake.cli.api.project.util import to_identifier
|
|
@@ -649,6 +650,7 @@ def resolve_without_follow(path: Path) -> Path:
|
|
|
649
650
|
return Path(os.path.abspath(path))
|
|
650
651
|
|
|
651
652
|
|
|
653
|
+
@span("bundle")
|
|
652
654
|
def build_bundle(
|
|
653
655
|
project_root: Path,
|
|
654
656
|
deploy_root: Path,
|
|
@@ -775,12 +777,16 @@ def find_version_info_in_manifest_file(
|
|
|
775
777
|
label: Optional[str] = None
|
|
776
778
|
|
|
777
779
|
version_info = manifest_content.get("version", None)
|
|
778
|
-
if version_info:
|
|
779
|
-
if
|
|
780
|
+
if version_info is not None:
|
|
781
|
+
if not isinstance(version_info, dict):
|
|
782
|
+
raise ClickException(
|
|
783
|
+
"Error occurred while reading manifest.yml. Received unexpected version format."
|
|
784
|
+
)
|
|
785
|
+
if version_info.get(name_field) is not None:
|
|
780
786
|
version_name = to_identifier(str(version_info[name_field]))
|
|
781
|
-
if patch_field
|
|
787
|
+
if version_info.get(patch_field) is not None:
|
|
782
788
|
patch_number = int(version_info[patch_field])
|
|
783
|
-
if label_field
|
|
789
|
+
if version_info.get(label_field) is not None:
|
|
784
790
|
label = str(version_info[label_field])
|
|
785
791
|
|
|
786
792
|
return VersionInfo(version_name, patch_number, label)
|
|
@@ -33,7 +33,6 @@ from snowflake.cli._plugins.nativeapp.codegen.snowpark.python_processor import (
|
|
|
33
33
|
from snowflake.cli._plugins.nativeapp.codegen.templates.templates_processor import (
|
|
34
34
|
TemplatesProcessor,
|
|
35
35
|
)
|
|
36
|
-
from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
|
|
37
36
|
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
38
37
|
from snowflake.cli.api.console import cli_console as cc
|
|
39
38
|
from snowflake.cli.api.metrics import CLICounterField
|
|
@@ -41,15 +40,7 @@ from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import (
|
|
|
41
40
|
ProcessorMapping,
|
|
42
41
|
)
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
NA_SETUP_PROCESSOR = "native app setup"
|
|
46
|
-
TEMPLATES_PROCESSOR = "templates"
|
|
47
|
-
|
|
48
|
-
_REGISTERED_PROCESSORS_BY_NAME = {
|
|
49
|
-
SNOWPARK_PROCESSOR: SnowparkAnnotationProcessor,
|
|
50
|
-
NA_SETUP_PROCESSOR: NativeAppSetupProcessor,
|
|
51
|
-
TEMPLATES_PROCESSOR: TemplatesProcessor,
|
|
52
|
-
}
|
|
43
|
+
ProcessorClassType = type[ArtifactProcessor]
|
|
53
44
|
|
|
54
45
|
|
|
55
46
|
class NativeAppCompiler:
|
|
@@ -66,10 +57,28 @@ class NativeAppCompiler:
|
|
|
66
57
|
bundle_ctx: BundleContext,
|
|
67
58
|
):
|
|
68
59
|
self._assert_absolute_paths(bundle_ctx)
|
|
60
|
+
self._processor_classes_by_name: Dict[str, ProcessorClassType] = {}
|
|
69
61
|
self._bundle_ctx = bundle_ctx
|
|
70
62
|
# dictionary of all processors created and shared between different artifact objects.
|
|
71
63
|
self.cached_processors: Dict[str, ArtifactProcessor] = {}
|
|
72
64
|
|
|
65
|
+
self.register(SnowparkAnnotationProcessor)
|
|
66
|
+
self.register(NativeAppSetupProcessor)
|
|
67
|
+
self.register(TemplatesProcessor)
|
|
68
|
+
|
|
69
|
+
def register(self, processor_cls: ProcessorClassType):
|
|
70
|
+
"""
|
|
71
|
+
Registers a processor class to enable.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
name = getattr(processor_cls, "NAME", None)
|
|
75
|
+
assert name is not None
|
|
76
|
+
|
|
77
|
+
if name in self._processor_classes_by_name:
|
|
78
|
+
raise ValueError(f"Processor {name} is already registered")
|
|
79
|
+
|
|
80
|
+
self._processor_classes_by_name[str(name)] = processor_cls
|
|
81
|
+
|
|
73
82
|
@staticmethod
|
|
74
83
|
def _assert_absolute_paths(bundle_ctx: BundleContext):
|
|
75
84
|
for name in ["Project", "Deploy", "Bundle", "Generated"]:
|
|
@@ -88,7 +97,10 @@ class NativeAppCompiler:
|
|
|
88
97
|
if not self._should_invoke_processors():
|
|
89
98
|
return
|
|
90
99
|
|
|
91
|
-
with
|
|
100
|
+
with (
|
|
101
|
+
cc.phase("Invoking artifact processors"),
|
|
102
|
+
get_cli_context().metrics.span("artifact_processors"),
|
|
103
|
+
):
|
|
92
104
|
if self._bundle_ctx.generated_root.exists():
|
|
93
105
|
raise ClickException(
|
|
94
106
|
f"Path {self._bundle_ctx.generated_root} already exists. Please choose a different name for your generated directory in the project definition file."
|
|
@@ -125,8 +137,8 @@ class NativeAppCompiler:
|
|
|
125
137
|
if current_processor is not None:
|
|
126
138
|
return current_processor
|
|
127
139
|
|
|
128
|
-
|
|
129
|
-
if
|
|
140
|
+
processor_cls = self._processor_classes_by_name.get(processor_name)
|
|
141
|
+
if processor_cls is None:
|
|
130
142
|
# No registered processor with the specified name
|
|
131
143
|
return None
|
|
132
144
|
|
|
@@ -138,7 +150,7 @@ class NativeAppCompiler:
|
|
|
138
150
|
processor_ctx.generated_root = (
|
|
139
151
|
self._bundle_ctx.generated_root / processor_subdirectory
|
|
140
152
|
)
|
|
141
|
-
current_processor =
|
|
153
|
+
current_processor = processor_cls(processor_ctx)
|
|
142
154
|
self.cached_processors[processor_name] = current_processor
|
|
143
155
|
|
|
144
156
|
return current_processor
|
|
@@ -151,6 +163,18 @@ class NativeAppCompiler:
|
|
|
151
163
|
return False
|
|
152
164
|
|
|
153
165
|
def _is_enabled(self, processor: ProcessorMapping) -> bool:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
166
|
+
"""
|
|
167
|
+
Determines is a process is enabled. All processors are considered enabled
|
|
168
|
+
unless they are explicitly disabled, typically via a feature flag.
|
|
169
|
+
"""
|
|
170
|
+
processor_name = processor.name.lower()
|
|
171
|
+
processor_cls = self._processor_classes_by_name.get(processor_name)
|
|
172
|
+
if processor_cls is None:
|
|
173
|
+
# Unknown processor, consider it enabled, even though trying to
|
|
174
|
+
# invoke it later will raise an exception
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
# if the processor class defines a static method named "is_enabled", then
|
|
178
|
+
# call it. Otherwise, it's considered enabled by default.
|
|
179
|
+
is_enabled_fn = getattr(processor_cls, "is_enabled", lambda: True)
|
|
180
|
+
return is_enabled_fn()
|
|
@@ -36,6 +36,7 @@ from snowflake.cli._plugins.nativeapp.codegen.sandbox import (
|
|
|
36
36
|
SandboxEnvBuilder,
|
|
37
37
|
execute_script_in_sandbox,
|
|
38
38
|
)
|
|
39
|
+
from snowflake.cli._plugins.nativeapp.feature_flags import FeatureFlag
|
|
39
40
|
from snowflake.cli._plugins.stage.diff import to_stage_path
|
|
40
41
|
from snowflake.cli.api.console import cli_console as cc
|
|
41
42
|
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import (
|
|
@@ -74,9 +75,15 @@ def safe_set(d: dict, *keys: str, **kwargs) -> None:
|
|
|
74
75
|
|
|
75
76
|
|
|
76
77
|
class NativeAppSetupProcessor(ArtifactProcessor):
|
|
78
|
+
NAME = "native app setup"
|
|
79
|
+
|
|
77
80
|
def __init__(self, *args, **kwargs):
|
|
78
81
|
super().__init__(*args, **kwargs)
|
|
79
82
|
|
|
83
|
+
@staticmethod
|
|
84
|
+
def is_enabled() -> bool:
|
|
85
|
+
return FeatureFlag.ENABLE_NATIVE_APP_PYTHON_SETUP.is_enabled()
|
|
86
|
+
|
|
80
87
|
def process(
|
|
81
88
|
self,
|
|
82
89
|
artifact_to_process: PathMapping,
|
|
@@ -48,7 +48,7 @@ from snowflake.cli._plugins.nativeapp.codegen.snowpark.models import (
|
|
|
48
48
|
NativeAppExtensionFunction,
|
|
49
49
|
)
|
|
50
50
|
from snowflake.cli._plugins.stage.diff import to_stage_path
|
|
51
|
-
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
51
|
+
from snowflake.cli.api.cli_global_context import get_cli_context, span
|
|
52
52
|
from snowflake.cli.api.console import cli_console as cc
|
|
53
53
|
from snowflake.cli.api.metrics import CLICounterField
|
|
54
54
|
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import (
|
|
@@ -164,9 +164,12 @@ class SnowparkAnnotationProcessor(ArtifactProcessor):
|
|
|
164
164
|
and generate SQL code for creation of extension functions based on those discovered objects.
|
|
165
165
|
"""
|
|
166
166
|
|
|
167
|
+
NAME = "snowpark"
|
|
168
|
+
|
|
167
169
|
def __init__(self, *args, **kwargs):
|
|
168
170
|
super().__init__(*args, **kwargs)
|
|
169
171
|
|
|
172
|
+
@span("snowpark_processor")
|
|
170
173
|
def process(
|
|
171
174
|
self,
|
|
172
175
|
artifact_to_process: PathMapping,
|
|
@@ -23,7 +23,7 @@ from snowflake.cli._plugins.nativeapp.codegen.artifact_processor import (
|
|
|
23
23
|
ArtifactProcessor,
|
|
24
24
|
)
|
|
25
25
|
from snowflake.cli._plugins.nativeapp.exceptions import InvalidTemplateInFileError
|
|
26
|
-
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
26
|
+
from snowflake.cli.api.cli_global_context import get_cli_context, span
|
|
27
27
|
from snowflake.cli.api.console import cli_console as cc
|
|
28
28
|
from snowflake.cli.api.metrics import CLICounterField
|
|
29
29
|
from snowflake.cli.api.project.schemas.v1.native_app.path_mapping import (
|
|
@@ -49,6 +49,8 @@ class TemplatesProcessor(ArtifactProcessor):
|
|
|
49
49
|
Processor class to perform template expansion on all relevant artifacts (specified in the project definition file).
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
|
+
NAME = "templates"
|
|
53
|
+
|
|
52
54
|
def expand_templates_in_file(
|
|
53
55
|
self, src: Path, dest: Path, template_context: dict[str, Any] | None = None
|
|
54
56
|
) -> None:
|
|
@@ -58,39 +60,47 @@ class TemplatesProcessor(ArtifactProcessor):
|
|
|
58
60
|
if src.is_dir():
|
|
59
61
|
return
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
):
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
63
|
+
src_file_name = src.relative_to(self._bundle_ctx.project_root)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
with self.edit_file(dest) as file:
|
|
67
|
+
if not has_client_side_templates(file.contents) and not (
|
|
68
|
+
_is_sql_file(dest) and has_sql_templates(file.contents)
|
|
69
|
+
):
|
|
70
|
+
return
|
|
71
|
+
cc.step(f"Expanding templates in {src_file_name}")
|
|
72
|
+
with cc.indented():
|
|
73
|
+
try:
|
|
74
|
+
jinja_env = (
|
|
75
|
+
choose_sql_jinja_env_based_on_template_syntax(
|
|
76
|
+
file.contents, reference_name=src_file_name
|
|
77
|
+
)
|
|
78
|
+
if _is_sql_file(dest)
|
|
79
|
+
else get_client_side_jinja_env()
|
|
80
|
+
)
|
|
81
|
+
expanded_template = jinja_env.from_string(file.contents).render(
|
|
82
|
+
template_context or get_cli_context().template_context
|
|
74
83
|
)
|
|
75
|
-
if _is_sql_file(dest)
|
|
76
|
-
else get_client_side_jinja_env()
|
|
77
|
-
)
|
|
78
|
-
expanded_template = jinja_env.from_string(file.contents).render(
|
|
79
|
-
template_context or get_cli_context().template_context
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
# For now, we are printing the source file path in the error message
|
|
83
|
-
# instead of the destination file path to make it easier for the user
|
|
84
|
-
# to identify the file that has the error, and edit the correct file.
|
|
85
|
-
except jinja2.TemplateSyntaxError as e:
|
|
86
|
-
raise InvalidTemplateInFileError(src_file_name, e, e.lineno) from e
|
|
87
|
-
|
|
88
|
-
except jinja2.UndefinedError as e:
|
|
89
|
-
raise InvalidTemplateInFileError(src_file_name, e) from e
|
|
90
|
-
|
|
91
|
-
if expanded_template != file.contents:
|
|
92
|
-
file.edited_contents = expanded_template
|
|
93
84
|
|
|
85
|
+
# For now, we are printing the source file path in the error message
|
|
86
|
+
# instead of the destination file path to make it easier for the user
|
|
87
|
+
# to identify the file that has the error, and edit the correct file.
|
|
88
|
+
except jinja2.TemplateSyntaxError as e:
|
|
89
|
+
raise InvalidTemplateInFileError(
|
|
90
|
+
src_file_name, e, e.lineno
|
|
91
|
+
) from e
|
|
92
|
+
|
|
93
|
+
except jinja2.UndefinedError as e:
|
|
94
|
+
raise InvalidTemplateInFileError(src_file_name, e) from e
|
|
95
|
+
|
|
96
|
+
if expanded_template != file.contents:
|
|
97
|
+
file.edited_contents = expanded_template
|
|
98
|
+
except UnicodeDecodeError as err:
|
|
99
|
+
cc.warning(
|
|
100
|
+
f"Could not read file {src_file_name}, error: {err.reason}. Skipping this file."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
@span("templates_processor")
|
|
94
104
|
def process(
|
|
95
105
|
self,
|
|
96
106
|
artifact_to_process: PathMapping,
|