snowflake-cli 3.11.0__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/cli_app.py +0 -1
- snowflake/cli/_app/printing.py +153 -19
- snowflake/cli/_plugins/dbt/commands.py +37 -8
- snowflake/cli/_plugins/dbt/manager.py +144 -10
- snowflake/cli/_plugins/dcm/commands.py +65 -90
- snowflake/cli/_plugins/dcm/manager.py +137 -50
- 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/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/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-3.11.0.dist-info → snowflake_cli-3.12.0.dist-info}/METADATA +5 -5
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.12.0.dist-info}/RECORD +36 -36
- snowflake/cli/_plugins/dcm/dcm_project_entity_model.py +0 -59
- snowflake/cli/_plugins/sql/snowsql_commands.py +0 -331
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.12.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.12.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.11.0.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
|
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
|
|
@@ -16,10 +16,6 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
from typing import Dict, List, Union, get_args
|
|
18
18
|
|
|
19
|
-
from snowflake.cli._plugins.dcm.dcm_project_entity_model import (
|
|
20
|
-
DCMProjectEntity,
|
|
21
|
-
DCMProjectEntityModel,
|
|
22
|
-
)
|
|
23
19
|
from snowflake.cli._plugins.nativeapp.entities.application import (
|
|
24
20
|
ApplicationEntity,
|
|
25
21
|
ApplicationEntityModel,
|
|
@@ -62,7 +58,6 @@ Entity = Union[
|
|
|
62
58
|
ApplicationPackageEntity,
|
|
63
59
|
StreamlitEntity,
|
|
64
60
|
ProcedureEntity,
|
|
65
|
-
DCMProjectEntity,
|
|
66
61
|
FunctionEntity,
|
|
67
62
|
ComputePoolEntity,
|
|
68
63
|
ImageRepositoryEntity,
|
|
@@ -79,7 +74,6 @@ EntityModel = Union[
|
|
|
79
74
|
ImageRepositoryEntityModel,
|
|
80
75
|
ServiceEntityModel,
|
|
81
76
|
NotebookEntityModel,
|
|
82
|
-
DCMProjectEntityModel,
|
|
83
77
|
]
|
|
84
78
|
|
|
85
79
|
ALL_ENTITIES: List[Entity] = [*get_args(Entity)]
|
|
@@ -35,6 +35,7 @@ _SQL_TEMPLATE_START = "<%"
|
|
|
35
35
|
_SQL_TEMPLATE_END = "%>"
|
|
36
36
|
_OLD_SQL_TEMPLATE_START = "&{"
|
|
37
37
|
_OLD_SQL_TEMPLATE_END = "}"
|
|
38
|
+
_JINJA_TEMPLATE_START, _JINJA_TEMPLATE_END = "{{", "}}"
|
|
38
39
|
RESERVED_KEYS = [CONTEXT_KEY, FUNCTION_KEY]
|
|
39
40
|
|
|
40
41
|
|
|
@@ -62,6 +63,7 @@ def has_sql_templates(template_content: str) -> bool:
|
|
|
62
63
|
return (
|
|
63
64
|
_OLD_SQL_TEMPLATE_START in template_content
|
|
64
65
|
or _SQL_TEMPLATE_START in template_content
|
|
66
|
+
or _JINJA_TEMPLATE_START in template_content
|
|
65
67
|
)
|
|
66
68
|
|
|
67
69
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snowflake-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.12.0
|
|
4
4
|
Summary: Snowflake CLI
|
|
5
5
|
Project-URL: Source code, https://github.com/snowflakedb/snowflake-cli
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/snowflakedb/snowflake-cli/issues
|
|
@@ -231,14 +231,14 @@ Requires-Dist: requests==2.32.4
|
|
|
231
231
|
Requires-Dist: requirements-parser==0.13.0
|
|
232
232
|
Requires-Dist: rich==14.0.0
|
|
233
233
|
Requires-Dist: setuptools==80.8.0
|
|
234
|
-
Requires-Dist: snowflake-connector-python[secure-local-storage]==3.17.
|
|
235
|
-
Requires-Dist: snowflake-core==1.
|
|
234
|
+
Requires-Dist: snowflake-connector-python[secure-local-storage]==3.17.3
|
|
235
|
+
Requires-Dist: snowflake-core==1.7.0
|
|
236
236
|
Requires-Dist: snowflake-snowpark-python==1.33.0; python_version < '3.12'
|
|
237
237
|
Requires-Dist: tomlkit==0.13.3
|
|
238
|
-
Requires-Dist: typer==0.
|
|
238
|
+
Requires-Dist: typer==0.17.3
|
|
239
239
|
Requires-Dist: urllib3<2.6,>=1.24.3
|
|
240
240
|
Provides-Extra: development
|
|
241
|
-
Requires-Dist: coverage==7.
|
|
241
|
+
Requires-Dist: coverage==7.10.4; extra == 'development'
|
|
242
242
|
Requires-Dist: factory-boy==3.3.3; extra == 'development'
|
|
243
243
|
Requires-Dist: faker==37.4.0; extra == 'development'
|
|
244
244
|
Requires-Dist: pre-commit>=3.5.0; extra == 'development'
|