snowflake-cli-labs 3.0.0rc1__py3-none-any.whl → 3.0.0rc3__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/cli_app.py +10 -1
- snowflake/cli/_app/commands_registration/builtin_plugins.py +2 -0
- snowflake/cli/_app/secret.py +9 -0
- snowflake/cli/_app/snow_connector.py +110 -51
- snowflake/cli/_app/telemetry.py +8 -4
- snowflake/cli/_app/version_check.py +74 -0
- snowflake/cli/_plugins/git/commands.py +55 -14
- snowflake/cli/_plugins/git/manager.py +53 -7
- snowflake/cli/_plugins/helpers/commands.py +57 -0
- snowflake/cli/{api/commands/typer_pre_execute.py → _plugins/helpers/plugin_spec.py} +14 -10
- snowflake/cli/_plugins/nativeapp/application_entity.py +651 -0
- snowflake/cli/{api/project/schemas/entities → _plugins/nativeapp}/application_entity_model.py +2 -2
- snowflake/cli/_plugins/nativeapp/application_package_entity.py +1107 -0
- snowflake/cli/{api/project/schemas/entities → _plugins/nativeapp}/application_package_entity_model.py +3 -3
- snowflake/cli/_plugins/nativeapp/artifacts.py +10 -9
- snowflake/cli/_plugins/nativeapp/bundle_context.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/models.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +3 -6
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +50 -32
- snowflake/cli/_plugins/nativeapp/commands.py +84 -16
- snowflake/cli/_plugins/nativeapp/exceptions.py +0 -9
- snowflake/cli/_plugins/nativeapp/manager.py +56 -92
- snowflake/cli/_plugins/nativeapp/policy.py +3 -0
- snowflake/cli/_plugins/nativeapp/project_model.py +2 -2
- snowflake/cli/_plugins/nativeapp/run_processor.py +65 -272
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +70 -0
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +11 -154
- snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +150 -40
- snowflake/cli/_plugins/nativeapp/version/commands.py +6 -24
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +35 -235
- snowflake/cli/_plugins/snowpark/commands.py +5 -5
- snowflake/cli/_plugins/snowpark/common.py +4 -4
- snowflake/cli/_plugins/snowpark/models.py +2 -1
- snowflake/cli/{api/entities → _plugins/snowpark}/snowpark_entity.py +2 -2
- snowflake/cli/{api/project/schemas/entities/snowpark_entity.py → _plugins/snowpark/snowpark_entity_model.py} +3 -6
- snowflake/cli/_plugins/snowpark/snowpark_project_paths.py +1 -1
- snowflake/cli/_plugins/stage/manager.py +9 -4
- snowflake/cli/_plugins/streamlit/commands.py +4 -4
- snowflake/cli/_plugins/streamlit/manager.py +17 -4
- snowflake/cli/{api/entities → _plugins/streamlit}/streamlit_entity.py +2 -2
- snowflake/cli/{api/project/schemas/entities → _plugins/streamlit}/streamlit_entity_model.py +5 -12
- snowflake/cli/_plugins/workspace/action_context.py +2 -1
- snowflake/cli/_plugins/workspace/commands.py +127 -48
- snowflake/cli/_plugins/workspace/manager.py +1 -0
- snowflake/cli/_plugins/workspace/plugin_spec.py +1 -1
- snowflake/cli/api/cli_global_context.py +136 -313
- snowflake/cli/api/commands/flags.py +76 -91
- snowflake/cli/api/commands/snow_typer.py +7 -5
- snowflake/cli/api/config.py +1 -1
- snowflake/cli/api/connections.py +214 -0
- snowflake/cli/api/console/abc.py +4 -2
- snowflake/cli/api/entities/common.py +4 -0
- snowflake/cli/api/entities/utils.py +41 -31
- snowflake/cli/api/errno.py +1 -0
- snowflake/cli/api/identifiers.py +7 -3
- snowflake/cli/api/project/definition.py +11 -0
- snowflake/cli/api/project/definition_conversion.py +175 -16
- snowflake/cli/api/project/schemas/entities/common.py +15 -14
- snowflake/cli/api/project/schemas/entities/entities.py +13 -10
- snowflake/cli/api/project/schemas/project_definition.py +107 -45
- snowflake/cli/api/project/schemas/v1/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{identifier_model.py → v1/identifier_model.py} +0 -7
- snowflake/cli/api/project/schemas/v1/native_app/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{native_app → v1/native_app}/native_app.py +4 -4
- snowflake/cli/api/project/schemas/v1/snowpark/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/callable.py +2 -2
- snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/snowpark.py +2 -2
- snowflake/cli/api/project/schemas/v1/streamlit/__init__.py +0 -0
- snowflake/cli/api/project/schemas/{streamlit → v1/streamlit}/streamlit.py +2 -1
- snowflake/cli/api/rendering/project_definition_templates.py +4 -0
- snowflake/cli/api/rendering/sql_templates.py +7 -0
- snowflake/cli/api/sql_execution.py +6 -15
- snowflake/cli/api/utils/definition_rendering.py +3 -1
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc3.dist-info}/METADATA +9 -9
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc3.dist-info}/RECORD +88 -81
- snowflake/cli/api/entities/application_entity.py +0 -12
- snowflake/cli/api/entities/application_package_entity.py +0 -553
- snowflake/cli/api/project/schemas/snowpark/__init__.py +0 -13
- snowflake/cli/api/project/schemas/streamlit/__init__.py +0 -13
- /snowflake/cli/{api/project/schemas/native_app → _plugins/helpers}/__init__.py +0 -0
- /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/application.py +0 -0
- /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/package.py +0 -0
- /snowflake/cli/api/project/schemas/{native_app → v1/native_app}/path_mapping.py +0 -0
- /snowflake/cli/api/project/schemas/{snowpark → v1/snowpark}/argument.py +0 -0
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc3.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc3.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc3.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/__about__.py
CHANGED
snowflake/cli/_app/cli_app.py
CHANGED
|
@@ -38,6 +38,10 @@ from snowflake.cli._app.dev.pycharm_remote_debug import (
|
|
|
38
38
|
)
|
|
39
39
|
from snowflake.cli._app.main_typer import SnowCliMainTyper
|
|
40
40
|
from snowflake.cli._app.printing import MessageResult, print_result
|
|
41
|
+
from snowflake.cli._app.version_check import (
|
|
42
|
+
get_new_version_msg,
|
|
43
|
+
show_new_version_banner_callback,
|
|
44
|
+
)
|
|
41
45
|
from snowflake.cli.api import Api, api_provider
|
|
42
46
|
from snowflake.cli.api.config import config_init
|
|
43
47
|
from snowflake.cli.api.output.formats import OutputFormat
|
|
@@ -145,8 +149,13 @@ def _info_callback(value: bool):
|
|
|
145
149
|
|
|
146
150
|
def app_factory() -> SnowCliMainTyper:
|
|
147
151
|
app = SnowCliMainTyper()
|
|
152
|
+
new_version_msg = get_new_version_msg()
|
|
148
153
|
|
|
149
|
-
@app.callback(
|
|
154
|
+
@app.callback(
|
|
155
|
+
invoke_without_command=True,
|
|
156
|
+
epilog=new_version_msg,
|
|
157
|
+
result_callback=show_new_version_banner_callback(new_version_msg),
|
|
158
|
+
)
|
|
150
159
|
def default(
|
|
151
160
|
ctx: typer.Context,
|
|
152
161
|
version: bool = typer.Option(
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
from snowflake.cli._plugins.connection import plugin_spec as connection_plugin_spec
|
|
16
16
|
from snowflake.cli._plugins.cortex import plugin_spec as cortex_plugin_spec
|
|
17
17
|
from snowflake.cli._plugins.git import plugin_spec as git_plugin_spec
|
|
18
|
+
from snowflake.cli._plugins.helpers import plugin_spec as migrate_plugin_spec
|
|
18
19
|
from snowflake.cli._plugins.init import plugin_spec as init_plugin_spec
|
|
19
20
|
from snowflake.cli._plugins.nativeapp import plugin_spec as nativeapp_plugin_spec
|
|
20
21
|
from snowflake.cli._plugins.notebook import plugin_spec as notebook_plugin_spec
|
|
@@ -31,6 +32,7 @@ from snowflake.cli._plugins.workspace import plugin_spec as workspace_plugin_spe
|
|
|
31
32
|
def get_builtin_plugin_name_to_plugin_spec():
|
|
32
33
|
plugin_specs = {
|
|
33
34
|
"connection": connection_plugin_spec,
|
|
35
|
+
"helpers": migrate_plugin_spec,
|
|
34
36
|
"spcs": spcs_plugin_spec,
|
|
35
37
|
"nativeapp": nativeapp_plugin_spec,
|
|
36
38
|
"object": object_plugin_spec,
|
|
@@ -24,12 +24,11 @@ from click.exceptions import ClickException
|
|
|
24
24
|
from snowflake.cli._app.constants import (
|
|
25
25
|
PARAM_APPLICATION_NAME,
|
|
26
26
|
)
|
|
27
|
+
from snowflake.cli._app.secret import SecretType
|
|
27
28
|
from snowflake.cli._app.telemetry import command_info
|
|
28
|
-
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
29
29
|
from snowflake.cli.api.config import (
|
|
30
30
|
get_connection_dict,
|
|
31
|
-
|
|
32
|
-
get_default_connection_name,
|
|
31
|
+
get_env_value,
|
|
33
32
|
)
|
|
34
33
|
from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB
|
|
35
34
|
from snowflake.cli.api.exceptions import (
|
|
@@ -46,6 +45,33 @@ log = logging.getLogger(__name__)
|
|
|
46
45
|
ENCRYPTED_PKCS8_PK_HEADER = b"-----BEGIN ENCRYPTED PRIVATE KEY-----"
|
|
47
46
|
UNENCRYPTED_PKCS8_PK_HEADER = b"-----BEGIN PRIVATE KEY-----"
|
|
48
47
|
|
|
48
|
+
# connection keys that can be set using SNOWFLAKE_* env vars
|
|
49
|
+
SUPPORTED_ENV_OVERRIDES = [
|
|
50
|
+
"account",
|
|
51
|
+
"user",
|
|
52
|
+
"password",
|
|
53
|
+
"authenticator",
|
|
54
|
+
"private_key_file",
|
|
55
|
+
"private_key_path",
|
|
56
|
+
"database",
|
|
57
|
+
"schema",
|
|
58
|
+
"role",
|
|
59
|
+
"warehouse",
|
|
60
|
+
"session_token",
|
|
61
|
+
"master_token",
|
|
62
|
+
"token_file_path",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# mapping of found key -> key to set
|
|
66
|
+
CONNECTION_KEY_ALIASES = {"private_key_path": "private_key_file"}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _resolve_alias(key_or_alias: str):
|
|
70
|
+
"""
|
|
71
|
+
Given the key of an override / env var, what key should it be set as in the connection parameters?
|
|
72
|
+
"""
|
|
73
|
+
return CONNECTION_KEY_ALIASES.get(key_or_alias, key_or_alias)
|
|
74
|
+
|
|
49
75
|
|
|
50
76
|
def connect_to_snowflake(
|
|
51
77
|
temporary_connection: bool = False,
|
|
@@ -58,6 +84,10 @@ def connect_to_snowflake(
|
|
|
58
84
|
) -> SnowflakeConnection:
|
|
59
85
|
if temporary_connection and connection_name:
|
|
60
86
|
raise ClickException("Can't use connection name and temporary connection.")
|
|
87
|
+
elif not temporary_connection and not connection_name:
|
|
88
|
+
raise ClickException(
|
|
89
|
+
"One of connection name or temporary connection is required."
|
|
90
|
+
)
|
|
61
91
|
|
|
62
92
|
using_session_token = (
|
|
63
93
|
"session_token" in overrides and overrides["session_token"] is not None
|
|
@@ -70,37 +100,33 @@ def connect_to_snowflake(
|
|
|
70
100
|
)
|
|
71
101
|
|
|
72
102
|
if connection_name:
|
|
73
|
-
connection_parameters =
|
|
74
|
-
|
|
103
|
+
connection_parameters = {
|
|
104
|
+
_resolve_alias(k): v
|
|
105
|
+
for k, v in get_connection_dict(connection_name).items()
|
|
106
|
+
}
|
|
75
107
|
elif temporary_connection:
|
|
76
108
|
connection_parameters = {} # we will apply overrides in next step
|
|
77
|
-
else:
|
|
78
|
-
connection_parameters = get_default_connection_dict()
|
|
79
|
-
get_cli_context().connection_context.set_connection_name(
|
|
80
|
-
get_default_connection_name()
|
|
81
|
-
)
|
|
82
109
|
|
|
83
110
|
# Apply overrides to connection details
|
|
111
|
+
# (1) Command line override case
|
|
84
112
|
for key, value in overrides.items():
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
connection_parameters[key] = value
|
|
88
|
-
continue
|
|
113
|
+
if value is not None:
|
|
114
|
+
connection_parameters[_resolve_alias(key)] = value
|
|
89
115
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
# (2) Generic environment variable case
|
|
117
|
+
# ... apply only if value not passed via flag or connection variable
|
|
118
|
+
for key in SUPPORTED_ENV_OVERRIDES:
|
|
119
|
+
generic_env_value = get_env_value(key=key)
|
|
120
|
+
connection_key = _resolve_alias(key)
|
|
121
|
+
if connection_key not in connection_parameters and generic_env_value:
|
|
122
|
+
connection_parameters[connection_key] = generic_env_value
|
|
95
123
|
|
|
96
124
|
# Clean up connection params
|
|
97
125
|
connection_parameters = {
|
|
98
126
|
k: v for k, v in connection_parameters.items() if v is not None
|
|
99
127
|
}
|
|
100
128
|
|
|
101
|
-
|
|
102
|
-
connection_parameters
|
|
103
|
-
)
|
|
129
|
+
update_connection_details_with_private_key(connection_parameters)
|
|
104
130
|
|
|
105
131
|
if mfa_passcode:
|
|
106
132
|
connection_parameters["passcode"] = mfa_passcode
|
|
@@ -169,13 +195,34 @@ def update_connection_details_with_private_key(connection_parameters: Dict):
|
|
|
169
195
|
_load_private_key(connection_parameters, "private_key_file")
|
|
170
196
|
elif "private_key_path" in connection_parameters:
|
|
171
197
|
_load_private_key(connection_parameters, "private_key_path")
|
|
198
|
+
elif "private_key_raw" in connection_parameters:
|
|
199
|
+
_load_private_key_from_parameters(connection_parameters, "private_key_raw")
|
|
172
200
|
return connection_parameters
|
|
173
201
|
|
|
174
202
|
|
|
175
203
|
def _load_private_key(connection_parameters: Dict, private_key_var_name: str) -> None:
|
|
176
204
|
if connection_parameters.get("authenticator") == "SNOWFLAKE_JWT":
|
|
177
|
-
|
|
178
|
-
|
|
205
|
+
private_key_pem = _load_pem_from_file(
|
|
206
|
+
connection_parameters[private_key_var_name]
|
|
207
|
+
)
|
|
208
|
+
private_key = _load_pem_to_der(private_key_pem)
|
|
209
|
+
connection_parameters["private_key"] = private_key.value
|
|
210
|
+
del connection_parameters[private_key_var_name]
|
|
211
|
+
else:
|
|
212
|
+
raise ClickException(
|
|
213
|
+
"Private Key authentication requires authenticator set to SNOWFLAKE_JWT"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _load_private_key_from_parameters(
|
|
218
|
+
connection_parameters: Dict, private_key_var_name: str
|
|
219
|
+
) -> None:
|
|
220
|
+
if connection_parameters.get("authenticator") == "SNOWFLAKE_JWT":
|
|
221
|
+
private_key_pem = _load_pem_from_parameters(
|
|
222
|
+
connection_parameters[private_key_var_name]
|
|
223
|
+
)
|
|
224
|
+
private_key = _load_pem_to_der(private_key_pem)
|
|
225
|
+
connection_parameters["private_key"] = private_key.value
|
|
179
226
|
del connection_parameters[private_key_var_name]
|
|
180
227
|
else:
|
|
181
228
|
raise ClickException(
|
|
@@ -191,41 +238,49 @@ def _update_connection_application_name(connection_parameters: Dict):
|
|
|
191
238
|
connection_parameters.update(connection_application_params)
|
|
192
239
|
|
|
193
240
|
|
|
194
|
-
def
|
|
195
|
-
"""
|
|
196
|
-
Given a private key file path (in PEM format), decode key data into DER
|
|
197
|
-
format
|
|
198
|
-
"""
|
|
199
|
-
|
|
241
|
+
def _load_pem_from_file(private_key_file: str) -> SecretType:
|
|
200
242
|
with SecurePath(private_key_file).open(
|
|
201
243
|
"rb", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB
|
|
202
244
|
) as f:
|
|
203
|
-
private_key_pem = f.read()
|
|
245
|
+
private_key_pem = SecretType(f.read())
|
|
246
|
+
return private_key_pem
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _load_pem_from_parameters(private_key_raw: str) -> SecretType:
|
|
250
|
+
return SecretType(private_key_raw.encode("utf-8"))
|
|
204
251
|
|
|
205
|
-
|
|
252
|
+
|
|
253
|
+
def _load_pem_to_der(private_key_pem: SecretType) -> SecretType:
|
|
254
|
+
"""
|
|
255
|
+
Given a private key file path (in PEM format), decode key data into DER
|
|
256
|
+
format
|
|
257
|
+
"""
|
|
258
|
+
private_key_passphrase = SecretType(os.getenv("PRIVATE_KEY_PASSPHRASE", None))
|
|
206
259
|
if (
|
|
207
|
-
private_key_pem.startswith(ENCRYPTED_PKCS8_PK_HEADER)
|
|
208
|
-
and private_key_passphrase is None
|
|
260
|
+
private_key_pem.value.startswith(ENCRYPTED_PKCS8_PK_HEADER)
|
|
261
|
+
and private_key_passphrase.value is None
|
|
209
262
|
):
|
|
210
263
|
raise ClickException(
|
|
211
264
|
"Encrypted private key, you must provide the"
|
|
212
265
|
"passphrase in the environment variable PRIVATE_KEY_PASSPHRASE"
|
|
213
266
|
)
|
|
214
267
|
|
|
215
|
-
if not private_key_pem.startswith(
|
|
268
|
+
if not private_key_pem.value.startswith(
|
|
216
269
|
ENCRYPTED_PKCS8_PK_HEADER
|
|
217
|
-
) and not private_key_pem.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
|
|
270
|
+
) and not private_key_pem.value.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
|
|
218
271
|
raise ClickException(
|
|
219
272
|
"Private key provided is not in PKCS#8 format. Please use correct format."
|
|
220
273
|
)
|
|
221
274
|
|
|
222
|
-
if private_key_pem.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
|
|
223
|
-
private_key_passphrase = None
|
|
275
|
+
if private_key_pem.value.startswith(UNENCRYPTED_PKCS8_PK_HEADER):
|
|
276
|
+
private_key_passphrase = SecretType(None)
|
|
224
277
|
|
|
225
278
|
return prepare_private_key(private_key_pem, private_key_passphrase)
|
|
226
279
|
|
|
227
280
|
|
|
228
|
-
def prepare_private_key(
|
|
281
|
+
def prepare_private_key(
|
|
282
|
+
private_key_pem: SecretType, private_key_passphrase: SecretType = SecretType(None)
|
|
283
|
+
):
|
|
229
284
|
from cryptography.hazmat.backends import default_backend
|
|
230
285
|
from cryptography.hazmat.primitives.serialization import (
|
|
231
286
|
Encoding,
|
|
@@ -234,17 +289,21 @@ def prepare_private_key(private_key_pem, private_key_passphrase=None):
|
|
|
234
289
|
load_pem_private_key,
|
|
235
290
|
)
|
|
236
291
|
|
|
237
|
-
private_key =
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
292
|
+
private_key = SecretType(
|
|
293
|
+
load_pem_private_key(
|
|
294
|
+
private_key_pem.value,
|
|
295
|
+
(
|
|
296
|
+
str.encode(private_key_passphrase.value)
|
|
297
|
+
if private_key_passphrase.value is not None
|
|
298
|
+
else private_key_passphrase.value
|
|
299
|
+
),
|
|
300
|
+
default_backend(),
|
|
301
|
+
)
|
|
245
302
|
)
|
|
246
|
-
return
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
303
|
+
return SecretType(
|
|
304
|
+
private_key.value.private_bytes(
|
|
305
|
+
encoding=Encoding.DER,
|
|
306
|
+
format=PrivateFormat.PKCS8,
|
|
307
|
+
encryption_algorithm=NoEncryption(),
|
|
308
|
+
)
|
|
250
309
|
)
|
snowflake/cli/_app/telemetry.py
CHANGED
|
@@ -22,7 +22,10 @@ from typing import Any, Dict, Union
|
|
|
22
22
|
import click
|
|
23
23
|
from snowflake.cli.__about__ import VERSION
|
|
24
24
|
from snowflake.cli._app.constants import PARAM_APPLICATION_NAME
|
|
25
|
-
from snowflake.cli.api.cli_global_context import
|
|
25
|
+
from snowflake.cli.api.cli_global_context import (
|
|
26
|
+
_CliGlobalContextAccess,
|
|
27
|
+
get_cli_context,
|
|
28
|
+
)
|
|
26
29
|
from snowflake.cli.api.commands.execution_metadata import ExecutionMetadata
|
|
27
30
|
from snowflake.cli.api.config import get_feature_flags_section
|
|
28
31
|
from snowflake.cli.api.output.formats import OutputFormat
|
|
@@ -106,8 +109,9 @@ def python_version() -> str:
|
|
|
106
109
|
|
|
107
110
|
|
|
108
111
|
class CLITelemetryClient:
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
@property
|
|
113
|
+
def _ctx(self) -> _CliGlobalContextAccess:
|
|
114
|
+
return get_cli_context()
|
|
111
115
|
|
|
112
116
|
@staticmethod
|
|
113
117
|
def generate_telemetry_data_dict(
|
|
@@ -143,7 +147,7 @@ class CLITelemetryClient:
|
|
|
143
147
|
self._telemetry.send_batch()
|
|
144
148
|
|
|
145
149
|
|
|
146
|
-
_telemetry = CLITelemetryClient(
|
|
150
|
+
_telemetry = CLITelemetryClient()
|
|
147
151
|
|
|
148
152
|
|
|
149
153
|
@ignore_exceptions()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
from packaging.version import Version
|
|
6
|
+
from snowflake.cli.__about__ import VERSION
|
|
7
|
+
from snowflake.cli.api.console import cli_console
|
|
8
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
9
|
+
from snowflake.connector.config_manager import CONFIG_MANAGER
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_new_version_msg() -> str | None:
|
|
13
|
+
last = _VersionCache().get_last_version()
|
|
14
|
+
current = Version(VERSION)
|
|
15
|
+
if last and last > current:
|
|
16
|
+
return f"\nNew version of Snowflake CLI available. Newest: {last}, current: {VERSION}\n"
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def show_new_version_banner_callback(msg):
|
|
21
|
+
def _callback(*args, **kwargs):
|
|
22
|
+
if msg:
|
|
23
|
+
cli_console.message(msg)
|
|
24
|
+
|
|
25
|
+
return _callback
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _VersionCache:
|
|
29
|
+
_last_time = "last_time_check"
|
|
30
|
+
_version = "version"
|
|
31
|
+
_version_cache_file = SecurePath(
|
|
32
|
+
CONFIG_MANAGER.file_path.parent / ".cli_version.cache"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self._cache_file = _VersionCache._version_cache_file
|
|
37
|
+
|
|
38
|
+
def _save_latest_version(self, version: str):
|
|
39
|
+
data = {
|
|
40
|
+
_VersionCache._last_time: time.time(),
|
|
41
|
+
_VersionCache._version: str(version),
|
|
42
|
+
}
|
|
43
|
+
self._cache_file.write_text(json.dumps(data))
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _get_version_from_pypi() -> str | None:
|
|
47
|
+
headers = {"Content-Type": "application/vnd.pypi.simple.v1+json"}
|
|
48
|
+
response = requests.get(
|
|
49
|
+
"https://pypi.org/pypi/snowflake-cli-labs/json", headers=headers, timeout=3
|
|
50
|
+
)
|
|
51
|
+
response.raise_for_status()
|
|
52
|
+
return response.json()["info"]["version"]
|
|
53
|
+
|
|
54
|
+
def _update_latest_version(self) -> Version | None:
|
|
55
|
+
version = self._get_version_from_pypi()
|
|
56
|
+
if version is None:
|
|
57
|
+
return None
|
|
58
|
+
self._save_latest_version(version)
|
|
59
|
+
return Version(version)
|
|
60
|
+
|
|
61
|
+
def _read_latest_version(self) -> Version | None:
|
|
62
|
+
if self._cache_file.exists():
|
|
63
|
+
data = json.loads(self._cache_file.read_text())
|
|
64
|
+
now = time.time()
|
|
65
|
+
if data[_VersionCache._last_time] > now - 60 * 60:
|
|
66
|
+
return Version(data[_VersionCache._version])
|
|
67
|
+
|
|
68
|
+
return self._update_latest_version()
|
|
69
|
+
|
|
70
|
+
def get_last_version(self) -> Version | None:
|
|
71
|
+
try:
|
|
72
|
+
return self._read_latest_version()
|
|
73
|
+
except: # anything, this it not crucial feature
|
|
74
|
+
return None
|
|
@@ -18,7 +18,7 @@ import itertools
|
|
|
18
18
|
import logging
|
|
19
19
|
from os import path
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
from typing import List, Optional
|
|
21
|
+
from typing import Dict, List, Optional
|
|
22
22
|
|
|
23
23
|
import typer
|
|
24
24
|
from click import ClickException
|
|
@@ -41,6 +41,7 @@ from snowflake.cli.api.console.console import cli_console
|
|
|
41
41
|
from snowflake.cli.api.constants import ObjectType
|
|
42
42
|
from snowflake.cli.api.output.types import CollectionResult, CommandResult, QueryResult
|
|
43
43
|
from snowflake.cli.api.utils.path_utils import is_stage_path
|
|
44
|
+
from snowflake.connector import DictCursor
|
|
44
45
|
|
|
45
46
|
app = SnowTyperFactory(
|
|
46
47
|
name="git",
|
|
@@ -98,6 +99,24 @@ def _validate_origin_url(url: str) -> None:
|
|
|
98
99
|
raise ClickException("Url address should start with 'https'")
|
|
99
100
|
|
|
100
101
|
|
|
102
|
+
def _unique_new_object_name(
|
|
103
|
+
om: ObjectManager, object_type: ObjectType, proposed_fqn: FQN
|
|
104
|
+
) -> str:
|
|
105
|
+
existing_objects: List[Dict] = om.show(
|
|
106
|
+
object_type=object_type.value.cli_name,
|
|
107
|
+
like=f"{proposed_fqn.name}%",
|
|
108
|
+
cursor_class=DictCursor,
|
|
109
|
+
).fetchall()
|
|
110
|
+
existing_names = set(o["name"].upper() for o in existing_objects)
|
|
111
|
+
|
|
112
|
+
result = proposed_fqn.name
|
|
113
|
+
i = 1
|
|
114
|
+
while result.upper() in existing_names:
|
|
115
|
+
result = proposed_fqn.name + str(i)
|
|
116
|
+
i += 1
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
|
|
101
120
|
@app.command("setup", requires_connection=True)
|
|
102
121
|
def setup(
|
|
103
122
|
repository_name: FQN = RepoNameArgument,
|
|
@@ -128,13 +147,29 @@ def setup(
|
|
|
128
147
|
should_create_secret = False
|
|
129
148
|
secret_name = None
|
|
130
149
|
if secret_needed:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
150
|
+
default_secret_name = (
|
|
151
|
+
FQN.from_string(f"{repository_name.name}_secret")
|
|
152
|
+
.set_schema(repository_name.schema)
|
|
153
|
+
.set_database(repository_name.database)
|
|
154
|
+
)
|
|
155
|
+
default_secret_name.set_name(
|
|
156
|
+
_unique_new_object_name(
|
|
157
|
+
om, object_type=ObjectType.SECRET, proposed_fqn=default_secret_name
|
|
158
|
+
),
|
|
134
159
|
)
|
|
135
|
-
|
|
160
|
+
secret_name = FQN.from_string(
|
|
161
|
+
typer.prompt(
|
|
162
|
+
"Secret identifier (will be created if not exists)",
|
|
163
|
+
default=default_secret_name.name,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
if not secret_name.database:
|
|
167
|
+
secret_name.set_database(repository_name.database)
|
|
168
|
+
if not secret_name.schema:
|
|
169
|
+
secret_name.set_schema(repository_name.schema)
|
|
170
|
+
|
|
136
171
|
if om.object_exists(
|
|
137
|
-
object_type=ObjectType.SECRET.value.cli_name, fqn=
|
|
172
|
+
object_type=ObjectType.SECRET.value.cli_name, fqn=secret_name
|
|
138
173
|
):
|
|
139
174
|
cli_console.step(f"Using existing secret '{secret_name}'")
|
|
140
175
|
else:
|
|
@@ -143,24 +178,30 @@ def setup(
|
|
|
143
178
|
secret_username = typer.prompt("username")
|
|
144
179
|
secret_password = typer.prompt("password/token", hide_input=True)
|
|
145
180
|
|
|
146
|
-
|
|
147
|
-
api_integration =
|
|
148
|
-
|
|
149
|
-
|
|
181
|
+
# API integration is an account-level object
|
|
182
|
+
api_integration = FQN.from_string(f"{repository_name.name}_api_integration")
|
|
183
|
+
api_integration.set_name(
|
|
184
|
+
typer.prompt(
|
|
185
|
+
"API integration identifier (will be created if not exists)",
|
|
186
|
+
default=_unique_new_object_name(
|
|
187
|
+
om,
|
|
188
|
+
object_type=ObjectType.INTEGRATION,
|
|
189
|
+
proposed_fqn=api_integration,
|
|
190
|
+
),
|
|
191
|
+
)
|
|
150
192
|
)
|
|
151
|
-
api_integration_fqn = FQN.from_string(api_integration)
|
|
152
193
|
|
|
153
194
|
if should_create_secret:
|
|
154
195
|
manager.create_password_secret(
|
|
155
|
-
name=
|
|
196
|
+
name=secret_name, username=secret_username, password=secret_password
|
|
156
197
|
)
|
|
157
198
|
cli_console.step(f"Secret '{secret_name}' successfully created.")
|
|
158
199
|
|
|
159
200
|
if not om.object_exists(
|
|
160
|
-
object_type=ObjectType.INTEGRATION.value.cli_name, fqn=
|
|
201
|
+
object_type=ObjectType.INTEGRATION.value.cli_name, fqn=api_integration
|
|
161
202
|
):
|
|
162
203
|
manager.create_api_integration(
|
|
163
|
-
name=
|
|
204
|
+
name=api_integration,
|
|
164
205
|
api_provider="git_https_api",
|
|
165
206
|
allowed_prefix=url,
|
|
166
207
|
secret=secret_name,
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
from pathlib import Path
|
|
17
|
+
from pathlib import Path, PurePosixPath
|
|
18
18
|
from textwrap import dedent
|
|
19
19
|
from typing import List
|
|
20
20
|
|
|
21
|
+
from click import UsageError
|
|
21
22
|
from snowflake.cli._plugins.stage.manager import (
|
|
22
23
|
USER_STAGE_PREFIX,
|
|
23
24
|
StageManager,
|
|
@@ -27,16 +28,22 @@ from snowflake.cli._plugins.stage.manager import (
|
|
|
27
28
|
from snowflake.cli.api.identifiers import FQN
|
|
28
29
|
from snowflake.connector.cursor import SnowflakeCursor
|
|
29
30
|
|
|
31
|
+
# Replace magic numbers with constants
|
|
32
|
+
OMIT_FIRST = slice(1, None)
|
|
33
|
+
OMIT_STAGE = slice(3, None)
|
|
34
|
+
OMIT_STAGE_IN_NEW_LIST_FILES = slice(2, None)
|
|
35
|
+
ONLY_STAGE = slice(3)
|
|
36
|
+
|
|
30
37
|
|
|
31
38
|
class GitStagePathParts(StagePathParts):
|
|
32
39
|
def __init__(self, stage_path: str):
|
|
33
40
|
self.stage = GitManager.get_stage_from_path(stage_path)
|
|
34
|
-
stage_path_parts =
|
|
41
|
+
stage_path_parts = GitManager.split_git_path(stage_path)
|
|
35
42
|
git_repo_name = stage_path_parts[0].split(".")[-1]
|
|
36
43
|
if git_repo_name.startswith("@"):
|
|
37
|
-
git_repo_name = git_repo_name[
|
|
44
|
+
git_repo_name = git_repo_name[OMIT_FIRST]
|
|
38
45
|
self.stage_name = "/".join([git_repo_name, *stage_path_parts[1:3], ""])
|
|
39
|
-
self.directory = "/".join(stage_path_parts[
|
|
46
|
+
self.directory = "/".join(stage_path_parts[OMIT_STAGE])
|
|
40
47
|
self.is_directory = True if stage_path.endswith("/") else False
|
|
41
48
|
|
|
42
49
|
@property
|
|
@@ -45,7 +52,12 @@ class GitStagePathParts(StagePathParts):
|
|
|
45
52
|
|
|
46
53
|
@classmethod
|
|
47
54
|
def get_directory(cls, stage_path: str) -> str:
|
|
48
|
-
|
|
55
|
+
git_path_parts = GitManager.split_git_path(stage_path)
|
|
56
|
+
# New file list does not have a stage name at the beginning
|
|
57
|
+
if stage_path.startswith("/"):
|
|
58
|
+
return "/".join(git_path_parts[OMIT_STAGE_IN_NEW_LIST_FILES])
|
|
59
|
+
else:
|
|
60
|
+
return "/".join(git_path_parts[OMIT_STAGE])
|
|
49
61
|
|
|
50
62
|
@property
|
|
51
63
|
def full_path(self) -> str:
|
|
@@ -53,7 +65,7 @@ class GitStagePathParts(StagePathParts):
|
|
|
53
65
|
|
|
54
66
|
def replace_stage_prefix(self, file_path: str) -> str:
|
|
55
67
|
stage = Path(self.stage).parts[0]
|
|
56
|
-
file_path_without_prefix = Path(file_path).parts[
|
|
68
|
+
file_path_without_prefix = Path(file_path).parts[OMIT_FIRST]
|
|
57
69
|
return f"{stage}/{'/'.join(file_path_without_prefix)}"
|
|
58
70
|
|
|
59
71
|
def add_stage_prefix(self, file_path: str) -> str:
|
|
@@ -95,7 +107,8 @@ class GitManager(StageManager):
|
|
|
95
107
|
Returns stage name from potential path on stage. For example
|
|
96
108
|
repo/branches/main/foo/bar -> repo/branches/main/
|
|
97
109
|
"""
|
|
98
|
-
|
|
110
|
+
path_parts = GitManager.split_git_path(path)
|
|
111
|
+
return f"{'/'.join(path_parts[ONLY_STAGE])}/"
|
|
99
112
|
|
|
100
113
|
@staticmethod
|
|
101
114
|
def _stage_path_part_factory(stage_path: str) -> StagePathParts:
|
|
@@ -103,3 +116,36 @@ class GitManager(StageManager):
|
|
|
103
116
|
if stage_path.startswith(USER_STAGE_PREFIX):
|
|
104
117
|
return UserStagePathParts(stage_path)
|
|
105
118
|
return GitStagePathParts(stage_path)
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def split_git_path(path: str):
|
|
122
|
+
# Check if path contains quotes and split it accordingly
|
|
123
|
+
if '/"' in path and '"/' in path:
|
|
124
|
+
if path.count('"') > 2:
|
|
125
|
+
raise UsageError(
|
|
126
|
+
f'Invalid string {path}, too much " in path, expected 2.'
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
path_parts = path.split('"')
|
|
130
|
+
before_quoted_part = GitManager._split_path_without_empty_parts(
|
|
131
|
+
path_parts[0]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if path_parts[2] == "/":
|
|
135
|
+
after_quoted_part = []
|
|
136
|
+
else:
|
|
137
|
+
after_quoted_part = GitManager._split_path_without_empty_parts(
|
|
138
|
+
path_parts[2]
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return [
|
|
142
|
+
*before_quoted_part,
|
|
143
|
+
f'"{path_parts[1]}"',
|
|
144
|
+
*after_quoted_part,
|
|
145
|
+
]
|
|
146
|
+
else:
|
|
147
|
+
return GitManager._split_path_without_empty_parts(path)
|
|
148
|
+
|
|
149
|
+
@staticmethod
|
|
150
|
+
def _split_path_without_empty_parts(path: str):
|
|
151
|
+
return [e for e in PurePosixPath(path).parts if e != "/"]
|