snowflake-cli-labs 3.0.0rc1__py3-none-any.whl → 3.0.0rc2__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/snow_connector.py +76 -29
- 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/nativeapp/codegen/snowpark/python_processor.py +2 -5
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +49 -31
- snowflake/cli/_plugins/nativeapp/manager.py +46 -87
- snowflake/cli/_plugins/nativeapp/run_processor.py +56 -260
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +74 -0
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +9 -152
- snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +91 -17
- snowflake/cli/_plugins/snowpark/commands.py +1 -1
- snowflake/cli/_plugins/snowpark/models.py +2 -1
- snowflake/cli/_plugins/streamlit/commands.py +1 -1
- snowflake/cli/_plugins/streamlit/manager.py +9 -0
- snowflake/cli/_plugins/workspace/action_context.py +2 -1
- snowflake/cli/_plugins/workspace/commands.py +48 -16
- snowflake/cli/_plugins/workspace/manager.py +1 -0
- snowflake/cli/api/cli_global_context.py +136 -313
- snowflake/cli/api/commands/flags.py +76 -91
- snowflake/cli/api/commands/snow_typer.py +6 -4
- 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/application_entity.py +687 -2
- snowflake/cli/api/entities/application_package_entity.py +151 -46
- snowflake/cli/api/entities/common.py +1 -0
- snowflake/cli/api/entities/utils.py +41 -17
- snowflake/cli/api/identifiers.py +3 -0
- snowflake/cli/api/project/definition.py +11 -0
- snowflake/cli/api/project/definition_conversion.py +171 -13
- snowflake/cli/api/project/schemas/entities/common.py +0 -12
- snowflake/cli/api/project/schemas/identifier_model.py +2 -2
- snowflake/cli/api/project/schemas/project_definition.py +101 -39
- snowflake/cli/api/rendering/project_definition_templates.py +4 -0
- snowflake/cli/api/rendering/sql_templates.py +7 -0
- snowflake/cli/api/utils/definition_rendering.py +3 -1
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/METADATA +6 -6
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/RECORD +44 -42
- snowflake/cli/api/commands/typer_pre_execute.py +0 -26
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/WHEEL +0 -0
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/entry_points.txt +0 -0
- {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
|
@@ -21,18 +21,18 @@ from typing import Any, Callable, Optional
|
|
|
21
21
|
import click
|
|
22
22
|
import typer
|
|
23
23
|
from click import ClickException
|
|
24
|
-
from snowflake.cli.api.cli_global_context import
|
|
24
|
+
from snowflake.cli.api.cli_global_context import (
|
|
25
|
+
_CliGlobalContextManager,
|
|
26
|
+
get_cli_context_manager,
|
|
27
|
+
)
|
|
25
28
|
from snowflake.cli.api.commands.common import OnErrorType
|
|
26
29
|
from snowflake.cli.api.commands.overrideable_parameter import OverrideableOption
|
|
27
|
-
from snowflake.cli.api.commands.typer_pre_execute import register_pre_execute_command
|
|
28
30
|
from snowflake.cli.api.commands.utils import parse_key_value_variables
|
|
29
31
|
from snowflake.cli.api.config import get_all_connections
|
|
32
|
+
from snowflake.cli.api.connections import ConnectionContext
|
|
30
33
|
from snowflake.cli.api.console import cli_console
|
|
31
|
-
from snowflake.cli.api.exceptions import MissingConfiguration
|
|
32
34
|
from snowflake.cli.api.identifiers import FQN
|
|
33
35
|
from snowflake.cli.api.output.formats import OutputFormat
|
|
34
|
-
from snowflake.cli.api.project.definition_manager import DefinitionManager
|
|
35
|
-
from snowflake.cli.api.rendering.jinja import CONTEXT_KEY
|
|
36
36
|
|
|
37
37
|
DEFAULT_CONTEXT_SETTINGS = {"help_option_names": ["--help", "-h"]}
|
|
38
38
|
|
|
@@ -40,10 +40,39 @@ _CONNECTION_SECTION = "Connection configuration"
|
|
|
40
40
|
_CLI_BEHAVIOUR = "Global configuration"
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def
|
|
43
|
+
def _connection_callback(prop: str):
|
|
44
|
+
"""Generates a setter for a field on the current context manager's connection context."""
|
|
45
|
+
if prop not in ConnectionContext.__dataclass_fields__:
|
|
46
|
+
raise KeyError(
|
|
47
|
+
f"Cannot generate setter for non-existent connection attr {prop}"
|
|
48
|
+
)
|
|
49
|
+
|
|
44
50
|
def callback(value):
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
try:
|
|
52
|
+
if click.get_current_context().resilient_parsing:
|
|
53
|
+
return
|
|
54
|
+
except RuntimeError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
setattr(get_cli_context_manager().connection_context, prop, value)
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
return callback
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _context_callback(prop: str):
|
|
64
|
+
"""Generates a setter for a field on the current context manager."""
|
|
65
|
+
if prop not in _CliGlobalContextManager.__dataclass_fields__:
|
|
66
|
+
raise KeyError(f"Cannot generate setter for non-existent context attr {prop}")
|
|
67
|
+
|
|
68
|
+
def callback(value):
|
|
69
|
+
try:
|
|
70
|
+
if click.get_current_context().resilient_parsing:
|
|
71
|
+
return
|
|
72
|
+
except RuntimeError:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
setattr(get_cli_context_manager(), prop, value)
|
|
47
76
|
return value
|
|
48
77
|
|
|
49
78
|
return callback
|
|
@@ -55,9 +84,7 @@ ConnectionOption = typer.Option(
|
|
|
55
84
|
"-c",
|
|
56
85
|
"--environment",
|
|
57
86
|
help=f"Name of the connection, as defined in your `config.toml`. Default: `default`.",
|
|
58
|
-
callback=
|
|
59
|
-
lambda: get_cli_context_manager().connection_context.set_connection_name
|
|
60
|
-
),
|
|
87
|
+
callback=_connection_callback("connection_name"),
|
|
61
88
|
show_default=False,
|
|
62
89
|
rich_help_panel=_CONNECTION_SECTION,
|
|
63
90
|
shell_complete=lambda _, __, ___: list(get_all_connections()),
|
|
@@ -68,9 +95,7 @@ TemporaryConnectionOption = typer.Option(
|
|
|
68
95
|
"--temporary-connection",
|
|
69
96
|
"-x",
|
|
70
97
|
help="Uses connection defined with command line parameters, instead of one defined in config",
|
|
71
|
-
callback=
|
|
72
|
-
lambda: get_cli_context_manager().connection_context.set_temporary_connection
|
|
73
|
-
),
|
|
98
|
+
callback=_connection_callback("temporary_connection"),
|
|
74
99
|
is_flag=True,
|
|
75
100
|
rich_help_panel=_CONNECTION_SECTION,
|
|
76
101
|
)
|
|
@@ -80,9 +105,7 @@ AccountOption = typer.Option(
|
|
|
80
105
|
"--account",
|
|
81
106
|
"--accountname",
|
|
82
107
|
help="Name assigned to your Snowflake account. Overrides the value specified for the connection.",
|
|
83
|
-
callback=
|
|
84
|
-
lambda: get_cli_context_manager().connection_context.set_account
|
|
85
|
-
),
|
|
108
|
+
callback=_connection_callback("account"),
|
|
86
109
|
show_default=False,
|
|
87
110
|
rich_help_panel=_CONNECTION_SECTION,
|
|
88
111
|
)
|
|
@@ -92,7 +115,7 @@ UserOption = typer.Option(
|
|
|
92
115
|
"--user",
|
|
93
116
|
"--username",
|
|
94
117
|
help="Username to connect to Snowflake. Overrides the value specified for the connection.",
|
|
95
|
-
callback=
|
|
118
|
+
callback=_connection_callback("user"),
|
|
96
119
|
show_default=False,
|
|
97
120
|
rich_help_panel=_CONNECTION_SECTION,
|
|
98
121
|
)
|
|
@@ -105,9 +128,7 @@ def _password_callback(value: str):
|
|
|
105
128
|
if value:
|
|
106
129
|
cli_console.message(PLAIN_PASSWORD_MSG)
|
|
107
130
|
|
|
108
|
-
return
|
|
109
|
-
value
|
|
110
|
-
)
|
|
131
|
+
return _connection_callback("password")(value)
|
|
111
132
|
|
|
112
133
|
|
|
113
134
|
PasswordOption = typer.Option(
|
|
@@ -125,9 +146,7 @@ AuthenticatorOption = typer.Option(
|
|
|
125
146
|
"--authenticator",
|
|
126
147
|
help="Snowflake authenticator. Overrides the value specified for the connection.",
|
|
127
148
|
hide_input=True,
|
|
128
|
-
callback=
|
|
129
|
-
lambda: get_cli_context_manager().connection_context.set_authenticator
|
|
130
|
-
),
|
|
149
|
+
callback=_connection_callback("authenticator"),
|
|
131
150
|
show_default=False,
|
|
132
151
|
rich_help_panel=_CONNECTION_SECTION,
|
|
133
152
|
)
|
|
@@ -138,9 +157,7 @@ PrivateKeyPathOption = typer.Option(
|
|
|
138
157
|
"--private-key-path",
|
|
139
158
|
help="Snowflake private key file path. Overrides the value specified for the connection.",
|
|
140
159
|
hide_input=True,
|
|
141
|
-
callback=
|
|
142
|
-
lambda: get_cli_context_manager().connection_context.set_private_key_file
|
|
143
|
-
),
|
|
160
|
+
callback=_connection_callback("private_key_file"),
|
|
144
161
|
show_default=False,
|
|
145
162
|
rich_help_panel=_CONNECTION_SECTION,
|
|
146
163
|
exists=True,
|
|
@@ -153,9 +170,7 @@ SessionTokenOption = typer.Option(
|
|
|
153
170
|
"--session-token",
|
|
154
171
|
help="Snowflake session token. Can be used only in conjunction with --master-token. Overrides the value specified for the connection.",
|
|
155
172
|
hide_input=True,
|
|
156
|
-
callback=
|
|
157
|
-
lambda: get_cli_context_manager().connection_context.set_session_token
|
|
158
|
-
),
|
|
173
|
+
callback=_connection_callback("session_token"),
|
|
159
174
|
show_default=False,
|
|
160
175
|
rich_help_panel=_CONNECTION_SECTION,
|
|
161
176
|
exists=True,
|
|
@@ -169,9 +184,7 @@ MasterTokenOption = typer.Option(
|
|
|
169
184
|
"--master-token",
|
|
170
185
|
help="Snowflake master token. Can be used only in conjunction with --session-token. Overrides the value specified for the connection.",
|
|
171
186
|
hide_input=True,
|
|
172
|
-
callback=
|
|
173
|
-
lambda: get_cli_context_manager().connection_context.set_master_token
|
|
174
|
-
),
|
|
187
|
+
callback=_connection_callback("master_token"),
|
|
175
188
|
show_default=False,
|
|
176
189
|
rich_help_panel=_CONNECTION_SECTION,
|
|
177
190
|
exists=True,
|
|
@@ -184,9 +197,7 @@ TokenFilePathOption = typer.Option(
|
|
|
184
197
|
None,
|
|
185
198
|
"--token-file-path",
|
|
186
199
|
help="Path to file with an OAuth token that should be used when connecting to Snowflake",
|
|
187
|
-
callback=
|
|
188
|
-
lambda: get_cli_context_manager().connection_context.set_token_file_path
|
|
189
|
-
),
|
|
200
|
+
callback=_connection_callback("token_file_path"),
|
|
190
201
|
show_default=False,
|
|
191
202
|
rich_help_panel=_CONNECTION_SECTION,
|
|
192
203
|
exists=True,
|
|
@@ -199,9 +210,7 @@ DatabaseOption = typer.Option(
|
|
|
199
210
|
"--database",
|
|
200
211
|
"--dbname",
|
|
201
212
|
help="Database to use. Overrides the value specified for the connection.",
|
|
202
|
-
callback=
|
|
203
|
-
lambda: get_cli_context_manager().connection_context.set_database
|
|
204
|
-
),
|
|
213
|
+
callback=_connection_callback("database"),
|
|
205
214
|
show_default=False,
|
|
206
215
|
rich_help_panel=_CONNECTION_SECTION,
|
|
207
216
|
)
|
|
@@ -211,7 +220,7 @@ SchemaOption = typer.Option(
|
|
|
211
220
|
"--schema",
|
|
212
221
|
"--schemaname",
|
|
213
222
|
help="Database schema to use. Overrides the value specified for the connection.",
|
|
214
|
-
callback=
|
|
223
|
+
callback=_connection_callback("schema"),
|
|
215
224
|
show_default=False,
|
|
216
225
|
rich_help_panel=_CONNECTION_SECTION,
|
|
217
226
|
)
|
|
@@ -221,7 +230,7 @@ RoleOption = typer.Option(
|
|
|
221
230
|
"--role",
|
|
222
231
|
"--rolename",
|
|
223
232
|
help="Role to use. Overrides the value specified for the connection.",
|
|
224
|
-
callback=
|
|
233
|
+
callback=_connection_callback("role"),
|
|
225
234
|
show_default=False,
|
|
226
235
|
rich_help_panel=_CONNECTION_SECTION,
|
|
227
236
|
)
|
|
@@ -230,9 +239,7 @@ WarehouseOption = typer.Option(
|
|
|
230
239
|
None,
|
|
231
240
|
"--warehouse",
|
|
232
241
|
help="Warehouse to use. Overrides the value specified for the connection.",
|
|
233
|
-
callback=
|
|
234
|
-
lambda: get_cli_context_manager().connection_context.set_warehouse
|
|
235
|
-
),
|
|
242
|
+
callback=_connection_callback("warehouse"),
|
|
236
243
|
show_default=False,
|
|
237
244
|
rich_help_panel=_CONNECTION_SECTION,
|
|
238
245
|
)
|
|
@@ -241,9 +248,7 @@ MfaPasscodeOption = typer.Option(
|
|
|
241
248
|
None,
|
|
242
249
|
"--mfa-passcode",
|
|
243
250
|
help="Token to use for multi-factor authentication (MFA)",
|
|
244
|
-
callback=
|
|
245
|
-
lambda: get_cli_context_manager().connection_context.set_mfa_passcode
|
|
246
|
-
),
|
|
251
|
+
callback=_connection_callback("mfa_passcode"),
|
|
247
252
|
prompt="MFA passcode",
|
|
248
253
|
prompt_required=False,
|
|
249
254
|
show_default=False,
|
|
@@ -254,9 +259,7 @@ EnableDiagOption = typer.Option(
|
|
|
254
259
|
False,
|
|
255
260
|
"--enable-diag",
|
|
256
261
|
help="Run python connector diagnostic test",
|
|
257
|
-
callback=
|
|
258
|
-
lambda: get_cli_context_manager().connection_context.set_enable_diag
|
|
259
|
-
),
|
|
262
|
+
callback=_connection_callback("enable_diag"),
|
|
260
263
|
show_default=False,
|
|
261
264
|
is_flag=True,
|
|
262
265
|
rich_help_panel=_CONNECTION_SECTION,
|
|
@@ -268,18 +271,17 @@ _DIAG_LOG_DEFAULT_VALUE = "<temporary_directory>"
|
|
|
268
271
|
|
|
269
272
|
|
|
270
273
|
def _diag_log_path_callback(path: str):
|
|
271
|
-
if path
|
|
272
|
-
|
|
273
|
-
|
|
274
|
+
if path == _DIAG_LOG_DEFAULT_VALUE:
|
|
275
|
+
path = tempfile.gettempdir()
|
|
276
|
+
get_cli_context_manager().connection_context.diag_log_path = Path(path)
|
|
277
|
+
return path
|
|
274
278
|
|
|
275
279
|
|
|
276
280
|
DiagLogPathOption: Path = typer.Option(
|
|
277
281
|
_DIAG_LOG_DEFAULT_VALUE,
|
|
278
282
|
"--diag-log-path",
|
|
279
283
|
help="Diagnostic report path",
|
|
280
|
-
callback=
|
|
281
|
-
lambda: get_cli_context_manager().connection_context.set_diag_log_path
|
|
282
|
-
),
|
|
284
|
+
callback=_diag_log_path_callback,
|
|
283
285
|
show_default=False,
|
|
284
286
|
rich_help_panel=_CONNECTION_SECTION,
|
|
285
287
|
exists=True,
|
|
@@ -290,9 +292,7 @@ DiagAllowlistPathOption: Path = typer.Option(
|
|
|
290
292
|
None,
|
|
291
293
|
"--diag-allowlist-path",
|
|
292
294
|
help="Diagnostic report path to optional allowlist",
|
|
293
|
-
callback=
|
|
294
|
-
lambda: get_cli_context_manager().connection_context.set_diag_allowlist_path
|
|
295
|
-
),
|
|
295
|
+
callback=_connection_callback("diag_allowlist_path"),
|
|
296
296
|
show_default=False,
|
|
297
297
|
rich_help_panel=_CONNECTION_SECTION,
|
|
298
298
|
exists=True,
|
|
@@ -304,7 +304,7 @@ OutputFormatOption = typer.Option(
|
|
|
304
304
|
"--format",
|
|
305
305
|
help="Specifies the output format.",
|
|
306
306
|
case_sensitive=False,
|
|
307
|
-
callback=
|
|
307
|
+
callback=_context_callback("output_format"),
|
|
308
308
|
rich_help_panel=_CLI_BEHAVIOUR,
|
|
309
309
|
)
|
|
310
310
|
|
|
@@ -312,7 +312,7 @@ SilentOption = typer.Option(
|
|
|
312
312
|
False,
|
|
313
313
|
"--silent",
|
|
314
314
|
help="Turns off intermediate output to console.",
|
|
315
|
-
callback=
|
|
315
|
+
callback=_context_callback("silent"),
|
|
316
316
|
is_flag=True,
|
|
317
317
|
rich_help_panel=_CLI_BEHAVIOUR,
|
|
318
318
|
is_eager=True,
|
|
@@ -323,7 +323,7 @@ VerboseOption = typer.Option(
|
|
|
323
323
|
"--verbose",
|
|
324
324
|
"-v",
|
|
325
325
|
help="Displays log entries for log levels `info` and higher.",
|
|
326
|
-
callback=
|
|
326
|
+
callback=_context_callback("verbose"),
|
|
327
327
|
is_flag=True,
|
|
328
328
|
rich_help_panel=_CLI_BEHAVIOUR,
|
|
329
329
|
)
|
|
@@ -332,7 +332,7 @@ DebugOption = typer.Option(
|
|
|
332
332
|
False,
|
|
333
333
|
"--debug",
|
|
334
334
|
help="Displays log entries for log levels `debug` and higher; debug logs contains additional information.",
|
|
335
|
-
callback=
|
|
335
|
+
callback=_context_callback("enable_tracebacks"),
|
|
336
336
|
is_flag=True,
|
|
337
337
|
rich_help_panel=_CLI_BEHAVIOUR,
|
|
338
338
|
)
|
|
@@ -435,7 +435,7 @@ def experimental_option(
|
|
|
435
435
|
"--experimental",
|
|
436
436
|
help=help_text,
|
|
437
437
|
hidden=True,
|
|
438
|
-
callback=
|
|
438
|
+
callback=_context_callback("experimental"),
|
|
439
439
|
is_flag=True,
|
|
440
440
|
rich_help_panel=_CLI_BEHAVIOUR,
|
|
441
441
|
)
|
|
@@ -486,30 +486,12 @@ def execution_identifier_argument(sf_object: str, example: str) -> typer.Argumen
|
|
|
486
486
|
)
|
|
487
487
|
|
|
488
488
|
|
|
489
|
-
def register_project_definition(is_optional: bool) -> None:
|
|
490
|
-
cli_context_manager = get_cli_context_manager()
|
|
491
|
-
project_path = cli_context_manager.project_path_arg
|
|
492
|
-
env_overrides_args = cli_context_manager.project_env_overrides_args
|
|
493
|
-
|
|
494
|
-
dm = DefinitionManager(project_path, {CONTEXT_KEY: {"env": env_overrides_args}})
|
|
495
|
-
project_definition = dm.project_definition
|
|
496
|
-
project_root = dm.project_root
|
|
497
|
-
template_context = dm.template_context
|
|
498
|
-
|
|
499
|
-
if not dm.has_definition_file and not is_optional:
|
|
500
|
-
raise MissingConfiguration(
|
|
501
|
-
"Cannot find project definition (snowflake.yml). Please provide a path to the project or run this command in a valid project directory."
|
|
502
|
-
)
|
|
503
|
-
|
|
504
|
-
cli_context_manager.set_project_definition(project_definition)
|
|
505
|
-
cli_context_manager.set_project_root(project_root)
|
|
506
|
-
cli_context_manager.set_template_context(template_context)
|
|
507
|
-
|
|
508
|
-
|
|
509
489
|
def project_definition_option(is_optional: bool):
|
|
510
|
-
def
|
|
511
|
-
get_cli_context_manager()
|
|
512
|
-
|
|
490
|
+
def project_path_callback(project_path: str) -> str:
|
|
491
|
+
ctx_mgr = get_cli_context_manager()
|
|
492
|
+
ctx_mgr.project_path_arg = project_path
|
|
493
|
+
ctx_mgr.project_is_optional = is_optional
|
|
494
|
+
return project_path
|
|
513
495
|
|
|
514
496
|
return typer.Option(
|
|
515
497
|
None,
|
|
@@ -517,23 +499,26 @@ def project_definition_option(is_optional: bool):
|
|
|
517
499
|
"--project",
|
|
518
500
|
help=f"Path where Snowflake project resides. "
|
|
519
501
|
f"Defaults to current working directory.",
|
|
520
|
-
callback=
|
|
502
|
+
callback=project_path_callback,
|
|
521
503
|
show_default=False,
|
|
522
504
|
)
|
|
523
505
|
|
|
524
506
|
|
|
525
507
|
def project_env_overrides_option():
|
|
526
|
-
def project_env_overrides_callback(
|
|
508
|
+
def project_env_overrides_callback(
|
|
509
|
+
env_overrides_args_list: list[str],
|
|
510
|
+
) -> dict[str, str]:
|
|
527
511
|
env_overrides_args_map = {
|
|
528
512
|
v.key: v.value for v in parse_key_value_variables(env_overrides_args_list)
|
|
529
513
|
}
|
|
530
|
-
get_cli_context_manager().
|
|
514
|
+
get_cli_context_manager().project_env_overrides_args = env_overrides_args_map
|
|
515
|
+
return env_overrides_args_map
|
|
531
516
|
|
|
532
517
|
return typer.Option(
|
|
533
518
|
[],
|
|
534
519
|
"--env",
|
|
535
520
|
help="String in format of key=value. Overrides variables from env section used for templates.",
|
|
536
|
-
callback=
|
|
521
|
+
callback=project_env_overrides_callback,
|
|
537
522
|
show_default=False,
|
|
538
523
|
)
|
|
539
524
|
|
|
@@ -30,11 +30,11 @@ from snowflake.cli.api.commands.execution_metadata import (
|
|
|
30
30
|
ExecutionStatus,
|
|
31
31
|
)
|
|
32
32
|
from snowflake.cli.api.commands.flags import DEFAULT_CONTEXT_SETTINGS
|
|
33
|
-
from snowflake.cli.api.commands.typer_pre_execute import run_pre_execute_commands
|
|
34
33
|
from snowflake.cli.api.exceptions import CommandReturnTypeError
|
|
35
34
|
from snowflake.cli.api.output.types import CommandResult
|
|
36
35
|
from snowflake.cli.api.sanitizers import sanitize_for_terminal
|
|
37
36
|
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
37
|
+
from snowflake.connector import DatabaseError
|
|
38
38
|
|
|
39
39
|
log = logging.getLogger(__name__)
|
|
40
40
|
|
|
@@ -107,8 +107,8 @@ class SnowTyper(typer.Typer):
|
|
|
107
107
|
execution.complete(ExecutionStatus.SUCCESS)
|
|
108
108
|
except Exception as err:
|
|
109
109
|
execution.complete(ExecutionStatus.FAILURE)
|
|
110
|
-
self.exception_handler(err, execution)
|
|
111
|
-
raise
|
|
110
|
+
exception = self.exception_handler(err, execution)
|
|
111
|
+
raise exception
|
|
112
112
|
finally:
|
|
113
113
|
self.post_execute(execution)
|
|
114
114
|
|
|
@@ -128,7 +128,6 @@ class SnowTyper(typer.Typer):
|
|
|
128
128
|
from snowflake.cli._app.telemetry import log_command_usage
|
|
129
129
|
|
|
130
130
|
log.debug("Executing command pre execution callback")
|
|
131
|
-
run_pre_execute_commands()
|
|
132
131
|
log_command_usage(execution)
|
|
133
132
|
if require_warehouse and not SqlExecutionMixin().session_has_warehouse():
|
|
134
133
|
raise ClickException(
|
|
@@ -157,6 +156,9 @@ class SnowTyper(typer.Typer):
|
|
|
157
156
|
|
|
158
157
|
log.debug("Executing command exception callback")
|
|
159
158
|
log_command_execution_error(exception, execution)
|
|
159
|
+
if isinstance(exception, DatabaseError):
|
|
160
|
+
return ClickException(exception.msg)
|
|
161
|
+
return exception
|
|
160
162
|
|
|
161
163
|
@staticmethod
|
|
162
164
|
def post_execute(execution: ExecutionMetadata):
|
snowflake/cli/api/config.py
CHANGED
|
@@ -296,7 +296,7 @@ def _initialise_config(config_file: Path) -> None:
|
|
|
296
296
|
|
|
297
297
|
|
|
298
298
|
def get_env_variable_name(*path, key: str) -> str:
|
|
299
|
-
return "
|
|
299
|
+
return ("_".join(["snowflake", *path, key])).upper()
|
|
300
300
|
|
|
301
301
|
|
|
302
302
|
def get_env_value(*path, key: str) -> str | None:
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Copyright (c) 2024 Snowflake Inc.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
import warnings
|
|
21
|
+
from dataclasses import asdict, dataclass, field, fields, replace
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Optional
|
|
24
|
+
|
|
25
|
+
from snowflake.cli.api.config import get_default_connection_name
|
|
26
|
+
from snowflake.cli.api.exceptions import InvalidSchemaError
|
|
27
|
+
from snowflake.connector import SnowflakeConnection
|
|
28
|
+
from snowflake.connector.compat import IS_WINDOWS
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
schema_pattern = re.compile(r".+\..+")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class ConnectionContext:
|
|
37
|
+
# FIXME: can reduce duplication using config.ConnectionConfig
|
|
38
|
+
connection_name: Optional[str] = None
|
|
39
|
+
account: Optional[str] = None
|
|
40
|
+
database: Optional[str] = None
|
|
41
|
+
role: Optional[str] = None
|
|
42
|
+
schema: Optional[str] = None
|
|
43
|
+
user: Optional[str] = None
|
|
44
|
+
password: Optional[str] = field(default=None, repr=False)
|
|
45
|
+
authenticator: Optional[str] = None
|
|
46
|
+
private_key_file: Optional[str] = None
|
|
47
|
+
warehouse: Optional[str] = None
|
|
48
|
+
mfa_passcode: Optional[str] = None
|
|
49
|
+
enable_diag: Optional[bool] = False
|
|
50
|
+
diag_log_path: Optional[Path] = None
|
|
51
|
+
diag_allowlist_path: Optional[Path] = None
|
|
52
|
+
temporary_connection: bool = False
|
|
53
|
+
session_token: Optional[str] = None
|
|
54
|
+
master_token: Optional[str] = None
|
|
55
|
+
token_file_path: Optional[Path] = None
|
|
56
|
+
|
|
57
|
+
VALIDATED_FIELD_NAMES = ["schema"]
|
|
58
|
+
|
|
59
|
+
def present_values_as_dict(self) -> dict:
|
|
60
|
+
"""Dictionary representation of this ConnectionContext for values that are not None"""
|
|
61
|
+
return {k: v for (k, v) in asdict(self).items() if v is not None}
|
|
62
|
+
|
|
63
|
+
def clone(self) -> ConnectionContext:
|
|
64
|
+
return replace(self)
|
|
65
|
+
|
|
66
|
+
def update(self, **updates):
|
|
67
|
+
"""
|
|
68
|
+
Given a dictionary of property (key, value) mappings, update properties
|
|
69
|
+
of this context object with equivalent names to the keys.
|
|
70
|
+
|
|
71
|
+
Raises KeyError if a non-property is specified as a key.
|
|
72
|
+
"""
|
|
73
|
+
field_map = {field.name: field for field in fields(self)}
|
|
74
|
+
for (key, value) in updates.items():
|
|
75
|
+
# ensure key represents a property
|
|
76
|
+
if key not in field_map:
|
|
77
|
+
raise KeyError(f"{key} is not a field of {self.__class__.__name__}")
|
|
78
|
+
setattr(self, key, value)
|
|
79
|
+
|
|
80
|
+
def __repr__(self) -> str:
|
|
81
|
+
"""Minimal repr where None values have their keys omitted."""
|
|
82
|
+
items = [f"{k}={repr(v)}" for (k, v) in self.present_values_as_dict().items()]
|
|
83
|
+
return f"{self.__class__.__name__}({', '.join(items)})"
|
|
84
|
+
|
|
85
|
+
def __setattr__(self, prop, val):
|
|
86
|
+
"""Runs registered validators before setting fields."""
|
|
87
|
+
if prop in self.VALIDATED_FIELD_NAMES:
|
|
88
|
+
validate = getattr(self, f"validate_{prop}")
|
|
89
|
+
validate(val)
|
|
90
|
+
super().__setattr__(prop, val)
|
|
91
|
+
|
|
92
|
+
def validate_schema(self, value: Optional[str]):
|
|
93
|
+
if (
|
|
94
|
+
value
|
|
95
|
+
and not (value.startswith('"') and value.endswith('"'))
|
|
96
|
+
# if schema is fully qualified name (db.schema)
|
|
97
|
+
and schema_pattern.match(value)
|
|
98
|
+
):
|
|
99
|
+
raise InvalidSchemaError(value)
|
|
100
|
+
|
|
101
|
+
def validate_and_complete(self):
|
|
102
|
+
"""
|
|
103
|
+
Ensure we can create a connection from this context.
|
|
104
|
+
"""
|
|
105
|
+
if not self.temporary_connection and not self.connection_name:
|
|
106
|
+
self.connection_name = get_default_connection_name()
|
|
107
|
+
|
|
108
|
+
def build_connection(self):
|
|
109
|
+
from snowflake.cli._app.snow_connector import connect_to_snowflake
|
|
110
|
+
|
|
111
|
+
# Ignore warnings about bad owner or permissions on Windows
|
|
112
|
+
# Telemetry omit our warning filter from config.py
|
|
113
|
+
if IS_WINDOWS:
|
|
114
|
+
warnings.filterwarnings(
|
|
115
|
+
action="ignore",
|
|
116
|
+
message="Bad owner or permissions.*",
|
|
117
|
+
module="snowflake.connector.config_manager",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return connect_to_snowflake(**self.present_values_as_dict())
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class OpenConnectionCache:
|
|
124
|
+
"""
|
|
125
|
+
A connection cache that transparently manages SnowflakeConnection objects
|
|
126
|
+
and is keyed by ConnectionContext objects, e.g. cache[ctx].execute_string(...).
|
|
127
|
+
Connections are automatically closed after CONNECTION_CLEANUP_SEC, but
|
|
128
|
+
are guaranteed to be open (if config is valid) when returned by the cache.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
connections: dict[str, SnowflakeConnection]
|
|
132
|
+
cleanup_futures: dict[str, asyncio.TimerHandle]
|
|
133
|
+
|
|
134
|
+
CONNECTION_CLEANUP_SEC: float = 10.0 * 60
|
|
135
|
+
"""Connections are closed this many seconds after the last time they are accessed."""
|
|
136
|
+
|
|
137
|
+
def __init__(self):
|
|
138
|
+
self.connections = {}
|
|
139
|
+
self.cleanup_futures = {}
|
|
140
|
+
|
|
141
|
+
def __getitem__(self, ctx):
|
|
142
|
+
if not isinstance(ctx, ConnectionContext):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"Expected key to be ConnectionContext but got {repr(ctx)}"
|
|
145
|
+
)
|
|
146
|
+
key = repr(ctx)
|
|
147
|
+
if not self._has_open_connection(key):
|
|
148
|
+
self._insert(key, ctx)
|
|
149
|
+
self._touch(key)
|
|
150
|
+
return self.connections[key]
|
|
151
|
+
|
|
152
|
+
def clear(self):
|
|
153
|
+
"""Closes all connections and resets the cache to its initial state."""
|
|
154
|
+
connection_keys = list(self.connections.keys())
|
|
155
|
+
for key in connection_keys:
|
|
156
|
+
self._cleanup(key)
|
|
157
|
+
|
|
158
|
+
# if any orphaned futures still exist, clean them up too
|
|
159
|
+
for future in self.cleanup_futures.values():
|
|
160
|
+
future.cancel()
|
|
161
|
+
self.cleanup_futures.clear()
|
|
162
|
+
|
|
163
|
+
def _has_open_connection(self, key: str):
|
|
164
|
+
return key in self.connections
|
|
165
|
+
|
|
166
|
+
def _insert(self, key: str, ctx: ConnectionContext):
|
|
167
|
+
try:
|
|
168
|
+
# N.B. build_connection ultimately calls connect_to_snowflake, which
|
|
169
|
+
# interpolates in connection dicts (from config) and environment variables.
|
|
170
|
+
# This means that we could return a stale (incorrect) connection for the
|
|
171
|
+
# given ConnectionContext if get_env_value or get_connection_dict would
|
|
172
|
+
# have returned different values (i.e. env / config have changed).
|
|
173
|
+
self.connections[key] = ctx.build_connection()
|
|
174
|
+
except Exception:
|
|
175
|
+
logger.debug(
|
|
176
|
+
"ConnectionCache: failed to connect using %s; not caching.", key
|
|
177
|
+
)
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
def _cancel_cleanup_future_if_exists(self, key: str):
|
|
181
|
+
if key in self.cleanup_futures:
|
|
182
|
+
self.cleanup_futures.pop(key).cancel()
|
|
183
|
+
|
|
184
|
+
def _touch(self, key: str):
|
|
185
|
+
"""
|
|
186
|
+
Extend the lifetime of the cached connection at the given key.
|
|
187
|
+
"""
|
|
188
|
+
loop = None
|
|
189
|
+
try:
|
|
190
|
+
loop = asyncio.get_event_loop()
|
|
191
|
+
except RuntimeError:
|
|
192
|
+
# Python 3.11+ will throw when no event loop;
|
|
193
|
+
# Python 3.10 will issue a DeprecationWarning and return None
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
if not loop:
|
|
197
|
+
logger.debug(
|
|
198
|
+
"ConnectionCache: no event loop; connections will close at exit."
|
|
199
|
+
)
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
self._cancel_cleanup_future_if_exists(key)
|
|
203
|
+
self.cleanup_futures[key] = loop.call_later(
|
|
204
|
+
self.CONNECTION_CLEANUP_SEC, lambda: self._cleanup(key)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _cleanup(self, key: str):
|
|
208
|
+
"""Closes the cached connection at the given key."""
|
|
209
|
+
if key not in self.connections:
|
|
210
|
+
logger.debug("Cleaning up connection %s, but not found in cache!", key)
|
|
211
|
+
|
|
212
|
+
# doesn't cancel in-flight async queries
|
|
213
|
+
self._cancel_cleanup_future_if_exists(key)
|
|
214
|
+
self.connections.pop(key).close()
|
snowflake/cli/api/console/abc.py
CHANGED
|
@@ -37,14 +37,16 @@ class AbstractConsole(ABC):
|
|
|
37
37
|
"""
|
|
38
38
|
|
|
39
39
|
_print_fn: Callable[[str], None]
|
|
40
|
-
_cli_context: _CliGlobalContextAccess
|
|
41
40
|
_in_phase: bool
|
|
42
41
|
|
|
43
42
|
def __init__(self):
|
|
44
43
|
super().__init__()
|
|
45
|
-
self._cli_context = get_cli_context()
|
|
46
44
|
self._in_phase = False
|
|
47
45
|
|
|
46
|
+
@property
|
|
47
|
+
def _cli_context(self) -> _CliGlobalContextAccess:
|
|
48
|
+
return get_cli_context()
|
|
49
|
+
|
|
48
50
|
@property
|
|
49
51
|
def is_silent(self) -> bool:
|
|
50
52
|
"""Returns information whether intermediate output is muted."""
|