snowflake-cli 3.0.2__py3-none-any.whl → 3.1.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 (57) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +3 -0
  3. snowflake/cli/_app/dev/docs/templates/overview.rst.jinja2 +1 -1
  4. snowflake/cli/_app/dev/docs/templates/usage.rst.jinja2 +2 -2
  5. snowflake/cli/_app/telemetry.py +69 -4
  6. snowflake/cli/_plugins/connection/commands.py +40 -2
  7. snowflake/cli/_plugins/git/commands.py +6 -3
  8. snowflake/cli/_plugins/git/manager.py +5 -0
  9. snowflake/cli/_plugins/nativeapp/artifacts.py +13 -3
  10. snowflake/cli/_plugins/nativeapp/codegen/artifact_processor.py +1 -1
  11. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +7 -0
  12. snowflake/cli/_plugins/nativeapp/codegen/sandbox.py +10 -10
  13. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +2 -2
  14. snowflake/cli/_plugins/nativeapp/codegen/snowpark/extension_function_utils.py +1 -1
  15. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +8 -8
  16. snowflake/cli/_plugins/nativeapp/commands.py +135 -186
  17. snowflake/cli/_plugins/nativeapp/entities/application.py +176 -24
  18. snowflake/cli/_plugins/nativeapp/entities/application_package.py +112 -136
  19. snowflake/cli/_plugins/nativeapp/exceptions.py +12 -0
  20. snowflake/cli/_plugins/nativeapp/manager.py +3 -26
  21. snowflake/cli/_plugins/nativeapp/v2_conversions/{v2_to_v1_decorator.py → compat.py} +131 -72
  22. snowflake/cli/_plugins/nativeapp/version/commands.py +30 -29
  23. snowflake/cli/_plugins/nativeapp/version/version_processor.py +1 -43
  24. snowflake/cli/_plugins/snowpark/common.py +60 -18
  25. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +2 -2
  26. snowflake/cli/_plugins/spcs/image_repository/commands.py +4 -37
  27. snowflake/cli/_plugins/spcs/image_repository/manager.py +4 -1
  28. snowflake/cli/_plugins/spcs/services/commands.py +36 -4
  29. snowflake/cli/_plugins/spcs/services/manager.py +36 -4
  30. snowflake/cli/_plugins/stage/commands.py +8 -3
  31. snowflake/cli/_plugins/stage/diff.py +16 -16
  32. snowflake/cli/_plugins/stage/manager.py +164 -73
  33. snowflake/cli/_plugins/stage/md5.py +1 -1
  34. snowflake/cli/_plugins/workspace/commands.py +21 -1
  35. snowflake/cli/_plugins/workspace/context.py +38 -0
  36. snowflake/cli/_plugins/workspace/manager.py +23 -13
  37. snowflake/cli/api/cli_global_context.py +3 -3
  38. snowflake/cli/api/commands/flags.py +23 -7
  39. snowflake/cli/api/config.py +7 -4
  40. snowflake/cli/api/connections.py +12 -1
  41. snowflake/cli/api/entities/common.py +4 -2
  42. snowflake/cli/api/entities/utils.py +17 -37
  43. snowflake/cli/api/exceptions.py +32 -0
  44. snowflake/cli/api/identifiers.py +8 -0
  45. snowflake/cli/api/project/definition_conversion.py +139 -40
  46. snowflake/cli/api/project/schemas/entities/common.py +11 -0
  47. snowflake/cli/api/project/schemas/project_definition.py +30 -25
  48. snowflake/cli/api/sql_execution.py +5 -7
  49. snowflake/cli/api/stage_path.py +241 -0
  50. snowflake/cli/api/utils/definition_rendering.py +3 -5
  51. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.1.0.dist-info}/METADATA +11 -11
  52. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.1.0.dist-info}/RECORD +55 -55
  53. snowflake/cli/_plugins/nativeapp/teardown_processor.py +0 -70
  54. snowflake/cli/_plugins/workspace/action_context.py +0 -18
  55. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.1.0.dist-info}/WHEEL +0 -0
  56. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.1.0.dist-info}/entry_points.txt +0 -0
  57. {snowflake_cli-3.0.2.dist-info → snowflake_cli-3.1.0.dist-info}/licenses/LICENSE +0 -0
