snowflake-cli 3.1.0__py3-none-any.whl → 3.2.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/dev/docs/templates/usage.rst.jinja2 +1 -1
- snowflake/cli/_plugins/connection/commands.py +124 -109
- snowflake/cli/_plugins/connection/util.py +54 -9
- snowflake/cli/_plugins/cortex/manager.py +1 -1
- snowflake/cli/_plugins/git/manager.py +4 -4
- snowflake/cli/_plugins/nativeapp/artifacts.py +64 -10
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
- snowflake/cli/_plugins/nativeapp/commands.py +10 -3
- snowflake/cli/_plugins/nativeapp/constants.py +1 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +501 -440
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +563 -885
- snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
- snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/compat.py +1 -89
- snowflake/cli/_plugins/nativeapp/version/commands.py +6 -3
- snowflake/cli/_plugins/notebook/manager.py +2 -2
- snowflake/cli/_plugins/object/commands.py +10 -1
- snowflake/cli/_plugins/object/manager.py +13 -5
- snowflake/cli/_plugins/snowpark/common.py +3 -3
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -1
- snowflake/cli/_plugins/spcs/common.py +29 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
- snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
- snowflake/cli/_plugins/spcs/image_repository/manager.py +1 -1
- snowflake/cli/_plugins/spcs/services/commands.py +64 -13
- snowflake/cli/_plugins/spcs/services/manager.py +75 -15
- snowflake/cli/_plugins/sql/commands.py +9 -1
- snowflake/cli/_plugins/sql/manager.py +9 -4
- snowflake/cli/_plugins/stage/commands.py +20 -16
- snowflake/cli/_plugins/stage/diff.py +1 -1
- snowflake/cli/_plugins/stage/manager.py +140 -11
- snowflake/cli/_plugins/streamlit/manager.py +5 -5
- snowflake/cli/_plugins/workspace/commands.py +6 -3
- snowflake/cli/api/cli_global_context.py +1 -0
- snowflake/cli/api/config.py +23 -5
- snowflake/cli/api/console/console.py +4 -19
- snowflake/cli/api/entities/utils.py +19 -32
- snowflake/cli/api/errno.py +2 -0
- snowflake/cli/api/exceptions.py +9 -0
- snowflake/cli/api/metrics.py +223 -7
- snowflake/cli/api/output/types.py +1 -1
- snowflake/cli/api/project/definition_conversion.py +179 -62
- snowflake/cli/api/rest_api.py +26 -4
- snowflake/cli/api/secure_utils.py +1 -1
- snowflake/cli/api/sql_execution.py +35 -22
- snowflake/cli/api/stage_path.py +5 -2
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +7 -8
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +56 -55
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
- snowflake/cli/_plugins/nativeapp/manager.py +0 -392
- snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
- snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -56
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.1.0.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/__about__.py
CHANGED
|
@@ -48,7 +48,7 @@ Options
|
|
|
48
48
|
{%- if param.type.name != "choice" %}{{ ' {' }}{% else %} {% endif %}{{ param.make_metavar() }}{% if param.type.name != "choice" %}{{ '}' }}
|
|
49
49
|
{%- endif %}
|
|
50
50
|
{%- endif %}`
|
|
51
|
-
{% if param.help %}{{ " " + param.help | replace("\n", " ") }}{% if param.help[-1] != '.' %}.{% endif %}{% if param.default is not none %} Default: {{ param.default }}.{% endif %}{% else %} TBD{% endif %}
|
|
51
|
+
{% if param.help %}{{ " " + param.help | replace("\n", " ") }}{% if param.help[-1] != '.' %}.{% endif %}{% if param.default is not none and param.default != "" %} Default: {{ param.default }}.{% endif %}{% else %} TBD{% endif %}
|
|
52
52
|
{% endfor -%}
|
|
53
53
|
{% else %}
|
|
54
54
|
|
|
@@ -16,20 +16,38 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
18
|
import os.path
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Optional
|
|
19
21
|
|
|
20
22
|
import typer
|
|
21
|
-
from click import
|
|
23
|
+
from click import ( # type: ignore
|
|
24
|
+
ClickException,
|
|
25
|
+
Context,
|
|
26
|
+
Parameter,
|
|
27
|
+
UsageError,
|
|
28
|
+
)
|
|
22
29
|
from click.core import ParameterSource # type: ignore
|
|
23
|
-
from click.types import StringParamType
|
|
24
30
|
from snowflake import connector
|
|
25
31
|
from snowflake.cli._plugins.connection.util import (
|
|
26
|
-
strip_and_check_if_exists,
|
|
27
32
|
strip_if_value_present,
|
|
28
33
|
)
|
|
29
34
|
from snowflake.cli._plugins.object.manager import ObjectManager
|
|
30
35
|
from snowflake.cli.api.cli_global_context import get_cli_context
|
|
31
36
|
from snowflake.cli.api.commands.flags import (
|
|
32
37
|
PLAIN_PASSWORD_MSG,
|
|
38
|
+
AccountOption,
|
|
39
|
+
AuthenticatorOption,
|
|
40
|
+
DatabaseOption,
|
|
41
|
+
HostOption,
|
|
42
|
+
NoInteractiveOption,
|
|
43
|
+
PasswordOption,
|
|
44
|
+
PortOption,
|
|
45
|
+
PrivateKeyPathOption,
|
|
46
|
+
RoleOption,
|
|
47
|
+
SchemaOption,
|
|
48
|
+
TokenFilePathOption,
|
|
49
|
+
UserOption,
|
|
50
|
+
WarehouseOption,
|
|
33
51
|
)
|
|
34
52
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
35
53
|
from snowflake.cli.api.config import (
|
|
@@ -64,11 +82,6 @@ class EmptyInput:
|
|
|
64
82
|
return "optional"
|
|
65
83
|
|
|
66
84
|
|
|
67
|
-
class OptionalPrompt(StringParamType):
|
|
68
|
-
def convert(self, value, param, ctx):
|
|
69
|
-
return None if isinstance(value, EmptyInput) else value
|
|
70
|
-
|
|
71
|
-
|
|
72
85
|
def _mask_password(connection_params: dict):
|
|
73
86
|
if "password" in connection_params:
|
|
74
87
|
connection_params["password"] = "****"
|
|
@@ -101,7 +114,7 @@ def list_connections(**options) -> CommandResult:
|
|
|
101
114
|
|
|
102
115
|
|
|
103
116
|
def require_integer(field_name: str):
|
|
104
|
-
def callback(value: str):
|
|
117
|
+
def callback(ctx: Context, param: Parameter, value: str):
|
|
105
118
|
if value is None:
|
|
106
119
|
return None
|
|
107
120
|
if value.strip().isdigit():
|
|
@@ -124,130 +137,91 @@ def add(
|
|
|
124
137
|
None,
|
|
125
138
|
"--connection-name",
|
|
126
139
|
"-n",
|
|
127
|
-
prompt="Name for this connection",
|
|
128
140
|
help="Name of the new connection.",
|
|
129
141
|
show_default=False,
|
|
130
|
-
callback=strip_if_value_present,
|
|
131
142
|
),
|
|
132
143
|
account: str = typer.Option(
|
|
133
144
|
None,
|
|
134
|
-
"--account",
|
|
135
145
|
"-a",
|
|
136
|
-
|
|
137
|
-
prompt="Snowflake account name",
|
|
146
|
+
*AccountOption.param_decls,
|
|
138
147
|
help="Account name to use when authenticating with Snowflake.",
|
|
139
148
|
show_default=False,
|
|
140
|
-
callback=strip_if_value_present,
|
|
141
149
|
),
|
|
142
150
|
user: str = typer.Option(
|
|
143
151
|
None,
|
|
144
|
-
"--user",
|
|
145
152
|
"-u",
|
|
146
|
-
|
|
147
|
-
prompt="Snowflake username",
|
|
153
|
+
*UserOption.param_decls,
|
|
148
154
|
show_default=False,
|
|
149
155
|
help="Username to connect to Snowflake.",
|
|
150
|
-
callback=strip_if_value_present,
|
|
151
156
|
),
|
|
152
|
-
password: str = typer.Option(
|
|
153
|
-
|
|
154
|
-
"--password",
|
|
157
|
+
password: Optional[str] = typer.Option(
|
|
158
|
+
None,
|
|
155
159
|
"-p",
|
|
156
|
-
|
|
160
|
+
*PasswordOption.param_decls,
|
|
157
161
|
callback=_password_callback,
|
|
158
|
-
prompt="Snowflake password",
|
|
159
162
|
help="Snowflake password.",
|
|
160
163
|
hide_input=True,
|
|
161
164
|
),
|
|
162
|
-
role: str = typer.Option(
|
|
163
|
-
|
|
164
|
-
"--role",
|
|
165
|
+
role: Optional[str] = typer.Option(
|
|
166
|
+
None,
|
|
165
167
|
"-r",
|
|
166
|
-
|
|
167
|
-
prompt="Role for the connection",
|
|
168
|
+
*RoleOption.param_decls,
|
|
168
169
|
help="Role to use on Snowflake.",
|
|
169
|
-
callback=strip_if_value_present,
|
|
170
170
|
),
|
|
171
|
-
warehouse: str = typer.Option(
|
|
172
|
-
|
|
173
|
-
"--warehouse",
|
|
171
|
+
warehouse: Optional[str] = typer.Option(
|
|
172
|
+
None,
|
|
174
173
|
"-w",
|
|
175
|
-
|
|
176
|
-
prompt="Warehouse for the connection",
|
|
174
|
+
*WarehouseOption.param_decls,
|
|
177
175
|
help="Warehouse to use on Snowflake.",
|
|
178
|
-
callback=strip_if_value_present,
|
|
179
176
|
),
|
|
180
|
-
database: str = typer.Option(
|
|
181
|
-
|
|
182
|
-
"--database",
|
|
177
|
+
database: Optional[str] = typer.Option(
|
|
178
|
+
None,
|
|
183
179
|
"-d",
|
|
184
|
-
|
|
185
|
-
prompt="Database for the connection",
|
|
180
|
+
*DatabaseOption.param_decls,
|
|
186
181
|
help="Database to use on Snowflake.",
|
|
187
|
-
callback=strip_if_value_present,
|
|
188
182
|
),
|
|
189
|
-
schema: str = typer.Option(
|
|
190
|
-
|
|
191
|
-
"--schema",
|
|
183
|
+
schema: Optional[str] = typer.Option(
|
|
184
|
+
None,
|
|
192
185
|
"-s",
|
|
193
|
-
|
|
194
|
-
prompt="Schema for the connection",
|
|
186
|
+
*SchemaOption.param_decls,
|
|
195
187
|
help="Schema to use on Snowflake.",
|
|
196
|
-
callback=strip_if_value_present,
|
|
197
188
|
),
|
|
198
|
-
host: str = typer.Option(
|
|
199
|
-
|
|
200
|
-
"--host",
|
|
189
|
+
host: Optional[str] = typer.Option(
|
|
190
|
+
None,
|
|
201
191
|
"-h",
|
|
202
|
-
|
|
203
|
-
prompt="Connection host",
|
|
192
|
+
*HostOption.param_decls,
|
|
204
193
|
help="Host name the connection attempts to connect to Snowflake.",
|
|
205
|
-
callback=strip_if_value_present,
|
|
206
194
|
),
|
|
207
|
-
port: int = typer.Option(
|
|
208
|
-
|
|
209
|
-
"--port",
|
|
195
|
+
port: Optional[int] = typer.Option(
|
|
196
|
+
None,
|
|
210
197
|
"-P",
|
|
211
|
-
|
|
212
|
-
prompt="Connection port",
|
|
198
|
+
*PortOption.param_decls,
|
|
213
199
|
help="Port to communicate with on the host.",
|
|
214
|
-
callback=require_integer(field_name="port"),
|
|
215
200
|
),
|
|
216
|
-
region: str = typer.Option(
|
|
217
|
-
|
|
201
|
+
region: Optional[str] = typer.Option(
|
|
202
|
+
None,
|
|
218
203
|
"--region",
|
|
219
204
|
"-R",
|
|
220
|
-
click_type=OptionalPrompt(),
|
|
221
|
-
prompt="Snowflake region",
|
|
222
205
|
help="Region name if not the default Snowflake deployment.",
|
|
223
|
-
callback=strip_if_value_present,
|
|
224
206
|
),
|
|
225
|
-
authenticator: str = typer.Option(
|
|
226
|
-
|
|
227
|
-
"--authenticator",
|
|
207
|
+
authenticator: Optional[str] = typer.Option(
|
|
208
|
+
None,
|
|
228
209
|
"-A",
|
|
229
|
-
|
|
230
|
-
prompt="Authentication method",
|
|
210
|
+
*AuthenticatorOption.param_decls,
|
|
231
211
|
help="Chosen authenticator, if other than password-based",
|
|
232
212
|
),
|
|
233
|
-
private_key_file: str = typer.Option(
|
|
234
|
-
|
|
213
|
+
private_key_file: Optional[str] = typer.Option(
|
|
214
|
+
None,
|
|
235
215
|
"--private-key",
|
|
236
|
-
"--private-key-path",
|
|
237
216
|
"-k",
|
|
238
|
-
|
|
239
|
-
prompt="Path to private key file",
|
|
217
|
+
*PrivateKeyPathOption.param_decls,
|
|
240
218
|
help="Path to file containing private key",
|
|
241
|
-
callback=strip_and_check_if_exists,
|
|
242
219
|
),
|
|
243
|
-
token_file_path: str = typer.Option(
|
|
244
|
-
|
|
245
|
-
"--token-file-path",
|
|
220
|
+
token_file_path: Optional[str] = typer.Option(
|
|
221
|
+
None,
|
|
246
222
|
"-t",
|
|
247
|
-
|
|
248
|
-
prompt="Path to token file",
|
|
223
|
+
*TokenFilePathOption.param_decls,
|
|
249
224
|
help="Path to file with an OAuth token that should be used when connecting to Snowflake",
|
|
250
|
-
callback=strip_and_check_if_exists,
|
|
251
225
|
),
|
|
252
226
|
set_as_default: bool = typer.Option(
|
|
253
227
|
False,
|
|
@@ -255,29 +229,62 @@ def add(
|
|
|
255
229
|
is_flag=True,
|
|
256
230
|
help="If provided the connection will be configured as default connection.",
|
|
257
231
|
),
|
|
232
|
+
no_interactive: bool = NoInteractiveOption,
|
|
258
233
|
**options,
|
|
259
234
|
) -> CommandResult:
|
|
260
235
|
"""Adds a connection to configuration file."""
|
|
236
|
+
connection_options = {
|
|
237
|
+
"connection_name": connection_name,
|
|
238
|
+
"account": account,
|
|
239
|
+
"user": user,
|
|
240
|
+
"password": password,
|
|
241
|
+
"role": role,
|
|
242
|
+
"warehouse": warehouse,
|
|
243
|
+
"database": database,
|
|
244
|
+
"schema": schema,
|
|
245
|
+
"host": host,
|
|
246
|
+
"port": port,
|
|
247
|
+
"region": region,
|
|
248
|
+
"authenticator": authenticator,
|
|
249
|
+
"private_key_file": private_key_file,
|
|
250
|
+
"token_file_path": token_file_path,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if not no_interactive:
|
|
254
|
+
for option in connection_options:
|
|
255
|
+
if connection_options[option] is None:
|
|
256
|
+
connection_options[option] = typer.prompt(
|
|
257
|
+
f"Enter {option.replace('_', ' ')}",
|
|
258
|
+
default="",
|
|
259
|
+
value_proc=lambda x: None if not x else x,
|
|
260
|
+
hide_input=option == "password",
|
|
261
|
+
show_default=False,
|
|
262
|
+
)
|
|
263
|
+
if isinstance(connection_options[option], str):
|
|
264
|
+
connection_options[option] = strip_if_value_present(
|
|
265
|
+
connection_options[option]
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if (value := connection_options["port"]) is not None:
|
|
269
|
+
connection_options["port"] = int(value)
|
|
270
|
+
|
|
271
|
+
if (path := connection_options["private_key_file"]) is not None:
|
|
272
|
+
if not Path(str(path)).exists():
|
|
273
|
+
raise UsageError(f"Path {path} does not exist.")
|
|
274
|
+
|
|
275
|
+
if (path := connection_options["token_file_path"]) is not None:
|
|
276
|
+
if not Path(str(path)).exists():
|
|
277
|
+
raise UsageError(f"Path {path} does not exist.")
|
|
278
|
+
|
|
279
|
+
connection_name = str(connection_options["connection_name"])
|
|
280
|
+
del connection_options["connection_name"]
|
|
281
|
+
|
|
261
282
|
if connection_exists(connection_name):
|
|
262
|
-
raise
|
|
283
|
+
raise UsageError(f"Connection {connection_name} already exists")
|
|
263
284
|
|
|
264
285
|
connections_file = add_connection_to_proper_file(
|
|
265
286
|
connection_name,
|
|
266
|
-
ConnectionConfig(
|
|
267
|
-
account=account,
|
|
268
|
-
user=user,
|
|
269
|
-
password=password,
|
|
270
|
-
host=host,
|
|
271
|
-
region=region,
|
|
272
|
-
port=port,
|
|
273
|
-
database=database,
|
|
274
|
-
schema=schema,
|
|
275
|
-
warehouse=warehouse,
|
|
276
|
-
role=role,
|
|
277
|
-
authenticator=authenticator,
|
|
278
|
-
private_key_file=private_key_file,
|
|
279
|
-
token_file_path=token_file_path,
|
|
280
|
-
),
|
|
287
|
+
ConnectionConfig(**connection_options),
|
|
281
288
|
)
|
|
282
289
|
if set_as_default:
|
|
283
290
|
set_config_value(
|
|
@@ -358,7 +365,7 @@ def set_default(
|
|
|
358
365
|
def generate_jwt(
|
|
359
366
|
**options,
|
|
360
367
|
) -> CommandResult:
|
|
361
|
-
"""Generate
|
|
368
|
+
"""Generate a JWT token, which will be printed out and displayed.."""
|
|
362
369
|
connection_details = get_cli_context().connection_context.update_from_config()
|
|
363
370
|
|
|
364
371
|
msq_template = (
|
|
@@ -370,22 +377,30 @@ def generate_jwt(
|
|
|
370
377
|
raise UsageError(msq_template.format("Account"))
|
|
371
378
|
if not connection_details.private_key_file:
|
|
372
379
|
raise UsageError(msq_template.format("Private key file"))
|
|
380
|
+
|
|
373
381
|
passphrase = os.getenv("PRIVATE_KEY_PASSPHRASE", None)
|
|
374
|
-
if not passphrase:
|
|
375
|
-
passphrase = typer.prompt(
|
|
376
|
-
"Enter private key file password (Press enter if none)",
|
|
377
|
-
hide_input=True,
|
|
378
|
-
type=str,
|
|
379
|
-
default="",
|
|
380
|
-
)
|
|
381
382
|
|
|
382
|
-
|
|
383
|
-
|
|
383
|
+
def _decrypt(passphrase: str | None):
|
|
384
|
+
return connector.auth.get_token_from_private_key(
|
|
384
385
|
user=connection_details.user,
|
|
385
386
|
account=connection_details.account,
|
|
386
387
|
privatekey_path=connection_details.private_key_file,
|
|
387
388
|
key_password=passphrase,
|
|
388
389
|
)
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
if passphrase is None:
|
|
393
|
+
try:
|
|
394
|
+
token = _decrypt(passphrase=None)
|
|
395
|
+
return MessageResult(token)
|
|
396
|
+
except TypeError:
|
|
397
|
+
passphrase = typer.prompt(
|
|
398
|
+
"Enter private key file password (press enter for empty)",
|
|
399
|
+
hide_input=True,
|
|
400
|
+
type=str,
|
|
401
|
+
default="",
|
|
402
|
+
)
|
|
403
|
+
token = _decrypt(passphrase=passphrase)
|
|
389
404
|
return MessageResult(token)
|
|
390
|
-
except ValueError as err:
|
|
405
|
+
except (ValueError, TypeError) as err:
|
|
391
406
|
raise ClickException(str(err))
|
|
@@ -17,7 +17,10 @@ from __future__ import annotations
|
|
|
17
17
|
import json
|
|
18
18
|
import logging
|
|
19
19
|
import os
|
|
20
|
-
from
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from functools import lru_cache
|
|
22
|
+
from textwrap import dedent
|
|
23
|
+
from typing import Any, Dict, Optional
|
|
21
24
|
|
|
22
25
|
from click.exceptions import ClickException
|
|
23
26
|
from snowflake.connector import SnowflakeConnection
|
|
@@ -25,12 +28,6 @@ from snowflake.connector.cursor import DictCursor
|
|
|
25
28
|
|
|
26
29
|
log = logging.getLogger(__name__)
|
|
27
30
|
|
|
28
|
-
REGIONLESS_QUERY = """
|
|
29
|
-
select value['value'] as REGIONLESS from table(flatten(
|
|
30
|
-
input => parse_json(SYSTEM$BOOTSTRAP_DATA_REQUEST()),
|
|
31
|
-
path => 'clientParamsInfo'
|
|
32
|
-
)) where value['name'] = 'UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT';
|
|
33
|
-
"""
|
|
34
31
|
|
|
35
32
|
ALLOWLIST_QUERY = "SELECT SYSTEM$ALLOWLIST()"
|
|
36
33
|
SNOWFLAKE_DEPLOYMENT = "SNOWFLAKE_DEPLOYMENT"
|
|
@@ -54,6 +51,50 @@ class MissingConnectionRegionError(ClickException):
|
|
|
54
51
|
)
|
|
55
52
|
|
|
56
53
|
|
|
54
|
+
class UIParameter(Enum):
|
|
55
|
+
NA_ENABLE_REGIONLESS_REDIRECT = "UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT"
|
|
56
|
+
NA_EVENT_SHARING_V2 = "ENABLE_EVENT_SHARING_V2_IN_THE_SAME_ACCOUNT"
|
|
57
|
+
NA_ENFORCE_MANDATORY_FILTERS = (
|
|
58
|
+
"ENFORCE_MANDATORY_FILTERS_FOR_SAME_ACCOUNT_INSTALLATION"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_ui_parameter(
|
|
63
|
+
conn: SnowflakeConnection, parameter: UIParameter, default: Any
|
|
64
|
+
) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Returns the value of a single UI parameter.
|
|
67
|
+
If the parameter is not found, the default value is returned.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
ui_parameters = get_ui_parameters(conn)
|
|
71
|
+
return ui_parameters.get(parameter, default)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@lru_cache()
|
|
75
|
+
def get_ui_parameters(conn: SnowflakeConnection) -> Dict[UIParameter, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Returns the UI parameters from the SYSTEM$BOOTSTRAP_DATA_REQUEST function
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
parameters_to_fetch = sorted([param.value for param in UIParameter])
|
|
81
|
+
|
|
82
|
+
query = dedent(
|
|
83
|
+
f"""
|
|
84
|
+
select value['value']::string as PARAM_VALUE, value['name']::string as PARAM_NAME from table(flatten(
|
|
85
|
+
input => parse_json(SYSTEM$BOOTSTRAP_DATA_REQUEST()),
|
|
86
|
+
path => 'clientParamsInfo'
|
|
87
|
+
)) where value['name'] in ('{"', '".join(parameters_to_fetch)}');
|
|
88
|
+
"""
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
*_, cursor = conn.execute_string(query, cursor_class=DictCursor)
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
UIParameter(row["PARAM_NAME"]): row["PARAM_VALUE"] for row in cursor.fetchall()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
57
98
|
def is_regionless_redirect(conn: SnowflakeConnection) -> bool:
|
|
58
99
|
"""
|
|
59
100
|
Determines if the deployment this connection refers to uses
|
|
@@ -62,8 +103,12 @@ def is_regionless_redirect(conn: SnowflakeConnection) -> bool:
|
|
|
62
103
|
assume it's regionless, as this is true for most production deployments.
|
|
63
104
|
"""
|
|
64
105
|
try:
|
|
65
|
-
|
|
66
|
-
|
|
106
|
+
return (
|
|
107
|
+
get_ui_parameter(
|
|
108
|
+
conn, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, "true"
|
|
109
|
+
).lower()
|
|
110
|
+
== "true"
|
|
111
|
+
)
|
|
67
112
|
except:
|
|
68
113
|
log.warning(
|
|
69
114
|
"Cannot determine regionless redirect; assuming True.", exc_info=True
|
|
@@ -180,7 +180,7 @@ class CortexManager(SqlExecutionMixin):
|
|
|
180
180
|
|
|
181
181
|
def _query_cortex_result_str(self, query: str) -> str:
|
|
182
182
|
try:
|
|
183
|
-
cursor = self.
|
|
183
|
+
cursor = self.execute_query(query, cursor_class=DictCursor)
|
|
184
184
|
if cursor.rowcount is None:
|
|
185
185
|
raise SnowflakeSQLExecutionError(query)
|
|
186
186
|
return str(cursor.fetchone()["CORTEX_RESULT"])
|
|
@@ -84,13 +84,13 @@ class GitManager(StageManager):
|
|
|
84
84
|
return StagePath.from_git_str(stage_path)
|
|
85
85
|
|
|
86
86
|
def show_branches(self, repo_name: str, like: str) -> SnowflakeCursor:
|
|
87
|
-
return self.
|
|
87
|
+
return self.execute_query(f"show git branches like '{like}' in {repo_name}")
|
|
88
88
|
|
|
89
89
|
def show_tags(self, repo_name: str, like: str) -> SnowflakeCursor:
|
|
90
|
-
return self.
|
|
90
|
+
return self.execute_query(f"show git tags like '{like}' in {repo_name}")
|
|
91
91
|
|
|
92
92
|
def fetch(self, fqn: FQN) -> SnowflakeCursor:
|
|
93
|
-
return self.
|
|
93
|
+
return self.execute_query(f"alter git repository {fqn} fetch")
|
|
94
94
|
|
|
95
95
|
def create(
|
|
96
96
|
self, repo_name: FQN, api_integration: str, url: str, secret: str
|
|
@@ -104,7 +104,7 @@ class GitManager(StageManager):
|
|
|
104
104
|
)
|
|
105
105
|
if secret is not None:
|
|
106
106
|
query += f"git_credentials = {secret}\n"
|
|
107
|
-
return self.
|
|
107
|
+
return self.execute_query(query)
|
|
108
108
|
|
|
109
109
|
@staticmethod
|
|
110
110
|
def get_stage_from_path(path: str):
|
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import itertools
|
|
18
18
|
import os
|
|
19
|
+
from collections import namedtuple
|
|
19
20
|
from pathlib import Path
|
|
20
21
|
from textwrap import dedent
|
|
21
22
|
from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
|
@@ -164,7 +165,7 @@ class _ArtifactPathMap:
|
|
|
164
165
|
if src_is_dir:
|
|
165
166
|
# mark all subdirectories of this source as directories so that we can
|
|
166
167
|
# detect accidental clobbering
|
|
167
|
-
for
|
|
168
|
+
for root, _, files in os.walk(absolute_src, followlinks=True):
|
|
168
169
|
canonical_subdir = Path(root).relative_to(absolute_src)
|
|
169
170
|
canonical_dest_subdir = dest / canonical_subdir
|
|
170
171
|
self._update_dest_is_dir(canonical_dest_subdir, is_dir=True)
|
|
@@ -383,7 +384,7 @@ class BundleMap:
|
|
|
383
384
|
if absolute_src.is_dir() and expand_directories:
|
|
384
385
|
# both src and dest are directories, and expanding directories was requested. Traverse src, and map each
|
|
385
386
|
# file to the dest directory
|
|
386
|
-
for
|
|
387
|
+
for root, subdirs, files in os.walk(absolute_src, followlinks=True):
|
|
387
388
|
relative_root = Path(root).relative_to(absolute_src)
|
|
388
389
|
for name in itertools.chain(subdirs, files):
|
|
389
390
|
src_file_for_output = src_for_output / relative_root / name
|
|
@@ -674,16 +675,27 @@ def build_bundle(
|
|
|
674
675
|
if resolved_root.exists():
|
|
675
676
|
delete(resolved_root)
|
|
676
677
|
|
|
677
|
-
bundle_map =
|
|
678
|
-
for artifact in artifacts:
|
|
679
|
-
bundle_map.add(artifact)
|
|
680
|
-
|
|
678
|
+
bundle_map = bundle_artifacts(project_root, deploy_root, artifacts)
|
|
681
679
|
if bundle_map.is_empty():
|
|
682
680
|
raise ArtifactError(
|
|
683
681
|
"No artifacts mapping found in project definition, nothing to do."
|
|
684
682
|
)
|
|
685
683
|
|
|
686
|
-
|
|
684
|
+
return bundle_map
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def bundle_artifacts(
|
|
688
|
+
project_root: Path, deploy_root: Path, artifacts: list[PathMapping]
|
|
689
|
+
):
|
|
690
|
+
"""
|
|
691
|
+
Internal implementation of build_bundle that assumes
|
|
692
|
+
that validation is being done by the caller.
|
|
693
|
+
"""
|
|
694
|
+
bundle_map = BundleMap(project_root=project_root, deploy_root=deploy_root)
|
|
695
|
+
for artifact in artifacts:
|
|
696
|
+
bundle_map.add(artifact)
|
|
697
|
+
|
|
698
|
+
for absolute_src, absolute_dest in bundle_map.all_mappings(
|
|
687
699
|
absolute=True, expand_directories=False
|
|
688
700
|
):
|
|
689
701
|
symlink_or_copy(absolute_src, absolute_dest, deploy_root=deploy_root)
|
|
@@ -716,7 +728,7 @@ def find_and_read_manifest_file(deploy_root: Path) -> Dict[str, Any]:
|
|
|
716
728
|
"r", read_file_limit_mb=DEFAULT_SIZE_LIMIT_MB
|
|
717
729
|
) as file:
|
|
718
730
|
manifest_content = safe_load(file.read())
|
|
719
|
-
return manifest_content
|
|
731
|
+
return manifest_content or {}
|
|
720
732
|
|
|
721
733
|
|
|
722
734
|
def find_setup_script_file(deploy_root: Path) -> Path:
|
|
@@ -743,19 +755,24 @@ def find_setup_script_file(deploy_root: Path) -> Path:
|
|
|
743
755
|
)
|
|
744
756
|
|
|
745
757
|
|
|
758
|
+
VersionInfo = namedtuple("VersionInfo", ["version_name", "patch_number", "label"])
|
|
759
|
+
|
|
760
|
+
|
|
746
761
|
def find_version_info_in_manifest_file(
|
|
747
762
|
deploy_root: Path,
|
|
748
|
-
) ->
|
|
763
|
+
) -> VersionInfo:
|
|
749
764
|
"""
|
|
750
765
|
Find version and patch, if available, in the manifest.yml file.
|
|
751
766
|
"""
|
|
752
767
|
name_field = "name"
|
|
753
768
|
patch_field = "patch"
|
|
769
|
+
label_field = "label"
|
|
754
770
|
|
|
755
771
|
manifest_content = find_and_read_manifest_file(deploy_root=deploy_root)
|
|
756
772
|
|
|
757
773
|
version_name: Optional[str] = None
|
|
758
774
|
patch_number: Optional[int] = None
|
|
775
|
+
label: Optional[str] = None
|
|
759
776
|
|
|
760
777
|
version_info = manifest_content.get("version", None)
|
|
761
778
|
if version_info:
|
|
@@ -763,5 +780,42 @@ def find_version_info_in_manifest_file(
|
|
|
763
780
|
version_name = to_identifier(str(version_info[name_field]))
|
|
764
781
|
if patch_field in version_info:
|
|
765
782
|
patch_number = int(version_info[patch_field])
|
|
783
|
+
if label_field in version_info:
|
|
784
|
+
label = str(version_info[label_field])
|
|
785
|
+
|
|
786
|
+
return VersionInfo(version_name, patch_number, label)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def find_events_definitions_in_manifest_file(
|
|
790
|
+
deploy_root: Path,
|
|
791
|
+
) -> List[Dict[str, str]]:
|
|
792
|
+
"""
|
|
793
|
+
Find events definitions, if available, in the manifest.yml file.
|
|
794
|
+
Events definitions can be found under this section in the manifest.yml file:
|
|
795
|
+
|
|
796
|
+
configuration:
|
|
797
|
+
telemetry_event_definitions:
|
|
798
|
+
- type: ERRORS_AND_WARNINGS
|
|
799
|
+
sharing: MANDATORY
|
|
800
|
+
- type: DEBUG_LOGS
|
|
801
|
+
sharing: OPTIONAL
|
|
802
|
+
"""
|
|
803
|
+
manifest_content = find_and_read_manifest_file(deploy_root=deploy_root)
|
|
804
|
+
|
|
805
|
+
configuration_section = manifest_content.get("configuration", None)
|
|
806
|
+
events_definitions = []
|
|
807
|
+
if configuration_section and isinstance(configuration_section, dict):
|
|
808
|
+
telemetry_section = configuration_section.get("telemetry_event_definitions", [])
|
|
809
|
+
if isinstance(telemetry_section, list):
|
|
810
|
+
for event in telemetry_section:
|
|
811
|
+
if isinstance(event, dict):
|
|
812
|
+
event_type = event.get("type", "")
|
|
813
|
+
events_definitions.append(
|
|
814
|
+
{
|
|
815
|
+
"name": f"SNOWFLAKE${event_type}",
|
|
816
|
+
"type": event_type,
|
|
817
|
+
"sharing": event.get("sharing", ""),
|
|
818
|
+
}
|
|
819
|
+
)
|
|
766
820
|
|
|
767
|
-
return
|
|
821
|
+
return events_definitions
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
from pathlib import Path
|
|
18
|
-
from typing import Optional
|
|
18
|
+
from typing import Any, Optional
|
|
19
19
|
|
|
20
20
|
import jinja2
|
|
21
21
|
from snowflake.cli._plugins.nativeapp.artifacts import BundleMap
|
|
@@ -49,7 +49,9 @@ 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
|
-
def expand_templates_in_file(
|
|
52
|
+
def expand_templates_in_file(
|
|
53
|
+
self, src: Path, dest: Path, template_context: dict[str, Any] | None = None
|
|
54
|
+
) -> None:
|
|
53
55
|
"""
|
|
54
56
|
Expand templates in the file.
|
|
55
57
|
"""
|
|
@@ -74,7 +76,7 @@ class TemplatesProcessor(ArtifactProcessor):
|
|
|
74
76
|
else get_client_side_jinja_env()
|
|
75
77
|
)
|
|
76
78
|
expanded_template = jinja_env.from_string(file.contents).render(
|
|
77
|
-
get_cli_context().template_context
|
|
79
|
+
template_context or get_cli_context().template_context
|
|
78
80
|
)
|
|
79
81
|
|
|
80
82
|
# For now, we are printing the source file path in the error message
|