snowflake-cli 3.0.2__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.
Files changed (84) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +3 -0
  3. snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +1 -1
  4. snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +2 -2
  5. snowflake/cli/_app/telemetry.py +69 -4
  6. snowflake/cli/_plugins/connection/commands.py +152 -99
  7. snowflake/cli/_plugins/connection/util.py +54 -9
  8. snowflake/cli/_plugins/cortex/manager.py +1 -1
  9. snowflake/cli/_plugins/git/commands.py +6 -3
  10. snowflake/cli/_plugins/git/manager.py +9 -4
  11. snowflake/cli/_plugins/nativeapp/artifacts.py +77 -13
  12. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  13. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +7 -0
  14. snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +10 -10
  15. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
  16. snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
  17. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +8 -8
  18. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
  19. snowflake/cli/_plugins/nativeapp/commands.py +144 -188
  20. snowflake/cli/_plugins/nativeapp/constants.py +1 -0
  21. snowflake/cli/_plugins/nativeapp/entities/application.py +564 -351
  22. snowflake/cli/_plugins/nativeapp/entities/application_package.py +583 -929
  23. snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
  24. snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
  25. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
  26. snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
  27. snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
  28. snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
  29. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
  30. snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +88 -117
  31. snowflake/cli/_plugins/nativeapp/version/commands.py +36 -32
  32. snowflake/cli/_plugins/notebook/manager.py +2 -2
  33. snowflake/cli/_plugins/object/commands.py +10 -1
  34. snowflake/cli/_plugins/object/manager.py +13 -5
  35. snowflake/cli/_plugins/snowpark/common.py +63 -21
  36. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -3
  37. snowflake/cli/_plugins/spcs/common.py +29 -0
  38. snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
  39. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  40. snowflake/cli/_plugins/spcs/image_repository/commands.py +4 -37
  41. snowflake/cli/_plugins/spcs/image_repository/manager.py +4 -1
  42. snowflake/cli/_plugins/spcs/services/commands.py +100 -17
  43. snowflake/cli/_plugins/spcs/services/manager.py +108 -16
  44. snowflake/cli/_plugins/sql/commands.py +9 -1
  45. snowflake/cli/_plugins/sql/manager.py +9 -4
  46. snowflake/cli/_plugins/stage/commands.py +28 -19
  47. snowflake/cli/_plugins/stage/diff.py +17 -17
  48. snowflake/cli/_plugins/stage/manager.py +304 -84
  49. snowflake/cli/_plugins/stage/md5.py +1 -1
  50. snowflake/cli/_plugins/streamlit/manager.py +5 -5
  51. snowflake/cli/_plugins/workspace/commands.py +27 -4
  52. snowflake/cli/_plugins/workspace/context.py +38 -0
  53. snowflake/cli/_plugins/workspace/manager.py +23 -13
  54. snowflake/cli/api/cli_global_context.py +4 -3
  55. snowflake/cli/api/commands/flags.py +23 -7
  56. snowflake/cli/api/config.py +30 -9
  57. snowflake/cli/api/connections.py +12 -1
  58. snowflake/cli/api/console/console.py +4 -19
  59. snowflake/cli/api/entities/common.py +4 -2
  60. snowflake/cli/api/entities/utils.py +36 -69
  61. snowflake/cli/api/errno.py +2 -0
  62. snowflake/cli/api/exceptions.py +41 -0
  63. snowflake/cli/api/identifiers.py +8 -0
  64. snowflake/cli/api/metrics.py +223 -7
  65. snowflake/cli/api/output/types.py +1 -1
  66. snowflake/cli/api/project/definition_conversion.py +293 -77
  67. snowflake/cli/api/project/schemas/entities/common.py +11 -0
  68. snowflake/cli/api/project/schemas/project_definition.py +30 -25
  69. snowflake/cli/api/rest_api.py +26 -4
  70. snowflake/cli/api/secure_utils.py +1 -1
  71. snowflake/cli/api/sql_execution.py +40 -29
  72. snowflake/cli/api/stage_path.py +244 -0
  73. snowflake/cli/api/utils/definition_rendering.py +3 -5
  74. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +14 -15
  75. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +78 -77
  76. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
  77. snowflake/cli/_plugins/nativeapp/manager.py +0 -415
  78. snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
  79. snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
  80. snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
  81. snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -98
  82. snowflake/cli/_plugins/workspace/action_context.py +0 -18
  83. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
  84. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -14,4 +14,4 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- VERSION = "3.0.2"