@@ -19,6 +19,7 @@ from typing import Any, Dict, List, Optional, Union
19
19
 
20
20
  from packaging.version import Version
21
21
  from pydantic import Field, ValidationError, field_validator, model_validator
22
+ from pydantic_core.core_schema import ValidationInfo
22
23
  from snowflake.cli._plugins.nativeapp.entities.application import ApplicationEntityModel
23
24
  from snowflake.cli.api.project.errors import SchemaValidationError
24
25
  from snowflake.cli.api.project.schemas.entities.common import (
@@ -115,7 +116,17 @@ class DefinitionV11(DefinitionV10):
115
116
 
116
117
 
117
118
  class DefinitionV20(_ProjectDefinitionBase):
118
- entities: Dict[str, AnnotatedEntity] = Field(title="Entity definitions.")
119
+ entities: Dict[str, AnnotatedEntity] = Field(
120
+ title="Entity definitions.", default={}
121
+ )
122
+ env: Optional[Dict[str, Union[str, int, bool]]] = Field(
123
+ title="Default environment specification for this project.",
124
+ default=None,
125
+ )
126
+ mixins: Optional[Dict[str, Dict]] = Field(
127
+ title="Mixins to apply to entities",
128
+ default=None,
129
+ )
119
130
 
120
131
  @model_validator(mode="after")
121
132
  def validate_entities_identifiers(self):
@@ -163,38 +174,32 @@ class DefinitionV20(_ProjectDefinitionBase):
163
174
  f"Target type mismatch. Expected {target_type.__name__}, got {actual_target_type.__name__}"
164
175
  )
165
176
 
166
- env: Optional[Dict[str, Union[str, int, bool]]] = Field(
167
- title="Default environment specification for this project.",
168
- default=None,
169
- )
170
-
171
- mixins: Optional[Dict[str, Dict]] = Field(
172
- title="Mixins to apply to entities",
173
- default=None,
174
- )
175
-
176
177
  @model_validator(mode="before")
177
178
  @classmethod
178
- def apply_mixins(cls, data: Dict) -> Dict:
179
+ def apply_mixins(cls, data: Dict, info: ValidationInfo) -> Dict:
179
180
  """
180
181
  Applies mixins to those entities, whose meta field contains the mixin name.
181
182
  """
182
183
  if "mixins" not in data or "entities" not in data:
183
184
  return data
184
185
 
185
- entities = data["entities"]
186
- for entity_name, entity in entities.items():
187
- entity_mixins = entity_mixins_to_list(
188
- entity.get("meta", {}).get("use_mixins")
189
- )
186
+ duplicated_run = (
187
+ info.context.get("is_duplicated_run", False) if info.context else False
188
+ )
189
+ if not duplicated_run:
190
+ entities = data["entities"]
191
+ for entity_name, entity in entities.items():
192
+ entity_mixins = entity_mixins_to_list(
193
+ entity.get("meta", {}).get("use_mixins")
194
+ )
190
195
 
191
- merged_values = cls._merge_mixins_with_entity(
192
- entity_id=entity_name,
193
- entity=entity,
194
- entity_mixins_names=entity_mixins,
195
- mixin_defs=data["mixins"],
196
- )
197
- entities[entity_name] = merged_values
196
+ merged_values = cls._merge_mixins_with_entity(
197
+ entity_id=entity_name,
198
+ entity=entity,
199
+ entity_mixins_names=entity_mixins,
200
+ mixin_defs=data["mixins"],
201
+ )
202
+ entities[entity_name] = merged_values
198
203
  return data
199
204
 
200
205
  @classmethod
@@ -325,6 +330,6 @@ def get_allowed_fields_for_entity(entity: Dict[str, Any]) -> List[str]:
325
330
  def _unique_extend(list_a: List, list_b: List) -> List:
