snowflake-cli-labs 3.0.0rc0__py3-none-any.whl → 3.0.0rc2__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 (66) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/cli_app.py +10 -1
  3. snowflake/cli/_app/snow_connector.py +91 -37
  4. snowflake/cli/_app/telemetry.py +8 -4
  5. snowflake/cli/_app/version_check.py +74 -0
  6. snowflake/cli/_plugins/connection/commands.py +3 -2
  7. snowflake/cli/_plugins/git/commands.py +55 -14
  8. snowflake/cli/_plugins/git/manager.py +14 -6
  9. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +18 -2
  10. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +123 -42
  11. snowflake/cli/_plugins/nativeapp/codegen/setup/setup_driver.py.source +5 -2
  12. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +6 -11
  13. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +111 -0
  14. snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
  15. snowflake/cli/_plugins/nativeapp/manager.py +74 -144
  16. snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
  17. snowflake/cli/_plugins/nativeapp/run_processor.py +56 -260
  18. snowflake/cli/_plugins/nativeapp/same_account_install_method.py +74 -0
  19. snowflake/cli/_plugins/nativeapp/teardown_processor.py +17 -246
  20. snowflake/cli/_plugins/nativeapp/v2_conversions/v2_to_v1_decorator.py +91 -17
  21. snowflake/cli/_plugins/snowpark/commands.py +5 -65
  22. snowflake/cli/_plugins/snowpark/common.py +17 -1
  23. snowflake/cli/_plugins/snowpark/models.py +2 -1
  24. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -35
  25. snowflake/cli/_plugins/sql/commands.py +1 -2
  26. snowflake/cli/_plugins/stage/commands.py +2 -2
  27. snowflake/cli/_plugins/stage/manager.py +46 -15
  28. snowflake/cli/_plugins/streamlit/commands.py +4 -63
  29. snowflake/cli/_plugins/streamlit/manager.py +13 -0
  30. snowflake/cli/_plugins/workspace/action_context.py +7 -0
  31. snowflake/cli/_plugins/workspace/commands.py +145 -32
  32. snowflake/cli/_plugins/workspace/manager.py +21 -4
  33. snowflake/cli/api/cli_global_context.py +136 -313
  34. snowflake/cli/api/commands/decorators.py +1 -1
  35. snowflake/cli/api/commands/flags.py +106 -102
  36. snowflake/cli/api/commands/snow_typer.py +15 -6
  37. snowflake/cli/api/config.py +18 -5
  38. snowflake/cli/api/connections.py +214 -0
  39. snowflake/cli/api/console/abc.py +4 -2
  40. snowflake/cli/api/constants.py +11 -0
  41. snowflake/cli/api/entities/application_entity.py +687 -2
  42. snowflake/cli/api/entities/application_package_entity.py +407 -9
  43. snowflake/cli/api/entities/common.py +7 -2
  44. snowflake/cli/api/entities/utils.py +80 -20
  45. snowflake/cli/api/exceptions.py +12 -2
  46. snowflake/cli/api/feature_flags.py +0 -2
  47. snowflake/cli/api/identifiers.py +3 -0
  48. snowflake/cli/api/project/definition.py +35 -1
  49. snowflake/cli/api/project/definition_conversion.py +352 -0
  50. snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
  51. snowflake/cli/api/project/schemas/entities/common.py +0 -12
  52. snowflake/cli/api/project/schemas/identifier_model.py +2 -2
  53. snowflake/cli/api/project/schemas/project_definition.py +102 -43
  54. snowflake/cli/api/rendering/jinja.py +2 -16
  55. snowflake/cli/api/rendering/project_definition_templates.py +5 -1
  56. snowflake/cli/api/rendering/sql_templates.py +14 -4
  57. snowflake/cli/api/secure_path.py +13 -18
  58. snowflake/cli/api/secure_utils.py +90 -1
  59. snowflake/cli/api/sql_execution.py +13 -0
  60. snowflake/cli/api/utils/definition_rendering.py +7 -7
  61. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/METADATA +9 -9
  62. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/RECORD +65 -61
  63. snowflake/cli/api/commands/typer_pre_execute.py +0 -26
  64. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/WHEEL +0 -0
  65. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/entry_points.txt +0 -0
  66. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc2.dist-info}/licenses/LICENSE +0 -0
