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.
Files changed (56) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +43 -1
  3. snowflake/cli/_app/commands_registration/builtin_plugins.py +1 -1
  4. snowflake/cli/_app/commands_registration/command_plugins_loader.py +14 -1
  5. snowflake/cli/_app/printing.py +153 -19
  6. snowflake/cli/_app/telemetry.py +25 -10
  7. snowflake/cli/_plugins/auth/__init__.py +0 -2
  8. snowflake/cli/_plugins/connection/commands.py +1 -78
  9. snowflake/cli/_plugins/dbt/commands.py +44 -19
  10. snowflake/cli/_plugins/dbt/constants.py +1 -1
  11. snowflake/cli/_plugins/dbt/manager.py +252 -47
  12. snowflake/cli/_plugins/dcm/commands.py +65 -90
  13. snowflake/cli/_plugins/dcm/manager.py +137 -50
  14. snowflake/cli/_plugins/logs/commands.py +7 -0
  15. snowflake/cli/_plugins/logs/manager.py +21 -1
  16. snowflake/cli/_plugins/nativeapp/entities/application_package.py +4 -1
  17. snowflake/cli/_plugins/nativeapp/sf_sql_facade.py +3 -1
  18. snowflake/cli/_plugins/object/manager.py +1 -0
  19. snowflake/cli/_plugins/snowpark/common.py +1 -0
  20. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +29 -5
  21. snowflake/cli/_plugins/snowpark/package_utils.py +44 -3
  22. snowflake/cli/_plugins/spcs/services/commands.py +19 -1
  23. snowflake/cli/_plugins/spcs/services/manager.py +17 -4
  24. snowflake/cli/_plugins/spcs/services/service_entity_model.py +5 -0
  25. snowflake/cli/_plugins/sql/lexer/types.py +1 -0
  26. snowflake/cli/_plugins/sql/repl.py +100 -26
  27. snowflake/cli/_plugins/sql/repl_commands.py +607 -0
  28. snowflake/cli/_plugins/sql/statement_reader.py +44 -20
  29. snowflake/cli/_plugins/streamlit/streamlit_entity.py +28 -2
  30. snowflake/cli/_plugins/streamlit/streamlit_entity_model.py +24 -4
  31. snowflake/cli/api/artifacts/bundle_map.py +32 -2
  32. snowflake/cli/api/artifacts/regex_resolver.py +54 -0
  33. snowflake/cli/api/artifacts/upload.py +5 -1
  34. snowflake/cli/api/artifacts/utils.py +12 -1
  35. snowflake/cli/api/cli_global_context.py +7 -0
  36. snowflake/cli/api/commands/decorators.py +7 -0
  37. snowflake/cli/api/commands/flags.py +24 -1
  38. snowflake/cli/api/console/abc.py +13 -2
  39. snowflake/cli/api/console/console.py +20 -0
  40. snowflake/cli/api/constants.py +9 -0
  41. snowflake/cli/api/entities/utils.py +10 -6
  42. snowflake/cli/api/feature_flags.py +3 -2
  43. snowflake/cli/api/identifiers.py +18 -1
  44. snowflake/cli/api/project/schemas/entities/entities.py +0 -6
  45. snowflake/cli/api/rendering/sql_templates.py +2 -0
  46. {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/METADATA +7 -7
  47. {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/RECORD +51 -54
  48. snowflake/cli/_plugins/auth/keypair/__init__.py +0 -0
  49. snowflake/cli/_plugins/auth/keypair/commands.py +0 -153
  50. snowflake/cli/_plugins/auth/keypair/manager.py +0 -331
  51. snowflake/cli/_plugins/dcm/dcm_project_entity_model.py +0 -59
  52. snowflake/cli/_plugins/sql/snowsql_commands.py +0 -331
  53. /snowflake/cli/_plugins/auth/{keypair/plugin_spec.py → plugin_spec.py} +0 -0
  54. {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/WHEEL +0 -0
  55. {snowflake_cli-3.11.0.dist-info → snowflake_cli-3.13.0.dist-info}/entry_points.txt +0 -0
  56. {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.snowsql_commands import (
11
- SnowSQLCommand,
12
- compile_snowsql_command,
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
- SNOWSQL_COMMAND = "snowsql_command"
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(path_part, timeout=10.0).read().decode()
96
- return cls(payload, StatementType.URL, path_part)
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
- path = SecurePath(path_part)
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(path_part, StatementType.FILE, raw_source, error_msg)
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.SNOWSQL_COMMAND, None)
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: SnowSQLCommand | None = None
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.SNOWSQL_COMMAND:
296
+ if stmt.statement_type == StatementType.REPL_COMMAND:
278
297
  if not stmt.error:
279
- cmd = (
298
+ command_text = (
280
299
  stmt.statement.read()
281
300
  .removesuffix(ASYNC_SUFFIX)
282
301
  .removesuffix(";")
283
- .split()
284
- )
285
- parsed_command = compile_snowsql_command(
286
- command=cmd[0], cmd_args=cmd[1:]
302
+ .strip()
287
303
  )
288
- if parsed_command.error_message:
289
- errors.append(parsed_command.error_message)
290
- else:
291
- compiled.append(CompiledStatement(command=parsed_command.command))
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: Optional[bool] = False,
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(replace=replace, from_stage_name=stage_root)
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
- DiscriminatorField,
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__(self, *, project_root: Path, deploy_root: Path):
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
- for resolved_src in self._project_root.glob(src):
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(project_paths: ProjectPaths, artifacts: Artifacts) -> BundleMap:
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.
@@ -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 three methods implemented:
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
 
@@ -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,