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.
- snowflake/cli/__about__.py +1 -1
- snowflake/cli/_app/cli_app.py +3 -0
- snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +1 -1
- snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +2 -2
- snowflake/cli/_app/telemetry.py +69 -4
- snowflake/cli/_plugins/connection/commands.py +152 -99
- snowflake/cli/_plugins/connection/util.py +54 -9
- snowflake/cli/_plugins/cortex/manager.py +1 -1
- snowflake/cli/_plugins/git/commands.py +6 -3
- snowflake/cli/_plugins/git/manager.py +9 -4
- snowflake/cli/_plugins/nativeapp/artifacts.py +77 -13
- snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/compiler.py +7 -0
- snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +10 -10
- snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
- snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +8 -8
- snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +5 -3
- snowflake/cli/_plugins/nativeapp/commands.py +144 -188
- snowflake/cli/_plugins/nativeapp/constants.py +1 -0
- snowflake/cli/_plugins/nativeapp/entities/application.py +564 -351
- snowflake/cli/_plugins/nativeapp/entities/application_package.py +583 -929
- snowflake/cli/_plugins/nativeapp/entities/models/event_sharing_telemetry.py +58 -0
- snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
- snowflake/cli/_plugins/nativeapp/same_account_install_method.py +0 -2
- snowflake/cli/_plugins/nativeapp/sf_facade.py +30 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_constants.py +25 -0
- snowflake/cli/_plugins/nativeapp/sf_facade_exceptions.py +117 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +525 -0
- snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +88 -117
- snowflake/cli/_plugins/nativeapp/version/commands.py +36 -32
- snowflake/cli/_plugins/notebook/manager.py +2 -2
- snowflake/cli/_plugins/object/commands.py +10 -1
- snowflake/cli/_plugins/object/manager.py +13 -5
- snowflake/cli/_plugins/snowpark/common.py +63 -21
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +3 -3
- snowflake/cli/_plugins/spcs/common.py +29 -0
- snowflake/cli/_plugins/spcs/compute_pool/manager.py +7 -9
- snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
- snowflake/cli/_plugins/spcs/image_repository/commands.py +4 -37
- snowflake/cli/_plugins/spcs/image_repository/manager.py +4 -1
- snowflake/cli/_plugins/spcs/services/commands.py +100 -17
- snowflake/cli/_plugins/spcs/services/manager.py +108 -16
- snowflake/cli/_plugins/sql/commands.py +9 -1
- snowflake/cli/_plugins/sql/manager.py +9 -4
- snowflake/cli/_plugins/stage/commands.py +28 -19
- snowflake/cli/_plugins/stage/diff.py +17 -17
- snowflake/cli/_plugins/stage/manager.py +304 -84
- snowflake/cli/_plugins/stage/md5.py +1 -1
- snowflake/cli/_plugins/streamlit/manager.py +5 -5
- snowflake/cli/_plugins/workspace/commands.py +27 -4
- snowflake/cli/_plugins/workspace/context.py +38 -0
- snowflake/cli/_plugins/workspace/manager.py +23 -13
- snowflake/cli/api/cli_global_context.py +4 -3
- snowflake/cli/api/commands/flags.py +23 -7
- snowflake/cli/api/config.py +30 -9
- snowflake/cli/api/connections.py +12 -1
- snowflake/cli/api/console/console.py +4 -19
- snowflake/cli/api/entities/common.py +4 -2
- snowflake/cli/api/entities/utils.py +36 -69
- snowflake/cli/api/errno.py +2 -0
- snowflake/cli/api/exceptions.py +41 -0
- snowflake/cli/api/identifiers.py +8 -0
- snowflake/cli/api/metrics.py +223 -7
- snowflake/cli/api/output/types.py +1 -1
- snowflake/cli/api/project/definition_conversion.py +293 -77
- snowflake/cli/api/project/schemas/entities/common.py +11 -0
- snowflake/cli/api/project/schemas/project_definition.py +30 -25
- snowflake/cli/api/rest_api.py +26 -4
- snowflake/cli/api/secure_utils.py +1 -1
- snowflake/cli/api/sql_execution.py +40 -29
- snowflake/cli/api/stage_path.py +244 -0
- snowflake/cli/api/utils/definition_rendering.py +3 -5
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/METADATA +14 -15
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/RECORD +78 -77
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/WHEEL +1 -1
- snowflake/cli/_plugins/nativeapp/manager.py +0 -415
- snowflake/cli/_plugins/nativeapp/project_model.py +0 -211
- snowflake/cli/_plugins/nativeapp/run_processor.py +0 -184
- snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
- snowflake/cli/_plugins/nativeapp/version/version_processor.py +0 -98
- snowflake/cli/_plugins/workspace/action_context.py +0 -18
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.2.0.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/__about__.py
CHANGED
snowflake/cli/_app/cli_app.py
CHANGED
|
@@ -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
|
|
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("
|
|
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("
|
|
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
|
|
snowflake/cli/_app/telemetry.py
CHANGED
|
@@ -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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
"--password",
|
|
157
|
+
password: Optional[str] = typer.Option(
|
|
158
|
+
None,
|
|
154
159
|
"-p",
|
|
155
|
-
|
|
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
|
-
|
|
163
|
-
"--role",
|
|
165
|
+
role: Optional[str] = typer.Option(
|
|
166
|
+
None,
|
|
164
167
|
"-r",
|
|
165
|
-
|
|
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
|
-
|
|
172
|
-
"--warehouse",
|
|
171
|
+
warehouse: Optional[str] = typer.Option(
|
|
172
|
+
None,
|
|
173
173
|
"-w",
|
|
174
|
-
|
|
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
|
-
|
|
181
|
-
"--database",
|
|
177
|
+
database: Optional[str] = typer.Option(
|
|
178
|
+
None,
|
|
182
179
|
"-d",
|
|
183
|
-
|
|
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
|
-
|
|
190
|
-
"--schema",
|
|
183
|
+
schema: Optional[str] = typer.Option(
|
|
184
|
+
None,
|
|
191
185
|
"-s",
|
|
192
|
-
|
|
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
|
-
|
|
199
|
-
"--host",
|
|
189
|
+
host: Optional[str] = typer.Option(
|
|
190
|
+
None,
|
|
200
191
|
"-h",
|
|
201
|
-
|
|
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
|
-
|
|
208
|
-
"--port",
|
|
195
|
+
port: Optional[int] = typer.Option(
|
|
196
|
+
None,
|
|
209
197
|
"-P",
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
"--authenticator",
|
|
207
|
+
authenticator: Optional[str] = typer.Option(
|
|
208
|
+
None,
|
|
227
209
|
"-A",
|
|
228
|
-
|
|
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
|
-
|
|
213
|
+
private_key_file: Optional[str] = typer.Option(
|
|
214
|
+
None,
|
|
234
215
|
"--private-key",
|
|
235
|
-
"--private-key-path",
|
|
236
216
|
"-k",
|
|
237
|
-
|
|
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
|
-
|
|
244
|
-
"--token-file-path",
|
|
220
|
+
token_file_path: Optional[str] = typer.Option(
|
|
221
|
+
None,
|
|
245
222
|
"-t",
|
|
246
|
-
|
|
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
|
|
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
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from functools import lru_cache
|
|
22
|
+
from textwrap import dedent
|
|
23
|
+
from typing import Any, Dict, Optional
|
|
21
24
|
|
|
22
25
|
from click.exceptions import ClickException
|
|
23
26
|
from snowflake.connector import SnowflakeConnection
|
|
@@ -25,12 +28,6 @@ from snowflake.connector.cursor import DictCursor
|
|
|
25
28
|
|
|
26
29
|
log = logging.getLogger(__name__)
|
|
27
30
|
|
|
28
|
-
REGIONLESS_QUERY = """
|
|
29
|
-
select value['value'] as REGIONLESS from table(flatten(
|
|
30
|
-
input => parse_json(SYSTEM$BOOTSTRAP_DATA_REQUEST()),
|
|
31
|
-
path => 'clientParamsInfo'
|
|
32
|
-
)) where value['name'] = 'UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT';
|
|
33
|
-
"""
|
|
34
31
|
|
|
35
32
|
ALLOWLIST_QUERY = "SELECT SYSTEM$ALLOWLIST()"
|
|
36
33
|
SNOWFLAKE_DEPLOYMENT = "SNOWFLAKE_DEPLOYMENT"
|
|
@@ -54,6 +51,50 @@ class MissingConnectionRegionError(ClickException):
|
|
|
54
51
|
)
|
|
55
52
|
|
|
56
53
|
|
|
54
|
+
class UIParameter(Enum):
|
|
55
|
+
NA_ENABLE_REGIONLESS_REDIRECT = "UI_SNOWSIGHT_ENABLE_REGIONLESS_REDIRECT"
|
|
56
|
+
NA_EVENT_SHARING_V2 = "ENABLE_EVENT_SHARING_V2_IN_THE_SAME_ACCOUNT"
|
|
57
|
+
NA_ENFORCE_MANDATORY_FILTERS = (
|
|
58
|
+
"ENFORCE_MANDATORY_FILTERS_FOR_SAME_ACCOUNT_INSTALLATION"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_ui_parameter(
|
|
63
|
+
conn: SnowflakeConnection, parameter: UIParameter, default: Any
|
|
64
|
+
) -> str:
|
|
65
|
+
"""
|
|
66
|
+
Returns the value of a single UI parameter.
|
|
67
|
+
If the parameter is not found, the default value is returned.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
ui_parameters = get_ui_parameters(conn)
|
|
71
|
+
return ui_parameters.get(parameter, default)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@lru_cache()
|
|
75
|
+
def get_ui_parameters(conn: SnowflakeConnection) -> Dict[UIParameter, Any]:
|
|
76
|
+
"""
|
|
77
|
+
Returns the UI parameters from the SYSTEM$BOOTSTRAP_DATA_REQUEST function
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
parameters_to_fetch = sorted([param.value for param in UIParameter])
|
|
81
|
+
|
|
82
|
+
query = dedent(
|
|
83
|
+
f"""
|
|
84
|
+
select value['value']::string as PARAM_VALUE, value['name']::string as PARAM_NAME from table(flatten(
|
|
85
|
+
input => parse_json(SYSTEM$BOOTSTRAP_DATA_REQUEST()),
|
|
86
|
+
path => 'clientParamsInfo'
|
|
87
|
+
)) where value['name'] in ('{"', '".join(parameters_to_fetch)}');
|
|
88
|
+
"""
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
*_, cursor = conn.execute_string(query, cursor_class=DictCursor)
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
UIParameter(row["PARAM_NAME"]): row["PARAM_VALUE"] for row in cursor.fetchall()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
57
98
|
def is_regionless_redirect(conn: SnowflakeConnection) -> bool:
|
|
58
99
|
"""
|
|
59
100
|
Determines if the deployment this connection refers to uses
|
|
@@ -62,8 +103,12 @@ def is_regionless_redirect(conn: SnowflakeConnection) -> bool:
|
|
|
62
103
|
assume it's regionless, as this is true for most production deployments.
|
|
63
104
|
"""
|
|
64
105
|
try:
|
|
65
|
-
|
|
66
|
-
|
|
106
|
+
return (
|
|
107
|
+
get_ui_parameter(
|
|
108
|
+
conn, UIParameter.NA_ENABLE_REGIONLESS_REDIRECT, "true"
|
|
109
|
+
).lower()
|
|
110
|
+
== "true"
|
|
111
|
+
)
|
|
67
112
|
except:
|
|
68
113
|
log.warning(
|
|
69
114
|
"Cannot determine regionless redirect; assuming True.", exc_info=True
|
|
@@ -180,7 +180,7 @@ class CortexManager(SqlExecutionMixin):
|
|
|
180
180
|
|
|
181
181
|
def _query_cortex_result_str(self, query: str) -> str:
|
|
182
182
|
try:
|
|
183
|
-
cursor = self.
|
|
183
|
+
cursor = self.execute_query(query, cursor_class=DictCursor)
|
|
184
184
|
if cursor.rowcount is None:
|
|
185
185
|
raise SnowflakeSQLExecutionError(query)
|
|
186
186
|
return str(cursor.fetchone()["CORTEX_RESULT"])
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|