@@ -19,13 +19,11 @@ 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 snowflake.cli.api.feature_flags import FeatureFlag
23
22
  from snowflake.cli.api.project.errors import SchemaValidationError
24
23
  from snowflake.cli.api.project.schemas.entities.application_entity_model import (
25
24
  ApplicationEntityModel,
26
25
  )
27
26
  from snowflake.cli.api.project.schemas.entities.common import (
28
- DefaultsField,
29
27
  TargetField,
30
28
  )
31
29
  from snowflake.cli.api.project.schemas.entities.entities import (
@@ -43,6 +41,7 @@ from snowflake.cli.api.utils.types import Context
43
41
  from typing_extensions import Annotated
44
42
 
45
43
  AnnotatedEntity = Annotated[EntityModel, Field(discriminator="type")]
44
+ scalar = str | int | float | bool
46
45
 
47
46
 
48
47
  @dataclass
@@ -63,6 +62,11 @@ class ProjectProperties:
63
62
  project_context: Context
64
63
 
65
64
 
65
+ @dataclass
66
+ class YamlOverride:
67
+ data: dict | list
68
+
69
+
66
70
  class _ProjectDefinitionBase(UpdatableModel):
67
71
  def __init__(self, *args, **kwargs):
68
72
  try:
@@ -115,31 +119,12 @@ class DefinitionV11(DefinitionV10):
115
119
  class DefinitionV20(_ProjectDefinitionBase):
116
120
  entities: Dict[str, AnnotatedEntity] = Field(title="Entity definitions.")
117
121
 
118
- @model_validator(mode="before")
119
- @classmethod
120
- def apply_defaults(cls, data: Dict) -> Dict:
121
- """
122
- Applies default values that exist on the model but not specified in yml
123
- """
124
- if "defaults" in data and "entities" in data:
125
- for key, entity in data["entities"].items():
126
- entity_fields = get_allowed_fields_for_entity(entity)
127
- if not entity_fields:
128
- continue
129
- for default_key, default_value in data["defaults"].items():
130
- if default_key in entity_fields and default_key not in entity:
131
- entity[default_key] = default_value
132
- return data
133
-
134
- @field_validator("entities", mode="after")
135
- @classmethod
136
- def validate_entities_identifiers(
137
- cls, entities: Dict[str, EntityModel]
138
- ) -> Dict[str, EntityModel]:
139
- for key, entity in entities.items():
122
+ @model_validator(mode="after")
123
+ def validate_entities_identifiers(self):
124
+ for key, entity in self.entities.items():
140
125
  entity.set_entity_id(key)
141
126
  entity.validate_identifier()
142
- return entities
127
+ return self
143
128
 
144
129
  @field_validator("entities", mode="after")
145
130
  @classmethod
@@ -180,11 +165,6 @@ class DefinitionV20(_ProjectDefinitionBase):
180
165
  f"Target type mismatch. Expected {target_type.__name__}, got {actual_target_type.__name__}"
181
166
  )
182
167
 
