snowflake-cli-labs 3.0.0rc0__py3-none-any.whl → 3.0.0rc1__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 (51) hide show
  1. snowflake/cli/__about__.py +1 -1
  2. snowflake/cli/_app/snow_connector.py +18 -11
  3. snowflake/cli/_plugins/connection/commands.py +3 -2
  4. snowflake/cli/_plugins/git/manager.py +14 -6
  5. snowflake/cli/_plugins/nativeapp/codegen/compiler.py +18 -2
  6. snowflake/cli/_plugins/nativeapp/codegen/setup/native_app_setup_processor.py +123 -42
  7. snowflake/cli/_plugins/nativeapp/codegen/setup/setup_driver.py.source +5 -2
  8. snowflake/cli/_plugins/nativeapp/codegen/snowpark/python_processor.py +4 -6
  9. snowflake/cli/_plugins/nativeapp/codegen/templates/templates_processor.py +93 -0
  10. snowflake/cli/_plugins/nativeapp/exceptions.py +3 -3
  11. snowflake/cli/_plugins/nativeapp/manager.py +29 -58
  12. snowflake/cli/_plugins/nativeapp/project_model.py +2 -9
  13. snowflake/cli/_plugins/nativeapp/teardown_processor.py +19 -105
  14. snowflake/cli/_plugins/snowpark/commands.py +5 -65
  15. snowflake/cli/_plugins/snowpark/common.py +17 -1
  16. snowflake/cli/_plugins/snowpark/package/anaconda_packages.py +1 -35
  17. snowflake/cli/_plugins/sql/commands.py +1 -2
  18. snowflake/cli/_plugins/stage/commands.py +2 -2
  19. snowflake/cli/_plugins/stage/manager.py +46 -15
  20. snowflake/cli/_plugins/streamlit/commands.py +4 -63
  21. snowflake/cli/_plugins/streamlit/manager.py +4 -0
  22. snowflake/cli/_plugins/workspace/action_context.py +6 -0
  23. snowflake/cli/_plugins/workspace/commands.py +103 -22
  24. snowflake/cli/_plugins/workspace/manager.py +20 -4
  25. snowflake/cli/api/cli_global_context.py +6 -6
  26. snowflake/cli/api/commands/decorators.py +1 -1
  27. snowflake/cli/api/commands/flags.py +31 -12
  28. snowflake/cli/api/commands/snow_typer.py +9 -2
  29. snowflake/cli/api/config.py +17 -4
  30. snowflake/cli/api/constants.py +11 -0
  31. snowflake/cli/api/entities/application_package_entity.py +296 -3
  32. snowflake/cli/api/entities/common.py +6 -2
  33. snowflake/cli/api/entities/utils.py +46 -10
  34. snowflake/cli/api/exceptions.py +12 -2
  35. snowflake/cli/api/feature_flags.py +0 -2
  36. snowflake/cli/api/project/definition.py +24 -1
  37. snowflake/cli/api/project/definition_conversion.py +194 -0
  38. snowflake/cli/api/project/schemas/entities/application_package_entity_model.py +17 -0
  39. snowflake/cli/api/project/schemas/project_definition.py +1 -4
  40. snowflake/cli/api/rendering/jinja.py +2 -16
  41. snowflake/cli/api/rendering/project_definition_templates.py +1 -1
  42. snowflake/cli/api/rendering/sql_templates.py +7 -4
  43. snowflake/cli/api/secure_path.py +13 -18
  44. snowflake/cli/api/secure_utils.py +90 -1
  45. snowflake/cli/api/sql_execution.py +13 -0
  46. snowflake/cli/api/utils/definition_rendering.py +4 -6
  47. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/METADATA +5 -5
  48. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/RECORD +51 -49
  49. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/WHEEL +0 -0
  50. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/entry_points.txt +0 -0
  51. {snowflake_cli_labs-3.0.0rc0.dist-info → snowflake_cli_labs-3.0.0rc1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,194 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import Any, Dict, Literal, Optional
4
+
5
+ from click import ClickException
6
+ from snowflake.cli._plugins.snowpark.common import is_name_a_templated_one
7
+ from snowflake.cli.api.constants import (
8
+ DEFAULT_ENV_FILE,
9
+ DEFAULT_PAGES_DIR,
10
+ PROJECT_TEMPLATE_VARIABLE_OPENING,
11
+ SNOWPARK_SHARED_MIXIN,
12
+ )
13
+ from snowflake.cli.api.project.schemas.project_definition import (
14
+ ProjectDefinition,
15
+ ProjectDefinitionV2,
16
+ )
17
+ from snowflake.cli.api.project.schemas.snowpark.callable import (
18
+ FunctionSchema,
19
+ ProcedureSchema,
20
+ )
21
+ from snowflake.cli.api.project.schemas.snowpark.snowpark import Snowpark
22
+ from snowflake.cli.api.project.schemas.streamlit.streamlit import Streamlit
23
+
24
+ log = logging.getLogger(__name__)
25
+
26
+
27
+ def convert_project_definition_to_v2(
28
+ pd: ProjectDefinition, accept_templates: bool = False
29
+ ) -> ProjectDefinitionV2:
30
+ _check_if_project_definition_meets_requirements(pd, accept_templates)
31
+
32
+ snowpark_data = convert_snowpark_to_v2_data(pd.snowpark) if pd.snowpark else {}
33
+ streamlit_data = convert_streamlit_to_v2_data(pd.streamlit) if pd.streamlit else {}
34
+ envs = convert_envs_to_v2(pd)
35
+
36
+ data = {
37
+ "definition_version": "2",
38
+ "entities": get_list_of_all_entities(
39
+ snowpark_data.get("entities", {}), streamlit_data.get("entities", {})
40
+ ),
41
+ "mixins": snowpark_data.get("mixins", None),
42
+ "env": envs,
43
+ }
44
+
45
+ return ProjectDefinitionV2(**data)
46
+
47
+
48
+ def convert_snowpark_to_v2_data(snowpark: Snowpark) -> Dict[str, Any]:
49
+ artifact_mapping = {"src": snowpark.src}
50
+ if snowpark.project_name:
51
+ artifact_mapping["dest"] = snowpark.project_name
52
+
53
+ data: dict = {
54
+ "mixins": {
55
+ SNOWPARK_SHARED_MIXIN: {
56
+ "stage": snowpark.stage_name,
57
+ "artifacts": [artifact_mapping],
58
+ }
59
+ },
60
+ "entities": {},
61
+ }
62
+
63
+ for index, entity in enumerate([*snowpark.procedures, *snowpark.functions]):
64
+ identifier = {"name": entity.name}
65
+ if entity.database is not None:
66
+ identifier["database"] = entity.database
67
+ if entity.schema_name is not None:
68
+ identifier["schema"] = entity.schema_name
69
+
70
+ entity_name = (
71
+ f"snowpark_entity_{index}"
72
+ if is_name_a_templated_one(entity.name)
73
+ else entity.name
74
+ )
75
+
76
+ if entity_name in data["entities"]:
77
+ raise ClickException(
78
+ f"Entity with name {entity_name} seems to be duplicated. Please rename it and try again."
79
+ )
80
+
81
+ v2_entity = {
82
+ "type": "function" if isinstance(entity, FunctionSchema) else "procedure",
83
+ "stage": snowpark.stage_name,
84
+ "handler": entity.handler,
85
+ "returns": entity.returns,
86
+ "signature": entity.signature,
87
+ "runtime": entity.runtime,
88
+ "external_access_integrations": entity.external_access_integrations,
89
+ "secrets": entity.secrets,
90
+ "imports": entity.imports,
91
+ "identifier": identifier,
92
+ "meta": {"use_mixins": [SNOWPARK_SHARED_MIXIN]},
93
+ }
94
+ if isinstance(entity, ProcedureSchema):
95
+ v2_entity["execute_as_caller"] = entity.execute_as_caller
96
+
97
+ data["entities"][entity_name] = v2_entity
98
+
99
+ return data
100
+
101
+
102
+ def convert_streamlit_to_v2_data(streamlit: Streamlit):
103
+ # Process env file and pages dir
104
+ environment_file = _process_streamlit_files(streamlit.env_file, "environment")
105
+ pages_dir = _process_streamlit_files(streamlit.pages_dir, "pages")
106
+
107
+ # Build V2 definition
108
+ artifacts = [
109
+ streamlit.main_file,
110
+ environment_file,
111
+ pages_dir,
112
+ ]
113
+ artifacts = [a for a in artifacts if a is not None]
114
+
115
+ if streamlit.additional_source_files:
116
+ artifacts.extend(streamlit.additional_source_files)
117
+
118
+ identifier = {"name": streamlit.name}
119
+ if streamlit.schema_name:
120
+ identifier["schema"] = streamlit.schema_name
121
+ if streamlit.database:
122
+ identifier["database"] = streamlit.database
123
+
124
+ streamlit_name = (
125
+ "streamlit_entity_1"
126
+ if is_name_a_templated_one(streamlit.name)
127
+ else streamlit.name
128
+ )
129
+
130
+ data = {
131
+ "entities": {
132
+ streamlit_name: {
133
+ "type": "streamlit",
134
+ "identifier": identifier,
135
+ "title": streamlit.title,
136
+ "query_warehouse": streamlit.query_warehouse,
137
+ "main_file": str(streamlit.main_file),
138
+ "pages_dir": str(streamlit.pages_dir),
139
+ "stage": streamlit.stage,
140
+ "artifacts": artifacts,
141
+ }
142
+ }
143
+ }
144
+ return data
145
+
146
+
147
+ def convert_envs_to_v2(pd: ProjectDefinition):
148
+ if hasattr(pd, "env") and pd.env:
149
+ data = {k: v for k, v in pd.env.items()}
150
+ return data
151
+ return None
152
+
153
+
154
+ def _check_if_project_definition_meets_requirements(
155
+ pd: ProjectDefinition, accept_templates: bool
156
+ ):
157
+ if pd.meets_version_requirement("2"):
158
+ raise ClickException("Project definition is already at version 2.")
159
+
160
+ if PROJECT_TEMPLATE_VARIABLE_OPENING in str(pd):
161
+ if not accept_templates:
162
+ raise ClickException(
163
+ "Project definition contains templates. They may not be migrated correctly, and require manual migration."
164
+ "You can try again with --accept-templates option, to attempt automatic migration."
165
+ )
166
+ log.warning(
167
+ "Your V1 definition contains templates. We cannot guarantee the correctness of the migration."
168
+ )
169
+ if pd.native_app:
170
+ raise ClickException(
171
+ "Your project file contains a native app definition. Conversion of Native apps is not yet supported"
172
+ )
173
+
174
+
175
+ def _process_streamlit_files(
176
+ file_name: Optional[str], file_type: Literal["pages", "environment"]
177
+ ):
178
+ default = DEFAULT_PAGES_DIR if file_type == "pages" else DEFAULT_ENV_FILE
179
+
180
+ if file_name and not Path(file_name).exists():
181
+ raise ClickException(f"Provided file {file_name} does not exist")
182
+ elif file_name is None and Path(default).exists():
183
+ file_name = default
184
+ return file_name
185
+
186
+
187
+ def get_list_of_all_entities(
188
+ snowpark_entities: Dict[str, Any], streamlit_entities: Dict[str, Any]
189
+ ):
190
+ if snowpark_entities.keys() & streamlit_entities.keys():
191
+ raise ClickException(
192
+ "In your project, streamlit and snowpark entities share the same name. Please rename them and try again."
193
+ )
194
+ return snowpark_entities | streamlit_entities
@@ -75,3 +75,20 @@ class ApplicationPackageEntityModel(EntityModelBase):
75
75
  if isinstance(input_value, Identifier):
76
76
  return input_value.model_copy(update=dict(name=with_suffix))
77
77
  return with_suffix
78
+
79
+ @field_validator("artifacts")
80
+ @classmethod
81
+ def transform_artifacts(
82
+ cls, orig_artifacts: List[Union[PathMapping, str]]
83
+ ) -> List[PathMapping]:
84
+ transformed_artifacts = []
85
+ if orig_artifacts is None:
86
+ return transformed_artifacts
87
+
88
+ for artifact in orig_artifacts:
89
+ if isinstance(artifact, PathMapping):
90
+ transformed_artifacts.append(artifact)
91
+ else:
92
+ transformed_artifacts.append(PathMapping(src=artifact))
93
+
94
+ return transformed_artifacts
@@ -19,7 +19,6 @@ 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,
@@ -244,9 +243,7 @@ ProjectDefinition = Union[ProjectDefinitionV1, ProjectDefinitionV2]
244
243
 
245
244
 
246
245
  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
246
+ version_map = {"1": DefinitionV10, "1.1": DefinitionV11, "2": DefinitionV20}
250
247
  return version_map
251
248
 
252
249
 
@@ -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,7 @@ _YML_TEMPLATE_START = "<%"
24
24
  _YML_TEMPLATE_END = "%>"
25
25
 
26
26
 
27
- def get_project_definition_cli_jinja_env() -> Environment:
27
+ def get_client_side_jinja_env() -> Environment:
28
28
  _random_block = "___very___unique___block___to___disable___logic___blocks___"
29
29
  return env_bootstrap(
30
30
  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,22 @@ 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 choose_sql_jinja_env_based_on_template_syntax(
59
+ template_content: str, reference_name: Optional[str] = None
60
+ ) -> Environment:
59
61
  old_syntax_env = _get_sql_jinja_env(_OLD_SQL_TEMPLATE_START, _OLD_SQL_TEMPLATE_END)
60
62
  new_syntax_env = _get_sql_jinja_env(_SQL_TEMPLATE_START, _SQL_TEMPLATE_END)
61
63
  has_old_syntax = _does_template_have_env_syntax(old_syntax_env, template_content)
62
64
  has_new_syntax = _does_template_have_env_syntax(new_syntax_env, template_content)
65
+ reference_name_str = f" in {reference_name}" if reference_name else ""
63
66
  if has_old_syntax and has_new_syntax:
64
67
  raise InvalidTemplate(
65
- f"The SQL query mixes {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax"
68
+ f"The SQL query{reference_name_str} mixes {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax"
66
69
  f" and {_SQL_TEMPLATE_START} ... {_SQL_TEMPLATE_END} syntax."
67
70
  )
68
71
  if has_old_syntax:
69
72
  cli_console.warning(
70
- f"Warning: {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax is deprecated."
73
+ f"Warning: {_OLD_SQL_TEMPLATE_START} ... {_OLD_SQL_TEMPLATE_END} syntax{reference_name_str} is deprecated."
71
74
  f" Use {_SQL_TEMPLATE_START} ... {_SQL_TEMPLATE_END} syntax instead."
72
75
  )
73
76
  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(
@@ -318,7 +318,7 @@ def render_definition_template(
318
318
  if definition is None:
319
319
  return ProjectProperties(None, {CONTEXT_KEY: {"env": environment_overrides}})
320
320
 
321
- template_env = TemplatedEnvironment(get_project_definition_cli_jinja_env())
321
+ template_env = TemplatedEnvironment(get_client_side_jinja_env())
322
322
 
323
323
  if "definition_version" not in definition or Version(
324
324
  definition["definition_version"]
@@ -353,9 +353,7 @@ def render_definition_template(
353
353
  )
354
354
 
355
355
  def on_cycle_action(node: Node[TemplateVar]):
356
- raise CycleDetectedError(
357
- f"Cycle detected in templating variable {node.data.key}"
358
- )
356
+ raise CycleDetectedError(f"Cycle detected in template variable {node.data.key}")
359
357
 
360
358
  dependencies_graph.dfs(
361
359
  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.0rc1
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
@@ -225,14 +225,14 @@ Requires-Dist: pluggy==1.5.0
225
225
  Requires-Dist: pydantic==2.8.2
226
226
  Requires-Dist: pyyaml==6.0.1
227
227
  Requires-Dist: requests==2.32.3
228
- Requires-Dist: requirements-parser==0.10.2
228
+ Requires-Dist: requirements-parser==0.11.0
229
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
230
+ Requires-Dist: setuptools==74.1.0
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'