snowflake-cli 3.11.0__py3-none-any.whl → 3.13.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 +43 -1
- snowflake/cli/_app/commands_registration/builtin_plugins.py +1 -1
- snowflake/cli/_app/commands_registration/command_plugins_loader.py +14 -1
- snowflake/cli/_app/printing.py +153 -19
- snowflake/cli/_app/telemetry.py +25 -10
- snowflake/cli/_plugins/auth/__init__.py +0 -2
- snowflake/cli/_plugins/connection/commands.py +1 -78
- snowflake/cli/_plugins/dbt/commands.py +44 -19
- snowflake/cli/_plugins/dbt/constants.py +1 -1
- snowflake/cli/_plugins/dbt/manager.py +252 -47
- 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/entities/application_package.py +4 -1
- snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +3 -1
- snowflake/cli/_plugins/object/manager.py +1 -0
- 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/commands.py +19 -1
- snowflake/cli/_plugins/spcs/services/manager.py +17 -4
- snowflake/cli/_plugins/spcs/services/service_entity_model.py +5 -0
- 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/_plugins/streamlit/streamlit_entity.py +28 -2
- snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +24 -4
- 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 +24 -1
- 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 +3 -2
- 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.13.0.dist-info}/METADATA +7 -7
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/RECORD +51 -54
- snowflake/cli/_plugins/auth/keypair/__init__.py +0 -0
- snowflake/cli/_plugins/auth/keypair/commands.py +0 -153
- snowflake/cli/_plugins/auth/keypair/manager.py +0 -331
- snowflake/cli/_plugins/dcm/dcm_project_entity_model.py +0 -59
- snowflake/cli/_plugins/sql/snowsql_commands.py +0 -331
- /snowflake/cli/_plugins/auth/{keypair/plugin_spec.py → plugin_spec.py} +0 -0
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/WHEEL +0 -0
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/entry_points.txt +0 -0
- {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.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)
|
|
@@ -6,7 +6,9 @@ from click import ClickException
|
|
|
6
6
|
from snowflake.cli._plugins.connection.util import make_snowsight_url
|
|
7
7
|
from snowflake.cli._plugins.nativeapp.artifacts import build_bundle
|
|
8
8
|
from snowflake.cli._plugins.stage.manager import StageManager
|
|
9
|
+
from snowflake.cli._plugins.streamlit.manager import StreamlitManager
|
|
9
10
|
from snowflake.cli._plugins.streamlit.streamlit_entity_model import (
|
|
11
|
+
SPCS_RUNTIME_V2_NAME,
|
|
10
12
|
StreamlitEntityModel,
|
|
11
13
|
)
|
|
12
14
|
from snowflake.cli._plugins.workspace.context import ActionContext
|
|
@@ -64,6 +66,14 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
64
66
|
self._conn, f"/#/streamlit-apps/{name.url_identifier}"
|
|
65
67
|
)
|
|
66
68
|
|
|
69
|
+
def _is_spcs_runtime_v2_mode(self, experimental: bool = False) -> bool:
|
|
70
|
+
"""Check if SPCS runtime v2 mode is enabled."""
|
|
71
|
+
return (
|
|
72
|
+
experimental
|
|
73
|
+
and self.model.runtime_name == SPCS_RUNTIME_V2_NAME
|
|
74
|
+
and self.model.compute_pool
|
|
75
|
+
)
|
|
76
|
+
|
|
67
77
|
def bundle(self, output_dir: Optional[Path] = None) -> BundleMap:
|
|
68
78
|
return build_bundle(
|
|
69
79
|
self.root,
|
|
@@ -83,7 +93,7 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
83
93
|
replace: bool,
|
|
84
94
|
prune: bool = False,
|
|
85
95
|
bundle_map: Optional[BundleMap] = None,
|
|
86
|
-
experimental:
|
|
96
|
+
experimental: bool = False,
|
|
87
97
|
*args,
|
|
88
98
|
**kwargs,
|
|
89
99
|
):
|
|
@@ -129,9 +139,15 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
129
139
|
console.step(f"Creating Streamlit object {self.model.fqn.sql_identifier}")
|
|
130
140
|
|
|
131
141
|
self._execute_query(
|
|
132
|
-
self.get_deploy_sql(
|
|
142
|
+
self.get_deploy_sql(
|
|
143
|
+
replace=replace,
|
|
144
|
+
from_stage_name=stage_root,
|
|
145
|
+
experimental=False,
|
|
146
|
+
)
|
|
133
147
|
)
|
|
134
148
|
|
|
149
|
+
StreamlitManager(connection=self._conn).grant_privileges(self.model)
|
|
150
|
+
|
|
135
151
|
return self.perform(EntityActions.GET_URL, action_context, *args, **kwargs)
|
|
136
152
|
|
|
137
153
|
def describe(self) -> SnowflakeCursor:
|
|
@@ -156,6 +172,7 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
156
172
|
artifacts_dir: Optional[Path] = None,
|
|
157
173
|
schema: Optional[str] = None,
|
|
158
174
|
database: Optional[str] = None,
|
|
175
|
+
experimental: bool = False,
|
|
159
176
|
*args,
|
|
160
177
|
**kwargs,
|
|
161
178
|
) -> str:
|
|
@@ -199,6 +216,12 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
199
216
|
if self.model.secrets:
|
|
200
217
|
query += "\n" + self.model.get_secrets_sql()
|
|
201
218
|
|
|
219
|
+
# SPCS runtime fields are only supported for FBE/versioned streamlits (FROM syntax)
|
|
220
|
+
# Never add these fields for stage-based deployments (ROOT_LOCATION syntax)
|
|
221
|
+
if not from_stage_name and self._is_spcs_runtime_v2_mode(experimental):
|
|
222
|
+
query += f"\nRUNTIME_NAME = '{self.model.runtime_name}'"
|
|
223
|
+
query += f"\nCOMPUTE_POOL = '{self.model.compute_pool}'"
|
|
224
|
+
|
|
202
225
|
return query + ";"
|
|
203
226
|
|
|
204
227
|
def get_describe_sql(self) -> str:
|
|
@@ -233,6 +256,7 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
233
256
|
self.get_deploy_sql(
|
|
234
257
|
if_not_exists=True,
|
|
235
258
|
replace=replace,
|
|
259
|
+
experimental=True,
|
|
236
260
|
)
|
|
237
261
|
)
|
|
238
262
|
try:
|
|
@@ -256,3 +280,5 @@ class StreamlitEntity(EntityBase[StreamlitEntityModel]):
|
|
|
256
280
|
print_diff=True,
|
|
257
281
|
force_overwrite=True, # files copied to streamlit vstage need to be overwritten
|
|
258
282
|
)
|
|
283
|
+
|
|
284
|
+
StreamlitManager(connection=self._conn).grant_privileges(self.model)
|
|
@@ -15,7 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
from typing import Literal, Optional
|
|
17
17
|
|
|
18
|
-
from pydantic import Field
|
|
18
|
+
from pydantic import Field, model_validator
|
|
19
19
|
from snowflake.cli.api.project.schemas.entities.common import (
|
|
20
20
|
Artifacts,
|
|
21
21
|
EntityModelBaseWithArtifacts,
|
|
@@ -23,9 +23,10 @@ from snowflake.cli.api.project.schemas.entities.common import (
|
|
|
23
23
|
GrantBaseModel,
|
|
24
24
|
ImportsBaseModel,
|
|
25
25
|
)
|
|
26
|
-
from snowflake.cli.api.project.schemas.updatable_model import
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
from snowflake.cli.api.project.schemas.updatable_model import DiscriminatorField
|
|
27
|
+
|
|
28
|
+
# SPCS Runtime v2 constants
|
|
29
|
+
SPCS_RUNTIME_V2_NAME = "SYSTEM$ST_CONTAINER_RUNTIME_PY3_11"
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class StreamlitEntityModel(
|
|
@@ -54,3 +55,22 @@ class StreamlitEntityModel(
|
|
|
54
55
|
title="List of paths or file source/destination pairs to add to the deploy root",
|
|
55
56
|
default=None,
|
|
56
57
|
)
|
|
58
|
+
runtime_name: Optional[str] = Field(
|
|
59
|
+
title="The runtime name to run the streamlit app on", default=None
|
|
60
|
+
)
|
|
61
|
+
compute_pool: Optional[str] = Field(
|
|
62
|
+
title="The compute pool name of the snowservices running the streamlit app",
|
|
63
|
+
default=None,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@model_validator(mode="after")
|
|
67
|
+
def validate_spcs_runtime_fields(self):
|
|
68
|
+
"""Validate that runtime_name and compute_pool are provided together for SPCS container runtime."""
|
|
69
|
+
# Only validate for SPCS container runtime, not warehouse runtime
|
|
70
|
+
if self.compute_pool and not self.runtime_name:
|
|
71
|
+
raise ValueError("compute_pool is specified without runtime_name")
|
|
72
|
+
if self.runtime_name == SPCS_RUNTIME_V2_NAME and not self.compute_pool:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"compute_pool is required when using {SPCS_RUNTIME_V2_NAME}"
|
|
75
|
+
)
|
|
76
|
+
return self
|
|
@@ -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
|
|
@@ -27,6 +27,7 @@ from snowflake.cli.api.commands.flags import (
|
|
|
27
27
|
ConnectionOption,
|
|
28
28
|
DatabaseOption,
|
|
29
29
|
DebugOption,
|
|
30
|
+
DecimalPrecisionOption,
|
|
30
31
|
DiagAllowlistPathOption,
|
|
31
32
|
DiagLogPathOption,
|
|
32
33
|
EnableDiagOption,
|
|
@@ -446,6 +447,12 @@ GLOBAL_OPTIONS = [
|
|
|
446
447
|
annotation=Optional[bool],
|
|
447
448
|
default=EnhancedExitCodesOption,
|
|
448
449
|
),
|
|
450
|
+
inspect.Parameter(
|
|
451
|
+
"decimal_precision",
|
|
452
|
+
inspect.Parameter.KEYWORD_ONLY,
|
|
453
|
+
annotation=Optional[int],
|
|
454
|
+
default=DecimalPrecisionOption,
|
|
455
|
+
),
|
|
449
456
|
]
|
|
450
457
|
|
|
451
458
|
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
from decimal import getcontext
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any, Callable, Optional
|
|
19
20
|
|
|
@@ -27,7 +28,7 @@ from snowflake.cli.api.cli_global_context import (
|
|
|
27
28
|
from snowflake.cli.api.commands.common import OnErrorType
|
|
28
29
|
from snowflake.cli.api.commands.overrideable_parameter import OverrideableOption
|
|
29
30
|
from snowflake.cli.api.commands.utils import parse_key_value_variables
|
|
30
|
-
from snowflake.cli.api.config import get_all_connections
|
|
31
|
+
from snowflake.cli.api.config import get_all_connections, get_config_value
|
|
31
32
|
from snowflake.cli.api.connections import ConnectionContext
|
|
32
33
|
from snowflake.cli.api.console import cli_console
|
|
33
34
|
from snowflake.cli.api.identifiers import FQN
|
|
@@ -513,6 +514,28 @@ EnhancedExitCodesOption = typer.Option(
|
|
|
513
514
|
envvar="SNOWFLAKE_ENHANCED_EXIT_CODES",
|
|
514
515
|
)
|
|
515
516
|
|
|
517
|
+
|
|
518
|
+
def _decimal_precision_callback(value: int | None):
|
|
519
|
+
"""Callback to set decimal precision globally when provided."""
|
|
520
|
+
if value is None:
|
|
521
|
+
try:
|
|
522
|
+
value = get_config_value(key="decimal_precision", default=None)
|
|
523
|
+
except Exception:
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
if value is not None:
|
|
527
|
+
getcontext().prec = value
|
|
528
|
+
return value
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
DecimalPrecisionOption = typer.Option(
|
|
532
|
+
None,
|
|
533
|
+
"--decimal-precision",
|
|
534
|
+
help="Number of decimal places to display for decimal values. Uses Python's default precision if not specified. [env var: SNOWFLAKE_DECIMAL_PRECISION]",
|
|
535
|
+
callback=_decimal_precision_callback,
|
|
536
|
+
rich_help_panel=_CLI_BEHAVIOUR,
|
|
537
|
+
)
|
|
538
|
+
|
|
516
539
|
# If IfExistsOption, IfNotExistsOption, or ReplaceOption are used with names other than those in CREATE_MODE_OPTION_NAMES,
|
|
517
540
|
# you must also override mutually_exclusive if you want to retain the validation that at most one of these flags is
|
|
518
541
|
# passed.
|
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,
|