183
- defaults: Optional[DefaultsField] = Field(
184
- title="Default key/value entity values that are merged recursively for each entity.",
185
- default=None,
186
- )
187
-
188
168
  env: Optional[Dict[str, Union[str, int, bool]]] = Field(
189
169
  title="Default environment specification for this project.",
190
170
  default=None,
@@ -204,22 +184,92 @@ class DefinitionV20(_ProjectDefinitionBase):
204
184
  if "mixins" not in data or "entities" not in data:
205
185
  return data
206
186
 
207
- for entity in data["entities"].values():
187
+ entities = data["entities"]
188
+ for entity_name, entity in entities.items():
208
189
  entity_mixins = entity_mixins_to_list(
209
190
  entity.get("meta", {}).get("use_mixins")
210
191
  )
211
192
 
212
- entity_fields = get_allowed_fields_for_entity(entity)
213
- if entity_fields and entity_mixins:
214
- for mixin_name in entity_mixins:
215
- if mixin_name in data["mixins"]:
216
- for key, value in data["mixins"][mixin_name].items():
217
- if key in entity_fields:
218
- entity[key] = value
219
- else:
220
- raise ValueError(f"Mixin {mixin_name} not found in mixins")
193
+ merged_values = cls._merge_mixins_with_entity(
194
+ entity_id=entity_name,
195
+ entity=entity,
196
+ entity_mixins_names=entity_mixins,
197
+ mixin_defs=data["mixins"],
198
+ )
199
+ entities[entity_name] = merged_values
200
+ return data
201
+
202
+ @classmethod
203
+ def _merge_mixins_with_entity(
204
+ cls,
205
+ entity_id: str,
206
+ entity: dict,
207
+ entity_mixins_names: list,
208
+ mixin_defs: dict,
209
+ ) -> dict:
210
+ # Validate mixins
211
+ for mixin_name in entity_mixins_names:
212
+ if mixin_name not in mixin_defs:
213
+ raise ValueError(f"Mixin {mixin_name} not defined")
214
+
215
+ # Build object override data from mixins
216
+ data: dict = {}
217
+ for mx_name in entity_mixins_names:
218
+ data = cls._merge_data(data, mixin_defs[mx_name])
219
+
220
+ for key, override_value in data.items():
221
+ if key not in get_allowed_fields_for_entity(entity):
222
+ raise ValueError(
223
+ f"Unsupported key '{key}' for entity {entity_id} of type {entity['type']} "
224
+ )
225
+
226
+ entity_value = entity.get(key)
227
+ if (
228
+ entity_value is not None
229
+ and not isinstance(entity_value, YamlOverride)
230
+ and not isinstance(entity_value, type(override_value))
231
+ ):
232
+ raise ValueError(
233
+ f"Value from mixins for property {key} is of type '{type(override_value).__name__}' "
234
+ f"while entity {entity_id} expects value of type '{type(entity_value).__name__}'"
235
+ )
236
+
237
+ # Apply entity data on top of mixins
238
+ data = cls._merge_data(data, entity)
221
239
  return data
222
240
 
241
+ @classmethod
242
+ def _merge_data(
243
+ cls,
244
+ left: dict | list | scalar | None,
245
+ right: dict | list | scalar | None | YamlOverride,
246
+ ):
247
+ """
248
+ Merges right data into left. Right and left is expected to be of the same type, if not right is returned.
249
+ If left is sequence then missing elements from right are appended.
250
+ If left is dictionary then we update it with data from right. The update is done recursively key by key.
251
+ """
252
+ if isinstance(right, YamlOverride):
253
+ return right.data
254
+
255
+ if left is None:
256
+ return right
257
+
258
+ # At that point left and right are of the same type
259
+ if isinstance(left, dict) and isinstance(right, dict):
260
+ data = dict(left)
261
+ for key in right:
262
+ data[key] = cls._merge_data(left=data.get(key), right=right[key])
263
+ return data
264
+
265
+ if isinstance(left, list) and isinstance(right, list):
266
+ return _unique_extend(left, right)
267
+
268
+ if not isinstance(right, type(left)):
269
+ raise ValueError(f"Could not merge {type(right)} and {type(left)}.")
270
+
271
+ return right
272
+
223
273
  def get_entities_by_type(self, entity_type: str):
224
274
  return {i: e for i, e in self.entities.items() if e.get_type() == entity_type}
225
275
 
@@ -244,9 +294,7 @@ ProjectDefinition = Union[ProjectDefinitionV1, ProjectDefinitionV2]
244
294
 
245
295
 
246
296
  def get_version_map():
247
- version_map = {"1": DefinitionV10, "1.1": DefinitionV11}
248
- if FeatureFlag.ENABLE_PROJECT_DEFINITION_V2.is_enabled():
249
- version_map["2"] = DefinitionV20
297
+ version_map = {"1": DefinitionV10, "1.1": DefinitionV11, "2": DefinitionV20}
250
298
  return version_map
251
299
 
252
300
 
@@ -266,8 +314,19 @@ def get_allowed_fields_for_entity(entity: Dict[str, Any]) -> List[str]:
266
314
  Get the allowed fields for the given entity.
267
315
  """
268
316
  entity_type = entity.get("type")
317
+ if entity_type is None:
318
+ raise ValueError("Entity is missing type declaration.")
319
+
269
320
  if entity_type not in v2_entity_model_types_map:
270
321
  return []
271
322
 
272
323
  entity_model = v2_entity_model_types_map[entity_type]
273
324
  return entity_model.model_fields
325
+
326
+
327
+ def _unique_extend(list_a: List, list_b: List) -> List:
328
+ new_list = list(list_a)
329
+ for item in list_b:
330
+ if item not in list_a:
331
+ new_list.append(item)
332
+ return new_list
@@ -82,7 +82,7 @@ class IgnoreAttrEnvironment(Environment):
82
82
  return self.undefined(obj=obj, name=argument)
83
83
 
84
84
 
85
- def _get_jinja_env(loader: Optional[loaders.BaseLoader] = None) -> Environment:
85
+ def get_basic_jinja_env(loader: Optional[loaders.BaseLoader] = None) -> Environment:
86
86
  return env_bootstrap(
87
87
  IgnoreAttrEnvironment(
88
88
  loader=loader or loaders.BaseLoader(),
@@ -92,20 +92,6 @@ def _get_jinja_env(loader: Optional[loaders.BaseLoader] = None) -> Environment:
92
92
  )
93
93
 
94
94
 
95
- def jinja_render_from_str(template_content: str, data: Dict[str, Any]) -> str:
96
- """
97
- Renders a jinja template and outputs either the rendered contents as string or writes to a file.
98
-
99
- Args:
100
- template_content (str): template contents
101
- data (dict): A dictionary of jinja variables and their actual values
102
-
103
- Returns:
104
- None if file path is provided, else returns the rendered string.
105
- """
106
- return _get_jinja_env().from_string(template_content).render(data)
107
-
108
-
109
95
  def jinja_render_from_file(
110
96
  template_path: Path, data: Dict[str, Any], output_file_path: Optional[Path] = None
111
97
  ) -> Optional[str]:
@@ -120,7 +106,7 @@ def jinja_render_from_file(
120
106
  Returns:
121
107
  None if file path is provided, else returns the rendered string.
122
108
  """
123
- env = _get_jinja_env(
109
+ env = get_basic_jinja_env(
124
110
  loader=loaders.FileSystemLoader(template_path.parent.as_posix())
125
111
  )
126
112
  loaded_template = env.get_template(template_path.name)
@@ -24,7 +24,11 @@ _YML_TEMPLATE_START = "<%"
24
24
  _YML_TEMPLATE_END = "%>"
25
25
 
26
26
 
27
- def get_project_definition_cli_jinja_env() -> Environment:
27
+ def has_client_side_templates(template_content: str) -> bool:
28
+ return _YML_TEMPLATE_START in template_content
29
+
30
+
31
+ def get_client_side_jinja_env() -> Environment:
28
32
  _random_block = "___very___unique___block___to___disable___logic___blocks___"
29
33
  return env_bootstrap(
30
34
  IgnoreAttrEnvironment(
@@ -14,7 +14,7 @@
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- from typing import Dict
17
+ from typing import Dict, Optional
18
18
 
19
19
  from click import ClickException
20
20
  from jinja2 import Environment, StrictUndefined, loaders, meta
@@ -55,19 +55,29 @@ def _does_template_have_env_syntax(env: Environment, template_content: str) -> b
55
55
  return bool(meta.find_undeclared_variables(template))
56
56
 
57
57
 
58
- def choose_sql_jinja_env_based_on_template_syntax(template_content: str) -> Environment:
58
+ def has_sql_templates(template_content: str) -> bool:
59
+ return (
60
+ _OLD_SQL_TEMPLATE_START in template_content
61
+ or _SQL_TEMPLATE_START in template_content
62
+ )
63
+
64
+
65
+ def choose_sql_jinja_env_based_on_template_syntax(
66
+ template_content: str, reference_name: Optional[str] = None
67
+ ) -> Environment:
59
68
  old_syntax_env = _get_sql_jinja_env(_OLD_SQL_TEMPLATE_START, _OLD_SQL_TEMPLATE_END)
60
69
  new_syntax_env = _get_sql_jinja_env(_SQL_TEMPLATE_START, _SQL_TEMPLATE_END)
61
70
  has_old_syntax = _does_template_have_env_syntax(old_syntax_env, template_content)
62
71
  has_new_syntax = _does_template_have_env_syntax(new_syntax_env, template_content)
72
+ reference_name_str = f" in {reference_name}" if reference_name else ""
63
73
  if has_old_syntax and has_new_syntax:
64
74
  raise InvalidTemplate(
65
- f"The SQL query mixes {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax"
75
+ f"The SQL query{reference_name_str} mixes {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax"
66
76
  f" and {_SQL_TEMPLATE_START} ... {_SQL_TEMPLATE_END} syntax."
67
77
  )
68
78
  if has_old_syntax:
69
79
  cli_console.warning(
70
- f"Warning: {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax is deprecated."
80
+ f"Warning: {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax{reference_name_str} is deprecated."
71
81
  f" Use {_SQL_TEMPLATE_START} ... {_SQL_TEMPLATE_END} syntax instead."
72
82
  )
73
83
  return old_syntax_env
@@ -24,6 +24,12 @@ from pathlib import Path
24
24
  from typing import Optional, Union
25
25
 
26
26
  from snowflake.cli.api.exceptions import DirectoryIsNotEmptyError, FileTooLargeError
27
+ from snowflake.cli.api.secure_utils import (
28
+ chmod as secure_chmod,
29
+ )
30
+ from snowflake.cli.api.secure_utils import (
31
+ restrict_file_permissions,
32
+ )
27
33
 
28
34
  log = logging.getLogger(__name__)
29
35
 
@@ -47,6 +53,12 @@ class SecurePath:
47
53
  """
48
54
  return self._path
49
55
 
56
+ def chmod(self, permissions_mask: int) -> None:
57
+ """
58
+ Change the file mode and permissions, like os.chmod().
59
+ """
60
+ secure_chmod(self._path, permissions_mask)
61
+
50
62
  @property
51
63
  def parent(self):
52
64
  """
@@ -97,28 +109,11 @@ class SecurePath:
97
109
  """A string representing the final path component."""
98
110
  return self._path.name
99
111
 
100
- def chmod(self, permissions_mask: int) -> None:
101
- """
102
- Change the file mode and permissions, like os.chmod().
103
- """
104
- log.info(
105
- "Update permissions of file %s to %s", self._path, oct(permissions_mask)
106
- )
107
- self._path.chmod(permissions_mask)
108
-
109
112
  def restrict_permissions(self) -> None:
110
113
  """
111
114
  Restrict file/directory permissions to owner-only.
112
115
  """
113
- import stat
114
-
115
- owner_permissions = (
116
- # https://docs.python.org/3/library/stat.html
117
- stat.S_IRUSR # readable by owner
118
- | stat.S_IWUSR # writeable by owner
119
- | stat.S_IXUSR # executable by owner
120
- )
121
- self.chmod(self._path.stat().st_mode & owner_permissions)
116
+ restrict_file_permissions(self._path)
122
117
 
123
118
  def touch(self, permissions_mask: int = 0o600, exist_ok: bool = True) -> None:
124
119
  """
@@ -12,11 +12,64 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
+ import logging
15
16
  import stat
16
17
  from pathlib import Path
18
+ from typing import List
17
19
 
20
+ from snowflake.connector.compat import IS_WINDOWS
18
21
 
19
- def file_permissions_are_strict(file_path: Path) -> bool:
22
+ log = logging.getLogger(__name__)
23
+
24
+
25
+ def _get_windows_whitelisted_users():
26
+ # whitelisted users list obtained in consultation with prodsec: CASEC-9627
27
+ import os
28
+
29
+ return [
30
+ "SYSTEM",
31
+ "Administrators",
32
+ "Network",
33
+ "Domain Admins",
34
+ "Domain Users",
35
+ os.getlogin(),
36
+ ]
37
+
38
+
39
+ def _run_icacls(file_path: Path) -> str:
40
+ import subprocess
41
+
42
+ return subprocess.check_output(["icacls", str(file_path)], text=True)
43
+
44
+
45
+ def _windows_permissions_are_denied(permission_codes: str) -> bool:
46
+ # according to https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/icacls
47
+ return "(DENY)" in permission_codes or "(N)" in permission_codes
48
+
49
+
50
+ def windows_get_not_whitelisted_users_with_access(file_path: Path) -> List[str]:
51
+ import re
52
+
53
+ # according to https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/icacls
54
+ icacls_output_regex = (
55
+ rf"({re.escape(str(file_path))})?.*\\(?P<user>.*):(?P<permissions>[(A-Z),]+)"
56
+ )
57
+ whitelisted_users = _get_windows_whitelisted_users()
58
+
59
+ users_with_access = []
60
+ for permission in re.finditer(icacls_output_regex, _run_icacls(file_path)):
61
+ if (permission.group("user") not in whitelisted_users) and (
62
+ not _windows_permissions_are_denied(permission.group("permissions"))
63
+ ):
64
+ users_with_access.append(permission.group("user"))
65
+ return list(set(users_with_access))
66
+
67
+
68
+ def _windows_file_permissions_are_strict(file_path: Path) -> bool:
69
+ return windows_get_not_whitelisted_users_with_access(file_path) == []
70
+
71
+
72
+ def _unix_file_permissions_are_strict(file_path: Path) -> bool:
20
73
  accessible_by_others = (
21
74
  # https://docs.python.org/3/library/stat.html
22
75
  stat.S_IRGRP # readable by group
@@ -27,3 +80,39 @@ def file_permissions_are_strict(file_path: Path) -> bool:
27
80
  | stat.S_IXOTH # executable by others
28
81
  )
29
82
  return (file_path.stat().st_mode & accessible_by_others) == 0
83
+
84
+
85
+ def file_permissions_are_strict(file_path: Path) -> bool:
86
+ if IS_WINDOWS:
87
+ return _windows_file_permissions_are_strict(file_path)
88
+ return _unix_file_permissions_are_strict(file_path)
89
+
90
+
91
+ def chmod(path: Path, permissions_mask: int) -> None:
92
+ log.info("Update permissions of file %s to %s", path, oct(permissions_mask))
93
+ path.chmod(permissions_mask)
94
+
95
+
96
+ def _unix_restrict_file_permissions(path: Path) -> None:
97
+ owner_permissions = (
98
+ # https://docs.python.org/3/library/stat.html
99
+ stat.S_IRUSR # readable by owner
100
+ | stat.S_IWUSR # writeable by owner
101
+ | stat.S_IXUSR # executable by owner
102
+ )
103
+ chmod(path, path.stat().st_mode & owner_permissions)
104
+
105
+
106
+ def _windows_restrict_file_permissions(path: Path) -> None:
107
+ import subprocess
108
+
109
+ for user in windows_get_not_whitelisted_users_with_access(path):
110
+ log.info("Removing permissions of user %s from file %s", user, path)
111
+ subprocess.run(["icacls", str(path), "/DENY", f"{user}:F"])
112
+
113
+
114
+ def restrict_file_permissions(file_path: Path) -> None:
115
+ if IS_WINDOWS:
116
+ _windows_restrict_file_permissions(file_path)
117
+ else:
118
+ _unix_restrict_file_permissions(file_path)
@@ -97,6 +97,13 @@ class SqlExecutor:
97
97
  f"Could not use {object_type} {name}. Object does not exist, or operation cannot be performed."
98
98
  )
99
99
 
100
+ def current_role(self) -> str:
101
+ *_, cursor = self._execute_string(
102
+ "select current_role()", cursor_class=DictCursor
103
+ )
104
+ role_result = cursor.fetchone()
105
+ return role_result["CURRENT_ROLE()"]
106
+
100
107
  @contextmanager
101
108
  def use_role(self, new_role: str):
102
109
  """
@@ -117,6 +124,12 @@ class SqlExecutor:
117
124
  if is_different_role:
118
125
  self._execute_query(f"use role {prev_role}")
119
126
 
127
+ def session_has_warehouse(self) -> bool:
128
+ result = self._execute_query(
129
+ "select current_warehouse() is not null as result", cursor_class=DictCursor
130
+ ).fetchone()
131
+ return bool(result.get("RESULT"))
132
+
120
133
  @contextmanager
121
134
  def use_warehouse(self, new_wh: str):
122
135
  """
@@ -28,7 +28,7 @@ from snowflake.cli.api.project.schemas.project_definition import (
28
28
  from snowflake.cli.api.project.schemas.updatable_model import context
29
29
  from snowflake.cli.api.rendering.jinja import CONTEXT_KEY, FUNCTION_KEY
30
30
  from snowflake.cli.api.rendering.project_definition_templates import (
31
- get_project_definition_cli_jinja_env,
31
+ get_client_side_jinja_env,
32
32
  )
33
33
  from snowflake.cli.api.utils.dict_utils import deep_merge_dicts, traverse
34
34
  from snowflake.cli.api.utils.graph import Graph, Node
@@ -96,7 +96,7 @@ class TemplatedEnvironment:
96
96
  )
97
97
  or current_attr_chain is not None
98
98
  ):
99
- raise InvalidTemplate(f"Unexpected templating syntax in {template_value}")
99
+ raise InvalidTemplate(f"Unexpected template syntax in {template_value}")
100
100
 
101
101
  for child_node in ast_node.iter_child_nodes():
102
102
  all_referenced_vars.update(
@@ -277,7 +277,9 @@ def _add_defaults_to_definition(original_definition: Definition) -> Definition:
277
277
  with context({"skip_validation_on_templates": True}):
278
278
  # pass a flag to Pydantic to skip validation for templated scalars
279
279
  # populate the defaults
280
- project_definition = build_project_definition(**original_definition)
280
+ project_definition = build_project_definition(
281
+ **copy.deepcopy(original_definition)
282
+ )
281
283
 
282
284
  definition_with_defaults = project_definition.model_dump(
283
285
  exclude_none=True, warnings=False, by_alias=True
@@ -318,7 +320,7 @@ def render_definition_template(
318
320
  if definition is None:
319
321
  return ProjectProperties(None, {CONTEXT_KEY: {"env": environment_overrides}})
320
322
 
321
- template_env = TemplatedEnvironment(get_project_definition_cli_jinja_env())
323
+ template_env = TemplatedEnvironment(get_client_side_jinja_env())
322
324
 
323
325
  if "definition_version" not in definition or Version(
324
326
  definition["definition_version"]
@@ -353,9 +355,7 @@ def render_definition_template(
353
355
  )
354
356
 
355
357
  def on_cycle_action(node: Node[TemplateVar]):
356
- raise CycleDetectedError(
357
- f"Cycle detected in templating variable {node.data.key}"
358
- )
358
+ raise CycleDetectedError(f"Cycle detected in template variable {node.data.key}")
359
359
 
360
360
  dependencies_graph.dfs(
361
361
  visit_action=lambda node: _render_graph_node(template_env, node),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: snowflake-cli-labs
3
- Version: 3.0.0rc0
3
+ Version: 3.0.0rc2
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
@@ -222,24 +222,24 @@ Requires-Dist: jinja2==3.1.4
222
222
  Requires-Dist: packaging
223
223
  Requires-Dist: pip
224
224
  Requires-Dist: pluggy==1.5.0
225
- Requires-Dist: pydantic==2.8.2
226
- Requires-Dist: pyyaml==6.0.1
225
+ Requires-Dist: pydantic==2.9.1
226
+ Requires-Dist: pyyaml==6.0.2
227
227
  Requires-Dist: requests==2.32.3
228
- Requires-Dist: requirements-parser==0.10.2
229
- Requires-Dist: rich==13.7.1
230
- Requires-Dist: setuptools==70.3.0
231
- Requires-Dist: snowflake-connector-python[secure-local-storage]==3.12.0
228
+ Requires-Dist: requirements-parser==0.11.0
229
+ Requires-Dist: rich==13.8.0
230
+ Requires-Dist: setuptools==74.1.2
231
+ Requires-Dist: snowflake-connector-python[secure-local-storage]==3.12.1
232
232
  Requires-Dist: snowflake-core==0.8.0; python_version < '3.12'
233
233
  Requires-Dist: snowflake-snowpark-python>=1.15.0; python_version < '3.12'
234
234
  Requires-Dist: tomlkit==0.13.2
235
- Requires-Dist: typer==0.12.4
235
+ Requires-Dist: typer==0.12.5
236
236
  Requires-Dist: urllib3<2.3,>=1.24.3
237
237
  Provides-Extra: development
238
238
  Requires-Dist: coverage==7.6.1; extra == 'development'
239
239
  Requires-Dist: pre-commit>=3.5.0; extra == 'development'
240
240
  Requires-Dist: pytest-randomly==3.15.0; extra == 'development'
241
241
  Requires-Dist: pytest==8.3.2; extra == 'development'
242
- Requires-Dist: syrupy==4.6.1; extra == 'development'
242
+ Requires-Dist: syrupy==4.7.1; extra == 'development'
243
243
  Description-Content-Type: text/markdown
244
244
 
245
245
  <!--