snowflake-cli 3.5.0__py3-none-any.whl → 3.6.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.
Files changed (34) hide show
  1. snowflake/cli/__about__.py +13 -1
  2. snowflake/cli/_app/commands_registration/builtin_plugins.py +2 -0
  3. snowflake/cli/_app/snow_connector.py +5 -4
  4. snowflake/cli/_app/telemetry.py +3 -15
  5. snowflake/cli/_app/version_check.py +4 -4
  6. snowflake/cli/_plugins/auth/__init__.py +11 -0
  7. snowflake/cli/_plugins/auth/keypair/__init__.py +0 -0
  8. snowflake/cli/_plugins/auth/keypair/commands.py +151 -0
  9. snowflake/cli/_plugins/auth/keypair/manager.py +331 -0
  10. snowflake/cli/_plugins/auth/keypair/plugin_spec.py +30 -0
  11. snowflake/cli/_plugins/connection/commands.py +77 -1
  12. snowflake/cli/_plugins/nativeapp/entities/application.py +4 -1
  13. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +33 -6
  14. snowflake/cli/_plugins/object/command_aliases.py +3 -1
  15. snowflake/cli/_plugins/object/manager.py +4 -2
  16. snowflake/cli/_plugins/project/commands.py +16 -0
  17. snowflake/cli/_plugins/spcs/compute_pool/commands.py +17 -5
  18. snowflake/cli/_plugins/sql/manager.py +42 -51
  19. snowflake/cli/_plugins/sql/source_reader.py +230 -0
  20. snowflake/cli/_plugins/stage/manager.py +8 -2
  21. snowflake/cli/api/commands/flags.py +12 -2
  22. snowflake/cli/api/constants.py +2 -0
  23. snowflake/cli/api/errno.py +1 -0
  24. snowflake/cli/api/exceptions.py +7 -0
  25. snowflake/cli/api/feature_flags.py +1 -0
  26. snowflake/cli/api/rest_api.py +2 -3
  27. snowflake/cli/{_app → api}/secret.py +4 -1
  28. snowflake/cli/api/secure_path.py +16 -4
  29. snowflake/cli/api/sql_execution.py +7 -3
  30. {snowflake_cli-3.5.0.dist-info → snowflake_cli-3.6.0.dist-info}/METADATA +7 -7
  31. {snowflake_cli-3.5.0.dist-info → snowflake_cli-3.6.0.dist-info}/RECORD +34 -28
  32. {snowflake_cli-3.5.0.dist-info → snowflake_cli-3.6.0.dist-info}/WHEEL +0 -0
  33. {snowflake_cli-3.5.0.dist-info → snowflake_cli-3.6.0.dist-info}/entry_points.txt +0 -0
  34. {snowflake_cli-3.5.0.dist-info → snowflake_cli-3.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -16,8 +16,9 @@ from __future__ import annotations
16
16
 
17
17
  import logging
18
18
  import os.path
19
+ from copy import deepcopy
19
20
  from pathlib import Path
20
- from typing import Optional
21
+ from typing import Dict, Optional, Tuple
21
22
 
22
23
  import typer
23
24
  from click import ( # type: ignore
@@ -28,10 +29,14 @@ from click import ( # type: ignore
28
29
  )
29
30
  from click.core import ParameterSource # type: ignore
30
31
  from snowflake import connector
32
+ from snowflake.cli._app.snow_connector import connect_to_snowflake
33
+ from snowflake.cli._plugins.auth.keypair.commands import KEY_PAIR_DEFAULT_PATH
34
+ from snowflake.cli._plugins.auth.keypair.manager import AuthManager
31
35
  from snowflake.cli._plugins.connection.util import (
32
36
  strip_if_value_present,
33
37
  )
34
38
  from snowflake.cli._plugins.object.manager import ObjectManager
39
+ from snowflake.cli.api import exceptions
35
40
  from snowflake.cli.api.cli_global_context import get_cli_context
36
41
  from snowflake.cli.api.commands.flags import (
37
42
  PLAIN_PASSWORD_MSG,
@@ -67,6 +72,8 @@ from snowflake.cli.api.output.types import (
67
72
  MessageResult,
68
73
  ObjectResult,
69
74
  )
75
+ from snowflake.cli.api.secret import SecretType
76
+ from snowflake.cli.api.secure_path import SecurePath
70
77
  from snowflake.connector import ProgrammingError
71
78
  from snowflake.connector.constants import CONNECTIONS_FILE
72
79
 
@@ -282,6 +289,13 @@ def add(
282
289
  if connection_exists(connection_name):
283
290
  raise UsageError(f"Connection {connection_name} already exists")
284
291
 
292
+ if not no_interactive:
293
+ connection_options, keypair_error = _extend_add_with_key_pair(
294
+ connection_name, connection_options
295
+ )
296
+ else:
297
+ keypair_error = ""
298
+
285
299
  connections_file = add_connection_to_proper_file(
286
300
  connection_name,
287
301
  ConnectionConfig(**connection_options),
@@ -289,6 +303,12 @@ def add(
289
303
  if set_as_default:
290
304
  set_config_value(path=["default_connection_name"], value=connection_name)
291
305
 
306
+ if keypair_error:
307
+ return MessageResult(
308
+ f"Wrote new password-based connection {connection_name} to {connections_file}, "
309
+ f"however there were some issues during key pair setup. Review the following error and check 'snow auth keypair' "
310
+ f"commands to setup key pair authentication:\n * {keypair_error}"
311
+ )
292
312
  return MessageResult(
293
313
  f"Wrote new connection {connection_name} to {connections_file}"
294
314
  )
@@ -402,3 +422,59 @@ def generate_jwt(
402
422
  return MessageResult(token)
403
423
  except (ValueError, TypeError) as err:
404
424
  raise ClickException(str(err))
425
+
426
+
427
+ def _extend_add_with_key_pair(
428
+ connection_name: str, connection_options: Dict
429
+ ) -> Tuple[Dict, str]:
430
+ if not _should_extend_with_key_pair(connection_options):
431
+ return connection_options, ""
432
+
433
+ configure_key_pair = typer.confirm(
434
+ "Do you want to configure key pair authentication?",
435
+ default=False,
436
+ )
437
+ if not configure_key_pair:
438
+ return connection_options, ""
439
+
440
+ key_length = typer.prompt(
441
+ "Key length",
442
+ default=2048,
443
+ show_default=True,
444
+ )
445
+
446
+ output_path = typer.prompt(
447
+ "Output path",
448
+ default=KEY_PAIR_DEFAULT_PATH,
449
+ show_default=True,
450
+ value_proc=lambda value: SecurePath(value),
451
+ )
452
+ private_key_passphrase = typer.prompt(
453
+ "Private key passphrase",
454
+ default="",
455
+ hide_input=True,
456
+ show_default=False,
457
+ value_proc=lambda value: SecretType(value),
458
+ )
459
+ connection = connect_to_snowflake(temporary_connection=True, **connection_options)
460
+ try:
461
+ connection_options = AuthManager(connection=connection).extend_connection_add(
462
+ connection_name=connection_name,
463
+ connection_options=deepcopy(connection_options),
464
+ key_length=key_length,
465
+ output_path=output_path,
466
+ private_key_passphrase=private_key_passphrase,
467
+ )
468
+ except exceptions.CouldNotSetKeyPairError:
469
+ return connection_options, "The public key is set already."
470
+ except Exception as e:
471
+ return connection_options, str(e)
472
+ return connection_options, ""
473
+
474
+
475
+ def _should_extend_with_key_pair(connection_options: Dict) -> bool:
476
+ return (
477
+ connection_options.get("password") is not None
478
+ and connection_options.get("private_key_file") is None
479
+ and connection_options.get("private_key_path") is None
480
+ )
@@ -669,7 +669,7 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
669
669
  role_to_use=package.role,
670
670
  )
671
671
 
672
- return get_snowflake_facade().create_application(
672
+ create_app_result, warnings = get_snowflake_facade().create_application(
673
673
  name=self.name,
674
674
  package_name=package.name,
675
675
  install_method=install_method,
@@ -680,6 +680,9 @@ class ApplicationEntity(EntityBase[ApplicationEntityModel]):
680
680
  warehouse=self.warehouse,
681
681
  release_channel=release_channel,
682
682
  )
683
+ for warning in warnings:
684
+ self.console.warning(warning)
685
+ return create_app_result
683
686
 
684
687
  @span("update_app_object")
685
688
  def create_or_upgrade_app(
@@ -60,6 +60,7 @@ from snowflake.cli.api.errno import (
60
60
  CANNOT_DISABLE_MANDATORY_TELEMETRY,
61
61
  CANNOT_DISABLE_RELEASE_CHANNELS,
62
62
  CANNOT_MODIFY_RELEASE_CHANNEL_ACCOUNTS,
63
+ CANNOT_SET_DEBUG_MODE_WITH_MANIFEST_VERSION,
63
64
  DOES_NOT_EXIST_OR_CANNOT_BE_PERFORMED,
64
65
  DOES_NOT_EXIST_OR_NOT_AUTHORIZED,
65
66
  INSUFFICIENT_PRIVILEGES,
@@ -854,7 +855,7 @@ class SnowflakeSQLFacade:
854
855
  debug_mode: bool | None,
855
856
  should_authorize_event_sharing: bool | None,
856
857
  release_channel: str | None = None,
857
- ) -> list[tuple[str]]:
858
+ ) -> tuple[list[tuple[str]], list[str]]:
858
859
  """
859
860
  Creates a new application object using an application package,
860
861
  running the setup script of the application package
@@ -868,6 +869,7 @@ class SnowflakeSQLFacade:
868
869
  @param debug_mode: Whether to enable debug mode; None means not explicitly enabled or disabled
869
870
  @param should_authorize_event_sharing: Whether to enable event sharing; None means not explicitly enabled or disabled
870
871
  @param release_channel [Optional]: Release channel to use when creating the application
872
+ @return: a tuple containing the result of the create application query and possible warning messages
871
873
  """
872
874
  package_name = to_identifier(package_name)
873
875
  name = to_identifier(name)
@@ -875,11 +877,9 @@ class SnowflakeSQLFacade:
875
877
 
876
878
  # by default, applications are created in debug mode when possible;
877
879
  # this can be overridden in the project definition
878
- debug_mode_clause = ""
880
+ initial_debug_mode = False
879
881
  if install_method.is_dev_mode:
880
882
  initial_debug_mode = debug_mode if debug_mode is not None else True
881
- debug_mode_clause = f"debug_mode = {initial_debug_mode}"
882
-
883
883
  authorize_telemetry_clause = ""
884
884
  if should_authorize_event_sharing is not None:
885
885
  self._log.info(
@@ -903,13 +903,13 @@ class SnowflakeSQLFacade:
903
903
  from application package {package_name}
904
904
  {using_clause}
905
905
  {release_channel_clause}
906
- {debug_mode_clause}
907
906
  {authorize_telemetry_clause}
908
907
  comment = {SPECIAL_COMMENT}
909
908
  """
910
909
  )
911
910
  ),
912
911
  )
912
+
913
913
  except Exception as err:
914
914
  if isinstance(err, ProgrammingError):
915
915
  if err.errno == APPLICATION_REQUIRES_TELEMETRY_SHARING:
@@ -927,9 +927,36 @@ class SnowflakeSQLFacade:
927
927
  f"Failed to create application {name} with the following error message:\n"
928
928
  f"{err.msg}"
929
929
  ) from err
930
+
930
931
  handle_unclassified_error(err, f"Failed to create application {name}.")
931
932
 
932
- return create_cursor.fetchall()
933
+ warnings = []
934
+ try:
935
+ if initial_debug_mode:
936
+ self._sql_executor.execute_query(
937
+ dedent(
938
+ _strip_empty_lines(
939
+ f"""\
940
+ alter application {name}
941
+ set debug_mode = {initial_debug_mode}
942
+ """
943
+ )
944
+ )
945
+ )
946
+ except Exception as err:
947
+ if (
948
+ isinstance(err, ProgrammingError)
949
+ and err.errno == CANNOT_SET_DEBUG_MODE_WITH_MANIFEST_VERSION
950
+ ):
951
+ warnings.append(
952
+ "Did not apply debug mode to application because the manifest version is set to 2 or higher. Please use session debugging instead."
953
+ )
954
+ else:
955
+ warnings.append(
956
+ f"Failed to set debug mode for application {name}. {str(err)}"
957
+ )
958
+
959
+ return create_cursor.fetchall(), warnings
933
960
 
934
961
  def create_application_package(
935
962
  self,
@@ -36,8 +36,10 @@ def add_object_command_aliases(
36
36
  name_argument: typer.Argument,
37
37
  like_option: Optional[typer.Option],
38
38
  scope_option: Optional[typer.Option],
39
- ommit_commands: List[str] = [],
39
+ ommit_commands: Optional[List[str]] = None,
40
40
  ):
41
+ if ommit_commands is None:
42
+ ommit_commands = list()
41
43
  if "list" not in ommit_commands:
42
44
  if not like_option:
43
45
  raise ClickException('[like_option] have to be defined for "list" command')
@@ -58,14 +58,16 @@ class ObjectManager(SqlExecutionMixin):
58
58
  object_name = _get_object_names(object_type).sf_name
59
59
  return self.execute_query(f"drop {object_name} {fqn.sql_identifier}")
60
60
 
61
- def describe(self, *, object_type: str, fqn: FQN):
61
+ def describe(self, *, object_type: str, fqn: FQN, **kwargs):
62
62
  # Image repository is the only supported object that does not have a DESCRIBE command.
63
63
  if object_type == "image-repository":
64
64
  raise ClickException(
65
65
  f"Describe is currently not supported for object of type image-repository"
66
66
  )
67
67
  object_name = _get_object_names(object_type).sf_name
68
- return self.execute_query(f"describe {object_name} {fqn.sql_identifier}")
68
+ return self.execute_query(
69
+ f"describe {object_name} {fqn.sql_identifier}", **kwargs
70
+ )
69
71
 
70
72
  def object_exists(self, *, object_type: str, fqn: FQN):
71
73
  try:
@@ -15,6 +15,8 @@
15
15
  from typing import List, Optional
16
16
 
17
17
  import typer
18
+ from snowflake.cli._plugins.object.command_aliases import add_object_command_aliases
19
+ from snowflake.cli._plugins.object.commands import scope_option
18
20
  from snowflake.cli._plugins.project.feature_flags import FeatureFlag
19
21
  from snowflake.cli._plugins.project.manager import ProjectManager
20
22
  from snowflake.cli._plugins.project.project_entity_model import (
@@ -27,11 +29,13 @@ from snowflake.cli.api.commands.decorators import with_project_definition
27
29
  from snowflake.cli.api.commands.flags import (
28
30
  entity_argument,
29
31
  identifier_argument,
32
+ like_option,
30
33
  variables_option,
31
34
  )
32
35
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
33
36
  from snowflake.cli.api.commands.utils import get_entity_for_operation
34
37
  from snowflake.cli.api.console.console import cli_console
38
+ from snowflake.cli.api.constants import ObjectType
35
39
  from snowflake.cli.api.identifiers import FQN
36
40
  from snowflake.cli.api.output.types import MessageResult, SingleQueryResult
37
41
  from snowflake.cli.api.project.project_paths import ProjectPaths
@@ -51,6 +55,18 @@ variables_flag = variables_option(
51
55
  )
52
56
 
53
57
 
58
+ add_object_command_aliases(
59
+ app=app,
60
+ object_type=ObjectType.PROJECT,
61
+ name_argument=project_identifier,
62
+ like_option=like_option(
63
+ help_example='`list --like "my%"` lists all projects that begin with “my”'
64
+ ),
65
+ scope_option=scope_option(help_example="`list --in database my_db`"),
66
+ ommit_commands=["drop", "create", "describe"],
67
+ )
68
+
69
+
54
70
  @app.command(requires_connection=True)
55
71
  def execute(
56
72
  identifier: FQN = project_identifier,
@@ -88,9 +88,17 @@ MaxNodesOption = OverrideableOption(
88
88
  _AUTO_RESUME_HELP = "The compute pool will automatically resume when a service or job is submitted to it."
89
89
 
90
90
  AutoResumeOption = OverrideableOption(
91
- True,
92
- "--auto-resume/--no-auto-resume",
91
+ False,
92
+ "--auto-resume",
93
93
  help=_AUTO_RESUME_HELP,
94
+ mutually_exclusive=["no_auto_resume"],
95
+ )
96
+
97
+ NoAutoResumeOption = OverrideableOption(
98
+ False,
99
+ "--no-auto-resume",
100
+ help=_AUTO_RESUME_HELP,
101
+ mutually_exclusive=["auto_resume"],
94
102
  )
95
103
 
96
104
  _AUTO_SUSPEND_SECS_HELP = "Number of seconds of inactivity after which you want Snowflake to automatically suspend the compute pool."
@@ -126,6 +134,7 @@ def create(
126
134
  min_nodes: int = MinNodesOption(),
127
135
  max_nodes: Optional[int] = MaxNodesOption(),
128
136
  auto_resume: bool = AutoResumeOption(),
137
+ no_auto_resume: bool = NoAutoResumeOption(),
129
138
  initially_suspended: bool = typer.Option(
130
139
  False,
131
140
  "--init-suspend/--no-init-suspend",
@@ -140,13 +149,14 @@ def create(
140
149
  """
141
150
  Creates a new compute pool.
142
151
  """
152
+ resume_option = True if auto_resume else False if no_auto_resume else True
143
153
  max_nodes = validate_and_set_instances(min_nodes, max_nodes, "nodes")
144
154
  cursor = ComputePoolManager().create(
145
155
  pool_name=name.identifier,
146
156
  min_nodes=min_nodes,
147
157
  max_nodes=max_nodes,
148
158
  instance_family=instance_family,
149
- auto_resume=auto_resume,
159
+ auto_resume=resume_option,
150
160
  initially_suspended=initially_suspended,
151
161
  auto_suspend_secs=auto_suspend_secs,
152
162
  tags=tags,
@@ -223,7 +233,8 @@ def set_property(
223
233
  name: FQN = ComputePoolNameArgument,
224
234
  min_nodes: Optional[int] = MinNodesOption(default=None, show_default=False),
225
235
  max_nodes: Optional[int] = MaxNodesOption(show_default=False),
226
- auto_resume: Optional[bool] = AutoResumeOption(default=None, show_default=False),
236
+ auto_resume: bool = AutoResumeOption(default=None, show_default=False),
237
+ no_auto_resume: bool = NoAutoResumeOption(default=None, show_default=False),
227
238
  auto_suspend_secs: Optional[int] = AutoSuspendSecsOption(
228
239
  default=None, show_default=False
229
240
  ),
@@ -235,11 +246,12 @@ def set_property(
235
246
  """
236
247
  Sets one or more properties for the compute pool.
237
248
  """
249
+ resume_option = True if auto_resume else False if no_auto_resume else None
238
250
  cursor = ComputePoolManager().set_property(
239
251
  pool_name=name.identifier,
240
252
  min_nodes=min_nodes,
241
253
  max_nodes=max_nodes,
242
- auto_resume=auto_resume,
254
+ auto_resume=resume_option,
243
255
  auto_suspend_secs=auto_suspend_secs,
244
256
  comment=comment,
245
257
  )
@@ -14,23 +14,29 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import logging
17
18
  import sys
18
- from io import StringIO
19
- from itertools import chain
19
+ from functools import partial
20
20
  from pathlib import Path
21
21
  from typing import Dict, Iterable, List, Tuple
22
22
 
23
23
  from click import ClickException, UsageError
24
- from jinja2 import UndefinedError
25
24
  from snowflake.cli._plugins.sql.snowsql_templating import transpile_snowsql_templates
25
+ from snowflake.cli._plugins.sql.source_reader import (
26
+ compile_statements,
27
+ files_reader,
28
+ query_reader,
29
+ )
30
+ from snowflake.cli.api.console import cli_console
26
31
  from snowflake.cli.api.rendering.sql_templates import snowflake_sql_jinja_render
27
- from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
32
+ from snowflake.cli.api.secure_path import SecurePath
28
33
  from snowflake.cli.api.sql_execution import SqlExecutionMixin, VerboseCursor
29
34
  from snowflake.connector.cursor import SnowflakeCursor
30
- from snowflake.connector.util_text import split_statements
31
35
 
32
36
  IsSingleStatement = bool
33
37
 
38
+ logger = logging.getLogger(__name__)
39
+
34
40
 
35
41
  class SqlManager(SqlExecutionMixin):
36
42
  def execute(
@@ -41,57 +47,42 @@ class SqlManager(SqlExecutionMixin):
41
47
  data: Dict | None = None,
42
48
  retain_comments: bool = False,
43
49
  ) -> Tuple[IsSingleStatement, Iterable[SnowflakeCursor]]:
44
- inputs = [query, files, std_in]
45
- # Check if any two inputs were provided simultaneously
46
- if len([i for i in inputs if i]) > 1:
47
- raise UsageError(
48
- "Multiple input sources specified. Please specify only one."
49
- )
50
+ """Reads, transforms and execute statements from input.
50
51
 
51
- if std_in:
52
- query = sys.stdin.read()
53
- if query:
54
- return self._execute_single_query(
55
- query=query, data=data, retain_comments=retain_comments
56
- )
52
+ Only one input can be consumed at a time.
53
+ When no compilation errors are detected, the sequence on queries
54
+ in executed and returned as tuple.
57
55
 
58
- if files:
59
- # Multiple files
60
- results = []
61
- single_statement = False
62
- for file in files:
63
- query_from_file = SecurePath(file).read_text(
64
- file_size_limit_mb=UNLIMITED
65
- )
66
- single_statement, result = self._execute_single_query(
67
- query=query_from_file, data=data, retain_comments=retain_comments
68
- )
69
- results.append(result)
56
+ Throws an exception ff multiple inputs are provided.
57
+ """
58
+ query = sys.stdin.read() if std_in else query
70
59
 
71
- # Use single_statement if there's only one, otherwise this is multi statement result
72
- single_statement = len(files) == 1 and single_statement
73
- return single_statement, chain.from_iterable(results)
60
+ stmt_operators = (
61
+ transpile_snowsql_templates,
62
+ partial(snowflake_sql_jinja_render, data=data),
63
+ )
64
+ remove_comments = not retain_comments
74
65
 
75
- # At that point, no stdin, query or files were provided
76
- raise UsageError("Use either query, filename or input option.")
66
+ if query:
67
+ stmt_reader = query_reader(query, stmt_operators, remove_comments)
68
+ elif files:
69
+ secured_files = [SecurePath(f) for f in files]
70
+ stmt_reader = files_reader(secured_files, stmt_operators, remove_comments)
71
+ else:
72
+ raise UsageError("Use either query, filename or input option.")
77
73
 
78
- def _execute_single_query(
79
- self, query: str, data: Dict | None = None, retain_comments: bool = False
80
- ) -> Tuple[IsSingleStatement, Iterable[SnowflakeCursor]]:
81
- try:
82
- query = transpile_snowsql_templates(query)
83
- query = snowflake_sql_jinja_render(content=query, data=data)
84
- except UndefinedError as err:
85
- raise ClickException(f"SQL template rendering error: {err}")
74
+ errors, stmt_count, compiled_statements = compile_statements(stmt_reader)
75
+ if not any((errors, stmt_count, compiled_statements)):
76
+ raise UsageError("Use either query, filename or input option.")
86
77
 
87
- statements = tuple(
88
- statement
89
- for statement, _ in split_statements(
90
- StringIO(query), remove_comments=not retain_comments
91
- )
92
- )
93
- single_statement = len(statements) == 1
78
+ if errors:
79
+ for error in errors:
80
+ logger.info("Statement compilation error: %s", error)
81
+ cli_console.warning(error)
82
+ raise ClickException("SQL rendering error")
94
83
 
95
- return single_statement, self._execute_string(
96
- "\n".join(statements), cursor_class=VerboseCursor
84
+ is_single_statement = not (stmt_count > 1)
85
+ return is_single_statement, self.execute_string(
86
+ "\n".join(compiled_statements),
87
+ cursor_class=VerboseCursor,
97
88
  )