snowflake-cli 3.10.1__py3-none-any.whl → 3.12.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/auth/__init__.py +13 -0
- snowflake/cli/_app/auth/errors.py +28 -0
- snowflake/cli/_app/auth/oidc_providers.py +393 -0
- snowflake/cli/_app/cli_app.py +0 -1
- snowflake/cli/_app/constants.py +10 -0
- snowflake/cli/_app/printing.py +153 -19
- snowflake/cli/_app/snow_connector.py +35 -0
- snowflake/cli/_plugins/auth/__init__.py +4 -2
- snowflake/cli/_plugins/auth/keypair/commands.py +2 -0
- snowflake/cli/_plugins/auth/oidc/__init__.py +13 -0
- snowflake/cli/_plugins/auth/oidc/commands.py +47 -0
- snowflake/cli/_plugins/auth/oidc/manager.py +66 -0
- snowflake/cli/_plugins/auth/oidc/plugin_spec.py +30 -0
- snowflake/cli/_plugins/connection/commands.py +37 -3
- snowflake/cli/_plugins/dbt/commands.py +37 -8
- snowflake/cli/_plugins/dbt/manager.py +144 -12
- snowflake/cli/_plugins/dcm/commands.py +102 -136
- snowflake/cli/_plugins/dcm/manager.py +136 -89
- snowflake/cli/_plugins/logs/commands.py +7 -0
- snowflake/cli/_plugins/logs/manager.py +21 -1
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +3 -1
- snowflake/cli/_plugins/notebook/notebook_entity.py +2 -0
- snowflake/cli/_plugins/notebook/notebook_entity_model.py +8 -1
- snowflake/cli/_plugins/object/command_aliases.py +16 -1
- snowflake/cli/_plugins/object/commands.py +27 -1
- snowflake/cli/_plugins/object/manager.py +12 -1
- snowflake/cli/_plugins/snowpark/commands.py +8 -1
- snowflake/cli/_plugins/snowpark/common.py +1 -0
- snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +29 -5
- snowflake/cli/_plugins/snowpark/package_utils.py +44 -3
- snowflake/cli/_plugins/spcs/services/manager.py +5 -4
- snowflake/cli/_plugins/sql/lexer/types.py +1 -0
- snowflake/cli/_plugins/sql/repl.py +100 -26
- snowflake/cli/_plugins/sql/repl_commands.py +607 -0
- snowflake/cli/_plugins/sql/statement_reader.py +44 -20
- snowflake/cli/api/artifacts/bundle_map.py +32 -2
- snowflake/cli/api/artifacts/regex_resolver.py +54 -0
- snowflake/cli/api/artifacts/upload.py +5 -1
- snowflake/cli/api/artifacts/utils.py +12 -1
- snowflake/cli/api/cli_global_context.py +7 -0
- snowflake/cli/api/commands/decorators.py +7 -0
- snowflake/cli/api/commands/flags.py +26 -0
- snowflake/cli/api/config.py +24 -0
- snowflake/cli/api/connections.py +1 -0
- snowflake/cli/api/console/abc.py +13 -2
- snowflake/cli/api/console/console.py +20 -0
- snowflake/cli/api/constants.py +9 -0
- snowflake/cli/api/entities/utils.py +10 -6
- snowflake/cli/api/feature_flags.py +1 -0
- snowflake/cli/api/identifiers.py +18 -1
- snowflake/cli/api/project/schemas/entities/entities.py +0 -6
- snowflake/cli/api/rendering/sql_templates.py +2 -0
- snowflake/cli/api/utils/dict_utils.py +42 -1
- {snowflake_cli-3.10.1.dist-info → snowflake_cli-3.12.0.dist-info}/METADATA +15 -41
- {snowflake_cli-3.10.1.dist-info → snowflake_cli-3.12.0.dist-info}/RECORD +59 -52
- snowflake/cli/_plugins/dcm/dcm_project_entity_model.py +0 -59
- snowflake/cli/_plugins/sql/snowsql_commands.py +0 -331
- {snowflake_cli-3.10.1.dist-info → snowflake_cli-3.12.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.10.1.dist-info → snowflake_cli-3.12.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.10.1.dist-info → snowflake_cli-3.12.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,9 +7,10 @@ from typing import Any, Callable, Generator, List, Literal, Sequence, Tuple
|
|
|
7
7
|
from urllib.request import urlopen
|
|
8
8
|
|
|
9
9
|
from jinja2 import UndefinedError
|
|
10
|
-
from snowflake.cli._plugins.sql.
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
from snowflake.cli._plugins.sql.repl_commands import (
|
|
11
|
+
ReplCommand,
|
|
12
|
+
UnknownCommandError,
|
|
13
|
+
compile_repl_command,
|
|
13
14
|
)
|
|
14
15
|
from snowflake.cli.api.secure_path import UNLIMITED, SecurePath
|
|
15
16
|
from snowflake.connector.util_text import split_statements
|
|
@@ -38,7 +39,7 @@ class StatementType(enum.Enum):
|
|
|
38
39
|
QUERY = "query"
|
|
39
40
|
UNKNOWN = "unknown"
|
|
40
41
|
URL = "url"
|
|
41
|
-
|
|
42
|
+
REPL_COMMAND = "repl_command"
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
class ParsedStatement:
|
|
@@ -88,12 +89,21 @@ class ParsedStatement:
|
|
|
88
89
|
def __repr__(self):
|
|
89
90
|
return f"{self.__class__.__name__}(statement_type={self.statement_type}, source_path={self.source_path}, error={self.error})"
|
|
90
91
|
|
|
92
|
+
@staticmethod
|
|
93
|
+
def drop_comments_from_path_parts(path_part: str) -> str:
|
|
94
|
+
"""Clean up path_part from trailing comments."""
|
|
95
|
+
uncommented, _ = next(
|
|
96
|
+
split_statements(io.StringIO(path_part), remove_comments=True)
|
|
97
|
+
)
|
|
98
|
+
return uncommented
|
|
99
|
+
|
|
91
100
|
@classmethod
|
|
92
101
|
def from_url(cls, path_part: str, raw_source: str) -> "ParsedStatement":
|
|
93
102
|
"""Constructor for loading from URL."""
|
|
103
|
+
stripped_comments_path_part = cls.drop_comments_from_path_parts(path_part)
|
|
94
104
|
try:
|
|
95
|
-
payload = urlopen(
|
|
96
|
-
return cls(payload, StatementType.URL,
|
|
105
|
+
payload = urlopen(stripped_comments_path_part, timeout=10.0).read().decode()
|
|
106
|
+
return cls(payload, StatementType.URL, stripped_comments_path_part)
|
|
97
107
|
|
|
98
108
|
except urllib.error.HTTPError as err:
|
|
99
109
|
error = f"Could not fetch {path_part}: {err}"
|
|
@@ -102,14 +112,20 @@ class ParsedStatement:
|
|
|
102
112
|
@classmethod
|
|
103
113
|
def from_file(cls, path_part: str, raw_source: str) -> "ParsedStatement":
|
|
104
114
|
"""Constructor for loading from file."""
|
|
105
|
-
|
|
115
|
+
stripped_comments_path_part = cls.drop_comments_from_path_parts(path_part)
|
|
116
|
+
path = SecurePath(stripped_comments_path_part)
|
|
106
117
|
|
|
107
118
|
if path.is_file():
|
|
108
119
|
payload = path.read_text(file_size_limit_mb=UNLIMITED)
|
|
109
120
|
return cls(payload, StatementType.FILE, path.as_posix())
|
|
110
121
|
|
|
111
122
|
error_msg = f"Could not read: {path_part}"
|
|
112
|
-
return cls(
|
|
123
|
+
return cls(
|
|
124
|
+
path_part,
|
|
125
|
+
StatementType.FILE,
|
|
126
|
+
raw_source,
|
|
127
|
+
error_msg,
|
|
128
|
+
)
|
|
113
129
|
|
|
114
130
|
|
|
115
131
|
RecursiveStatementReader = Generator[ParsedStatement, Any, Any]
|
|
@@ -154,7 +170,10 @@ def parse_statement(source: str, operators: OperatorFunctions) -> ParsedStatemen
|
|
|
154
170
|
)
|
|
155
171
|
|
|
156
172
|
case "queries" | "result" | "abort", (str(),):
|
|
157
|
-
return ParsedStatement(statement, StatementType.
|
|
173
|
+
return ParsedStatement(statement, StatementType.REPL_COMMAND, None)
|
|
174
|
+
|
|
175
|
+
case "edit", (str(),):
|
|
176
|
+
return ParsedStatement(statement, StatementType.REPL_COMMAND, command_args)
|
|
158
177
|
|
|
159
178
|
case _:
|
|
160
179
|
error_msg = f"Unknown command: {command}"
|
|
@@ -240,7 +259,7 @@ def query_reader(
|
|
|
240
259
|
class CompiledStatement:
|
|
241
260
|
statement: str | None = None
|
|
242
261
|
execute_async: bool = False
|
|
243
|
-
command:
|
|
262
|
+
command: ReplCommand | None = None
|
|
244
263
|
|
|
245
264
|
|
|
246
265
|
def _is_empty_statement(statement: str) -> bool:
|
|
@@ -274,21 +293,26 @@ def compile_statements(
|
|
|
274
293
|
if not is_async:
|
|
275
294
|
expected_results_cnt += 1
|
|
276
295
|
|
|
277
|
-
if stmt.statement_type == StatementType.
|
|
296
|
+
if stmt.statement_type == StatementType.REPL_COMMAND:
|
|
278
297
|
if not stmt.error:
|
|
279
|
-
|
|
298
|
+
command_text = (
|
|
280
299
|
stmt.statement.read()
|
|
281
300
|
.removesuffix(ASYNC_SUFFIX)
|
|
282
301
|
.removesuffix(";")
|
|
283
|
-
.
|
|
284
|
-
)
|
|
285
|
-
parsed_command = compile_snowsql_command(
|
|
286
|
-
command=cmd[0], cmd_args=cmd[1:]
|
|
302
|
+
.strip()
|
|
287
303
|
)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
304
|
+
try:
|
|
305
|
+
parsed_command = compile_repl_command(command_text)
|
|
306
|
+
if parsed_command.error_message:
|
|
307
|
+
errors.append(parsed_command.error_message)
|
|
308
|
+
else:
|
|
309
|
+
compiled.append(
|
|
310
|
+
CompiledStatement(command=parsed_command.command)
|
|
311
|
+
)
|
|
312
|
+
except UnknownCommandError as e:
|
|
313
|
+
errors.append(str(e))
|
|
314
|
+
except Exception as e:
|
|
315
|
+
errors.append(f"Error parsing command: {e}")
|
|
292
316
|
|
|
293
317
|
if stmt.error:
|
|
294
318
|
errors.append(stmt.error)
|
|
@@ -11,6 +11,9 @@ from snowflake.cli.api.artifacts.common import (
|
|
|
11
11
|
SourceNotFoundError,
|
|
12
12
|
TooManyFilesError,
|
|
13
13
|
)
|
|
14
|
+
from snowflake.cli.api.artifacts.regex_resolver import RegexResolver
|
|
15
|
+
from snowflake.cli.api.constants import PatternMatchingType
|
|
16
|
+
from snowflake.cli.api.exceptions import CliError
|
|
14
17
|
from snowflake.cli.api.project.schemas.entities.common import PathMapping
|
|
15
18
|
from snowflake.cli.api.utils.path_utils import resolve_without_follow
|
|
16
19
|
|
|
@@ -38,9 +41,16 @@ class BundleMap:
|
|
|
38
41
|
|
|
39
42
|
:param project_root: The root directory of the project and base for all relative paths. Must be an absolute path.
|
|
40
43
|
:param deploy_root: The directory where artifacts should be copied to. Must be an absolute path.
|
|
44
|
+
:param pattern_type: The pattern matching type to use for artifact resolution. Defaults to GLOB.
|
|
41
45
|
"""
|
|
42
46
|
|
|
43
|
-
def __init__(
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
project_root: Path,
|
|
51
|
+
deploy_root: Path,
|
|
52
|
+
pattern_type: PatternMatchingType = PatternMatchingType.GLOB,
|
|
53
|
+
):
|
|
44
54
|
# If a relative path ends up here, it's a bug in the app and can lead to other
|
|
45
55
|
# subtle bugs as paths would be resolved relative to the current working directory.
|
|
46
56
|
assert (
|
|
@@ -52,6 +62,7 @@ class BundleMap:
|
|
|
52
62
|
|
|
53
63
|
self._project_root: Path = resolve_without_follow(project_root)
|
|
54
64
|
self._deploy_root: Path = resolve_without_follow(deploy_root)
|
|
65
|
+
self._pattern_type: PatternMatchingType = pattern_type
|
|
55
66
|
self._artifact_map = _ArtifactPathMap(project_root=self._project_root)
|
|
56
67
|
|
|
57
68
|
def is_empty(self) -> bool:
|
|
@@ -112,7 +123,14 @@ class BundleMap:
|
|
|
112
123
|
if src_path.is_absolute():
|
|
113
124
|
raise ArtifactError("Source path must be a relative path")
|
|
114
125
|
|
|
115
|
-
|
|
126
|
+
if self._pattern_type == PatternMatchingType.REGEX:
|
|
127
|
+
resolved_sources = self._resolve_regex_pattern(src)
|
|
128
|
+
elif self._pattern_type == PatternMatchingType.GLOB:
|
|
129
|
+
resolved_sources = self._project_root.glob(src)
|
|
130
|
+
else:
|
|
131
|
+
raise CliError(f"Unsupported pattern type: {self._pattern_type}")
|
|
132
|
+
|
|
133
|
+
for resolved_src in resolved_sources:
|
|
116
134
|
match_found = True
|
|
117
135
|
|
|
118
136
|
if dest:
|
|
@@ -140,6 +158,18 @@ class BundleMap:
|
|
|
140
158
|
if not match_found:
|
|
141
159
|
raise SourceNotFoundError(src)
|
|
142
160
|
|
|
161
|
+
def _resolve_regex_pattern(self, pattern: str):
|
|
162
|
+
"""
|
|
163
|
+
Resolve files matching a regex pattern.
|
|
164
|
+
"""
|
|
165
|
+
resolver = RegexResolver()
|
|
166
|
+
for path in self._project_root.rglob("*"):
|
|
167
|
+
if path.is_file():
|
|
168
|
+
relative_path = str(path.relative_to(self._project_root))
|
|
169
|
+
relative_path = relative_path.replace("\\", "/")
|
|
170
|
+
if resolver.does_match(pattern, relative_path):
|
|
171
|
+
yield path
|
|
172
|
+
|
|
143
173
|
def add(self, mapping: PathMapping) -> None:
|
|
144
174
|
"""
|
|
145
175
|
Adds an artifact mapping rule to this instance.
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
18
|
+
from pydantic_core import SchemaError
|
|
19
|
+
from snowflake.cli.api.artifacts.common import ArtifactError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RegexResolver:
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self._pattern_classes = {}
|
|
25
|
+
|
|
26
|
+
def does_match(self, pattern: str, text: str) -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Check if text matches pattern.
|
|
29
|
+
"""
|
|
30
|
+
if len(pattern) > 1000:
|
|
31
|
+
raise ArtifactError(
|
|
32
|
+
f"Regex pattern too long ({len(pattern)} chars, max 1000): "
|
|
33
|
+
"potentially unsafe for performance"
|
|
34
|
+
)
|
|
35
|
+
if pattern not in self._pattern_classes:
|
|
36
|
+
self._pattern_classes[pattern] = self._generate_pattern_class(pattern)
|
|
37
|
+
|
|
38
|
+
pattern_class = self._pattern_classes[pattern]
|
|
39
|
+
try:
|
|
40
|
+
pattern_class(test_field=text)
|
|
41
|
+
return True
|
|
42
|
+
except ValidationError:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _generate_pattern_class(pattern: str) -> type:
|
|
47
|
+
try:
|
|
48
|
+
|
|
49
|
+
class _RegexTestModel(BaseModel):
|
|
50
|
+
test_field: str = Field(pattern=pattern)
|
|
51
|
+
|
|
52
|
+
return _RegexTestModel
|
|
53
|
+
except SchemaError as e:
|
|
54
|
+
raise ArtifactError(f"Invalid regex pattern: {e}") from e
|
|
@@ -3,6 +3,7 @@ from typing import List, Optional
|
|
|
3
3
|
from snowflake.cli._plugins.stage.manager import StageManager
|
|
4
4
|
from snowflake.cli.api.artifacts.utils import bundle_artifacts
|
|
5
5
|
from snowflake.cli.api.console import cli_console
|
|
6
|
+
from snowflake.cli.api.constants import PatternMatchingType
|
|
6
7
|
from snowflake.cli.api.entities.utils import sync_deploy_root_with_stage
|
|
7
8
|
from snowflake.cli.api.project.project_paths import ProjectPaths
|
|
8
9
|
from snowflake.cli.api.project.schemas.entities.common import PathMapping
|
|
@@ -12,8 +13,10 @@ from snowflake.cli.api.secure_path import SecurePath
|
|
|
12
13
|
def sync_artifacts_with_stage(
|
|
13
14
|
project_paths: ProjectPaths,
|
|
14
15
|
stage_root: str,
|
|
16
|
+
use_temporary_stage: bool = False,
|
|
15
17
|
prune: bool = False,
|
|
16
18
|
artifacts: Optional[List[PathMapping]] = None,
|
|
19
|
+
pattern_type: PatternMatchingType = PatternMatchingType.GLOB,
|
|
17
20
|
):
|
|
18
21
|
if artifacts is None:
|
|
19
22
|
artifacts = []
|
|
@@ -21,7 +24,7 @@ def sync_artifacts_with_stage(
|
|
|
21
24
|
project_paths.remove_up_bundle_root()
|
|
22
25
|
SecurePath(project_paths.bundle_root).mkdir(parents=True, exist_ok=True)
|
|
23
26
|
|
|
24
|
-
bundle_map = bundle_artifacts(project_paths, artifacts)
|
|
27
|
+
bundle_map = bundle_artifacts(project_paths, artifacts, pattern_type=pattern_type)
|
|
25
28
|
stage_path_parts = StageManager().stage_path_parts_from_str(stage_root)
|
|
26
29
|
# We treat the bundle root as deploy root
|
|
27
30
|
sync_deploy_root_with_stage(
|
|
@@ -31,6 +34,7 @@ def sync_artifacts_with_stage(
|
|
|
31
34
|
prune=prune,
|
|
32
35
|
recursive=True,
|
|
33
36
|
stage_path_parts=stage_path_parts,
|
|
37
|
+
use_temporary_stage=use_temporary_stage,
|
|
34
38
|
print_diff=True,
|
|
35
39
|
)
|
|
36
40
|
project_paths.clean_up_output()
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
from snowflake.cli.api.artifacts.bundle_map import BundleMap
|
|
7
7
|
from snowflake.cli.api.artifacts.common import NotInDeployRootError
|
|
8
|
+
from snowflake.cli.api.constants import PatternMatchingType
|
|
8
9
|
from snowflake.cli.api.project.project_paths import ProjectPaths
|
|
9
10
|
from snowflake.cli.api.project.schemas.entities.common import Artifacts
|
|
10
11
|
from snowflake.cli.api.secure_path import SecurePath
|
|
@@ -54,16 +55,26 @@ def symlink_or_copy(src: Path, dst: Path, deploy_root: Path) -> None:
|
|
|
54
55
|
)
|
|
55
56
|
|
|
56
57
|
|
|
57
|
-
def bundle_artifacts(
|
|
58
|
+
def bundle_artifacts(
|
|
59
|
+
project_paths: ProjectPaths,
|
|
60
|
+
artifacts: Artifacts,
|
|
61
|
+
pattern_type: PatternMatchingType = PatternMatchingType.GLOB,
|
|
62
|
+
) -> BundleMap:
|
|
58
63
|
"""
|
|
59
64
|
Creates a bundle directory (project_paths.bundle_root) with all artifacts (using symlink_or_copy function above).
|
|
60
65
|
Previous contents of the directory are deleted.
|
|
61
66
|
|
|
62
67
|
Returns a BundleMap containing the mapping between artifacts and their location in bundle directory.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
project_paths: Project paths configuration
|
|
71
|
+
artifacts: List of artifacts to bundle
|
|
72
|
+
pattern_type: The pattern matching type to use for artifact resolution. Defaults to GLOB.
|
|
63
73
|
"""
|
|
64
74
|
bundle_map = BundleMap(
|
|
65
75
|
project_root=project_paths.project_root,
|
|
66
76
|
deploy_root=project_paths.bundle_root,
|
|
77
|
+
pattern_type=pattern_type,
|
|
67
78
|
)
|
|
68
79
|
for artifact in artifacts:
|
|
69
80
|
bundle_map.add(artifact)
|
|
@@ -29,6 +29,7 @@ from snowflake.cli.api.rendering.jinja import CONTEXT_KEY
|
|
|
29
29
|
from snowflake.connector import SnowflakeConnection
|
|
30
30
|
|
|
31
31
|
if TYPE_CHECKING:
|
|
32
|
+
from snowflake.cli._plugins.sql.repl import Repl
|
|
32
33
|
from snowflake.cli.api.project.definition_manager import DefinitionManager
|
|
33
34
|
from snowflake.cli.api.project.schemas.project_definition import ProjectDefinition
|
|
34
35
|
|
|
@@ -48,6 +49,7 @@ class _CliGlobalContextManager:
|
|
|
48
49
|
experimental: bool = False
|
|
49
50
|
enable_tracebacks: bool = True
|
|
50
51
|
is_repl: bool = False
|
|
52
|
+
repl_instance: Repl | None = None
|
|
51
53
|
|
|
52
54
|
metrics: CLIMetrics = field(default_factory=CLIMetrics)
|
|
53
55
|
|
|
@@ -209,6 +211,11 @@ class _CliGlobalContextAccess:
|
|
|
209
211
|
def is_repl(self) -> bool:
|
|
210
212
|
return self._manager.is_repl
|
|
211
213
|
|
|
214
|
+
@property
|
|
215
|
+
def repl(self) -> Repl | None:
|
|
216
|
+
"""Get the current REPL instance if running in REPL mode."""
|
|
217
|
+
return self._manager.repl_instance
|
|
218
|
+
|
|
212
219
|
|
|
213
220
|
_CLI_CONTEXT_MANAGER: ContextVar[_CliGlobalContextManager | None] = ContextVar(
|
|
214
221
|
"cli_context", default=None
|
|
@@ -57,6 +57,7 @@ from snowflake.cli.api.commands.flags import (
|
|
|
57
57
|
UserOption,
|
|
58
58
|
VerboseOption,
|
|
59
59
|
WarehouseOption,
|
|
60
|
+
WorkloadIdentityProviderOption,
|
|
60
61
|
experimental_option,
|
|
61
62
|
project_definition_option,
|
|
62
63
|
project_env_overrides_option,
|
|
@@ -262,6 +263,12 @@ GLOBAL_CONNECTION_OPTIONS = [
|
|
|
262
263
|
annotation=Optional[str],
|
|
263
264
|
default=AuthenticatorOption,
|
|
264
265
|
),
|
|
266
|
+
inspect.Parameter(
|
|
267
|
+
"workload_identity_provider",
|
|
268
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
269
|
+
annotation=Optional[str],
|
|
270
|
+
default=WorkloadIdentityProviderOption,
|
|
271
|
+
),
|
|
265
272
|
inspect.Parameter(
|
|
266
273
|
"private_key_file",
|
|
267
274
|
inspect.Parameter.KEYWORD_ONLY,
|
|
@@ -34,6 +34,7 @@ from snowflake.cli.api.identifiers import FQN
|
|
|
34
34
|
from snowflake.cli.api.output.formats import OutputFormat
|
|
35
35
|
from snowflake.cli.api.secret import SecretType
|
|
36
36
|
from snowflake.cli.api.stage_path import StagePath
|
|
37
|
+
from snowflake.connector.auth.workload_identity import ApiFederatedAuthenticationType
|
|
37
38
|
|
|
38
39
|
DEFAULT_CONTEXT_SETTINGS = {"help_option_names": ["--help", "-h"]}
|
|
39
40
|
|
|
@@ -150,6 +151,21 @@ def _password_callback(value: str):
|
|
|
150
151
|
return _connection_callback("password")(value)
|
|
151
152
|
|
|
152
153
|
|
|
154
|
+
def _workload_identity_provider_callback(value: str):
|
|
155
|
+
if value is not None:
|
|
156
|
+
try:
|
|
157
|
+
# Validate that the value is one of the enum values
|
|
158
|
+
ApiFederatedAuthenticationType(value)
|
|
159
|
+
except ValueError:
|
|
160
|
+
valid_values = [e.value for e in ApiFederatedAuthenticationType]
|
|
161
|
+
raise ClickException(
|
|
162
|
+
f"Invalid workload identity provider '{value}'. "
|
|
163
|
+
f"Valid values are: {', '.join(valid_values)}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return _connection_callback("workload_identity_provider")(value)
|
|
167
|
+
|
|
168
|
+
|
|
153
169
|
PasswordOption = typer.Option(
|
|
154
170
|
None,
|
|
155
171
|
"--password",
|
|
@@ -170,6 +186,16 @@ AuthenticatorOption = typer.Option(
|
|
|
170
186
|
rich_help_panel=_CONNECTION_SECTION,
|
|
171
187
|
)
|
|
172
188
|
|
|
189
|
+
WorkloadIdentityProviderOption = typer.Option(
|
|
190
|
+
None,
|
|
191
|
+
"--workload-identity-provider",
|
|
192
|
+
help="Workload identity provider (AWS, AZURE, GCP, OIDC). Overrides the value specified for the connection",
|
|
193
|
+
hide_input=True,
|
|
194
|
+
callback=_workload_identity_provider_callback,
|
|
195
|
+
show_default=False,
|
|
196
|
+
rich_help_panel=_CONNECTION_SECTION,
|
|
197
|
+
)
|
|
198
|
+
|
|
173
199
|
PrivateKeyPathOption = typer.Option(
|
|
174
200
|
None,
|
|
175
201
|
"--private-key-file",
|
snowflake/cli/api/config.py
CHANGED
|
@@ -34,6 +34,7 @@ from snowflake.cli.api.secure_utils import (
|
|
|
34
34
|
file_permissions_are_strict,
|
|
35
35
|
windows_get_not_whitelisted_users_with_access,
|
|
36
36
|
)
|
|
37
|
+
from snowflake.cli.api.utils.dict_utils import remove_key_from_nested_dict_if_exists
|
|
37
38
|
from snowflake.cli.api.utils.types import try_cast_to_bool
|
|
38
39
|
from snowflake.connector.compat import IS_WINDOWS
|
|
39
40
|
from snowflake.connector.config_manager import CONFIG_MANAGER
|
|
@@ -82,6 +83,7 @@ class ConnectionConfig:
|
|
|
82
83
|
warehouse: Optional[str] = None
|
|
83
84
|
role: Optional[str] = None
|
|
84
85
|
authenticator: Optional[str] = None
|
|
86
|
+
workload_identity_provider: Optional[str] = None
|
|
85
87
|
private_key_file: Optional[str] = None
|
|
86
88
|
token_file_path: Optional[str] = None
|
|
87
89
|
oauth_client_id: Optional[str] = None
|
|
@@ -158,6 +160,19 @@ def add_connection_to_proper_file(name: str, connection_config: ConnectionConfig
|
|
|
158
160
|
return CONFIG_MANAGER.file_path
|
|
159
161
|
|
|
160
162
|
|
|
163
|
+
def remove_connection_from_proper_file(name: str):
|
|
164
|
+
if CONNECTIONS_FILE.exists():
|
|
165
|
+
existing_connections = _read_connections_toml()
|
|
166
|
+
if name not in existing_connections:
|
|
167
|
+
raise MissingConfigurationError(f"Connection {name} is not configured")
|
|
168
|
+
del existing_connections[name]
|
|
169
|
+
_update_connections_toml(existing_connections)
|
|
170
|
+
return CONNECTIONS_FILE
|
|
171
|
+
else:
|
|
172
|
+
unset_config_value(path=[CONNECTIONS_SECTION, name])
|
|
173
|
+
return CONFIG_MANAGER.file_path
|
|
174
|
+
|
|
175
|
+
|
|
161
176
|
_DEFAULT_LOGS_CONFIG = {
|
|
162
177
|
"save_logs": True,
|
|
163
178
|
"path": str(CONFIG_MANAGER.file_path.parent / "logs"),
|
|
@@ -228,6 +243,15 @@ def set_config_value(path: List[str], value: Any) -> None:
|
|
|
228
243
|
current_config_dict[path[-1]] = value
|
|
229
244
|
|
|
230
245
|
|
|
246
|
+
def unset_config_value(path: List[str]) -> None:
|
|
247
|
+
"""Unsets value in config.
|
|
248
|
+
For example to unset value for key "key" in section [a.b.c], call
|
|
249
|
+
unset_config_value(["a", "b", "c", "key"]).
|
|
250
|
+
"""
|
|
251
|
+
with _config_file() as conf_file_cache:
|
|
252
|
+
remove_key_from_nested_dict_if_exists(conf_file_cache, path)
|
|
253
|
+
|
|
254
|
+
|
|
231
255
|
def get_logs_config() -> dict:
|
|
232
256
|
logs_config = _DEFAULT_LOGS_CONFIG.copy()
|
|
233
257
|
if config_section_exists(*LOGS_SECTION_PATH):
|
snowflake/cli/api/connections.py
CHANGED
|
@@ -45,6 +45,7 @@ class ConnectionContext:
|
|
|
45
45
|
user: Optional[str] = None
|
|
46
46
|
password: Optional[str] = field(default=None, repr=False)
|
|
47
47
|
authenticator: Optional[str] = None
|
|
48
|
+
workload_identity_provider: Optional[str] = None
|
|
48
49
|
private_key_file: Optional[str] = None
|
|
49
50
|
warehouse: Optional[str] = None
|
|
50
51
|
mfa_passcode: Optional[str] = None
|
snowflake/cli/api/console/abc.py
CHANGED
|
@@ -29,11 +29,15 @@ from snowflake.cli.api.cli_global_context import (
|
|
|
29
29
|
class AbstractConsole(ABC):
|
|
30
30
|
"""Interface for cli console implementation.
|
|
31
31
|
|
|
32
|
-
Each console should have
|
|
32
|
+
Each console should have the following methods implemented:
|
|
33
33
|
- `step` - for more detailed information on steps
|
|
34
34
|
- `warning` - for displaying messages in a style that makes it
|
|
35
35
|
visually stand out from other output
|
|
36
|
-
- `phase` a context manager for organising steps into logical group
|
|
36
|
+
- `phase` - a context manager for organising steps into logical group
|
|
37
|
+
- `indented` - a context manager for temporarily indenting messages and warnings
|
|
38
|
+
- 'message' - displays an informational message to output
|
|
39
|
+
- 'panel' - displays visually separated messages
|
|
40
|
+
- 'spinner' - context manager for indicating a long-running operation
|
|
37
41
|
"""
|
|
38
42
|
|
|
39
43
|
_print_fn: Callable[[str], None]
|
|
@@ -98,3 +102,10 @@ class AbstractConsole(ABC):
|
|
|
98
102
|
"""Displays message in a panel that makes it visually stand out from other output.
|
|
99
103
|
|
|
100
104
|
Intended for displaying visually separated messages."""
|
|
105
|
+
|
|
106
|
+
@contextmanager
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def spinner(self):
|
|
109
|
+
"""
|
|
110
|
+
A context manager for indicating a long-running operation.
|
|
111
|
+
"""
|
|
@@ -19,6 +19,7 @@ from typing import Optional
|
|
|
19
19
|
|
|
20
20
|
from rich import get_console
|
|
21
21
|
from rich.panel import Panel
|
|
22
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
22
23
|
from rich.style import Style
|
|
23
24
|
from rich.text import Text
|
|
24
25
|
from snowflake.cli.api.console.abc import AbstractConsole
|
|
@@ -31,6 +32,7 @@ get_console().soft_wrap = True
|
|
|
31
32
|
get_console()._markup = False # noqa: SLF001
|
|
32
33
|
|
|
33
34
|
PHASE_STYLE: Style = Style(bold=True)
|
|
35
|
+
SPINNER_STYLE: Style = Style(bold=True)
|
|
34
36
|
STEP_STYLE: Style = Style(italic=True)
|
|
35
37
|
INFO_STYLE: Style = Style()
|
|
36
38
|
PANEL_STYLE: Style = Style()
|
|
@@ -98,6 +100,24 @@ class CliConsole(AbstractConsole):
|
|
|
98
100
|
finally:
|
|
99
101
|
self._extra_indent -= 1
|
|
100
102
|
|
|
103
|
+
@contextmanager
|
|
104
|
+
def spinner(self):
|
|
105
|
+
"""
|
|
106
|
+
A context manager for displaying a spinner while executing a long-running operation.
|
|
107
|
+
|
|
108
|
+
Usage:
|
|
109
|
+
with cli_console.spinner("Processing data") as spinner:
|
|
110
|
+
spinner.add_task(description="Long operation", total=None)
|
|
111
|
+
# Long running operation here
|
|
112
|
+
result = some_operation()
|
|
113
|
+
"""
|
|
114
|
+
with Progress(
|
|
115
|
+
SpinnerColumn(),
|
|
116
|
+
TextColumn("[progress.description]{task.description}", style=SPINNER_STYLE),
|
|
117
|
+
transient=True,
|
|
118
|
+
) as progress:
|
|
119
|
+
yield progress
|
|
120
|
+
|
|
101
121
|
def step(self, message: str):
|
|
102
122
|
"""Displays a message to output.
|
|
103
123
|
|
snowflake/cli/api/constants.py
CHANGED
|
@@ -75,6 +75,15 @@ class ObjectType(Enum):
|
|
|
75
75
|
return self.value.cli_name
|
|
76
76
|
|
|
77
77
|
|
|
78
|
+
class PatternMatchingType(Enum):
|
|
79
|
+
"""
|
|
80
|
+
Enum for different pattern matching types used in artifact resolution.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
GLOB = "glob"
|
|
84
|
+
REGEX = "regex"
|
|
85
|
+
|
|
86
|
+
|
|
78
87
|
OBJECT_TO_NAMES = {o.value.cli_name: o.value for o in ObjectType}
|
|
79
88
|
UNSUPPORTED_OBJECTS = {
|
|
80
89
|
ObjectType.APPLICATION.value.cli_name,
|
|
@@ -87,6 +87,7 @@ def sync_deploy_root_with_stage(
|
|
|
87
87
|
prune: bool,
|
|
88
88
|
recursive: bool,
|
|
89
89
|
stage_path_parts: StagePathParts,
|
|
90
|
+
use_temporary_stage: bool = False,
|
|
90
91
|
role: str | None = None,
|
|
91
92
|
package_name: str | None = None,
|
|
92
93
|
local_paths_to_sync: List[Path] | None = None,
|
|
@@ -103,6 +104,7 @@ def sync_deploy_root_with_stage(
|
|
|
103
104
|
prune (bool): Whether to prune artifacts from the stage that don't exist locally.
|
|
104
105
|
recursive (bool): Whether to traverse directories recursively.
|
|
105
106
|
stage_path_parts (StagePathParts): stage path parts object.
|
|
107
|
+
use_temporary_stage (bool): specifies if new stage should be temporary.
|
|
106
108
|
|
|
107
109
|
package_name (str): supported for Native App compatibility. Should be None out of Native App context.
|
|
108
110
|
|
|
@@ -120,8 +122,11 @@ def sync_deploy_root_with_stage(
|
|
|
120
122
|
elif not package_name:
|
|
121
123
|
# ensure stage exists
|
|
122
124
|
stage_fqn = FQN.from_stage(stage_path_parts.stage)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
+
if use_temporary_stage:
|
|
126
|
+
console.step(f"Creating temporary stage {stage_fqn}.")
|
|
127
|
+
else:
|
|
128
|
+
console.step(f"Creating stage {stage_fqn} if not exists.")
|
|
129
|
+
StageManager().create(fqn=stage_fqn, temporary=use_temporary_stage)
|
|
125
130
|
else:
|
|
126
131
|
# ensure stage exists - nativeapp behavior
|
|
127
132
|
sql_facade = get_snowflake_facade()
|
|
@@ -134,10 +139,10 @@ def sync_deploy_root_with_stage(
|
|
|
134
139
|
)
|
|
135
140
|
if not sql_facade.stage_exists(stage_fqn):
|
|
136
141
|
sql_facade.create_schema(schema, database=package_name)
|
|
137
|
-
sql_facade.create_stage(stage_fqn)
|
|
142
|
+
sql_facade.create_stage(stage_fqn, temporary=use_temporary_stage)
|
|
138
143
|
|
|
139
144
|
# Perform a diff operation and display results to the user for informational purposes
|
|
140
|
-
if print_diff:
|
|
145
|
+
if print_diff and not use_temporary_stage:
|
|
141
146
|
console.step(
|
|
142
147
|
f"Performing a diff between the Snowflake stage: {stage_path_parts.path} and your local deploy_root: {deploy_root.resolve()}."
|
|
143
148
|
)
|
|
@@ -199,8 +204,7 @@ def sync_deploy_root_with_stage(
|
|
|
199
204
|
# Upload diff-ed files to the stage
|
|
200
205
|
if diff.has_changes():
|
|
201
206
|
console.step(
|
|
202
|
-
"
|
|
203
|
-
% deploy_root.resolve(),
|
|
207
|
+
f"Uploading files from local {deploy_root.resolve()} directory to{' temporary' if use_temporary_stage else ''} stage."
|
|
204
208
|
)
|
|
205
209
|
sync_local_diff_with_stage(
|
|
206
210
|
role=role,
|
|
@@ -65,6 +65,7 @@ class FeatureFlag(FeatureFlagMixin):
|
|
|
65
65
|
ENABLE_SNOWPARK_GLOB_SUPPORT = BooleanFlag("ENABLE_SNOWPARK_GLOB_SUPPORT", False)
|
|
66
66
|
ENABLE_SPCS_SERVICE_EVENTS = BooleanFlag("ENABLE_SPCS_SERVICE_EVENTS", False)
|
|
67
67
|
ENABLE_DBT = BooleanFlag("ENABLE_DBT", False)
|
|
68
|
+
ENABLE_DBT_GA_FEATURES = BooleanFlag("ENABLE_DBT_GA_FEATURES", False)
|
|
68
69
|
ENABLE_AUTH_KEYPAIR = BooleanFlag("ENABLE_AUTH_KEYPAIR", False)
|
|
69
70
|
ENABLE_NATIVE_APP_PYTHON_SETUP = BooleanFlag(
|
|
70
71
|
"ENABLE_NATIVE_APP_PYTHON_SETUP", False
|
snowflake/cli/api/identifiers.py
CHANGED
|
@@ -15,14 +15,20 @@
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
17
|
import re
|
|
18
|
+
import time
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
20
21
|
from click import ClickException
|
|
22
|
+
from snowflake.cli.api.constants import ObjectType
|
|
21
23
|
from snowflake.cli.api.exceptions import FQNInconsistencyError, FQNNameError
|
|
22
24
|
from snowflake.cli.api.project.schemas.v1.identifier_model import (
|
|
23
25
|
ObjectIdentifierBaseModel,
|
|
24
26
|
)
|
|
25
|
-
from snowflake.cli.api.project.util import
|
|
27
|
+
from snowflake.cli.api.project.util import (
|
|
28
|
+
VALID_IDENTIFIER_REGEX,
|
|
29
|
+
identifier_for_url,
|
|
30
|
+
unquote_identifier,
|
|
31
|
+
)
|
|
26
32
|
|
|
27
33
|
|
|
28
34
|
class FQN:
|
|
@@ -167,6 +173,17 @@ class FQN:
|
|
|
167
173
|
|
|
168
174
|
return fqn.set_database(model.database).set_schema(model.schema_)
|
|
169
175
|
|
|
176
|
+
@classmethod
|
|
177
|
+
def from_resource(
|
|
178
|
+
cls, resource_type: ObjectType, resource_fqn: FQN, purpose: str
|
|
179
|
+
) -> "FQN":
|
|
180
|
+
"""Create an instance related to another Snowflake resource."""
|
|
181
|
+
unquoted_name = unquote_identifier(resource_fqn.name)
|
|
182
|
+
safe_cli_name = resource_type.value.cli_name.upper().replace("-", "_")
|
|
183
|
+
return cls.from_string(
|
|
184
|
+
f"{safe_cli_name}_{unquoted_name}_{int(time.time())}_{purpose.upper()}"
|
|
185
|
+
).using_context()
|
|
186
|
+
|
|
170
187
|
def set_database(self, database: str | None) -> "FQN":
|
|
171
188
|
if database:
|
|
172
189
|
self._database = database
|