snowflake-cli 3.8.3__py3-none-any.whl → 3.9.1__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 (32) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/commands_registration/builtin_plugins.py +2 -0
  3. snowflake/cli/_app/version_check.py +14 -4
  4. snowflake/cli/_plugins/cortex/commands.py +34 -8
  5. snowflake/cli/_plugins/cortex/constants.py +2 -1
  6. snowflake/cli/_plugins/cortex/manager.py +81 -21
  7. snowflake/cli/_plugins/dbt/__init__.py +13 -0
  8. snowflake/cli/_plugins/dbt/commands.py +187 -0
  9. snowflake/cli/_plugins/dbt/constants.py +41 -0
  10. snowflake/cli/_plugins/dbt/manager.py +182 -0
  11. snowflake/cli/_plugins/dbt/plugin_spec.py +30 -0
  12. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +4 -1
  13. snowflake/cli/_plugins/project/commands.py +33 -6
  14. snowflake/cli/_plugins/project/manager.py +6 -1
  15. snowflake/cli/_plugins/snowpark/snowpark_entity.py +1 -1
  16. snowflake/cli/_plugins/spcs/image_registry/commands.py +2 -2
  17. snowflake/cli/_plugins/spcs/image_registry/manager.py +2 -2
  18. snowflake/cli/_plugins/stage/commands.py +14 -3
  19. snowflake/cli/_plugins/stage/manager.py +39 -19
  20. snowflake/cli/_plugins/streamlit/streamlit_entity.py +19 -30
  21. snowflake/cli/api/commands/snow_typer.py +3 -0
  22. snowflake/cli/api/constants.py +2 -0
  23. snowflake/cli/api/entities/common.py +0 -52
  24. snowflake/cli/api/feature_flags.py +1 -0
  25. snowflake/cli/api/rest_api.py +3 -2
  26. snowflake/cli/api/secure_path.py +9 -0
  27. snowflake/cli/api/sql_execution.py +12 -9
  28. {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.1.dist-info}/METADATA +3 -3
  29. {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.1.dist-info}/RECORD +32 -27
  30. {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.1.dist-info}/WHEEL +0 -0
  31. {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.1.dist-info}/entry_points.txt +0 -0
  32. {snowflake_cli-3.8.3.dist-info → snowflake_cli-3.9.1.dist-info}/licenses/LICENSE +0 -0
@@ -16,7 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  from enum import Enum, unique
18
18
 
19
- VERSION = "3.8.3"
19
+ VERSION = "3.9.1"
20
20
 
21
21
 
22
22
  @unique
@@ -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
- REPOSITORY_URL = "https://pypi.org/pypi/snowflake-cli/json"
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(REPOSITORY_URL, headers=headers, timeout=3)
52
+ response = requests.get(REPOSITORY_URL_PIP, headers=headers, timeout=3)
52
53
  response.raise_for_status()
53
- return response.json()["info"]["version"]
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 = self._get_version_from_pypi()
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 DEFAULT_MODEL
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
- result_text = manager.complete_for_prompt(
148
- text=Text(text),
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 file:
152
- result_text = manager.complete_for_conversation(
153
- conversation_json_file=SecurePath(file),
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("Either --file option or TEXT argument has to be provided.")
183
+ raise UsageError("--backend option should be either rest or sql.")
158
184
 
159
185
  return MessageResult(result_text.strip())
160
186
 
@@ -14,4 +14,5 @@
14
14
 
15
15
  from snowflake.cli._plugins.cortex.types import Model
16
16
 
17
- DEFAULT_MODEL: Model = Model("snowflake-arctic")
17
+ DEFAULT_MODEL: Model = Model("llama3.1-70b")
18
+ DEFAULT_BACKEND = "rest"
@@ -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 complete_for_prompt(
66
+ def complete(
41
67
  self,
42
68
  text: Text,
43
69
  model: Model,
70
+ is_file_input: bool = False,
44
71
  ) -> str:
45
- query = f"""\
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
- return self._query_cortex_result_str(query)
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 complete_for_conversation(
92
+ def make_rest_complete_request(
53
93
  self,
54
- conversation_json_file: SecurePath,
55
94
  model: Model,
56
- ) -> str:
57
- json_content = conversation_json_file.read_text(
58
- file_size_limit_mb=DEFAULT_SIZE_LIMIT_MB
59
- )
60
- query = f"""\
61
- SELECT SNOWFLAKE.CORTEX.COMPLETE(
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
+ ]