326
331
  new_list = list(list_a)
327
332
  for item in list_b:
328
- if item not in list_a:
333
+ if all(item != x for x in list_a):
329
334
  new_list.append(item)
330
335
  return new_list
@@ -25,8 +25,10 @@ from snowflake.cli.api.cli_global_context import get_cli_context
25
25
  from snowflake.cli.api.console import cli_console
26
26
  from snowflake.cli.api.constants import ObjectType
27
27
  from snowflake.cli.api.exceptions import (
28
+ CouldNotUseObjectError,
28
29
  DatabaseNotProvidedError,
29
30
  SchemaNotProvidedError,
31
+ ShowSpecificObjectMultipleRowsError,
30
32
  SnowflakeSQLExecutionError,
31
33
  )
32
34
  from snowflake.cli.api.identifiers import FQN
@@ -91,11 +93,9 @@ class SqlExecutor:
91
93
  def use(self, object_type: ObjectType, name: str):
92
94
  try:
93
95
  self._execute_query(f"use {object_type.value.sf_name} {name}")
94
- except ProgrammingError:
96
+ except ProgrammingError as err:
95
97
  # Rewrite the error to make the message more useful.
96
- raise ProgrammingError(
97
- f"Could not use {object_type} {name}. Object does not exist, or operation cannot be performed."
98
- )
98
+ raise CouldNotUseObjectError(object_type=object_type, name=name) from err
99
99
 
100
100
  def current_role(self) -> str:
101
101
  return self._execute_query(f"select current_role()").fetchone()[0]
@@ -247,9 +247,7 @@ class SqlExecutor:
247
247
  if show_obj_cursor.rowcount is None:
248
248
  raise SnowflakeSQLExecutionError(show_obj_query)
249
249
  elif show_obj_cursor.rowcount > 1:
250
- raise ProgrammingError(
251
- f"Received multiple rows from result of SQL statement: {show_obj_query}. Usage of 'show_specific_object' may not be properly scoped."
252
- )
250
+ raise ShowSpecificObjectMultipleRowsError(show_obj_query=show_obj_query)
253
251
 
