snowflake-cli 3.8.3__py3-none-any.whl → 3.9.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/commands_registration/builtin_plugins.py +2 -0
- snowflake/cli/_app/version_check.py +14 -4
- snowflake/cli/_plugins/cortex/commands.py +34 -8
- snowflake/cli/_plugins/cortex/constants.py +2 -1
- snowflake/cli/_plugins/cortex/manager.py +81 -21
- snowflake/cli/_plugins/dbt/__init__.py +13 -0
- snowflake/cli/_plugins/dbt/commands.py +187 -0
- snowflake/cli/_plugins/dbt/constants.py +41 -0
- snowflake/cli/_plugins/dbt/manager.py +182 -0
- snowflake/cli/_plugins/dbt/plugin_spec.py +30 -0
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +4 -1
- snowflake/cli/_plugins/project/commands.py +26 -3
- snowflake/cli/_plugins/project/manager.py +6 -1
- snowflake/cli/_plugins/snowpark/snowpark_entity.py +1 -1
- snowflake/cli/_plugins/stage/commands.py +14 -3
- snowflake/cli/_plugins/stage/manager.py +39 -19
- snowflake/cli/_plugins/streamlit/streamlit_entity.py +19 -30
- snowflake/cli/api/commands/snow_typer.py +3 -0
- snowflake/cli/api/constants.py +2 -0
- snowflake/cli/api/entities/common.py +0 -52
- snowflake/cli/api/feature_flags.py +1 -0
- snowflake/cli/api/rest_api.py +3 -2
- snowflake/cli/api/secure_path.py +9 -0
- snowflake/cli/api/sql_execution.py +12 -9
- {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.0.dist-info}/METADATA +3 -3
- {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.0.dist-info}/RECORD +30 -25
- {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.0.dist-info}/licenses/LICENSE +0 -0
snowflake/cli/__about__.py
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
from snowflake.cli._plugins.auth.keypair import plugin_spec as auth_plugin_spec
|
|
16
16
|
from snowflake.cli._plugins.connection import plugin_spec as connection_plugin_spec
|
|
17
17
|
from snowflake.cli._plugins.cortex import plugin_spec as cortex_plugin_spec
|
|
18
|
+
from snowflake.cli._plugins.dbt import plugin_spec as dbt_plugin_spec
|
|
18
19
|
from snowflake.cli._plugins.git import plugin_spec as git_plugin_spec
|
|
19
20
|
from snowflake.cli._plugins.helpers import plugin_spec as migrate_plugin_spec
|
|
20
21
|
from snowflake.cli._plugins.init import plugin_spec as init_plugin_spec
|
|
@@ -52,6 +53,7 @@ def get_builtin_plugin_name_to_plugin_spec():
|
|
|
52
53
|
"init": init_plugin_spec,
|
|
53
54
|
"workspace": workspace_plugin_spec,
|
|
54
55
|
"plugin": plugin_plugin_spec,
|
|
56
|
+
"dbt": dbt_plugin_spec,
|
|
55
57
|
"logs": logs_plugin_spec,
|
|
56
58
|
}
|
|
57
59
|
|
|
@@ -9,7 +9,8 @@ from snowflake.cli.api.cli_global_context import get_cli_context
|
|
|
9
9
|
from snowflake.cli.api.secure_path import SecurePath
|
|
10
10
|
from snowflake.connector.config_manager import CONFIG_MANAGER
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
REPOSITORY_URL_PIP = "https://pypi.org/pypi/snowflake-cli/json"
|
|
13
|
+
REPOSITORY_URL_BREW = "https://formulae.brew.sh/api/formula/snowflake-cli.json"
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def get_new_version_msg() -> str | None:
|
|
@@ -48,12 +49,21 @@ class _VersionCache:
|
|
|
48
49
|
@staticmethod
|
|
49
50
|
def _get_version_from_pypi() -> str | None:
|
|
50
51
|
headers = {"Content-Type": "application/vnd.pypi.simple.v1+json"}
|
|
51
|
-
response = requests.get(
|
|
52
|
+
response = requests.get(REPOSITORY_URL_PIP, headers=headers, timeout=3)
|
|
52
53
|
response.raise_for_status()
|
|
53
|
-
return response.json()
|
|
54
|
+
return response.json().get("info", {}).get("version", None)
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _get_version_from_brew() -> str | None:
|
|
58
|
+
response = requests.get(REPOSITORY_URL_BREW, timeout=3)
|
|
59
|
+
response.raise_for_status()
|
|
60
|
+
return response.json().get("versions", {}).get("stable", None)
|
|
54
61
|
|
|
55
62
|
def _update_latest_version(self) -> Version | None:
|
|
56
|
-
version
|
|
63
|
+
# Use brew version, fallback to pypi if brew is not available.
|
|
64
|
+
# Brew repo takes longer to propagate the upgrade and is triggered later in our release process,
|
|
65
|
+
# we treat it as "slowest point" and determinant that the released version is available.
|
|
66
|
+
version = self._get_version_from_brew() or self._get_version_from_pypi()
|
|
57
67
|
if version is None:
|
|
58
68
|
return None
|
|
59
69
|
self._save_latest_version(version)
|
|
@@ -15,13 +15,17 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import sys
|
|
18
|
+
from enum import Enum
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
from typing import List, Optional
|
|
20
21
|
|
|
21
22
|
import click
|
|
22
23
|
import typer
|
|
23
24
|
from click import UsageError
|
|
24
|
-
from snowflake.cli._plugins.cortex.constants import
|
|
25
|
+
from snowflake.cli._plugins.cortex.constants import (
|
|
26
|
+
DEFAULT_BACKEND,
|
|
27
|
+
DEFAULT_MODEL,
|
|
28
|
+
)
|
|
25
29
|
from snowflake.cli._plugins.cortex.manager import CortexManager
|
|
26
30
|
from snowflake.cli._plugins.cortex.types import (
|
|
27
31
|
Language,
|
|
@@ -36,7 +40,7 @@ from snowflake.cli.api.commands.overrideable_parameter import (
|
|
|
36
40
|
OverrideableOption,
|
|
37
41
|
)
|
|
38
42
|
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
39
|
-
from snowflake.cli.api.constants import PYTHON_3_12
|
|
43
|
+
from snowflake.cli.api.constants import DEFAULT_SIZE_LIMIT_MB, PYTHON_3_12
|
|
40
44
|
from snowflake.cli.api.output.types import (
|
|
41
45
|
CollectionResult,
|
|
42
46
|
CommandResult,
|
|
@@ -115,6 +119,11 @@ def search(
|
|
|
115
119
|
return CollectionResult(response.results)
|
|
116
120
|
|
|
117
121
|
|
|
122
|
+
class Backend(Enum):
|
|
123
|
+
SQL = "sql"
|
|
124
|
+
REST = "rest"
|
|
125
|
+
|
|
126
|
+
|
|
118
127
|
@app.command(
|
|
119
128
|
name="complete",
|
|
120
129
|
requires_connection=True,
|
|
@@ -130,6 +139,11 @@ def complete(
|
|
|
130
139
|
"--model",
|
|
131
140
|
help="String specifying the model to be used.",
|
|
132
141
|
),
|
|
142
|
+
backend: Optional[Backend] = typer.Option(
|
|
143
|
+
DEFAULT_BACKEND,
|
|
144
|
+
"--backend",
|
|
145
|
+
help="String specifying whether to use sql or rest backend.",
|
|
146
|
+
),
|
|
133
147
|
file: Optional[Path] = ExclusiveReadableFileOption(
|
|
134
148
|
help="JSON file containing conversation history to be used to generate a completion. Cannot be combined with TEXT argument.",
|
|
135
149
|
),
|
|
@@ -143,18 +157,30 @@ def complete(
|
|
|
143
157
|
|
|
144
158
|
manager = CortexManager()
|
|
145
159
|
|
|
160
|
+
is_file_input: bool = False
|
|
146
161
|
if text:
|
|
147
|
-
|
|
148
|
-
|
|
162
|
+
prompt = text
|
|
163
|
+
elif file:
|
|
164
|
+
prompt = SecurePath(file).read_text(file_size_limit_mb=DEFAULT_SIZE_LIMIT_MB)
|
|
165
|
+
is_file_input = True
|
|
166
|
+
else:
|
|
167
|
+
raise UsageError("Either --file option or TEXT argument has to be provided.")
|
|
168
|
+
|
|
169
|
+
if backend == Backend.SQL:
|
|
170
|
+
result_text = manager.complete(
|
|
171
|
+
text=Text(prompt),
|
|
149
172
|
model=Model(model),
|
|
173
|
+
is_file_input=is_file_input,
|
|
150
174
|
)
|
|
151
|
-
elif
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
elif backend == Backend.REST:
|
|
176
|
+
root = get_cli_context().snow_api_root
|
|
177
|
+
result_text = manager.rest_complete(
|
|
178
|
+
text=Text(prompt),
|
|
154
179
|
model=Model(model),
|
|
180
|
+
root=root,
|
|
155
181
|
)
|
|
156
182
|
else:
|
|
157
|
-
raise UsageError("
|
|
183
|
+
raise UsageError("--backend option should be either rest or sql.")
|
|
158
184
|
|
|
159
185
|
return MessageResult(result_text.strip())
|
|
160
186
|
|
|
@@ -32,43 +32,103 @@ from snowflake.cli.api.secure_path import SecurePath
|
|
|
32
32
|
from snowflake.cli.api.sql_execution import SqlExecutionMixin
|
|
33
33
|
from snowflake.connector import ProgrammingError
|
|
34
34
|
from snowflake.connector.cursor import DictCursor
|
|
35
|
+
from snowflake.core._root import Root
|
|
36
|
+
from snowflake.core.cortex.inference_service import CortexInferenceService
|
|
37
|
+
from snowflake.core.cortex.inference_service._generated.models import CompleteRequest
|
|
38
|
+
from snowflake.core.cortex.inference_service._generated.models.complete_request_messages_inner import (
|
|
39
|
+
CompleteRequestMessagesInner,
|
|
40
|
+
)
|
|
35
41
|
|
|
36
42
|
log = logging.getLogger(__name__)
|
|
37
43
|
|
|
38
44
|
|
|
45
|
+
class ResponseParseError(Exception):
|
|
46
|
+
"""This exception is raised when the server response cannot be parsed."""
|
|
47
|
+
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MidStreamError(Exception):
|
|
52
|
+
"""The SSE (Server-sent Event) stream can contain error messages in the middle of the stream,
|
|
53
|
+
using the “error” event type. This exception is raised when there is such a mid-stream error."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
reason: Optional[str] = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
message = ""
|
|
60
|
+
if reason is not None:
|
|
61
|
+
message = reason
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
|
|
64
|
+
|
|
39
65
|
class CortexManager(SqlExecutionMixin):
|
|
40
|
-
def
|
|
66
|
+
def complete(
|
|
41
67
|
self,
|
|
42
68
|
text: Text,
|
|
43
69
|
model: Model,
|
|
70
|
+
is_file_input: bool = False,
|
|
44
71
|
) -> str:
|
|
45
|
-
|
|
72
|
+
if not is_file_input:
|
|
73
|
+
query = f"""\
|
|
74
|
+
SELECT SNOWFLAKE.CORTEX.COMPLETE(
|
|
75
|
+
'{model}',
|
|
76
|
+
'{self._escape_input(text)}'
|
|
77
|
+
) AS CORTEX_RESULT;"""
|
|
78
|
+
return self._query_cortex_result_str(query)
|
|
79
|
+
else:
|
|
80
|
+
query = f"""\
|
|
46
81
|
SELECT SNOWFLAKE.CORTEX.COMPLETE(
|
|
47
82
|
'{model}',
|
|
48
|
-
'{self._escape_input(text)}'
|
|
83
|
+
PARSE_JSON('{self._escape_input(text)}'),
|
|
84
|
+
{{}}
|
|
49
85
|
) AS CORTEX_RESULT;"""
|
|
50
|
-
|
|
86
|
+
raw_result = self._query_cortex_result_str(query)
|
|
87
|
+
json_result = json.loads(raw_result)
|
|
88
|
+
return self._extract_text_result_from_json_result(
|
|
89
|
+
lambda: json_result["choices"][0]["messages"]
|
|
90
|
+
)
|
|
51
91
|
|
|
52
|
-
def
|
|
92
|
+
def make_rest_complete_request(
|
|
53
93
|
self,
|
|
54
|
-
conversation_json_file: SecurePath,
|
|
55
94
|
model: Model,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
'{model}',
|
|
63
|
-
PARSE_JSON('{self._escape_input(json_content)}'),
|
|
64
|
-
{{}}
|
|
65
|
-
) AS CORTEX_RESULT;"""
|
|
66
|
-
raw_result = self._query_cortex_result_str(query)
|
|
67
|
-
json_result = json.loads(raw_result)
|
|
68
|
-
return self._extract_text_result_from_json_result(
|
|
69
|
-
lambda: json_result["choices"][0]["messages"]
|
|
95
|
+
prompt: Text,
|
|
96
|
+
) -> CompleteRequest:
|
|
97
|
+
return CompleteRequest(
|
|
98
|
+
model=str(model),
|
|
99
|
+
messages=[CompleteRequestMessagesInner(content=str(prompt))],
|
|
100
|
+
stream=True,
|
|
70
101
|
)
|
|
71
102
|
|
|
103
|
+
def rest_complete(
|
|
104
|
+
self,
|
|
105
|
+
text: Text,
|
|
106
|
+
model: Model,
|
|
107
|
+
root: "Root",
|
|
108
|
+
) -> str:
|
|
109
|
+
complete_request = self.make_rest_complete_request(model=model, prompt=text)
|
|
110
|
+
cortex_inference_service = CortexInferenceService(root=root)
|
|
111
|
+
try:
|
|
112
|
+
raw_resp = cortex_inference_service.complete(
|
|
113
|
+
complete_request=complete_request
|
|
114
|
+
)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
raise
|
|
117
|
+
result = ""
|
|
118
|
+
for event in raw_resp.events():
|
|
119
|
+
try:
|
|
120
|
+
parsed_resp = json.loads(event.data)
|
|
121
|
+
except json.JSONDecodeError:
|
|
122
|
+
raise ResponseParseError("Server response cannot be parsed")
|
|
123
|
+
try:
|
|
124
|
+
result += parsed_resp["choices"][0]["delta"]["content"]
|
|
125
|
+
except (json.JSONDecodeError, KeyError, IndexError):
|
|
126
|
+
if parsed_resp.get("error"):
|
|
127
|
+
raise MidStreamError(reason=event.data)
|
|
128
|
+
else:
|
|
129
|
+
pass
|
|
130
|
+
return result
|
|
131
|
+
|
|
72
132
|
def extract_answer_from_source_document(
|
|
73
133
|
self,
|
|
74
134
|
source_document: SourceDocument,
|
|
@@ -170,7 +230,7 @@ class CortexManager(SqlExecutionMixin):
|
|
|
170
230
|
|
|
171
231
|
@staticmethod
|
|
172
232
|
def _extract_text_result_from_json_result(
|
|
173
|
-
extract_function: Callable[[], str]
|
|
233
|
+
extract_function: Callable[[], str],
|
|
174
234
|
) -> str:
|
|
175
235
|
try:
|
|
176
236
|
return extract_function()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Copyright (c) 2025 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.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Copyright (c) 2025 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 logging
|
|
18
|
+
from typing import Optional
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
from click import types
|
|
22
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
23
|
+
from snowflake.cli._plugins.dbt.constants import (
|
|
24
|
+
DBT_COMMANDS,
|
|
25
|
+
OUTPUT_COLUMN_NAME,
|
|
26
|
+
PROFILES_FILENAME,
|
|
27
|
+
RESULT_COLUMN_NAME,
|
|
28
|
+
)
|
|
29
|
+
from snowflake.cli._plugins.dbt.manager import DBTManager
|
|
30
|
+
from snowflake.cli._plugins.object.command_aliases import add_object_command_aliases
|
|
31
|
+
from snowflake.cli._plugins.object.commands import scope_option
|
|
32
|
+
from snowflake.cli.api.commands.decorators import global_options_with_connection
|
|
33
|
+
from snowflake.cli.api.commands.flags import identifier_argument, like_option
|
|
34
|
+
from snowflake.cli.api.commands.snow_typer import SnowTyperFactory
|
|
35
|
+
from snowflake.cli.api.constants import ObjectType
|
|
36
|
+
from snowflake.cli.api.exceptions import CliError
|
|
37
|
+
from snowflake.cli.api.feature_flags import FeatureFlag
|
|
38
|
+
from snowflake.cli.api.identifiers import FQN
|
|
39
|
+
from snowflake.cli.api.output.types import (
|
|
40
|
+
CommandResult,
|
|
41
|
+
MessageResult,
|
|
42
|
+
QueryResult,
|
|
43
|
+
)
|
|
44
|
+
from snowflake.cli.api.secure_path import SecurePath
|
|
45
|
+
|
|
46
|
+
app = SnowTyperFactory(
|
|
47
|
+
name="dbt",
|
|
48
|
+
help="Manages dbt on Snowflake projects",
|
|
49
|
+
is_hidden=FeatureFlag.ENABLE_DBT.is_disabled,
|
|
50
|
+
)
|
|
51
|
+
log = logging.getLogger(__name__)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
DBTNameArgument = identifier_argument(sf_object="DBT Project", example="my_pipeline")
|
|
55
|
+
|
|
56
|
+
# in passthrough commands we need to support that user would either provide the name of dbt object or name of dbt
|
|
57
|
+
# command, in which case FQN validation could fail
|
|
58
|
+
DBTNameOrCommandArgument = identifier_argument(
|
|
59
|
+
sf_object="DBT Project", example="my_pipeline", click_type=types.StringParamType()
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
add_object_command_aliases(
|
|
63
|
+
app=app,
|
|
64
|
+
object_type=ObjectType.DBT_PROJECT,
|
|
65
|
+
name_argument=DBTNameArgument,
|
|
66
|
+
like_option=like_option(
|
|
67
|
+
help_example='`list --like "my%"` lists all dbt projects that begin with “my”'
|
|
68
|
+
),
|
|
69
|
+
scope_option=scope_option(help_example="`list --in database my_db`"),
|
|
70
|
+
ommit_commands=["drop", "create", "describe"],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command(
|
|
75
|
+
"deploy",
|
|
76
|
+
requires_connection=True,
|
|
77
|
+
)
|
|
78
|
+
def deploy_dbt(
|
|
79
|
+
name: FQN = DBTNameArgument,
|
|
80
|
+
source: Optional[str] = typer.Option(
|
|
81
|
+
help="Path to directory containing dbt files to deploy. Defaults to current working directory.",
|
|
82
|
+
show_default=False,
|
|
83
|
+
default=None,
|
|
84
|
+
),
|
|
85
|
+
profiles_dir: Optional[str] = typer.Option(
|
|
86
|
+
help=f"Path to directory containing {PROFILES_FILENAME}. Defaults to directory provided in --source or current working directory",
|
|
87
|
+
show_default=False,
|
|
88
|
+
default=None,
|
|
89
|
+
),
|
|
90
|
+
force: Optional[bool] = typer.Option(
|
|
91
|
+
False,
|
|
92
|
+
help="Overwrites conflicting files in the project, if any.",
|
|
93
|
+
),
|
|
94
|
+
**options,
|
|
95
|
+
) -> CommandResult:
|
|
96
|
+
"""
|
|
97
|
+
Copy dbt files and either recreate dbt on Snowflake if `--force` flag is
|
|
98
|
+
provided; or create a new one if it doesn't exist; or update files and
|
|
99
|
+
create a new version if it exists.
|
|
100
|
+
"""
|
|
101
|
+
project_path = SecurePath(source) if source is not None else SecurePath.cwd()
|
|
102
|
+
profiles_dir_path = SecurePath(profiles_dir) if profiles_dir else project_path
|
|
103
|
+
return QueryResult(
|
|
104
|
+
DBTManager().deploy(
|
|
105
|
+
name,
|
|
106
|
+
project_path.resolve(),
|
|
107
|
+
profiles_dir_path.resolve(),
|
|
108
|
+
force=force,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
dbt_execute_app = SnowTyperFactory(
|
|
114
|
+
name="execute",
|
|
115
|
+
help="Execute a dbt command on Snowflake. Subcommand name and all "
|
|
116
|
+
"parameters following it will be passed over to dbt.",
|
|
117
|
+
subcommand_metavar="DBT_COMMAND",
|
|
118
|
+
)
|
|
119
|
+
app.add_typer(dbt_execute_app)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dbt_execute_app.callback()
|
|
123
|
+
@global_options_with_connection
|
|
124
|
+
def before_callback(
|
|
125
|
+
name: str = DBTNameOrCommandArgument,
|
|
126
|
+
run_async: Optional[bool] = typer.Option(
|
|
127
|
+
False, help="Run dbt command asynchronously and check it's result later."
|
|
128
|
+
),
|
|
129
|
+
**options,
|
|
130
|
+
):
|
|
131
|
+
"""Handles global options passed before the command and takes pipeline name to be accessed through child context later"""
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
for cmd in DBT_COMMANDS:
|
|
136
|
+
|
|
137
|
+
@dbt_execute_app.command(
|
|
138
|
+
name=cmd,
|
|
139
|
+
requires_connection=False,
|
|
140
|
+
requires_global_options=False,
|
|
141
|
+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
|
|
142
|
+
help=f"Execute {cmd} command on Snowflake. Command name and all parameters following it will be passed over to dbt.",
|
|
143
|
+
add_help_option=False,
|
|
144
|
+
)
|
|
145
|
+
def _dbt_execute(
|
|
146
|
+
ctx: typer.Context,
|
|
147
|
+
) -> CommandResult:
|
|
148
|
+
dbt_cli_args = ctx.args
|
|
149
|
+
dbt_command = ctx.command.name
|
|
150
|
+
name = FQN.from_string(ctx.parent.params["name"])
|
|
151
|
+
run_async = ctx.parent.params["run_async"]
|
|
152
|
+
execute_args = (dbt_command, name, run_async, *dbt_cli_args)
|
|
153
|
+
dbt_manager = DBTManager()
|
|
154
|
+
|
|
155
|
+
if run_async is True:
|
|
156
|
+
result = dbt_manager.execute(*execute_args)
|
|
157
|
+
return MessageResult(
|
|
158
|
+
f"Command submitted. You can check the result with `snow sql -q \"select execution_status from table(information_schema.query_history_by_user()) where query_id in ('{result.sfqid}');\"`"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
with Progress(
|
|
162
|
+
SpinnerColumn(),
|
|
163
|
+
TextColumn("[progress.description]{task.description}"),
|
|
164
|
+
transient=True,
|
|
165
|
+
) as progress:
|
|
166
|
+
progress.add_task(description=f"Executing 'dbt {dbt_command}'", total=None)
|
|
167
|
+
|
|
168
|
+
result = dbt_manager.execute(*execute_args)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
columns = [column.name for column in result.description]
|
|
172
|
+
success_column_index = columns.index(RESULT_COLUMN_NAME)
|
|
173
|
+
stdout_column_index = columns.index(OUTPUT_COLUMN_NAME)
|
|
174
|
+
except ValueError:
|
|
175
|
+
raise CliError("Malformed server response")
|
|
176
|
+
try:
|
|
177
|
+
is_success, output = [
|
|
178
|
+
(row[success_column_index], row[stdout_column_index])
|
|
179
|
+
for row in result
|
|
180
|
+
][-1]
|
|
181
|
+
except IndexError:
|
|
182
|
+
raise CliError("No data returned from server")
|
|
183
|
+
|
|
184
|
+
if is_success is True:
|
|
185
|
+
return MessageResult(output)
|
|
186
|
+
else:
|
|
187
|
+
raise CliError(output)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Copyright (c) 2025 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
|
+
RESULT_COLUMN_NAME = "SUCCESS"
|
|
16
|
+
OUTPUT_COLUMN_NAME = "STDOUT"
|
|
17
|
+
PROFILES_FILENAME = "profiles.yml"
|
|
18
|
+
|
|
19
|
+
DBT_COMMANDS = [
|
|
20
|
+
"build",
|
|
21
|
+
"compile",
|
|
22
|
+
"deps",
|
|
23
|
+
"list",
|
|
24
|
+
"parse",
|
|
25
|
+
"run",
|
|
26
|
+
"run-operation",
|
|
27
|
+
"seed",
|
|
28
|
+
"show",
|
|
29
|
+
"snapshot",
|
|
30
|
+
"test",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
UNSUPPORTED_COMMANDS = [
|
|
34
|
+
"clean",
|
|
35
|
+
"clone",
|
|
36
|
+
"debug",
|
|
37
|
+
"docs",
|
|
38
|
+
"init",
|
|
39
|
+
"retry",
|
|
40
|
+
"source",
|
|
41
|
+
]
|