17
+ VERSION = "3.2.0"
@@ -15,6 +15,7 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import logging
18
+ import os
18
19
  import platform
19
20
  import sys
20
21
  from dataclasses import dataclass
@@ -141,6 +142,7 @@ def _info_callback(value: bool):
141
142
  {"key": "python_version", "value": sys.version},
142
143
  {"key": "system_info", "value": platform.platform()},
143
144
  {"key": "feature_flags", "value": get_feature_flags_section()},
145
+ {"key": "SNOWFLAKE_HOME", "value": os.getenv("SNOWFLAKE_HOME")},
144
146
  ],
145
147
  )
146
148
  print_result(result, output_format=OutputFormat.JSON)
@@ -155,6 +157,7 @@ def app_factory() -> SnowCliMainTyper:
155
157
  invoke_without_command=True,
156
158
  epilog=new_version_msg,
157
159
  result_callback=show_new_version_banner_callback(new_version_msg),
160
+ help=f"Snowflake CLI tool for developers [v{__about__.VERSION}]",
158
161
  )
159
162
  def default(
160
163
  ctx: typer.Context,
@@ -5,5 +5,5 @@ Global options
5
5
  ===============================================================================
6
6
  {% for param in options if not param.hidden %}
7
7
  :samp:`{% for p in param.opts %}{{ p }}{{ ", " if not loop.last }}{% endfor %}{% if not param.is_flag %} {{ '{' }}{{ param.name }}{{ '}' }}{% endif %}`
8
- {% if param.help %}{{ " " + param.help | replace("`", "``") }}{% if param.help[-1] != '.' %}.{% endif %}{% else %} TBD{% endif %}
8
+ {% if param.help %}{{ " " + param.help}}{% if param.help[-1] != '.' %}.{% endif %}{% else %} TBD{% endif %}
9
9
  {% endfor %}
@@ -26,7 +26,7 @@ Arguments
26
26
  {{ param.make_metavar().replace("[", "").replace("]", "").lower() }}
27
27
  {%- endif -%}
28
28
  {{ '}' }}`
29
- {% if param.help %}{{ " " + param.help | replace("`", "``") | replace("\n", " ") }}{% if param.help[-1] != '.' %}.{% endif %}{% if param.default %} Default: {{ param.default }}.{% endif %}{% else %} TBD{% endif %}
29
+ {% if param.help %}{{ " " + param.help | replace("\n", " ") }}{% if param.help[-1] != '.' %}.{% endif %}{% if param.default %} Default: {{ param.default }}.{% endif %}{% else %} TBD{% endif %}
30
30
  {% endfor %}
31
31
  {% else %}
32
32
 
@@ -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("`", "``") | 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
 
@@ -14,12 +14,14 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import os
17
18
  import platform
18
19
  import sys
19
20
  from enum import Enum, unique
20
21
  from typing import Any, Dict, Union
21
22
 
22
23
  import click
24
+ import typer
23
25
  from snowflake.cli.__about__ import VERSION
24
26
  from snowflake.cli._app.constants import PARAM_APPLICATION_NAME
25
27
  from snowflake.cli.api.cli_global_context import (
@@ -30,6 +32,7 @@ from snowflake.cli.api.commands.execution_metadata import ExecutionMetadata
30
32
  from snowflake.cli.api.config import get_feature_flags_section
31
33
  from snowflake.cli.api.output.formats import OutputFormat
32
34
  from snowflake.cli.api.utils.error_handling import ignore_exceptions
35
+ from snowflake.connector import ProgrammingError
33
36
  from snowflake.connector.telemetry import (
34
37
  TelemetryData,
35
38
  TelemetryField,
@@ -59,6 +62,7 @@ class CLITelemetryField(Enum):
59
62
  COMMAND_RESULT_STATUS = "command_result_status"
60
63
  COMMAND_OUTPUT_TYPE = "command_output_type"
61
64
  COMMAND_EXECUTION_TIME = "command_execution_time"
65
+ COMMAND_CI_ENVIRONMENT = "command_ci_environment"
62
66
  # Configuration
63
67
  CONFIG_FEATURE_FLAGS = "config_feature_flags"
64
68
  # Metrics
@@ -67,6 +71,9 @@ class CLITelemetryField(Enum):
67
71
  EVENT = "event"
68
72
  ERROR_MSG = "error_msg"
69
73
  ERROR_TYPE = "error_type"
74
+ ERROR_CODE = "error_code"
75
+ ERROR_CAUSE = "error_cause"
76
+ SQL_STATE = "sql_state"
70
77
  IS_CLI_EXCEPTION = "is_cli_exception"
71
78
  # Project context
72
79
  PROJECT_DEFINITION_VERSION = "project_definition_version"
@@ -81,6 +88,43 @@ class TelemetryEvent(Enum):
81
88
  TelemetryDict = Dict[Union[CLITelemetryField, TelemetryField], Any]
82
89
 
83
90
 
91
+ def _is_cli_exception(exception: Exception) -> bool:
92
+ return isinstance(
93
+ exception,
94
+ (
95
+ click.ClickException,
96
+ typer.Exit,
97
+ typer.Abort,
98
+ BrokenPipeError,
99
+ KeyboardInterrupt,
100
+ ),
101
+ )
102
+
103
+
104
+ def _get_additional_exception_information(exception: Exception) -> TelemetryDict:
105
+ """
106
+ Attach the errno and sqlstate if the exception or the
107
+ cause of the exception is a ProgrammingError
108
+ """
109
+ additional_info = {}
110
+
111
+ if isinstance(exception, ProgrammingError):
112
+ additional_info[CLITelemetryField.ERROR_CODE] = exception.errno
113
+ additional_info[CLITelemetryField.SQL_STATE] = exception.sqlstate
114
+
115
+ if exception.__cause__:
116
+ cause = exception.__cause__
117
+ additional_info[CLITelemetryField.ERROR_CAUSE] = type(cause).__name__
118
+
119
+ if isinstance(cause, ProgrammingError):
120
+ if not additional_info.get(CLITelemetryField.ERROR_CODE):
121
+ additional_info[CLITelemetryField.ERROR_CODE] = cause.errno
122
+ if not additional_info.get(CLITelemetryField.SQL_STATE):
123
+ additional_info[CLITelemetryField.SQL_STATE] = cause.sqlstate
124
+
125
+ return additional_info
126
+
127
+
84
128
  def _get_command_metrics() -> TelemetryDict:
85
129
  cli_context = get_cli_context()
86
130
 
@@ -110,9 +154,14 @@ def _find_command_info() -> TelemetryDict:
110
154
 
111
155
 
112
156
  def _get_definition_version() -> str | None:
113
- cli_context = get_cli_context()
114
- if cli_context.project_definition:
115
- return cli_context.project_definition.definition_version
157
+ try:
158
+ cli_context = get_cli_context()
159
+ if cli_context.project_definition:
160
+ return cli_context.project_definition.definition_version
161
+ except Exception:
162
+ # Don't let an invalid project definition file break telemetry
163
+ # (especially for commands that don't normally load it)
164
+ pass
116
165
  return None
117
166
 
118
167
 
@@ -122,6 +171,20 @@ def _get_installation_source() -> CLIInstallationSource:
122
171
  return CLIInstallationSource.PYPI
123
172
 
124
173
 
174
+ def _get_ci_environment_type() -> str:
175
+ if "GITHUB_ACTIONS" in os.environ:
176
+ return "GITHUB_ACTIONS"
177
+ if "GITLAB_CI" in os.environ:
178
+ return "GITLAB_CI"
179
+ if "CIRCLECI" in os.environ:
180
+ return "CIRCLECI"
181
+ if "JENKINS_URL" in os.environ or "HUDSON_URL" in os.environ:
182
+ return "JENKINS"
183
+ if "TF_BUILD" in os.environ:
184
+ return "AZURE_DEVOPS"
185
+ return "UNKNOWN"
186
+
187
+
125
188
  def command_info() -> str:
126
189
  info = _find_command_info()
127
190
  command = ".".join(info[CLITelemetryField.COMMAND])
@@ -148,6 +211,7 @@ class CLITelemetryClient:
148
211
  CLITelemetryField.VERSION_CLI: VERSION,
149
212
  CLITelemetryField.VERSION_OS: platform.platform(),
150
213
  CLITelemetryField.VERSION_PYTHON: python_version(),
214
+ CLITelemetryField.COMMAND_CI_ENVIRONMENT: _get_ci_environment_type(),
151
215
  CLITelemetryField.CONFIG_FEATURE_FLAGS: {
152
216
  k: str(v) for k, v in get_feature_flags_section().items()
153
217
  },
@@ -202,7 +266,7 @@ def log_command_result(execution: ExecutionMetadata):
202
266
  @ignore_exceptions()
203
267
  def log_command_execution_error(exception: Exception, execution: ExecutionMetadata):
204
268
  exception_type: str = type(exception).__name__
205
- is_cli_exception: bool = issubclass(exception.__class__, click.ClickException)
269
+ is_cli_exception: bool = _is_cli_exception(exception)
206
270
  _telemetry.send(
207
271
  {
208
272
  TelemetryField.KEY_TYPE: TelemetryEvent.CMD_EXECUTION_ERROR.value,
@@ -210,6 +274,7 @@ def log_command_execution_error(exception: Exception, execution: ExecutionMetada
210
274
  CLITelemetryField.ERROR_TYPE: exception_type,
211
275
  CLITelemetryField.IS_CLI_EXCEPTION: is_cli_exception,
212
276
  CLITelemetryField.COMMAND_EXECUTION_TIME: execution.get_duration(),
277
+ **_get_additional_exception_information(exception),
213
278
  **_get_command_metrics(),
214
279
  }
215
280
  )
@@ -16,19 +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 ClickException, Context, Parameter # type: ignore
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
30
+ from snowflake import connector
24
31
  from snowflake.cli._plugins.connection.util import (
25
- strip_and_check_if_exists,
26
32
  strip_if_value_present,
27
33
  )
28
34
  from snowflake.cli._plugins.object.manager import ObjectManager
29
35
  from snowflake.cli.api.cli_global_context import get_cli_context
30
36
  from snowflake.cli.api.commands.flags import (
31
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,
32
51
  )
33
52
  from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
34
53
  from snowflake.cli.api.config import (
@@ -63,11 +82,6 @@ class EmptyInput:
63
82
  return "optional"
64
83
 
65
84
 
66
- class OptionalPrompt(StringParamType):
67
- def convert(self, value, param, ctx):
68
- return None if isinstance(value, EmptyInput) else value
69
-
70
-
71
85
  def _mask_password(connection_params: dict):
72
86
  if "password" in connection_params:
73
87
  connection_params["password"] = "****"
@@ -100,7 +114,7 @@ def list_connections(**options) -> CommandResult:
100
114
 
101
115
 
102
116
  def require_integer(field_name: str):
103
- def callback(value: str):
117
+ def callback(ctx: Context, param: Parameter, value: str):
104
118
  if value is None:
105
119
  return None
106
120
  if value.strip().isdigit():
@@ -123,130 +137,91 @@ def add(
123
137
  None,
124
138
  "--connection-name",
125
139
  "-n",
126
- prompt="Name for this connection",
127
140
  help="Name of the new connection.",
128
141
  show_default=False,
129
- callback=strip_if_value_present,
130
142
  ),
131
143
  account: str = typer.Option(
132
144
  None,
133
- "--account",
134
145
  "-a",
135
- "--accountname",
136
- prompt="Snowflake account name",
146
+ *AccountOption.param_decls,
137
147
  help="Account name to use when authenticating with Snowflake.",
138
148
  show_default=False,
139
- callback=strip_if_value_present,
140
149
  ),
141
150
  user: str = typer.Option(
142
151
  None,
143
- "--user",
144
152
  "-u",
145
- "--username",
146
- prompt="Snowflake username",
153
+ *UserOption.param_decls,
147
154
  show_default=False,
148
155
  help="Username to connect to Snowflake.",
149
- callback=strip_if_value_present,
150
156
  ),
151
- password: str = typer.Option(
152
- EmptyInput(),
153
- "--password",
157
+ password: Optional[str] = typer.Option(
158
+ None,
154
159
  "-p",
155
- click_type=OptionalPrompt(),
160
+ *PasswordOption.param_decls,
156
161
  callback=_password_callback,
157
- prompt="Snowflake password",
158
162
  help="Snowflake password.",
159
163
  hide_input=True,
160
164
  ),
161
- role: str = typer.Option(
162
- EmptyInput(),
163
- "--role",
165
+ role: Optional[str] = typer.Option(
166
+ None,
164
167
  "-r",
165
- click_type=OptionalPrompt(),
166
- prompt="Role for the connection",
168
+ *RoleOption.param_decls,
167
169
  help="Role to use on Snowflake.",
168
- callback=strip_if_value_present,
169
170
  ),
170
- warehouse: str = typer.Option(
171
- EmptyInput(),
172
- "--warehouse",
171
+ warehouse: Optional[str] = typer.Option(
172
+ None,
173
173
  "-w",
174
- click_type=OptionalPrompt(),
175
- prompt="Warehouse for the connection",
174
+ *WarehouseOption.param_decls,
176
175
  help="Warehouse to use on Snowflake.",
177
- callback=strip_if_value_present,
178
176
  ),
179
- database: str = typer.Option(
180
- EmptyInput(),
181
- "--database",
177
+ database: Optional[str] = typer.Option(
178
+ None,
182
179
  "-d",
183
- click_type=OptionalPrompt(),
184
- prompt="Database for the connection",
180
+ *DatabaseOption.param_decls,
185
181
  help="Database to use on Snowflake.",
186
- callback=strip_if_value_present,
187
182
  ),
188
- schema: str = typer.Option(
189
- EmptyInput(),
190
- "--schema",
183
+ schema: Optional[str] = typer.Option(
184
+ None,
191
185
  "-s",
192
- click_type=OptionalPrompt(),
193
- prompt="Schema for the connection",
186
+ *SchemaOption.param_decls,
194
187
  help="Schema to use on Snowflake.",
195
- callback=strip_if_value_present,
196
188
  ),
197
- host: str = typer.Option(
198
- EmptyInput(),
199
- "--host",
189
+ host: Optional[str] = typer.Option(
190
+ None,
200
191
  "-h",
201
- click_type=OptionalPrompt(),
202
- prompt="Connection host",
192
+ *HostOption.param_decls,
203
193
  help="Host name the connection attempts to connect to Snowflake.",
204
- callback=strip_if_value_present,
205
194
  ),
206
- port: int = typer.Option(
207
- EmptyInput(),
208
- "--port",
195
+ port: Optional[int] = typer.Option(
196
+ None,
209
197
  "-P",
210
- click_type=OptionalPrompt(),
211
- prompt="Connection port",
198
+ *PortOption.param_decls,
212
199
  help="Port to communicate with on the host.",
213
- callback=require_integer(field_name="port"),
214
200
  ),
215
- region: str = typer.Option(
216
- EmptyInput(),
201
+ region: Optional[str] = typer.Option(
202
+ None,
217
203
  "--region",
218
204
  "-R",
219
- click_type=OptionalPrompt(),
220
- prompt="Snowflake region",
221
205
  help="Region name if not the default Snowflake deployment.",
222
- callback=strip_if_value_present,
223
206
  ),
224
- authenticator: str = typer.Option(
225
- EmptyInput(),
226
- "--authenticator",
207
+ authenticator: Optional[str] = typer.Option(
208
+ None,
227
209
  "-A",
228
- click_type=OptionalPrompt(),
229
- prompt="Authentication method",
210
+ *AuthenticatorOption.param_decls,
230
211
  help="Chosen authenticator, if other than password-based",
231
212
  ),
232
- private_key_file: str = typer.Option(
233
- EmptyInput(),
213
+ private_key_file: Optional[str] = typer.Option(
214
+ None,
234
215
  "--private-key",
235
- "--private-key-path",
236
216
  "-k",
237
- click_type=OptionalPrompt(),
238
- prompt="Path to private key file",
217
+ *PrivateKeyPathOption.param_decls,
239
218
  help="Path to file containing private key",
240
- callback=strip_and_check_if_exists,
241
219
  ),
242
- token_file_path: str = typer.Option(
243
- EmptyInput(),
244
- "--token-file-path",
220
+ token_file_path: Optional[str] = typer.Option(
221
+ None,
245
222
  "-t",
246
- click_type=OptionalPrompt(),
247
- prompt="Path to token file",
223
+ *TokenFilePathOption.param_decls,
248
224
  help="Path to file with an OAuth token that should be used when connecting to Snowflake",
249
- callback=strip_and_check_if_exists,
250
225
  ),
251
226
  set_as_default: bool = typer.Option(
252
227
  False,
@@ -254,29 +229,62 @@ def add(
254
229
  is_flag=True,
255
230
  help="If provided the connection will be configured as default connection.",
256
231
  ),
232
+ no_interactive: bool = NoInteractiveOption,
257
233
  **options,
258
234
  ) -> CommandResult:
259
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
+
260
282
  if connection_exists(connection_name):
261
- raise ClickException(f"Connection {connection_name} already exists")
283
+ raise UsageError(f"Connection {connection_name} already exists")
262
284
 
263
285
  connections_file = add_connection_to_proper_file(
264
286
  connection_name,
265
- ConnectionConfig(
266
- account=account,
267
- user=user,
268
- password=password,
269
- host=host,
270
- region=region,
271
- port=port,
272
- database=database,
273
- schema=schema,
274
- warehouse=warehouse,
275
- role=role,
276
- authenticator=authenticator,
277
- private_key_file=private_key_file,
278
- token_file_path=token_file_path,
279
- ),
287
+ ConnectionConfig(**connection_options),
280
288
  )
281
289
  if set_as_default:
282
290
  set_config_value(
@@ -342,7 +350,7 @@ def test(
342
350
  @app.command(requires_connection=False)
343
351
  def set_default(
344
352
  name: str = typer.Argument(
345
- help="Name of the connection, as defined in your `config.toml`",
353
+ help="Name of the connection, as defined in your `config.toml` file",
346
354
  show_default=False,
347
355
  ),
348
356
  **options,
@@ -351,3 +359,48 @@ def set_default(
351
359
  get_connection_dict(connection_name=name)
352
360
  set_config_value(section=None, key="default_connection_name", value=name)
353
361
  return MessageResult(f"Default connection set to: {name}")
362
+
363
+
364
+ @app.command(requires_connection=True)
365
+ def generate_jwt(
366
+ **options,
367
+ ) -> CommandResult:
368
+ """Generate a JWT token, which will be printed out and displayed.."""
369
+ connection_details = get_cli_context().connection_context.update_from_config()
370
+
371
+ msq_template = (
372
+ "{} is not set in the connection context, but required for JWT generation."
373
+ )
374
+ if not connection_details.user:
375
+ raise UsageError(msq_template.format("User"))
376
+ if not connection_details.account:
377
+ raise UsageError(msq_template.format("Account"))
378
+ if not connection_details.private_key_file:
379
+ raise UsageError(msq_template.format("Private key file"))
380
+
381
+ passphrase = os.getenv("PRIVATE_KEY_PASSPHRASE", None)
382
+
383
+ def _decrypt(passphrase: str | None):
384
+ return connector.auth.get_token_from_private_key(
385
+ user=connection_details.user,
386
+ account=connection_details.account,
387
+ privatekey_path=connection_details.private_key_file,
388
+ key_password=passphrase,
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)
404
+ return MessageResult(token)
405
+ except (ValueError, TypeError) as err:
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 typing import Optional
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
- *_, cursor = conn.execute_string(REGIONLESS_QUERY, cursor_class=DictCursor)
66
- return cursor.fetchone()["REGIONLESS"].lower() == "true"
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._execute_query(query, cursor_class=DictCursor)
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"])
@@ -333,12 +333,15 @@ def execute(
333
333
  **options,
334
334
  ):
335
335
  """
336
- Execute immediate all files from the repository path. Files can be filtered with glob like pattern,
336
+ Execute immediate all files from the repository path. Files can be filtered with a glob-like pattern,
337
337
  e.g. `@my_repo/branches/main/*.sql`, `@my_repo/branches/main/dev/*`. Only files with `.sql`
338
- extension will be executed.
338
+ or `.py` extension will be executed.
339
339
  """
340
340
  results = GitManager().execute(
341
- stage_path=repository_path, on_error=on_error, variables=variables
341
+ stage_path_str=repository_path,
342
+ on_error=on_error,
343
+ variables=variables,
344
+ requires_temporary_stage=True,
342
345
  )
343
346
  return CollectionResult(results)
344
347