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.
Files changed (45) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +10 -1
  3. snowflake/cli/_app/snow_connector.py +76 -29
  4. snowflake/cli/_app/telemetry.py +8 -4
  5. snowflake/cli/_app/version_check.py +74 -0
  6. snowflake/cli/_plugins/git/commands.py +55 -14
  7. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +2 -5
  8. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +49 -31
  9. snowflake/cli/_plugins/nativeapp/manager.py +46 -87
  10. snowflake/cli/_plugins/nativeapp/run_processor.py +56 -260
  11. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +74 -0
  12. snowflake/cli/_plugins/nativeapp/teardown_processor.py +9 -152
  13. snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +91 -17
  14. snowflake/cli/_plugins/snowpark/commands.py +1 -1
  15. snowflake/cli/_plugins/snowpark/models.py +2 -1
  16. snowflake/cli/_plugins/streamlit/commands.py +1 -1
  17. snowflake/cli/_plugins/streamlit/manager.py +9 -0
  18. snowflake/cli/_plugins/workspace/action_context.py +2 -1
  19. snowflake/cli/_plugins/workspace/commands.py +48 -16
  20. snowflake/cli/_plugins/workspace/manager.py +1 -0
  21. snowflake/cli/api/cli_global_context.py +136 -313
  22. snowflake/cli/api/commands/flags.py +76 -91
  23. snowflake/cli/api/commands/snow_typer.py +6 -4
  24. snowflake/cli/api/config.py +1 -1
  25. snowflake/cli/api/connections.py +214 -0
  26. snowflake/cli/api/console/abc.py +4 -2
  27. snowflake/cli/api/entities/application_entity.py +687 -2
  28. snowflake/cli/api/entities/application_package_entity.py +151 -46
  29. snowflake/cli/api/entities/common.py +1 -0
  30. snowflake/cli/api/entities/utils.py +41 -17
  31. snowflake/cli/api/identifiers.py +3 -0
  32. snowflake/cli/api/project/definition.py +11 -0
  33. snowflake/cli/api/project/definition_conversion.py +171 -13
  34. snowflake/cli/api/project/schemas/entities/common.py +0 -12
  35. snowflake/cli/api/project/schemas/identifier_model.py +2 -2
  36. snowflake/cli/api/project/schemas/project_definition.py +101 -39
  37. snowflake/cli/api/rendering/project_definition_templates.py +4 -0
  38. snowflake/cli/api/rendering/sql_templates.py +7 -0
  39. snowflake/cli/api/utils/definition_rendering.py +3 -1
  40. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/METADATA +6 -6
  41. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/RECORD +44 -42
  42. snowflake/cli/api/commands/typer_pre_execute.py +0 -26
  43. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/WHEEL +0 -0
  44. {snowflake_cli_labs-3.0.0rc1.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/entry_points.txt +0 -0
  45. {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 get_cli_context_manager
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 _callback(provide_setter: Callable[[], Callable[[Any], Any]]):
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
- set_value = provide_setter()
46
- set_value(value)
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=_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=_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=_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=_callback(lambda: get_cli_context_manager().connection_context.set_user),
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 _callback(lambda: get_cli_context_manager().connection_context.set_password)(
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=_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=_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=_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=_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=_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=_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=_callback(lambda: get_cli_context_manager().connection_context.set_schema),
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=_callback(lambda: get_cli_context_manager().connection_context.set_role),
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=_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=_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=_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 != _DIAG_LOG_DEFAULT_VALUE:
272
- return path
273
- return tempfile.gettempdir()
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=_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=_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=_callback(lambda: get_cli_context_manager().set_output_format),
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=_callback(lambda: get_cli_context_manager().set_silent),
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=_callback(lambda: get_cli_context_manager().set_verbose),
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=_callback(lambda: get_cli_context_manager().set_enable_tracebacks),
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=_callback(lambda: get_cli_context_manager().set_experimental),
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 project_definition_callback(project_path: str) -> None:
511
- get_cli_context_manager().set_project_path_arg(project_path)
512
- register_pre_execute_command(lambda: register_project_definition(is_optional))
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=_callback(lambda: project_definition_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(env_overrides_args_list: list[str]) -> None:
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().set_project_env_overrides_args(env_overrides_args_map)
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=_callback(lambda: project_env_overrides_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):
@@ -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 "SNOWFLAKE_" + "_".join(p.upper() for p in path) + f"_{key.upper()}"
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()
@@ -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."""