254
252
  show_obj_row = find_first_row(
255
253
  show_obj_cursor,
@@ -0,0 +1,241 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from pathlib import Path, PurePosixPath
5
+
6
+ from snowflake.cli.api.identifiers import FQN
7
+ from snowflake.cli.api.project.util import (
8
+ to_string_literal,
9
+ )
10
+
11
+ USER_STAGE_PREFIX = "~"
12
+
13
+
14
+ class StagePath:
15
+ def __init__(
16
+ self,
17
+ stage_name: str,
18
+ path: str | PurePosixPath | None = None,
19
+ git_ref: str | None = None,
20
+ trailing_slash: bool = False,
21
+ ):
22
+ self._stage_name = self.strip_stage_prefixes(stage_name)
23
+ self._path = PurePosixPath(path) if path else PurePosixPath(".")
24
+
25
+ self._trailing_slash = trailing_slash
26
+ # Check if user stage
27
+ self._is_user_stage = self._stage_name.startswith(USER_STAGE_PREFIX)
28
+
29
+ # Setup git information
30
+ self._git_ref = None
31
+ self._is_git_repo = False
32
+ if git_ref:
33
+ self._git_ref = git_ref
34
+ self._is_git_repo = True
35
+
36
+ @classmethod
37
+ def get_user_stage(cls) -> StagePath:
38
+ return cls.from_stage_str("~")
39
+
40
+ @property
41
+ def stage(self) -> str:
42
+ return self._stage_name
43
+
44
+ @property
45
+ def path(self) -> PurePosixPath:
46
+ return self._path
47
+
48
+ @property
49
+ def stage_with_at(self) -> str:
50
+ return self.add_at_prefix(self._stage_name)
51
+
52
+ def is_user_stage(self) -> bool:
53
+ return self._is_user_stage
54
+
55
+ def is_git_repo(self) -> bool:
56
+ return self._is_git_repo
57
+
58
+ @property
59
+ def git_ref(self) -> str | None:
60
+ return self._git_ref
61
+
62
+ @staticmethod
63
+ def add_at_prefix(text: str):
64
+ if not text.startswith("@"):
65
+ return "@" + text
66
+ return text
67
+
68
+ @staticmethod
69
+ def strip_at_prefix(text: str):
70
+ if text.startswith("@"):
71
+ return text[1:]
72
+ return text
73
+
74
+ @staticmethod
75
+ def strip_snow_prefix(text: str):
76
+ if text.startswith("snow://"):
77
+ return text[len("snow://") :]
78
+ return text
79
+
80
+ @classmethod
81
+ def strip_stage_prefixes(cls, text: str):
82
+ return cls.strip_at_prefix(cls.strip_snow_prefix(text))
83
+
84
+ @classmethod
85
+ def from_stage_str(cls, stage_str: str | FQN):
86
+ stage_str = cls.strip_stage_prefixes(str(stage_str))
87
+ parts = stage_str.split("/", maxsplit=1)
88
+ parts = [p for p in parts if p]
89
+ if len(parts) == 2:
90
+ stage_string, path = parts
91
+ else:
92
+ stage_string = parts[0]
93
+ path = None
94
+ return cls(
95
+ stage_name=stage_string, path=path, trailing_slash=stage_str.endswith("/")
96
+ )
97
+
98
+ @classmethod
99
+ def from_git_str(cls, git_str: str):
100
+ """
101
+ @configuration_repo / branches/main / scripts/setup.sql
102
+ @configuration_repo / branches/"foo/main" / scripts/setup.sql
103
+ """
104
+ repo_name, git_ref, path = cls._split_repo_path(
105
+ cls.strip_stage_prefixes(git_str)
106
+ )
107
+ return cls(
108
+ stage_name=repo_name,
109
+ path=path,
110
+ git_ref=git_ref,
111
+ trailing_slash=git_str.endswith("/"),
112
+ )
113
+
114
+ @staticmethod
115
+ def _split_repo_path(git_str: str) -> tuple[str, str, str]:
116
+ parts = []
117
+ slash_index = 0
118
+ skipping_mode = False
119
+ for current_idx, (char, next_char) in enumerate(zip(git_str[:-1], git_str[1:])):
120
+ if not skipping_mode:
121
+ if char != "/":
122
+ continue
123
+
124
+ # Normal split
125
+ parts.append(git_str[slash_index:current_idx])
126
+ slash_index = current_idx + 1
127
+
128
+ if next_char == '"':
129
+ skipping_mode = not skipping_mode
130
+ # Add last part
131
+ parts.append(git_str[slash_index:])
132
+ repo_name = parts[0]
133
+ ref = parts[1] + "/" + parts[2]
134
+ path = "/".join(parts[3:]) if len(parts) > 2 else ""
135
+ return repo_name, ref, path
136
+
137
+ def absolute_path(self, no_fqn=False, at_prefix=True) -> str:
138
+ stage_name = self._stage_name
139
+ if not self.is_user_stage() and no_fqn:
140
+ stage_name = FQN.from_string(self._stage_name).name
141
+
142
+ path = PurePosixPath(stage_name)
143
+ if self.git_ref:
144
+ path = path / self.git_ref
145
+ if not self.is_root():
146
+ path = path / self._path
147
+
148
+ str_path = str(path)
149
+ if at_prefix:
150
+ str_path = self.add_at_prefix(str_path)
151
+
152
+ if self._trailing_slash:
153
+ return str_path.rstrip("/") + "/"
154
+ return str_path
155
+
156
+ def joinpath(self, path: str) -> StagePath:
157
+ if self.is_file():
158
+ raise ValueError("Cannot join path to a file")
159
+
160
+ return StagePath(
161
+ stage_name=self._stage_name,
162
+ path=PurePosixPath(self._path) / path.lstrip("/"),
163
+ git_ref=self._git_ref,
164
+ )
165
+
166
+ def __truediv__(self, path: str):
167
+ return self.joinpath(path)
168
+
169
+ def with_stage(self, stage_name: str) -> StagePath:
170
+ """Returns a new path with new stage name"""
171
+ return StagePath(
172
+ stage_name=stage_name,
173
+ path=self._path,
174
+ git_ref=self._git_ref,
175
+ )
176
+
177
+ @property
178
+ def parts(self) -> tuple[str, ...]:
179
+ return self._path.parts
180
+
181
+ @property
182
+ def name(self) -> str:
183
+ return self._path.name
184
+
185
+ def is_dir(self) -> bool:
186
+ return "." not in self.name
187
+
188
+ def is_file(self) -> bool:
189
+ return not self.is_dir()
190
+
191
+ @property
192
+ def suffix(self) -> str:
193
+ return self._path.suffix
194
+
195
+ @property
196
+ def stem(self) -> str:
197
+ return self._path.stem
198
+
199
+ @property
200
+ def parent(self) -> StagePath:
201
+ return StagePath(
202
+ stage_name=self._stage_name, path=self._path.parent, git_ref=self._git_ref
203
+ )
204
+
205
+ def is_root(self) -> bool:
206
+ return self._path == PurePosixPath(".")
207
+
208
+ def root_path(self) -> StagePath:
209
+ if self.is_git_repo():
210
+ return StagePath(stage_name=self._stage_name, git_ref=self._git_ref)
211
+ return StagePath(stage_name=self._stage_name)
212
+
213
+ def is_quoted(self) -> bool:
214
+ path = self.absolute_path()
215
+ return path.startswith("'") and path.endswith("'")
216
+
217
+ def path_for_sql(self) -> str:
218
+ path = self.absolute_path()
219
+ if not re.fullmatch(r"@([\w./$])+", path):
220
+ return to_string_literal(path)
221
+ return path
222
+
223
+ def quoted_absolute_path(self) -> str:
224
+ if self.is_quoted():
225
+ return self.absolute_path()
226
+ return to_string_literal(self.absolute_path())
227
+
228
+ def relative_to(self, stage_path: StagePath) -> PurePosixPath:
229
+ return self.path.relative_to(stage_path.path)
230
+
231
+ def get_local_target_path(self, target_dir: Path, stage_root: StagePath):
232
+ # Case for downloading @stage/aa/file.py with root @stage/aa
233
+ if self.relative_to(stage_root) == PurePosixPath("."):
234
+ return target_dir
235
+ return (target_dir / self.relative_to(stage_root)).parent
236
+
237
+ def __str__(self):
238
+ return self.absolute_path()
239
+
240
+ def __eq__(self, other):
241
+ return self.absolute_path() == other.absolute_path()
@@ -285,9 +285,7 @@ def _add_defaults_to_definition(original_definition: Definition) -> Definition:
285
285
  with context({"skip_validation_on_templates": True}):
286
286
  # pass a flag to Pydantic to skip validation for templated scalars
287
287
  # populate the defaults
288
- project_definition = build_project_definition(
289
- **copy.deepcopy(original_definition)
290
- )
288
+ project_definition = build_project_definition(**original_definition)
291
289
 
292
290
  definition_with_defaults = project_definition.model_dump(
293
291
  exclude_none=True, warnings=False, by_alias=True
@@ -392,8 +390,8 @@ def render_definition_template(
392
390
  definition,
393
391
  update_action=lambda val: template_env.render(val, final_context),
394
392
  )
395
-
396
- project_definition = build_project_definition(**definition)
393
+ with context({"is_duplicated_run": True}):
394
+ project_definition = build_project_definition(**definition)
397
395
 
398
396
  # Use the values originally provided by the user as the template context
399
397
  # This intentionally doesn't reflect any field changes made by
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: snowflake-cli
3
- Version: 3.0.2
3
+ Version: 3.1.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
@@ -226,7 +226,7 @@ Requires-Dist: pydantic==2.9.2
226
226
  Requires-Dist: pyyaml==6.0.2
227
227
  Requires-Dist: requests==2.32.3
228
228
  Requires-Dist: requirements-parser==0.11.0
229
- Requires-Dist: rich==13.8.1
229
+ Requires-Dist: rich==13.9.2
230
230
  Requires-Dist: setuptools==75.1.0
231
231
  Requires-Dist: snowflake-connector-python[secure-local-storage]==3.12.2
232
232
  Requires-Dist: snowflake-core==0.12.1; python_version < '3.12'
@@ -235,11 +235,13 @@ Requires-Dist: tomlkit==0.13.2
235
235
  Requires-Dist: typer==0.12.5
236
236
  Requires-Dist: urllib3<2.3,>=1.24.3
237
237
  Provides-Extra: development
238
- Requires-Dist: coverage==7.6.1; extra == 'development'
238
+ Requires-Dist: coverage==7.6.3; extra == 'development'
239
+ Requires-Dist: factory-boy==3.3.1; extra == 'development'
240
+ Requires-Dist: faker==30.3.0; extra == 'development'
239
241
  Requires-Dist: pre-commit>=3.5.0; extra == 'development'
240
242
  Requires-Dist: pytest-randomly==3.15.0; extra == 'development'
241
243
  Requires-Dist: pytest==8.3.3; extra == 'development'
242
- Requires-Dist: syrupy==4.7.1; extra == 'development'
244
+ Requires-Dist: syrupy==4.7.2; extra == 'development'
243
245
  Provides-Extra: packaging
244
246
  Requires-Dist: pyinstaller~=6.10; extra == 'packaging'
245
247
  Description-Content-Type: text/markdown
@@ -272,15 +274,13 @@ Snowflake CLI is an open-source command-line tool explicitly designed for develo
272
274
 
273
275
  With Snowflake CLI, developers can create, manage, update, and view apps running on Snowflake across workloads such as Streamlit in Snowflake, the Snowflake Native App Framework, Snowpark Container Services, and Snowpark. It supports a range of Snowflake features, including user-defined functions, stored procedures, Streamlit in Snowflake, and SQL execution.
274
276
 
275
-
276
277
  **Note**: Snowflake CLI is in Public Preview (PuPr).
277
278
 
278
- Docs: https://docs.snowflake.com/en/developer-guide/snowflake-cli-v2/index.
279
-
280
- Quick start: https://quickstarts.snowflake.com/guide/getting-started-with-snowflake-cli
279
+ Docs: <https://docs.snowflake.com/en/developer-guide/snowflake-cli-v2/index>.
281
280
 
282
- Cheatsheet: https://github.com/Snowflake-Labs/sf-cheatsheets/blob/main/snowflake-cli.md
281
+ Quick start: <https://quickstarts.snowflake.com/guide/getting-started-with-snowflake-cli>
283
282
 
283
+ Cheatsheet: <https://github.com/Snowflake-Labs/sf-cheatsheets/blob/main/snowflake-cli.md>
284
284
 
285
285
  ## Install Snowflake CLI
286
286
 
@@ -289,7 +289,7 @@ Cheatsheet: https://github.com/Snowflake-Labs/sf-cheatsheets/blob/main/snowflake
289
289
  We recommend installing Snowflake CLI in isolated environment using [pipx](https://pipx.pypa.io/stable/). Requires Python >= 3.10
290
290
 
291
291
  ```bash
292
- pipx install snowflake-cli-labs
292
+ pipx install snowflake-cli
293
293
  snow --help
294
294
  ```
295
295
 
@@ -322,4 +322,4 @@ You should now be able to run `snow` and get the CLI message.
322
322
  ## Get involved
323
323
 
324
324
  Have a feature idea? Running into a bug? Want to contribute? We'd love to hear from you!
325
- Please open or review issues, open pull requests, or reach out to us on developers@snowflake.com
325
+ Please open or review issues, open pull requests, or reach out to us on <developers@snowflake.com>